¿Qué hay de malo con los genéricos de Java? [cerrado]

49

He visto varias veces en este sitio publicaciones que denuncian la implementación de genéricos en Java. Ahora, puedo decir honestamente que no he tenido ningún problema con su uso. Sin embargo, no he intentado hacer una clase genérica yo mismo. Entonces, ¿cuáles son sus problemas con el soporte genérico de Java?

Michael K
fuente
1
Ver también stackoverflow.com/questions/31693/…
Bill Michell
como Clinton Begin (el "tipo iBatis"), dijo, "no lo hacen el trabajo ..."
mosquito

Respuestas:

54

La implementación genérica de Java utiliza borrado de tipo . Esto significa que sus colecciones genéricas fuertemente tipadas son en realidad de tipo Objecten tiempo de ejecución. Esto tiene algunas consideraciones de rendimiento, ya que significa que los tipos primitivos deben encuadrarse cuando se agregan a una colección genérica. Por supuesto, los beneficios de la corrección de tipo de tiempo de compilación superan la tontería general de borrado de tipo y el enfoque obsesivo en la compatibilidad con versiones anteriores.

ChaosPandion
fuente
44
Solo para agregar, debido a la TypeLiteral eliminación
Jeremy Heiler
43
Lo que significa quenew ArrayList<String>.getClass() == new ArrayList<Integer>.getClass()
Nota personal - piense en un nombre
99
@ Job no, no lo hace. Y C ++ utiliza un enfoque completamente diferente a la metaprogramación llamado plantillas. Utiliza la escritura de pato y puede hacer cosas útiles como, template<T> T add(T v1, T v2) { return v1->add(v2); }y allí tiene una forma verdaderamente genérica de crear una función que addes dos cosas, sin importar cuáles sean esas cosas, solo tienen que tener un método nombrado add()con un parámetro.
Trinidad
77
@ Job es un código generado, de hecho. Las plantillas están destinadas a reemplazar el preprocesador C y, para hacerlo, deberían ser capaces de hacer algunos trucos muy inteligentes y, por sí mismas, son un lenguaje completo de Turing. Los genéricos de Java son solo azúcar para contenedores de tipo seguro, y los genéricos de C # son mejores pero siguen siendo la plantilla de C ++ de un hombre pobre.
Trinidad
3
@Job AFAIK, los genéricos de Java no generan una clase para cada nuevo tipo genérico, solo agrega tipos de letra al código que usa métodos genéricos, por lo que en realidad no es metaprogramación IMO. Las plantillas de C # generan un nuevo código para cada tipo genérico, es decir, si usa List <int> y List <double> en el mundo de C # generaría código para cada uno de ellos. En comparación con las plantillas, los genéricos de C # aún requieren saber a qué tipo puede alimentarlo. No puede implementar el Addejemplo simple que di, no hay manera sin saber de antemano a qué clases se puede aplicar, lo cual es un inconveniente.
Trinidad
26

Observe que las respuestas que ya se han proporcionado se concentran en la combinación de Java-the-language, JVM y Java Class Library.

No hay nada malo con los genéricos de Java en lo que respecta al lenguaje Java. Como se describe en C # frente a los genéricos de Java , los genéricos de Java están bastante bien en el nivel de lenguaje 1 .

Lo subóptimo es que la JVM no admite genéricos directamente, lo que tiene varias consecuencias importantes:

  • las partes relacionadas con la reflexión de la Biblioteca de clases no pueden exponer la información de tipo completo que estaba disponible en Java-the-language
  • implica alguna penalización de rendimiento

Supongo que la distinción que hago es pedante, ya que Java-the-language se compila ubicuamente con JVM como destino y Java Class Library como biblioteca principal.

1 Con la posible excepción de los comodines, que se cree que hacen que la inferencia de tipo sea indecidible en el caso general. Esta es una diferencia importante entre C # y los genéricos de Java que no se menciona con mucha frecuencia. Gracias antimonio.

Roman Starkov
fuente
14
La JVM no necesita admitir genéricos. Esto sería lo mismo que decir que C ++ no tiene plantillas porque la máquina real ix86 no admite plantillas, lo cual es falso. El problema es el enfoque que toma el compilador para implementar tipos genéricos. Y de nuevo, las plantillas de C ++ no implican penalización en el tiempo de ejecución, no se realiza ninguna reflexión en absoluto, supongo que la única razón por la que sería necesario hacerlo en Java es simplemente un diseño de lenguaje deficiente.
Trinidad
2
@Trinidad pero no dije que Java no tiene genéricos, así que no veo cómo es lo mismo. Y sí, la JVM no necesita admitirlos, al igual que C ++ no necesita optimizar el código. Pero no optimizarlo ciertamente contaría como "algo mal".
Roman Starkov
3
Lo que sostuve es que no es necesario que la JVM tenga soporte directo para los genéricos para poder usar la reflexión sobre ellos. Otros lo han hecho para que se pueda hacer, el problema es que Java no genera nuevas clases a partir de genéricos, lo que no requiere reificación.
Trinidad
44
@Trinidad: en C ++, suponiendo que el compilador tenga suficiente tiempo y memoria, las plantillas se pueden usar para hacer cualquier cosa y todo lo que se puede hacer con la información que está disponible en el momento de la compilación (ya que la compilación de plantillas es completa de Turing), pero hay no hay forma de crear plantillas utilizando información que no está disponible antes de ejecutar el programa. En .net, por el contrario, es posible que un programa cree tipos basados ​​en la entrada; sería bastante fácil escribir un programa en el que la cantidad de tipos diferentes que podrían crearse exceda la cantidad de electrones en el universo.
supercat
2
@Trinidad: Obviamente, ninguna ejecución única del programa podría crear todos esos tipos (o, de hecho, algo más allá de una fracción infinitesimal de ellos), pero el punto es que no tiene que hacerlo. Solo necesita crear tipos que realmente se usan. Uno podría tener un programa que aceptara cualquier número arbitrario (por ejemplo 8675309) y crear un tipo único para ese número (por ejemplo Z8<Z6<Z7<Z5<Z3<Z0<Z9>>>>>>>), que tendría miembros diferentes de cualquier otro tipo. En C ++, todos los tipos que podrían generarse a partir de cualquier entrada tendrían que producirse en tiempo de compilación.
supercat
13

La crítica habitual es la falta de reificación. Es decir, los objetos en tiempo de ejecución no contienen información sobre sus argumentos genéricos (aunque la información todavía está presente en campos, métodos, constructores y clases e interfaces extendidas). Esto significa que podría lanzar, digamos, ArrayList<String>a List<File>. El compilador le dará advertencias, pero también le avisará si toma una ArrayList<String>asignación y la envía a una Objectreferencia List<String>. Lo bueno es que con los genéricos probablemente no deberías estar haciendo el casting, el rendimiento es mejor sin los datos innecesarios y, por supuesto, la compatibilidad con versiones anteriores.

Algunas personas se quejan de que no puede sobrecargarse sobre la base de argumentos genéricos ( void fn(Set<String>)y void fn(Set<File>)). Necesita usar mejores nombres de métodos en su lugar. Tenga en cuenta que esta sobrecarga no requeriría reificación, ya que la sobrecarga es un problema estático en tiempo de compilación.

Los tipos primitivos no funcionan con los genéricos.

Los comodines y los límites son bastante complicados. Son muy útiles Si Java prefería las interfaces de inmutabilidad y tell-don't-ask, entonces los genéricos del lado de la declaración en lugar del lado del uso serían más apropiados.

Tom Hawtin - tackline
fuente
"Tenga en cuenta que esta sobrecarga no requeriría reificación, ya que la sobrecarga es un problema estático en tiempo de compilación". ¿No es así? ¿Cómo funcionaría sin reificación? ¿No tendría que saber la JVM en tiempo de ejecución qué tipo de conjunto tiene para saber a qué método llamar?
MatrixFrog
2
@MatrixFrog No. Como digo, la sobrecarga es un problema en tiempo de compilación. El compilador usa el tipo estático de la expresión para seleccionar una sobrecarga particular, que es el método escrito en el archivo de clase. Si lo tienes, Object cs = new char[] { 'H', 'i' }; System.out.println(cs);te imprimen tonterías. Cambie el tipo de csa char[]y obtendrá Hi.
Tom Hawtin - tackline
10

Los genéricos de Java apestan porque no puedes hacer lo siguiente:

public class AsyncAdapter<Parser,Adapter> extends AsyncTask<String,Integer,Adapter> {
    proptected Adapter doInBackground(String... keywords) {
      Parser p = new Parser(keywords[0]); // this is an error
      /* some more stuff I was hoping for but couldn't do because
         the compiler wouldn't let me
      */
    }
}

No necesitaría cortar todas las clases en el código anterior para que funcione como debería si los genéricos fueran en realidad parámetros genéricos de la clase.

davidk01
fuente
No necesitas matar todas las clases. Solo necesita agregar una clase de fábrica adecuada e inyectarla en AsyncAdapter. (Y fundamentalmente no se trata de genéricos, sino del hecho de que los constructores no se heredan en Java).
Peter Taylor
13
@PeterTaylor: una clase de fábrica adecuada simplemente empuja toda la placa repetitiva a otro desastre fuera de la vista y todavía no resuelve el problema. Ahora, cada vez que quiera crear una instancia de una nueva instancia Parser, tendré que hacer que el código de fábrica sea aún más complicado. Mientras que "genérico" realmente significa genérico, entonces no habría necesidad de fábricas y todas las demás tonterías que se conocen con el nombre de patrones de diseño. Es una de las muchas razones por las que prefiero trabajar en lenguajes dinámicos.
davidk01
2
A veces es similar en C ++, donde no puedes pasar la dirección de un constructor, cuando usas plantillas + virtuales: puedes usar un functor de fábrica en su lugar.
Mark K Cowan
¿Java apesta porque no puedes escribir "protegido" antes del nombre de un método? Pobrecito. Apégate a los lenguajes dinámicos. Y tenga especial cuidado con Haskell.
fdreger
5
  • las advertencias del compilador para los parámetros genéricos faltantes que no sirven para nada hacen que el lenguaje sea inútilmente detallado, por ejemplo public String getName(Class<?> klazz){ return klazz.getName();}

  • Los genéricos no juegan bien con las matrices

  • La información de tipo perdido hace que la reflexión sea un desastre de fundición y cinta adhesiva.

Steve B.
fuente
Me molesta cuando recibo una advertencia por usar en HashMaplugar de HashMap<String, String>.
Michael K
1
@Michael, pero eso en realidad debería usarse con genéricos ...
alternativa el
El problema de la matriz va a la reiabilidad. Consulte el libro de Java Generics (Alligator) para obtener una explicación.
ncmathsadist
5

Creo que otras respuestas han dicho esto hasta cierto punto, pero no muy claramente. Uno de los problemas con los genéricos es perder los tipos genéricos durante el proceso de reflexión. Así por ejemplo:

List<String> arr = new ArrayList<String>();
assertTrue( ArrayList.class, arr.getClass() );
TypeVarible[] types = arr.getClass().getTypedVariables();

Desafortunadamente, los tipos devueltos no pueden decirle que los tipos genéricos de arr son String. Es una diferencia sutil, pero es importante. Debido a que arr se crea en tiempo de ejecución, los tipos genéricos se borran en tiempo de ejecución, por lo que no puede resolverlo. Como algunos dijeron, se ArrayList<Integer>ve igual que ArrayList<String>desde el punto de vista de la reflexión.

Es posible que esto no le importe al usuario de Generics, pero supongamos que queríamos crear un marco sofisticado que utilizara la reflexión para descubrir cosas elegantes sobre cómo el usuario declaró los tipos genéricos concretos de una instancia.

Factory<MySpecialObject> factory = new Factory<MySpecialObject>();
MySpecialObject obj = factory.create();

Digamos que queríamos una fábrica genérica para crear una instancia MySpecialObjectporque ese es el tipo genérico concreto que declaramos para esta instancia. Bueno, la clase Factory no puede interrogarse para averiguar el tipo concreto declarado para esta instancia porque Java los borró.

En los genéricos de .Net puede hacer esto porque en tiempo de ejecución el objeto sabe que son tipos genéricos porque el compilador lo cumplió en el binario. Con borrados, Java no puede hacer esto.

chubbsondubs
fuente
1

Podría decir algunas cosas buenas sobre los genéricos, pero esa no era la cuestión. Podría quejarme de que no están disponibles en tiempo de ejecución y no funcionan con matrices, pero eso se ha mencionado.

Una gran molestia psicológica: ocasionalmente me meto en situaciones en las que no se puede hacer que los genéricos funcionen. (Las matrices son el ejemplo más simple). Y no puedo entender si los genéricos no pueden hacer el trabajo o si soy simplemente estúpido. Odio eso. Algo como los genéricos debería funcionar siempre. Cada dos veces que no puedo hacer lo que quiero usando Java-the-language, sé que el problema soy yo y sé que si sigo presionando, llegaré allí eventualmente. Con los genéricos, si me vuelvo demasiado persistente, puedo perder mucho tiempo.

Pero el verdadero problema es que los genéricos agregan demasiada complejidad por muy poca ganancia. En los casos más simples, puede evitar que agregue una manzana a una lista que contiene autos. Multa. Pero sin genéricos, este error arrojaría una ClassCastException realmente rápida en tiempo de ejecución con poco tiempo perdido. Si agrego un automóvil con un asiento para niños con un niño dentro, ¿necesito una advertencia en tiempo de compilación de que la lista es solo para automóviles con asientos para niños que contienen chimpancés bebés? Una lista de instancias de objetos simples comienza a parecer una buena idea.

El código genérico puede tener muchas palabras y caracteres y ocupar mucho espacio extra y tomar mucho más tiempo para leer. Puedo pasar mucho tiempo haciendo que todo ese código extra funcione correctamente. Cuando lo hace, me siento increíblemente inteligente. También he perdido varias horas o más de mi propio tiempo y tengo que preguntarme si alguna vez alguien más podrá descifrar el código. Me gustaría poder entregarlo para mantenimiento a personas que son menos inteligentes que yo o que tienen menos tiempo que perder.

Por otro lado (me sentí un poco mal y siento la necesidad de proporcionar algo de equilibrio), es bueno cuando se usan colecciones y mapas simples para agregar, poner y verificar cuando se escribe, y generalmente no agrega mucho complejidad del código ( si alguien más escribe la colección o el mapa) . Y Java es mejor en esto que C #. Parece que ninguna de las colecciones de C # que quiero usar maneja genéricos. (Admito que tengo gustos extraños en las colecciones).

RalphChapin
fuente
Bonita diatriba. Sin embargo, hay muchas bibliotecas / marcos que podrían existir sin genéricos, por ejemplo, Guice, Gson, Hibernate. Y los genéricos no son tan difíciles una vez que te acostumbras. Escribir y leer los argumentos de tipo es una verdadera PITA; aquí val ayuda si puedes usarlo.
maaartinus
1
@maaartinus: lo escribí hace un tiempo. También OP pidió "problemas". Encuentro que los genéricos son extremadamente útiles y me gustan mucho, mucho más ahora que entonces. Sin embargo, si está escribiendo su propia colección, son muy difíciles de aprender. Y su utilidad se desvanece cuando el tipo de su colección se determina en tiempo de ejecución, cuando la colección tiene un método que le indica la clase de sus entradas. En este punto, los genéricos no funcionan y su IDE producirá cientos de advertencias sin sentido. El 99.99% de la programación Java no involucra esto. Pero me gustaría simplemente tenía un montón de problemas con esto cuando escribí lo anterior.
RalphChapin
Siendo desarrollador de Java y C #, me pregunto por qué preferiría las colecciones de Java?
Ivaylo Slavov
Fine. But without generics this error would throw a ClassCastException really quick at run time with little time wasted.Eso realmente depende de cuánto tiempo de ejecución se produzca entre el inicio del programa y la línea de código en cuestión. Si un usuario informa el error y le toma varios minutos (o, en el peor de los casos, horas o incluso días) reproducir, esa verificación en tiempo de compilación comienza a verse cada vez mejor ...
Mason Wheeler