¿Por qué debería importarme que Java no tenga genéricos reificados?

102

Esto surgió como una pregunta que hice en una entrevista recientemente como algo que el candidato deseaba que se agregara al lenguaje Java. Es comúnmente identificado como una molestia que Java no tenga genéricos reificados pero, cuando se le presionó, el candidato no pudo decirme el tipo de cosas que podría haber logrado si estuvieran allí.

Obviamente, debido a que los tipos sin procesar están permitidos en Java (y las comprobaciones inseguras), es posible subvertir los genéricos y terminar con un List<Integer>que (por ejemplo) realmente contiene Strings. Esto claramente podría volverse imposible si la información tipográfica se reificara; ¡Pero debe haber más que esto !

¿Podrían las personas publicar ejemplos de cosas que realmente querrían hacer si estuvieran disponibles los genéricos reificados? Quiero decir, obviamente podrías obtener el tipo de Listen tiempo de ejecución, pero ¿qué harías con él?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDITAR : Una actualización rápida de esto, ya que las respuestas parecen estar principalmente preocupadas por la necesidad de pasar a Classcomo parámetro (por ejemplo EnumSet.noneOf(TimeUnit.class)). Estaba buscando más algo parecido a donde esto simplemente no es posible . Por ejemplo:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?
oxbow_lakes
fuente
¿Significaría esto que podría obtener la clase de un tipo genérico en tiempo de ejecución? (si es así, ¡tengo un ejemplo!)
James B
Creo que la mayor parte del deseo con genéricos confiables proviene de personas que usan genéricos principalmente con colecciones y quieren que esas colecciones se comporten más como matrices.
kdgregory
2
La pregunta más interesante (para mí): ¿qué se necesitaría para implementar genéricos de estilo C ++ en Java? Ciertamente parece factible en el tiempo de ejecución, pero rompería todos los cargadores de clases existentes (porque findClass()tendría que ignorar la parametrización, pero defineClass()no podría). Y como sabemos, The Powers That Be mantienen la compatibilidad con versiones anteriores de manera primordial.
kdgregory
1
De hecho, Java proporciona genéricos reificados de una manera muy restringida . Proporciono más detalles en este hilo SO: stackoverflow.com/questions/879855/…
Richard Gomes
El JavaOne Keynote indica que Java 9 admitirá la reificación.
Rangi Keen

Respuestas:

81

De las pocas veces que me encontré con esta "necesidad", finalmente se reduce a esta construcción:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Esto funciona en C # asumiendo que Ttiene un constructor predeterminado . Incluso puede obtener el tipo de tiempo de ejecución typeof(T)y obtener los constructores Type.GetConstructor().

La solución común de Java sería pasar el Class<T>argumento as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(no es necesario pasarlo como argumento de constructor, ya que un argumento de método también está bien, lo anterior es solo un ejemplo, también try-catchse omite por brevedad)

Para todas las demás construcciones de tipo genérico, el tipo real se puede resolver fácilmente con un poco de ayuda de reflexión. Las siguientes preguntas y respuestas ilustran los casos de uso y las posibilidades:

BalusC
fuente
5
¿Esto funciona (por ejemplo) en C #? ¿Cómo sabe que T tiene un constructor predeterminado?
Thomas Jung
4
Si lo hace. Y este es solo un ejemplo básico (supongamos que trabajamos con JavaBeans). El punto es que con Java no puede obtener la clase durante el tiempo de ejecución por T.classo T.getClass(), por lo que podría acceder a todos sus campos, constructores y métodos. Hace que la construcción también sea imposible.
BalusC
3
Esto me parece realmente débil como el "gran problema", particularmente porque solo es probable que sea útil junto con una reflexión muy frágil sobre constructores / parámetros, etc.
oxbow_lakes
33
Se compila en C # siempre que declare el tipo como: public class Foo <T> donde T: new (). Lo que limitará los tipos válidos de T a aquellos que contienen un constructor sin parámetros.
Martin Harris
3
@Colin De hecho, puede decirle a java que se requiere un tipo genérico determinado para extender más de una clase o interfaz. Consulte la sección "Varios límites" de docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Por supuesto, no se puede decir que el objeto será uno de un conjunto específico que no comparte métodos que podrían abstraerse en una interfaz, que podría implementarse de alguna manera en C ++ usando la especialización de plantillas).
JAB
99

Lo que más me muerde es la incapacidad de aprovechar el envío múltiple en múltiples tipos genéricos. Lo siguiente no es posible y hay muchos casos en los que sería la mejor solución:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }
RHSeeger
fuente
5
No hay absolutamente ninguna necesidad de reificación para poder hacer eso. La selección del método se realiza en tiempo de compilación cuando la información de tipo en tiempo de compilación está disponible.
Tom Hawtin - tackline
26
@Tom: esto ni siquiera se compila debido al borrado de tipo. Ambos se compilan como public void my_method(List input) {}. Sin embargo, nunca me he encontrado con esta necesidad, simplemente porque no tendrían el mismo nombre. Si tienen el mismo nombre, me preguntaría si public <T extends Object> void my_method(List<T> input) {}no es una mejor idea.
BalusC
1
Hm, tendería a evitar la sobrecarga con un número idéntico de parámetros por completo, y preferiría algo como myStringsMethod(List<String> input)e myIntegersMethod(List<Integer> input)incluso si la sobrecarga para tal caso fuera posible en Java.
Fabian Steeg
4
@Fabian: Lo que significa que debes tener un código separado y evitar el tipo de ventajas que obtienes <algorithm>en C ++.
David Thornley
Estoy confundido, esto es esencialmente lo mismo que mi segundo punto, pero ¿obtuve 2 votos en contra? ¿Alguien quiere iluminarme?
rsp
34

Me viene a la mente la seguridad de tipos . Downcasting a un tipo parametrizado siempre será inseguro sin genéricos reificados:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Además, las abstracciones se filtrarían menos , al menos las que pueden estar interesadas en información de tiempo de ejecución sobre sus parámetros de tipo. Hoy en día, si necesita algún tipo de información de tiempo de ejecución sobre el tipo de uno de los parámetros genéricos, también debe transmitirlo Class. De esa manera, su interfaz externa depende de su implementación (ya sea que use RTTI sobre sus parámetros o no).

gustafc
fuente
1
Sí, tengo una forma de evitar esto ya que creo un ParametrizedListque copia los datos en los tipos de verificación de la colección de origen. Es un poco parecido, Collections.checkedListpero se puede sembrar con una colección para empezar.
oxbow_lakes
@tackline - bueno, algunas abstracciones filtrarían menos. Si necesita acceso para escribir metadatos en su implementación, la interfaz externa se lo informará porque los clientes deben enviarle un objeto de clase.
gustafc
... lo que significa que con los genéricos reificados, puede agregar cosas como T.class.getAnnotation(MyAnnotation.class)(donde Testá un tipo genérico) sin cambiar la interfaz externa.
gustafc
@gustafc: si cree que las plantillas de C ++ le brindan seguridad de tipos completa, lea esto: kdgregory.com/index.php?page=java.generics.cpp
kdgregory
@kdgregory: Nunca dije que C ++ fuera 100% seguro para los tipos, solo que el borrado daña la seguridad del tipo. Como usted mismo dice, "resulta que C ++ tiene su propia forma de borrado de tipos, conocida como conversión de punteros al estilo C". Pero Java solo realiza conversiones dinámicas (no reinterpreta), por lo que la reificación conectaría todo esto en el sistema de tipos.
gustafc
26

Podrías crear matrices genéricas en tu código.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}
Turnor
fuente
¿Qué pasa con Object? Una matriz de objetos es una matriz de referencias de todos modos. No es que los datos del objeto estén almacenados en la pila, todo está en el montón.
Ran Biron
19
Escriba seguridad. Puedo poner lo que quiera en Object [], pero solo Strings en String [].
Turnor
3
Ran: Sin ser sarcástico: es posible que le guste usar un lenguaje de secuencias de comandos en lugar de Java, ¡entonces tiene la flexibilidad de las variables sin tipo en cualquier lugar!
oveja voladora
2
Las matrices son covariantes (y, por lo tanto, no son seguras en tipos) en ambos idiomas. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Se compila bien en ambos idiomas. Funciona notsofine.
Martijn
16

Esta es una pregunta antigua, hay un montón de respuestas, pero creo que las respuestas existentes están fuera de lugar.

"reificado" solo significa real y generalmente solo significa lo opuesto al borrado de tipo.

El gran problema relacionado con Java Generics:

  • Este horrible requisito de boxeo y desconexión entre los tipos primitivos y de referencia. Esto no está directamente relacionado con la reificación o el borrado de tipos. C # / Scala corrige esto.
  • Sin tipos propios. JavaFX 8 tuvo que eliminar "constructores" por este motivo. Absolutamente nada que ver con el borrado de tipos. Scala corrige esto, no estoy seguro de C #.
  • Sin variación de tipo de lado de declaración. C # 4.0 / Scala tiene esto. Absolutamente nada que ver con el borrado de tipos.
  • No se puede sobrecargar void method(List<A> l)y method(List<B> l). Esto se debe al borrado de tipos, pero es extremadamente mezquino.
  • No se admite la reflexión del tipo de tiempo de ejecución. Este es el corazón del borrado de tipos. Si te gustan los compiladores súper avanzados que verifican y prueban la mayor parte de la lógica de tu programa en tiempo de compilación, debes usar la reflexión lo menos posible y este tipo de borrado de tipo no debería molestarte. Si le gusta la programación de tipo más irregular, scripty, dinámica y no le importa tanto que un compilador demuestre que la mayor parte de su lógica es correcta, entonces desea una mejor reflexión y corregir el borrado de tipo es importante.
usuario2684301
fuente
1
Sobre todo me resulta difícil con los casos de serialización. A menudo le gustaría poder olfatear los tipos de clase de cosas genéricas que se serializan, pero se detiene en seco debido al borrado de tipos. Hace que sea difícil hacer algo como estodeserialize(thingy, List<Integer>.class)
Cogman
3
Creo que esta es la mejor respuesta. Especialmente las partes que describen qué problemas se deben fundamentalmente al borrado de tipos y qué son solo problemas de diseño del lenguaje Java. Lo primero que le digo a la gente que empieza a quejarse es que Scala y Haskell también están trabajando con borrado de tipos.
aemxdp
13

La serialización sería más sencilla con la cosificación. Lo que querríamos es

deserialize(thingy, List<Integer>.class);

Lo que tenemos que hacer es

deserialize(thing, new TypeReference<List<Integer>>(){});

se ve feo y funciona de manera extraña.

También hay casos en los que sería muy útil decir algo como

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Estas cosas no pican a menudo, pero sí pican cuando ocurren.

Cogman
fuente
Esta. Mil veces esto. Cada vez que intento escribir código de deserialización en Java, paso el día lamentándome de que no estoy trabajando en otra cosa
Básico
11

Mi exposición a Java Geneircs es bastante limitada, y aparte de los puntos que otras respuestas ya han mencionado, hay un escenario explicado en el libro Java Generics and Collections , de Maurice Naftalin y Philip Walder, donde los genéricos reificados son útiles.

Dado que los tipos no son confiables, no es posible tener excepciones parametrizadas.

Por ejemplo, la declaración del formulario siguiente no es válida.

class ParametericException<T> extends Exception // compile error

Esto se debe a que la cláusula catch comprueba si la excepción lanzada coincide con un tipo determinado. Esta verificación es la misma que la verificación realizada por la prueba de instancia y dado que el tipo no es confiable, la forma de declaración anterior no es válida.

Si el código anterior fuera válido, entonces habría sido posible el manejo de excepciones de la siguiente manera:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

El libro también menciona que si los genéricos de Java se definen de manera similar a la forma en que se definen las plantillas de C ++ (expansión), puede conducir a una implementación más eficiente ya que esto ofrece más oportunidades de optimización. Pero no ofrece ninguna explicación más que esta, por lo que cualquier explicación (consejos) de la gente conocedora sería útil.

sateesh
fuente
1
Es un punto válido, pero no estoy muy seguro de por qué una clase de excepción tan parametrizada sería útil. ¿Podría modificar su respuesta para que contenga un breve ejemplo de cuándo podría ser útil?
oxbow_lakes
@oxbow_lakes: Lo siento Mi conocimiento de Java Generics es bastante limitado y estoy intentando mejorarlo. Así que ahora no puedo pensar en ningún ejemplo en el que la excepción parametrizada pueda ser útil. Intentaré pensar en ello. Gracias.
sábado
Podría actuar como un sustituto de la herencia múltiple de tipos de excepción.
meriton
La mejora del rendimiento es que actualmente, un parámetro de tipo debe heredar de Object, lo que requiere el encajonamiento de tipos primitivos, lo que impone una sobrecarga de ejecución y memoria.
meriton
8

Las matrices probablemente funcionarían mucho mejor con los genéricos si estuvieran cosificados.

Hank Gay
fuente
2
Seguro, pero aún tendrían problemas. List<String>no es un List<Object>.
Tom Hawtin - tackline
De acuerdo, pero solo para primitivas (Integer, Long, etc.). Para el objeto "normal", esto es lo mismo. Dado que los primitivos no pueden ser un tipo parametrizado (un problema mucho más serio, al menos en mi humilde opinión), no veo esto como un verdadero dolor.
Ran Biron
3
El problema con las matrices es su covarianza, nada que ver con la reificación.
Recurse
5

Tengo un contenedor que presenta un conjunto de resultados jdbc como un iterador (significa que puedo realizar pruebas unitarias de operaciones originadas en la base de datos mucho más fácilmente mediante la inyección de dependencias).

La API se parece a Iterator<T> donde T es un tipo que se puede construir usando solo cadenas en el constructor. El iterador luego mira las cadenas que se devuelven de la consulta sql y luego intenta hacer coincidirlas con un constructor de tipo T.

En la forma actual en que se implementan los genéricos, también tengo que pasar la clase de los objetos que crearé a partir de mi conjunto de resultados. Si entiendo correctamente, si los genéricos fueran reificados, podría simplemente llamar a T.getClass () para obtener sus constructores, y luego no tener que emitir el resultado de Class.newInstance (), que sería mucho más ordenado.

Básicamente, creo que facilita la escritura de API (en lugar de simplemente escribir una aplicación), porque puede inferir mucho más de los objetos y, por lo tanto, será necesaria menos configuración ... No aprecié las implicaciones de las anotaciones hasta que los vi siendo usados ​​en cosas como spring o xstream en lugar de montones de config.

James B
fuente
Pero pasar en la clase me parece más seguro en todos los sentidos. En cualquier caso, la creación reflexiva de instancias a partir de consultas de bases de datos es extremadamente frágil ante cambios como la refactorización de todos modos (tanto en su código como en la base de datos). Supongo que estaba buscando cosas en las que simplemente no es posible proporcionar la clase
oxbow_lakes
5

Una cosa buena sería evitar el boxing para tipos primitivos (de valor). Esto está relacionado de alguna manera con la queja de la matriz que otros han planteado, y en los casos en que el uso de la memoria está restringido, en realidad podría marcar una diferencia significativa.

También hay varios tipos de problemas al escribir un marco donde es importante poder reflexionar sobre el tipo parametrizado. Por supuesto, esto se puede solucionar pasando un objeto de clase en tiempo de ejecución, pero esto oscurece la API y coloca una carga adicional sobre el usuario del marco.

kvb
fuente
2
¡Esto esencialmente se reduce a poder hacer new T[]donde T es de tipo primitivo!
oxbow_lakes
2

No es que consigas algo extraordinario. Será más sencillo de entender. El borrado de tipos parece un momento difícil para los principiantes y, en última instancia, requiere la comprensión de la forma en que funciona el compilador.

Mi opinión es que los genéricos son simplemente un extra que ahorra mucho casting redundante.

Bozho
fuente
Esto es ciertamente lo que son los genéricos en Java . En C # y otros lenguajes son una herramienta poderosa
Básico
1

Algo que todas las respuestas aquí han pasado por alto y que es constantemente un dolor de cabeza para mí es que, dado que los tipos se borran, no se puede heredar una interfaz genérica dos veces. Esto puede ser un problema cuando desea crear interfaces de grano fino.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!
ExCodeVaquero
fuente
0

Aquí hay uno que me atrapó hoy: sin cosificación, si escribe un método que acepta una lista varargs de elementos genéricos ... las personas que llaman pueden PENSAR que son seguras, pero accidentalmente pasan cualquier basura vieja y explotan su método.

¿Parece poco probable que eso suceda? ... Claro, hasta que ... use Class como su tipo de datos. En este punto, la persona que llama le enviará con mucho gusto muchos objetos de clase, pero un simple error tipográfico le enviará objetos de clase que no se adhieren a T y se producirá un desastre.

(NB: Puede que haya cometido un error aquí, pero al buscar en Google "varargs genéricos", lo anterior parece ser justo lo que cabría esperar. Lo que hace que esto sea un problema práctico es el uso de Class, creo: las personas que llaman parecen tener menos cuidado :()


Por ejemplo, estoy usando un paradigma que usa objetos Class como clave en mapas (es más complejo que un mapa simple, pero conceptualmente eso es lo que está sucediendo).

por ejemplo, esto funciona muy bien en Java Generics (ejemplo trivial):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

por ejemplo, sin reificación en Java Generics, este acepta CUALQUIER objeto "Class". Y es solo una pequeña extensión del código anterior:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Los métodos anteriores deben escribirse miles de veces en un proyecto individual, por lo que la posibilidad de error humano aumenta. Depurar errores está demostrando que "no es divertido". Actualmente estoy tratando de encontrar una alternativa, pero no tengo muchas esperanzas.

Adán
fuente