matrices - ¿Por qué los nuevos métodos java.util.Arrays en Java 8 no están sobrecargados para todos los tipos primitivos?
vector java ejemplo (1)
Estoy revisando los cambios de API para Java 8 y noté que los nuevos métodos en java.util.Arrays
no están sobrecargados para todas las primitivas. Los métodos que noté son:
Actualmente, estos nuevos métodos solo manejan primitivas int
, long
y double
.
int
, long
y double
son probablemente las primitivas más utilizadas, por lo que tiene sentido que, si tuvieran que limitar la API, elegirían esas tres, pero ¿por qué tendrían que limitar la API?
Para abordar las preguntas como un todo, y no solo este escenario en particular, creo que todos queremos saber ...
Por qué hay contaminación de la interfaz en Java 8
Por ejemplo, en un lenguaje como C #, hay un conjunto de tipos de funciones predefinidas que aceptan cualquier cantidad de argumentos con un tipo de retorno opcional ( Func y Action cada uno subiendo a 16 parámetros de diferentes tipos T1
, T2
, T3
, ... , T16
), pero en el JDK 8 lo que tenemos es un conjunto de diferentes interfaces funcionales, con diferentes nombres y diferentes nombres de métodos , y cuyos métodos abstractos representan un subconjunto de funciones bien conocidas (es decir, nullary, unary, binary, ternary, etc.) Y luego tenemos una explosión de casos que tratan con tipos primitivos, e incluso hay otros escenarios que causan la explosión de interfaces más funcionales.
El problema de borrado de tipo
Entonces, de alguna manera, ambos lenguajes sufren de alguna forma de contaminación de interfaz (o delegan contaminación en C #). La única diferencia es que en C # todos tienen el mismo nombre. Desafortunadamente, en Java, debido a la supresión del tipo , no hay diferencia entre la Function<T1,T2>
y la Function<T1,T2,T3>
o la Function<T1,T2,T3,...Tn>
, así que evidentemente no pudimos Simplemente los nombramos todos de la misma manera y tuvimos que crear nombres creativos para todos los tipos posibles de combinaciones de funciones.
No crea que el grupo de expertos no tuvo problemas con este problema. En palabras de Brian Goetz en la lista de correo de lambda :
[...] Como un solo ejemplo, tomemos los tipos de funciones. El lambda strawman ofrecido en devoxx tenía tipos de funciones. Insistí en que los eliminemos, y esto me hizo impopular. Pero mi objeción a los tipos de funciones no era que no me gustan los tipos de funciones, me encantan los tipos de funciones, sino que los tipos de funciones peleaban mal con un aspecto existente del sistema de tipo Java, el borrado. Los tipos de funciones borradas son los peores de ambos mundos. Así que eliminamos esto del diseño.
Pero no estoy dispuesto a decir "Java nunca tendrá tipos de funciones" (aunque reconozco que Java puede que nunca tenga tipos de funciones). Creo que para llegar a los tipos de funciones, primero tenemos que lidiar con el borrado. Eso puede o no ser posible. Pero en un mundo de tipos estructurales reificados, los tipos de funciones comienzan a tener mucho más sentido [...]
Una ventaja de este enfoque es que podemos definir nuestros propios tipos de interfaz con métodos que aceptan tantos argumentos como quisiéramos, y podríamos usarlos para crear expresiones lambda y referencias de métodos como lo consideremos apropiado. En otras palabras, tenemos el poder de contaminar el mundo con aún más interfaces funcionales nuevas. También podemos crear expresiones lambda incluso para interfaces en versiones anteriores de JDK o para versiones anteriores de nuestras propias API que definieron tipos de SAM como estos. Y ahora tenemos el poder de usar Runnable
y Callable
como interfaces funcionales.
Sin embargo, estas interfaces se vuelven más difíciles de memorizar ya que todas tienen diferentes nombres y métodos.
Aún así, soy uno de los que se preguntan por qué no resolvieron el problema como en Scala, definiendo interfaces como Function0
, Function1
, Function2
, ..., FunctionN
. Quizás, el único argumento con el que se me puede ocurrir es que querían maximizar las posibilidades de definir expresiones lambda para interfaces en versiones anteriores de las API como se mencionó anteriormente.
Falta de tipos de valor Problema
Entonces, evidentemente, la borradura de tipo es una fuerza impulsora aquí. Pero si usted es uno de los que se preguntan por qué también necesitamos todas estas interfaces funcionales adicionales con nombres y firmas de métodos similares y cuya única diferencia es el uso de un tipo primitivo, entonces déjeme recordarle que en Java también carecemos de tipos de valores como aquellos en un lenguaje como C #. Esto significa que los tipos genéricos utilizados en nuestras clases genéricas solo pueden ser tipos de referencia, y no tipos primitivos.
En otras palabras, no podemos hacer esto:
List<int> numbers = asList(1,2,3,4,5);
Pero podemos hacer esto:
List<Integer> numbers = asList(1,2,3,4,5);
El segundo ejemplo, sin embargo, incurre en el costo de encajonar y desempaquetar los objetos envueltos desde y hacia tipos primitivos. Esto puede volverse realmente costoso en operaciones relacionadas con colecciones de valores primitivos. Entonces, el grupo de expertos decidió crear esta explosión de interfaces para lidiar con los diferentes escenarios. Para empeorar las cosas, decidieron tratar solo tres tipos básicos: int, long y double.
Citando las palabras de Brian Goetz en la lista de correo de lambda :
[...] En términos más generales: la filosofía detrás de tener flujos primitivos especializados (por ejemplo, IntStream) está plagado de desagradables intercambios. Por un lado, es una gran cantidad de duplicación de código feo, contaminación de interfaz, etc. Por otro lado, cualquier tipo de aritmética en operaciones en caja es una mierda, y no tener una historia para reducir los niveles excesivos sería terrible. Así que estamos en un rincón difícil, y estamos tratando de no empeorar las cosas.
El truco n. ° 1 para no empeorar es: no estamos haciendo los ocho tipos primitivos. Estamos haciendo int, long y double; todos los demás podrían ser simulados por estos. Podría decirse que también podríamos deshacernos de la int, pero no creemos que la mayoría de los desarrolladores de Java estén listos para eso. Sí, habrá llamadas para Carácter, y la respuesta es "pegarlo en un int". (Cada especialización se proyecta a ~ 100K a la huella de JRE).
El truco n. ° 2 es: estamos usando flujos primitivos para exponer las cosas que mejor se hacen en el dominio primitivo (clasificación, reducción) pero no tratando de duplicar todo lo que puede hacer en el dominio en caja. Por ejemplo, no hay IntStream.into (), como señala Aleksey. (Si hubiera, la próxima pregunta sería "¿Dónde está IntCollection? IntArrayList? IntConcurrentSkipListMap?) La intención es que muchas secuencias puedan comenzar como flujos de referencia y terminen como flujos primitivos, pero no al revés. Eso está bien, y eso reduce el número de conversiones necesarias (p. ej., no hay sobrecarga de mapa para int -> T, no se especializa en Function para int -> T, etc.) [...]
Podemos ver que esta fue una decisión difícil para el grupo de expertos. Creo que pocos estarían de acuerdo en que esto es genial, y la mayoría de nosotros probablemente aceptaría que era necesario.
El problema de excepciones controladas
Hubo una tercera fuerza motriz que podría haber empeorado las cosas , y es el hecho de que Java admite dos tipos de excepciones: marcada y sin marcar. El compilador requiere que manejemos o declaremos explícitamente las excepciones marcadas, pero no requiere nada para las que no están marcadas. Por lo tanto, esto crea un problema interesante, ya que las firmas de método de la mayoría de las interfaces funcionales no declaran arrojar ninguna excepción. Entonces, por ejemplo, esto no es posible:
Writer out = new StringWriter();
Consumer<String> printer = s -> out.write(s); //oops! compiler error
No se puede hacer porque la operación de write
arroja una excepción marcada (es decir, IOException
) pero la firma del método del Consumer
no declara que arroje ninguna excepción. Por lo tanto, la única solución a este problema habría sido crear aún más interfaces, algunas declarando excepciones y otras no (o presentar otro mecanismo en el nivel de idioma para la transparencia de excepción . Una vez más, para hacer las cosas "peor", el experto grupo decidió no hacer nada en este caso.
En palabras de Brian Goetz en la lista de correo de lambda :
[...] Sí, tendrías que proporcionar tus propios SAM excepcionales. Pero entonces la conversión lambda funcionaría bien con ellos.
El EG discutió el lenguaje adicional y el soporte bibliotecario para este problema, y al final sintió que esto era una mala compensación de costo / beneficio.
Las soluciones basadas en bibliotecas causan una explosión de 2x en los tipos de SAM (excepcionales vs no), que interactúan mal con las explosiones combinatorias existentes para la especialización primitiva.
Las soluciones disponibles basadas en el lenguaje fueron perdedores de un compromiso de complejidad / valor. Aunque hay algunas soluciones alternativas que vamos a continuar explorando, aunque claramente no para 8 y probablemente tampoco para 9.
Mientras tanto, tienes las herramientas para hacer lo que quieras. Supongo que prefiere que proporcionemos esa última milla para usted (y, en segundo lugar, su solicitud es realmente una solicitud escasamente velada de "¿por qué no acaba de darse por vencido con las excepciones comprobadas?"), Pero creo que el estado actual permite haces tu trabajo hecho. [...]
Por lo tanto, nos corresponde a nosotros, los desarrolladores, crear aún más explosiones de interfaz para tratar estos caso por caso:
interface IOConsumer<T> {
void accept(T t) throws IOException;
}
static<T> Consumer<T> exceptionWrappingBlock(IOConsumer<T> b) {
return e -> {
try { b.accept(e); }
catch (Exception ex) { throw new RuntimeException(ex); }
};
}
Con el fin de hacer:
Writer out = new StringWriter();
Consumer<String> printer = exceptionWrappingBlock(s -> out.write(s));
Probablemente, en el futuro (tal vez JDK 9) cuando obtengamos soporte para tipos de valores en Java y Reification, podremos deshacernos de (o al menos ya no necesitar usar más) algunas de estas interfaces múltiples.
En resumen, podemos ver que el grupo de expertos luchó con varios problemas de diseño. La necesidad, el requisito o la restricción para mantener la compatibilidad con versiones anteriores dificultan las cosas, luego tenemos otras condiciones importantes como la falta de tipos de valores, el borrado de tipos y las excepciones comprobadas. Si Java tuviera el primero y careciera de los otros dos, el diseño de JDK 8 probablemente habría sido diferente. Entonces, todos debemos entender que estos fueron problemas difíciles con muchas concesiones y el EG tuvo que trazar una línea en algún lado y tomar decisiones.