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

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

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 hacer CharStream , que es razonable, con métodos diferentes en lugar de chars() , pensaría en:

  • Stream chars() , que dan una secuencia de caracteres de cuadros, que tendrán una cierta penalización de rendimiento ligero.
  • IntStream unboxedChars() , que se usaría para el código de rendimiento.

Sin embargo , en lugar de centrarme en por qué se hace de esta manera actualmente, creo que esta respuesta debe centrarse en mostrar una forma de hacerlo con la API que hemos obtenido con Java 8.

En Java 7 lo hubiera 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í IntStream un IntStream y lo mapeo a un objeto a través del lambda i -> (char)i , esto lo IntStream automáticamente en un Stream , y luego podemos hacer lo que queramos, y aún usar referencias de método como más.

Tenga en cuenta que debe hacer mapToObj , si olvida y utiliza el map , entonces nada se quejará, pero igual terminará con un IntStream , y es posible que se quede preguntándose por qué imprime los valores enteros en lugar de las cadenas que representan el caracteres.

Otras alternativas feas para Java 8:

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

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

Por otra parte, el uso de referencias de métodos 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 dará un error de comstackció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 debe devolver un Stream , y la solución actual es usar mapToObj(i -> (char)i) en un IntStream para ser capaz de trabajar adecuadamente con ellos.

La respuesta de skiwi cubrió muchos de los puntos principales ya. Voy a completar un poco más de fondo.

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

Los primitivos han estado en Java desde 1.0. Hacen de Java un lenguaje orientado a objetos “impuro”, ya que los primitivos 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 estamos viviendo hoy, casi 20 años después. La función de autoboxing agregada en Java 5 eliminó la necesidad de saturar el código fuente con las llamadas al método de boxeo y unboxing, pero la sobrecarga aún está allí. En muchos casos no es notable. Sin embargo, si realizara un boxeo o unboxing dentro de un bucle interno, vería que puede imponer una sobrecarga significativa de CPU y recolección de basura.

Al diseñar la API de Streams, estaba claro que teníamos que admitir primitivas. La sobrecarga del boxeo / 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 puedes ver el uso de un ShortStream ?) “Todos” o “ninguno” son lugares cómodos para un diseño, sin embargo ninguno fue aceptable. Entonces tuvimos que encontrar un valor razonable de “algunos”. Terminamos con especializaciones primitivas para int , long y double . (Personalmente me hubiera quedado fuera pero así soy yo.)

Para CharSequence.chars() consideramos la devolución de Stream (un prototipo inicial podría haber implementado esto) pero fue rechazado debido a la sobrecarga del boxeo. Teniendo en cuenta que una Cadena tiene valores char como primitivos, parecería un error imponer el boxeo incondicionalmente cuando la persona que llama probablemente solo haga un poco de procesamiento en el valor y lo vuelva a poner en una cadena.

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

La penalización que esto impone a las personas que llaman es que tienen que saber que IntStream contiene los valores char representados como ints y que la conversión se debe realizar 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 adicional de confusión debido a que la llamada a codePoints() también devuelve un IntStream pero 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 resulta en 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, dando una API de alto rendimiento y tamaño moderado que impone una carga relativamente pequeña a los llamantes en un rango bastante estrecho de casos de uso (procesamiento de char).

Elegimos el último.