¿Por qué la mayoría de las manipulaciones de cadenas en Java se basan en expresiones regulares?
En Java hay un montón de métodos que tienen que ver con la manipulación de cadenas. El ejemplo más simple es el método String.split("algo").
Ahora bien, la definición real de muchos de esos métodos es que todos toman una expresión regular como parámetro(s) de entrada. Lo que los convierte en bloques de construcción muy poderosos.
Ahora hay dos efectos que verás en muchos de esos métodos:
- Recompilan la expresión cada vez que se invoca el método. Como tales, imponen un impacto en el rendimiento.
- Descubrí que en la mayoría de situaciones de la "vida real" estos métodos se llaman con textos "fijos". El uso más común del método de división es aún peor: generalmente se llama con un solo carácter (generalmente un ' ', un ';' o un '&') para dividir.
Así que no es sólo que los métodos predeterminados sean poderosos, sino que también parecen abrumadores para el propósito que realmente se utilizan. Internamente hemos desarrollado un método "fastSplit" que divide en cadenas fijas. Escribí una prueba en casa para ver cuánto más rápido podía hacerlo si se supiera que era un solo carácter. Ambos son significativamente más rápidos que el método de división "estándar".
Entonces me preguntaba: ¿por qué se eligió la API de Java como está ahora? ¿Cuál fue la buena razón para optar por esto en lugar de tener algo como split(char) y split(String) y splitRegex(String)?
Actualización: realicé algunas llamadas para ver cuánto tiempo tomarían las distintas formas de dividir una cadena.
Breve resumen: ¡Hace una gran diferencia!
Hice 10000000 iteraciones para cada caso de prueba, siempre usando la entrada
"aap,noot,mies,wim,zus,jet,teun"
y siempre usando "," o "," como argumento de división.
Esto es lo que tengo en mi sistema Linux (es una caja Atom D510, por lo que es un poco lento):
fastSplit STRING
Test 1 : 11405 milliseconds: Split in several pieces
Test 2 : 3018 milliseconds: Split in 2 pieces
Test 3 : 4396 milliseconds: Split in 3 pieces
homegrown fast splitter based on char
Test 4 : 9076 milliseconds: Split in several pieces
Test 5 : 2024 milliseconds: Split in 2 pieces
Test 6 : 2924 milliseconds: Split in 3 pieces
homegrown splitter based on char that always splits in 2 pieces
Test 7 : 1230 milliseconds: Split in 2 pieces
String.split(regex)
Test 8 : 32913 milliseconds: Split in several pieces
Test 9 : 30072 milliseconds: Split in 2 pieces
Test 10 : 31278 milliseconds: Split in 3 pieces
String.split(regex) using precompiled Pattern
Test 11 : 26138 milliseconds: Split in several pieces
Test 12 : 23612 milliseconds: Split in 2 pieces
Test 13 : 24654 milliseconds: Split in 3 pieces
StringTokenizer
Test 14 : 27616 milliseconds: Split in several pieces
Test 15 : 28121 milliseconds: Split in 2 pieces
Test 16 : 27739 milliseconds: Split in 3 pieces
Como puede ver, hace una gran diferencia si tiene que hacer muchas divisiones de "caracteres fijos".
Para darles una idea; Actualmente estoy en el campo de los archivos de registro de Apache y Hadoop con los datos de un gran sitio web. Así que para mí esto realmente importa :)
Algo que no he tenido en cuenta aquí es el recolector de basura. Por lo que puedo decir, compilar una expresión regular en un Pattern/Matcher/.. asignará muchos objetos, que deben recopilarse en algún momento. Así que quizás a la larga las diferencias entre estas versiones sean aún mayores….o menores.
Mis conclusiones hasta ahora:
- Optimice esto solo si tiene MUCHAS cadenas para dividir.
- Si utiliza los métodos de expresiones regulares, siempre precompile si utiliza repetidamente el mismo patrón.
- Olvídese del (obsoleto) StringTokenizer
- Si desea dividir en un solo carácter, utilice un método personalizado, especialmente si solo necesita dividirlo en una cantidad específica de partes (como... 2).
PD: Les estoy dando todos mis métodos locales divididos por caracteres para que jueguen (bajo la licencia bajo la cual se encuentra todo en este sitio :)). Nunca los probé completamente... todavía. Divertirse.
private static String[]
stringSplitChar(final String input,
final char separator) {
int pieces = 0;
// First we count how many pieces we will need to store ( = separators + 1 )
int position = 0;
do {
pieces++;
position = input.indexOf(separator, position + 1);
} while (position != -1);
// Then we allocate memory
final String[] result = new String[pieces];
// And start cutting and copying the pieces.
int previousposition = 0;
int currentposition = input.indexOf(separator);
int piece = 0;
final int lastpiece = pieces - 1;
while (piece < lastpiece) {
result[piece++] = input.substring(previousposition, currentposition);
previousposition = currentposition + 1;
currentposition = input.indexOf(separator, previousposition);
}
result[piece] = input.substring(previousposition);
return result;
}
private static String[]
stringSplitChar(final String input,
final char separator,
final int maxpieces) {
if (maxpieces <= 0) {
return stringSplitChar(input, separator);
}
int pieces = maxpieces;
// Then we allocate memory
final String[] result = new String[pieces];
// And start cutting and copying the pieces.
int previousposition = 0;
int currentposition = input.indexOf(separator);
int piece = 0;
final int lastpiece = pieces - 1;
while (currentposition != -1 && piece < lastpiece) {
result[piece++] = input.substring(previousposition, currentposition);
previousposition = currentposition + 1;
currentposition = input.indexOf(separator, previousposition);
}
result[piece] = input.substring(previousposition);
// All remaining array elements are uninitialized and assumed to be null
return result;
}
private static String[]
stringChop(final String input,
final char separator) {
String[] result;
// Find the separator.
final int separatorIndex = input.indexOf(separator);
if (separatorIndex == -1) {
result = new String[1];
result[0] = input;
}
else {
result = new String[2];
result[0] = input.substring(0, separatorIndex);
result[1] = input.substring(separatorIndex + 1);
}
return result;
}
Tenga en cuenta que no es necesario volver a compilar la expresión regular cada vez. Del Javadoc :
Una invocación de este método de la forma
str.split(regex, n)
produce el mismo resultado que la expresión
Pattern.compile(regex).split(str, n)
Es decir, si le preocupa el rendimiento, puede precompilar el patrón y luego reutilizarlo:
Pattern p = Pattern.compile(regex);
...
String[] tokens1 = p.split(str1);
String[] tokens2 = p.split(str2);
...
en lugar de
String[] tokens1 = str1.split(regex);
String[] tokens2 = str2.split(regex);
...
Creo que la razón principal para este diseño de API es la conveniencia. Dado que las expresiones regulares también incluyen todas las cadenas/caracteres "fijos", simplifica la API al tener un método en lugar de varios. Y si alguien está preocupado por el rendimiento, la expresión regular aún se puede precompilar como se muestra arriba.
Mi sensación (que no puedo respaldar con ninguna evidencia estadística) es que la mayoría de los casos String.split()
se utiliza en un contexto donde el rendimiento no es un problema. Por ejemplo, se trata de una acción única o la diferencia de rendimiento es insignificante en comparación con otros factores. En mi opinión, son raros los casos en los que se dividen cadenas usando la misma expresión regular miles de veces en un bucle cerrado, donde la optimización del rendimiento realmente tiene sentido.
Sería interesante ver una comparación de rendimiento de una implementación de comparador de expresiones regulares con cadenas/caracteres fijos en comparación con la de un comparador especializado en estos. Es posible que la diferencia no sea lo suficientemente grande como para justificar la implementación por separado.
No diría que la mayoría de las manipulaciones de cadenas se basan en expresiones regulares en Java. Realmente solo estamos hablando de split
y replaceAll
/ replaceFirst
. Pero estoy de acuerdo, es un gran error.
Aparte de lo feo de que una característica del lenguaje de bajo nivel (cadenas) se vuelva dependiente de una característica de nivel superior (regex), también es una trampa desagradable para los nuevos usuarios que naturalmente podrían asumir que un método con la firma String.replaceAll(String, String)
sería una cadena. reemplazar la función. El código escrito bajo esa suposición parecerá que está funcionando, hasta que aparezca un carácter especial de expresión regular, momento en el cual tendrá errores confusos, difíciles de depurar (y tal vez incluso significativos para la seguridad).
Es divertido que un lenguaje que puede ser tan pedante y estricto a la hora de escribir cometa el error de tratar una cadena y una expresión regular como la misma cosa. Es menos divertido que todavía no exista un método integrado para reemplazar o dividir una cadena simple. Tienes que usar un reemplazo de expresiones regulares con una Pattern.quote
cadena d. Y eso sólo se obtiene a partir de Java 5 en adelante. Desesperanzado.
@Tim Pietzcker:
¿Hay otros idiomas que hacen lo mismo?
Las cadenas de JavaScript están parcialmente modeladas en Java y también son confusas en el caso de replace()
. Al pasar una cadena, obtienes un reemplazo de cadena simple, pero solo reemplaza la primera coincidencia, que rara vez es lo que se desea. Para obtener un reemplazo completo, debe pasar un RegExp
objeto con la /g
bandera, lo que nuevamente tiene problemas si desea crearlo dinámicamente a partir de una cadena (no existe un RegExp.quote
método integrado en JS). Por suerte, split()
se basa exclusivamente en cadenas, por lo que puedes usar el modismo:
s.split(findstr).join(replacestr)
Además, por supuesto, Perl hace absolutamente todo con regexen, porque es así de perverso.
(Este es un comentario más que una respuesta, pero es demasiado grande para una sola. ¿ Por qué Java hizo esto? No sé, cometieron muchos errores en los primeros días. Algunos de ellos se han solucionado desde entonces. Sospecho que si lo hubieran hecho Se pensó en poner la funcionalidad de expresiones regulares en el cuadro marcado Pattern
en 1.0, el diseño String
sería más limpio para igualar).