¿Por qué algunos afirman que la implementación de genéricos de Java es mala?

115

Ocasionalmente he escuchado que con los genéricos, Java no lo hizo bien. (referencia más cercana, aquí )

Disculpe mi inexperiencia, pero ¿qué los habría mejorado?

sdellysse
fuente
23
No veo ninguna razón para cerrar esto; Con toda la mierda sobre los genéricos, tener alguna aclaración entre las diferencias entre Java y C # podría ayudar a las personas que intentan esquivar la confusión mal informada.
Rob

Respuestas:

146

Malo:

  • La información de tipo se pierde en el momento de la compilación, por lo que en el momento de la ejecución no se puede saber qué tipo está "destinado" a ser
  • No se puede usar para tipos de valor (esto es un gran problema: en .NET, a List<byte>realmente está respaldado por, byte[]por ejemplo, y no se requiere boxing)
  • Sintaxis para llamar a métodos genéricos apesta (IMO)
  • La sintaxis de las restricciones puede resultar confusa
  • El comodín es generalmente confuso
  • Varias restricciones debido a lo anterior: casting, etc.

Bueno:

  • El comodín permite especificar la covarianza / contravarianza en el lado de la llamada, lo cual es muy claro en muchas situaciones
  • ¡Es mejor que nada!
Jon Skeet
fuente
51
@Paul: Creo que en realidad hubiera preferido una rotura temporal pero luego una API decente. O tome la ruta .NET e introduzca nuevos tipos de colección. (Los genéricos .NET en 2.0 no rompieron las aplicaciones 1.1.)
Jon Skeet
6
Con una gran base de código existente, me gusta el hecho de que puedo comenzar a cambiar la firma de un método para usar genéricos sin cambiar a todos los que lo llaman.
Paul Tomblin
38
Pero las desventajas de eso son enormes y permanentes. Hay una gran ganancia a corto plazo y una pérdida a largo plazo, en mi opinión.
Jon Skeet
11
Jon - "Es mejor que nada" es subjetivo :)
MetroidFan2002
6
@Rogerio: Dijiste que mi primer artículo "Malo" no es correcto. Mi primer elemento "Malo" dice que la información se pierde durante la compilación. Para que eso sea incorrecto, debes estar diciendo que la información no se pierde en el momento de la compilación ... En cuanto a confundir a otros desarrolladores, parece que eres el único confundido por lo que estoy diciendo.
Jon Skeet
26

El mayor problema es que los genéricos de Java son una cosa solo en tiempo de compilación, y puede subvertirlos en tiempo de ejecución. C # es elogiado porque realiza más comprobaciones en tiempo de ejecución. Hay una muy buena discusión en esta publicación y se vincula a otras discusiones.

Paul Tomblin
fuente
Eso no es realmente un problema, nunca fue diseñado para ser utilizado en tiempo de ejecución. Es como si dijeras que los barcos son un problema porque no pueden escalar montañas. Como lenguaje, Java hace lo que mucha gente necesita: expresar tipos de forma significativa. Para los informes de tipo de tiempo de ejecución, las personas aún pueden pasar Classobjetos.
Vincent Cantin
1
El tiene razón. su anología es incorrecta porque la gente que diseñó el barco DEBERÍA haberlo diseñado para escalar montañas. Sus objetivos de diseño no estaban muy bien pensados ​​para lo que se necesitaba. Ahora todos estamos atrapados con solo un bote y no podemos escalar montañas.
WiegleyJ
1
@VincentCantin - definitivamente es un problema. Por lo tanto, todos nos quejamos de ello. Los genéricos de Java están a medias.
Josh M.
17

El principal problema es que Java en realidad no tiene genéricos en tiempo de ejecución. Es una función de tiempo de compilación.

Cuando creas una clase genérica en Java, usan un método llamado "Borrado de tipos" para eliminar todos los tipos genéricos de la clase y, esencialmente, reemplazarlos con Object. La versión más alta de los genéricos es que el compilador simplemente inserta conversiones en el tipo genérico especificado siempre que aparece en el cuerpo del método.

Esto tiene muchos inconvenientes. Uno de los más importantes, en mi humilde opinión, es que no se puede utilizar la reflexión para inspeccionar un tipo genérico. Los tipos no son realmente genéricos en el código de bytes y, por lo tanto, no se pueden inspeccionar como genéricos.

Gran descripción general de las diferencias aquí: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
fuente
2
No estoy seguro de por qué fue rechazado. Por lo que tengo entendido, tienes razón y ese enlace es muy informativo. Votado a favor.
Jason Jackson
@Jason, gracias. Hubo un error tipográfico en mi versión original, supongo que me rechazaron por eso.
JaredPar
3
Err, porque puede usar la reflexión para mirar un tipo genérico, una firma de método genérico. Simplemente no puede usar la reflexión para inspeccionar la información genérica asociada con una instancia parametrizada. Por ejemplo, si tengo una clase con un método foo (List <String>), puedo encontrar "String" de forma
reflexiva
Hay muchos artículos que dicen que el borrado de tipo hace imposible encontrar los parámetros de tipo reales. Digamos: Objeto x = nuevo X [Y] (); ¿Puedes mostrar cómo, dado x, encontrar el tipo Y?
user48956
@oxbow_lakes: tener que usar la reflexión para encontrar un tipo genérico es casi tan malo como no poder hacer eso. Por ejemplo, es una mala solución.
Josh M.
14
  1. Implementación en tiempo de ejecución (es decir, no borrado de tipo);
  2. La capacidad de usar tipos primitivos (esto está relacionado con (1));
  3. Si bien el comodín es útil, la sintaxis y saber cuándo usarlo es algo que desconcierta a mucha gente. y
  4. Sin mejora de rendimiento (debido a (1); los genéricos de Java son un azúcar sintáctico para la conversión de objetos).

(1) conduce a un comportamiento muy extraño. El mejor ejemplo en el que puedo pensar es. Asumir:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

luego declare dos variables:

MyClass<T> m1 = ...
MyClass m2 = ...

Ahora llame getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

El segundo tiene su argumento de tipo genérico eliminado por el compilador porque es un tipo sin formato (lo que significa que el tipo parametrizado no se proporciona) aunque no tiene nada que ver con el tipo parametrizado.

También mencionaré mi declaración favorita del JDK:

public class Enum<T extends Enum<T>>

Aparte del comodín (que es una mezcla), creo que los genéricos .Net son mejores.

cletus
fuente
Pero el compilador se lo dirá, particularmente con la opción lint de rawtypes.
Tom Hawtin - tackline
Lo siento, ¿por qué la última línea es un error de compilación? Estoy jugando con él en Eclipse y no puedo hacer que falle allí, si agrego suficientes cosas para que se compile el resto.
oconnor0
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 No es un fallo de compilación, sino una advertencia del compilador, sin razón aparente:The expression of type List needs unchecked conversion to conform to List<String>
artbristol
Enum<T extends Enum<T>>puede parecer extraño / redundante al principio, pero en realidad es bastante interesante, al menos dentro de las limitaciones de Java / sus genéricos. Las enumeraciones tienen un values()método estático que proporciona una matriz de sus elementos escritos como enumeración, no Enum, y ese tipo está determinado por el parámetro genérico, lo que significa que desea Enum<T>. Por supuesto, esa escritura solo tiene sentido en el contexto de un tipo enumerado, y todas las enumeraciones son subclases de Enum, por lo tanto, lo desea Enum<T extends Enum>. Sin embargo, a Java no le gusta mezclar tipos sin procesar con genéricos, Enum<T extends Enum<T>>por lo tanto, por coherencia.
JAB
10

Voy a lanzar una opinión muy controvertida. Los genéricos complican el lenguaje y complican el código. Por ejemplo, digamos que tengo un mapa que asigna una cadena a una lista de cadenas. En los viejos tiempos, podría declarar esto simplemente como

Map someMap;

Ahora, tengo que declararlo como

Map<String, List<String>> someMap;

Y cada vez que lo paso a algún método, tengo que repetir esa gran declaración de nuevo. En mi opinión, toda esa escritura extra distrae al desarrollador y lo saca de "la zona". Además, cuando el código está lleno de mucho contenido, a veces es difícil volver a él más tarde y examinar rápidamente todo el contenido para encontrar la lógica importante.

Java ya tiene una mala reputación por ser uno de los lenguajes más detallados de uso común, y los genéricos se suman a ese problema.

¿Y qué compras realmente con toda esa verbosidad extra? ¿Cuántas veces ha tenido realmente problemas en los que alguien puso un Integer en una colección que se supone que contiene cadenas, o cuando alguien intentó extraer una cadena de una colección de Integers? En mis 10 años de experiencia trabajando en la creación de aplicaciones comerciales de Java, esto nunca ha sido una gran fuente de errores. Entonces, no estoy muy seguro de lo que obtienes por la verbosidad adicional. Realmente me parece un equipaje extra burocrático.

Ahora me voy a poner realmente polémico. Lo que veo como el mayor problema con las colecciones en Java 1.4 es la necesidad de encasillar en todas partes. Veo esos encasillamientos como un cruft extra y detallado que tienen muchos de los mismos problemas que los genéricos. Entonces, por ejemplo, no puedo simplemente hacer

List someList = someMap.get("some key");

tengo que hacer

List someList = (List) someMap.get("some key");

La razón, por supuesto, es que get () devuelve un objeto que es un supertipo de List. Entonces, la asignación no se puede hacer sin un encasillado. Una vez más, piense cuánto le compra realmente esa regla. Por mi experiencia, no mucho.

Creo que Java hubiera estado mucho mejor si 1) no hubiera agregado genéricos pero 2) en su lugar hubiera permitido la conversión implícita de un supertipo a un subtipo. Deje que los lanzamientos incorrectos se detecten en tiempo de ejecución. Entonces podría haber tenido la sencillez de definir

Map someMap;

y luego haciendo

List someList = someMap.get("some key");

todo el problema se habría ido, y realmente no creo que esté introduciendo una gran fuente nueva de errores en mi código.

Clint Miller
fuente
15
Lo siento, no estoy de acuerdo con casi todo lo que dijiste. Pero no voy a rechazarlo porque lo discutas bien.
Paul Tomblin
2
Por supuesto, hay muchos ejemplos de sistemas grandes y exitosos construidos en lenguajes como Python y Ruby que hacen exactamente lo que sugerí en mi respuesta.
Clint Miller
Creo que toda mi objeción a este tipo de idea no es que sea una mala idea en sí, sino que, a medida que los lenguajes modernos como Python y Ruby hacen la vida más y más fácil para los desarrolladores, los desarrolladores a su vez se vuelven intelectualmente complacientes y eventualmente tienen una menor comprensión de su propio código.
Dan Tao
4
"¿Y qué compras realmente con toda esa verbosidad extra? ..." Le dice al programador de mantenimiento deficiente lo que se supone que hay en esas colecciones. He descubierto que esto es un problema al trabajar con aplicaciones comerciales de Java.
richj
4
"¿Cuántas veces ha tenido realmente problemas en los que alguien puso una [x] en una colección que se supone que contiene [y] s?" - ¡Ay, he perdido la cuenta! Además, incluso si no hay ningún error, es un asesino de legibilidad. Y escanear muchos archivos para averiguar cuál será el objeto (o depurar) realmente me saca de la zona. Puede que te guste Haskell, incluso la escritura fuerte pero menos cruft (porque se infieren los tipos).
Martin Capodici
8

Otro efecto secundario de que sean de tiempo de compilación y no de tiempo de ejecución es que no puede llamar al constructor del tipo genérico. Entonces no puede usarlos para implementar una fábrica genérica ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++

jdkoftinoff
fuente
6

Los genéricos de Java se comprueban para verificar su exactitud en el momento de la compilación y luego se elimina toda la información de tipo (el proceso se llama borrado de tipo . Por lo tanto, genérico List<Integer>se reducirá a su tipo sin formato , no genérico List, que puede contener objetos de clase arbitraria.

Esto da como resultado la posibilidad de insertar objetos arbitrarios en la lista en tiempo de ejecución, y ahora es imposible saber qué tipos se usaron como parámetros genéricos. Este último a su vez resulta en

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Anton Gogolev
fuente
5

Desearía que esto fuera un wiki para poder agregar a otras personas ... pero ...

Problemas:

  • Tipo de borrado (sin disponibilidad de tiempo de ejecución)
  • Sin soporte para tipos primitivos
  • Incompatibilidad con las anotaciones (ambas se agregaron en 1.5, todavía no estoy seguro de por qué las anotaciones no permiten genéricos además de acelerar las funciones)
  • Incompatibilidad con matrices. (¿A veces realmente quiero hacer algo como Class <? Extiende MyObject >[], pero no estoy permitido)
  • Comportamiento y sintaxis de comodines extraños
  • El hecho de que el soporte genérico es inconsistente entre las clases de Java. Lo agregaron a la mayoría de los métodos de recopilación, pero de vez en cuando, te encuentras con una instancia donde no está allí.
Laplie Anderson
fuente
5

Ignorando todo el lío de borrado de tipos, los genéricos como se especifica simplemente no funcionan.

Esto compila:

List<Integer> x = Collections.emptyList();

Pero este es un error de sintaxis:

foo(Collections.emptyList());

Donde foo se define como:

void foo(List<Integer> x) { /* method body not important */ }

Entonces, si un tipo de expresión verifica depende de si se está asignando a una variable local o un parámetro real de una llamada a un método. ¿Qué tan loco es eso?

Nat
fuente
3
La inferencia inconsistente de javac es una mierda.
mP.
3
Asumiría que la razón por la que se rechaza la última forma es porque puede haber más de una versión de "foo" debido a la sobrecarga del método.
Jason Creighton
2
esta crítica ya no se aplica ya que Java 8 introdujo una inferencia de tipo de objetivo mejorada
oldrinb
4

La introducción de genéricos en Java fue una tarea difícil porque los arquitectos estaban tratando de equilibrar la funcionalidad, la facilidad de uso y la compatibilidad con versiones anteriores del código heredado. Como era de esperar, hubo que hacer concesiones.

Hay quienes también sienten que la implementación de genéricos en Java aumentó la complejidad del lenguaje a un nivel inaceptable (ver " Genéricos considerados perjudiciales " de Ken Arnold ). Las preguntas frecuentes sobre genéricos de Angelika Langer dan una idea bastante clara de lo complicadas que pueden volverse las cosas.

Zach Scrivena
fuente
3

Java no aplica los genéricos en tiempo de ejecución, solo en tiempo de compilación.

Esto significa que puede hacer cosas interesantes como agregar los tipos incorrectos a las colecciones genéricas.

Señor de poder
fuente
1
El compilador le dirá que ha cometido un error si lo logra.
Tom Hawtin - tackline
@Tom, no necesariamente. Hay formas de engañar al compilador.
Paul Tomblin
2
¿No escuchas lo que te dice el compilador ?? (ver mi primer comentario)
Tom Hawtin - tackline
1
@Tom, si tiene una ArrayList <String> y la pasa a un método antiguo (digamos, código heredado o de terceros) que toma una Lista simple, ese método puede agregar lo que quiera a esa Lista. Si se aplica en tiempo de ejecución, obtendrá una excepción.
Paul Tomblin
1
static void addToList (lista de lista) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (lista); Eso se compila e incluso funciona en tiempo de ejecución. La única vez que ve un problema es cuando elimina de la lista esperando una cadena y obtiene un int.
Laplie Anderson
3

Los genéricos de Java son solo en tiempo de compilación y se compilan en código no genérico. En C #, el MSIL compilado real es genérico. Esto tiene enormes implicaciones para el rendimiento porque Java todavía se lanza durante el tiempo de ejecución. Vea aquí para más .

Szymon Rozga
fuente
2

Si escuchas Java Posse # 279 - Entrevista con Joe Darcy y Alex Buckley , hablan sobre este tema. Eso también se vincula a una publicación de blog de Neal Gafter titulada Reified Generics for Java que dice:

Mucha gente no está satisfecha con las restricciones causadas por la forma en que se implementan los genéricos en Java. Específicamente, no están contentos de que los parámetros de tipo genérico no estén reificados: no están disponibles en tiempo de ejecución. Los genéricos se implementan mediante el borrado, en el que los parámetros de tipo genérico simplemente se eliminan en tiempo de ejecución.

Esa publicación de blog hace referencia a una entrada anterior, Puzzling Through Erasure: sección de respuestas , que enfatizó el punto sobre la compatibilidad de la migración en los requisitos.

El objetivo era proporcionar compatibilidad con versiones anteriores del código fuente y objeto, y también compatibilidad con la migración.

Kevin Hakanson
fuente