¿Por qué String.chars () es una secuencia de entradas en Java 8?

198

En Java 8, hay un nuevo método String.chars()que devuelve una secuencia de ints ( IntStream) que representa los códigos de caracteres. Supongo que mucha gente esperaría una corriente de chars aquí en su lugar. ¿Cuál fue la motivación para diseñar la API de esta manera?

Adam Dyga
fuente
44
@RohitJain No me refería a ninguna transmisión en particular. Si CharStreamno existe, ¿cuál sería el problema para agregarlo?
Adam Dyga
55
@AdamDyga: Los diseñadores eligieron explícitamente evitar la explosión de clases y métodos limitando las corrientes primitivas a 3 tipos, ya que los otros tipos (char, short, float) pueden representarse por su equivalente más grande (int, double) sin ningún significado significativo penalización de rendimiento.
JB Nizet
3
@JBNizet lo entiendo. Pero todavía se siente como una solución sucia solo por salvar un par de clases nuevas.
Adam Dyga
9
@JB Nizet: Para mí, parece que ya tenemos una explosión de interfaces dada la sobrecarga de la transmisión, así como todas las interfaces de funciones ...
Holger
55
Sí, ya hay una explosión, incluso con solo tres especializaciones de flujo primitivas. ¿Qué sería si las ocho primitivas tuvieran especializaciones de flujo? ¿Un cataclismo? :-)
Stuart Marks

Respuestas:

218

Como otros ya han mencionado, la decisión de diseño detrás de esto fue evitar la explosión de métodos y clases.

Aún así, personalmente creo que esta fue una decisión muy mala, y debería, dado que no quieren tomar CharStream, lo cual es razonable, métodos diferentes en lugar de chars(), pensaría en:

  • Stream<Character> chars(), que proporciona una secuencia de caracteres de cuadros, que tendrá una ligera penalización de rendimiento.
  • IntStream unboxedChars(), que se utilizaría para el código de rendimiento.

Sin embargo , en lugar de centrarnos en por qué se hace de esta manera actualmente, creo que esta respuesta debería centrarse en mostrar una manera de hacerlo con la API que obtuvimos con Java 8.

En Java 7 lo habría hecho así:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

Y creo que un método razonable para hacerlo en Java 8 es el siguiente:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

Aquí obtengo un IntStreamy lo mapeo a un objeto a través de la lambda i -> (char)i, esto automáticamente lo encuadrará en un Stream<Character>, y luego podemos hacer lo que queramos, y aún usar referencias de métodos como un plus.

Sin embargo, tenga en cuenta que debe hacerlo mapToObj, si olvida y usa map, entonces nada se quejará, pero aún terminará con un IntStream, y es posible que se pregunte por qué imprime los valores enteros en lugar de las cadenas que representan los caracteres.

Otras alternativas feas para Java 8:

Al permanecer en un IntStreamy querer imprimirlos en última instancia, ya no puede usar referencias de métodos para imprimir:

hello.chars()
        .forEach(i -> System.out.println((char)i));

Además, ¡usar referencias de método a su propio método ya no funciona! Considera lo siguiente:

private void print(char c) {
    System.out.println(c);
}

y entonces

hello.chars()
        .forEach(this::print);

Esto generará un error de compilación, ya que posiblemente haya una conversión con pérdida.

Conclusión:

La API se diseñó de esta manera por no querer agregar CharStream, personalmente creo que el método debería devolver un Stream<Character>, y la solución actual es usar mapToObj(i -> (char)i)en un IntStreampara poder trabajar correctamente con ellos.

skiwi
fuente
77
Mi conclusión: esta parte de API está rota por diseño. Pero gracias por la extensa respuesta
Adam Dyga
27
+1, pero mi propuesta es usar en codePoints()lugar de chars()y encontrará muchas funciones de biblioteca que ya aceptan un intpunto de código adicional char, por ejemplo, todos los métodos java.lang.Character, así como StringBuilder.appendCodePoint, etc. Este soporte existe desde entonces jdk1.5.
Holger
66
Buen punto sobre puntos de código. Usarlos manejará caracteres suplementarios, que se representan como pares sustitutos en a Stringo char[]. Apuesto a que la mayoría del charcódigo de procesamiento maneja mal los pares sustitutos.
Stuart Marks
2
@skiwi, define void print(int ch) { System.out.println((char)ch); }y luego puedes usar referencias de métodos.
Stuart Marks
2
Vea mi respuesta de por qué Stream<Character>fue rechazado.
Stuart Marks
90

La respuesta de skiwi cubrió muchos de los puntos más importantes. Completaré un poco más de antecedentes.

El diseño de cualquier API es una serie de compensaciones. En Java, uno de los problemas difíciles es lidiar con las decisiones de diseño que se tomaron hace mucho tiempo.

Las primitivas han estado en Java desde 1.0. Hacen de Java un lenguaje orientado a objetos "impuro", ya que las primitivas no son objetos. La adición de primitivas fue, creo, una decisión pragmática para mejorar el rendimiento a expensas de la pureza orientada a objetos.

Esta es una compensación con la que todavía vivimos hoy, casi 20 años después. La característica de autoboxing agregada en Java 5 eliminó principalmente la necesidad de desordenar el código fuente con llamadas al método de boxing y unboxing, pero la sobrecarga sigue ahí. En muchos casos no se nota. Sin embargo, si realizara un boxeo o unboxing dentro de un bucle interno, vería que puede imponer una sobrecarga considerable de CPU y recolección de basura.

Al diseñar la API de Streams, estaba claro que teníamos que soportar primitivas. La sobrecarga de boxing / unboxing mataría cualquier beneficio de rendimiento del paralelismo. Sin embargo, no queríamos admitir todas las primitivas, ya que eso habría agregado una gran cantidad de desorden a la API. (¿Realmente puede ver el uso de a ShortStream?) "Todos" o "ninguno" son lugares cómodos para un diseño, pero ninguno era aceptable. Así que tuvimos que encontrar un valor razonable de "algunos". Terminamos con especializaciones primitivas para int, longy double. (Personalmente, me hubiera dejado fuera, intpero solo soy yo).

Porque CharSequence.chars()consideramos regresar Stream<Character>(un prototipo temprano podría haber implementado esto) pero fue rechazado debido a la sobrecarga del boxeo. Teniendo en cuenta que una Cadena tiene charvalores como primitivos, parecería un error imponer el boxeo incondicionalmente cuando la persona que llama probablemente solo procesará un poco el valor y lo desempaquetará nuevamente en una cadena.

También consideramos una CharStreamespecialización primitiva, pero su uso parece ser bastante limitado en comparación con la cantidad de volumen que agregaría a la API. No valía la pena agregarlo.

La penalidad que esto impone a las personas que llaman es que deben saber que IntStreamcontiene charvalores representados como intsy que el lanzamiento debe realizarse en el lugar adecuado. Esto es doblemente confuso porque hay llamadas API sobrecargadas como PrintStream.print(char)y PrintStream.print(int)que difieren notablemente en su comportamiento. Posiblemente surja un punto de confusión adicional porque la codePoints()llamada también devuelve un, IntStreampero los valores que contiene son bastante diferentes.

Entonces, esto se reduce a elegir pragmáticamente entre varias alternativas:

  1. No podríamos proporcionar especializaciones primitivas, lo que da como resultado una API simple, elegante y consistente, pero que impone un alto rendimiento y una sobrecarga de GC;

  2. podríamos proporcionar un conjunto completo de especializaciones primitivas, a costa de saturar la API e imponer una carga de mantenimiento a los desarrolladores de JDK; o

  3. podríamos proporcionar un subconjunto de especializaciones primitivas, proporcionando una API de tamaño moderado y alto rendimiento que impone una carga relativamente pequeña a las personas que llaman en un rango bastante limitado de casos de uso (procesamiento de caracteres).

Elegimos el último.

Stuart Marks
fuente
1
¡Buena respuesta! Sin embargo, no responde por qué no puede haber dos métodos diferentes chars(), uno que devuelve un Stream<Character>(con una pequeña penalización de rendimiento) y otro IntStream, ¿también se consideró? Es muy probable que las personas terminen mapeándolo de Stream<Character>todos modos si creen que la conveniencia vale la pena por la penalización de rendimiento.
skiwi
3
El minimalismo entra aquí. Si ya hay un chars()método que devuelve los valores de caracteres en un IntStream, no agrega mucho tener otra llamada API que obtenga los mismos valores pero en forma de recuadro. La persona que llama puede encuadrar los valores sin muchos problemas. Claro, sería más conveniente no tener que hacer esto en este caso (probablemente raro), pero a costa de agregar desorden a la API.
Stuart Marks
55
Gracias a la pregunta duplicada, noté esta. Estoy de acuerdo en que chars()regresar IntStreamno es un gran problema, especialmente dado el hecho de que este método rara vez se usa en absoluto. Sin embargo, sería bueno tener una forma integrada de convertir de nuevo IntStreama String. Se puede hacer .reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString(), pero es realmente largo.
Tagir Valeev
77
@TagirValeev Sí, es algo engorroso. Con una corriente de elementos de código (una IntStream) no es demasiado malo: collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(). Supongo que no es realmente más corto, pero el uso de puntos de código evita los (char)lanzamientos y permite el uso de referencias de métodos. Además, maneja sustitutos correctamente.
Stuart Marks
2
@IlyaBystrov Lamentablemente, las transmisiones primitivas como IntStreamno tienen un collect()método que tome un Collector. Solo tienen un collect()método de tres argumentos como se mencionó en comentarios anteriores.
Stuart Marks