programacion - metodos genericos java
Métodos genéricos de Java en clases de genéricos (6)
''para la compatibilidad con versiones anteriores'' parece una razón suficiente para borrar tipos de tipos genéricos de clase; es necesario, por ejemplo, para permitirle devolver una Lista sin tipo y pasarla a algún código heredado. La extensión de esto a métodos genéricos parece una subventaja complicada.
El fragmento de JLS de 4.8 (que cita) cubre constructores, métodos de instancia y campos de miembros: los métodos genéricos son solo un caso particular de los métodos de instancia en general. Por lo tanto, parece que su caso está cubierto por este fragmento.
Adaptación de JLS 4.8 a este caso específico:
El tipo de método genérico es el tipo sin procesar que corresponde al borrado de su tipo en la declaración genérica correspondiente a C.
(aquí el ''tipo'' del método incluiría todos los tipos de parámetros y de retorno). Si interpreta "borrado" como "borrado de todos los genéricos", parece que coincide con el comportamiento observado, aunque no es muy intuitivo o útil. Casi parece una consistencia demasiado entusiasta, borrar todos los genéricos, en lugar de solo los parámetros genéricos de la clase (aunque ¿quién soy? ¿Adivino a los diseñadores?).
Quizás haya problemas donde los parámetros genéricos de clase interactúen con los parámetros genéricos del método; en su código son completamente independientes, pero podría imaginarse otros casos en los que se asignan / mezclan. Creo que vale la pena señalar que no se recomienda el uso de tipos crudos, según el JLS:
El uso de tipos crudos solo se permite como concesión a la compatibilidad del código heredado. Se desaconseja encarecidamente el uso de tipos crudos en el código escrito después de la introducción de genericidad en el lenguaje de programación Java. Es posible que las versiones futuras del lenguaje de programación Java no permitan el uso de tipos crudos
Parte del pensamiento de los desarrolladores de Java es evidente aquí:
http://bugs.sun.com/view_bug.do?bug_id=6400189
(error + solución que muestra que el tipo de devolución de un método se trata como parte del tipo de método para los fines de este tipo de borrado)
También existe esta solicitud , en la que alguien parece solicitar el comportamiento que usted describe, solo borra los parámetros genéricos de la clase, no otros genéricos, pero fue rechazado con este razonamiento:
La solicitud es modificar el borrado de tipo para que en la declaración de tipo
Foo<T>
, el borrado solo elimineT
de los tipos parametrizados. Entonces, sucede que dentro de la declaración deMap<K,V>
,Set<Map.Entry<K,V>>
borra aSet<Map.Entry>
.Pero si
Map<K,V>
tuviera un método que tomara el tipoMap<String,V>
, su borrado seríaMap<String>
. El borrado de tipos para cambiar la cantidad de parámetros de tipo es horrible, especialmente para la resolución de métodos en tiempo de compilación. Absolutamente no vamos a aceptar esta solicitud.Es demasiado esperar que se puedan usar tipos crudos (
Map
) mientras se obtienen algunos de los tipos de seguridad de los genéricos (Set<Map.Entry>
).
Si crea una clase genérica en Java (la clase tiene parámetros de tipo genérico), ¿puede usar métodos genéricos (el método toma los parámetros de tipo genérico)?
Considere el siguiente ejemplo:
public class MyClass {
public <K> K doSomething(K k){
return k;
}
}
public class MyGenericClass<T> {
public <K> K doSomething(K k){
return k;
}
public <K> List<K> makeSingletonList(K k){
return Collections.singletonList(k);
}
}
Como era de esperar con un método genérico, puedo llamar a doSomething(K)
en instancias de MyClass
con cualquier objeto:
MyClass clazz = new MyClass();
String string = clazz.doSomething("String");
Integer integer = clazz.doSomething(1);
Sin embargo, si trato de usar instancias de MyGenericClass
sin especificar un tipo genérico, al invocar doSomething(K)
devuelve un Object
, independientemente de lo que se haya pasado K
:
MyGenericClass untyped = new MyGenericClass();
// this doesn''t compile - "Incompatible types. Required: String, Found: Object"
String string = untyped.doSomething("String");
Curiosamente, se compilará si el tipo de devolución es una clase genérica, por ejemplo, List<K>
(en realidad, esto se puede explicar, ver respuesta a continuación):
MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String"); // this compiles
Además, se compilará si se escribe la clase genérica, aunque solo sea con comodines:
MyGenericClass<?> wildcard = new MyGenericClass();
String string = wildcard.doSomething("String"); // this compiles
¿Hay una buena razón por la que no debería funcionar llamar a un método genérico en una clase genérica sin tipo?
¿Existe algún truco inteligente relacionado con las clases genéricas y los métodos genéricos que me falta?
EDITAR:
Para aclarar, esperaría que una clase genérica sin tipo o sin formato no respetara los parámetros de tipo de la clase genérica (porque no se han proporcionado). Sin embargo, no está claro para mí por qué una clase genérica sin tipo o sin formato significaría que los métodos genéricos no se cumplen.
Resulta que este tema ya ha sido planteado en SO, cf esta pregunta . Las respuestas a esto explican que cuando una clase está sin tipo / en su forma cruda, todos los genéricos se eliminan de la clase, incluida la tipificación de métodos genéricos.
Sin embargo, no hay realmente una explicación de por qué este es el caso. Permítanme aclarar mi pregunta:
- ¿Por qué Java elimina el método genérico al escribir en clases genéricas sin tipo o sin formato? ¿Hay una buena razón para esto o fue solo un descuido?
EDITAR - discusión de JLS:
Se ha sugerido (en respuesta a la pregunta SO anterior y a esta pregunta) que esto se trata en JLS 4.8 , que establece:
El tipo de un constructor (§8.8), el método de instancia (§8.4, §9.4) o el campo no estático (§8.3) M de un tipo C sin procesar que no se hereda de sus superclases o superinterfaces es el tipo bruto que le corresponde a la eliminación de su tipo en la declaración genérica correspondiente a C.
Me queda claro cómo esto se relaciona con una clase sin tipo: los tipos genéricos de clase se reemplazan con los tipos de borrado. Si los genéricos de clase están vinculados, entonces el tipo de borrado corresponde a esos límites. Si no están vinculados, entonces el tipo de borrado es Objeto, por ejemplo
// unbound class types
public class MyGenericClass<T> {
public T doSomething(T t) { return t; }
}
MyGenericClass untyped = new MyGenericClass();
Object t = untyped.doSomething("String");
// bound class types
public class MyBoundedGenericClass<T extends Number> {
public T doSomething(T t) { return t; }
}
MyBoundedGenericClass bounded = new MyBoundedGenericClass();
Object t1 = bounded.doSomething("String"); // does not compile
Number t2 = bounded.doSomething(1); // does compile
Si bien los métodos genéricos son métodos de instancia, no me queda claro que JLS 4.8 se aplique a métodos genéricos. El tipo de método genérico ( <K>
en el ejemplo anterior) no está sin tipo, ya que su tipo está determinado por los parámetros del método: solo la clase está sin tipo / sin formato.
Con Java Generics, si usa la forma cruda de una clase genérica, todos los genéricos de la clase, incluso los métodos genéricos no relacionados como los métodos makeSingletonList
y makeSingletonList
, se convierten en raw. La razón, según tengo entendido, es la de ofrecer compatibilidad con versiones anteriores del código Java escrito pre-genéricos.
Si no hay uso para su parámetro de tipo genérico T
, simplemente elimínelo de MyGenericClass
, dejando sus métodos genéricos con K
De lo contrario, tendrá que vivir con el hecho de que el parámetro de tipo de clase T
debe asignarse a su clase para usar genéricos en cualquier otra cosa de la clase.
De mi comentario sobre la respuesta de MAnyKeys:
Creo que el borrado de tipo completo tiene sentido combinado con la compatibilidad hacia atrás mencionada. Cuando el objeto se crea sin argumentos de tipo genérico, ¿puede (o debería ser?) Asumir que el uso del objeto tiene lugar en un código no genérico.
Considere este código heredado:
public class MyGenericClass {
public Object doSomething(Object k){
return k;
}
}
llamado así:
MyGenericClass foo = new MyGenricClass();
NotKType notKType = foo.doSomething(new NotKType());
Ahora la clase y su método se hacen genéricos:
public class MyGenericClass<T> {
public <K extends KType> K doSomething(K k){
return k;
}
}
Ahora el código de llamada anterior no se compilará más ya que NotKType
no es una suposición de KType
. Para evitar esto, los tipos genéricos se reemplazan con Object
. Aunque hay casos en los que no haría ninguna diferencia (como su ejemplo), al menos es muy complejo para el compilador analizar cuándo. Podría ser incluso imposible.
Sé que este szenario parece un poco construido, pero estoy seguro de que sucede de vez en cuando.
Esto no responde a la pregunta fundamental, pero aborda el problema de por qué makeSingletonList(...)
compila mientras que doSomething(...)
no:
En una implementación sin tipo de MyGenericClass
:
MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String");
...es equivalente a:
MyGenericClass untyped = new MyGenericClass();
List list = untyped.makeSingletonList("String");
List<String> typedList = list;
Esto se compilará (con varias advertencias), pero luego estará abierto a errores de tiempo de ejecución.
De hecho, esto también es análogo a:
MyGenericClass untyped = new MyGenericClass();
List<Integer> list = untyped.makeSingletonList("String"); // this compiles!!!
Integer a = list.get(0);
... que compila pero arrojará una ClassCastException
tiempo de ejecución cuando intente obtener el valor String
y convertirlo en un Integer
.
He encontrado una razón para descartar completamente los genéricos (sin embargo, no es tan bueno). La razón es que: los genéricos pueden estar limitados. Considera esta clase:
public static class MyGenericClass<T> {
public <K extends T> K doSomething(K k){
return k;
}
public <K> List<K> makeSingletonList(K k){
return Collections.singletonList(k);
}
}
El compilador debe descartar los genéricos en doSomething
, cuando utiliza la clase sin genéricos. Y creo que todos los genéricos se descartan para ser consistentes con este comportamiento.
makeSingletonList
compila porque Java realiza un lanzamiento no seleccionado de List
to List<K>
(sin embargo, el compilador muestra una advertencia).
La razón de esto es la compatibilidad con el código pre-genérico. El código pre-genérico no usaba argumentos genéricos, sino que usaba lo que hoy parece ser un tipo crudo. El código pre-genérico usaría referencias a Object
lugar de referencias usando el tipo genérico, y los tipos crudos usarían el Object
para todos los argumentos genéricos, por lo que el código era verdaderamente retrocompatible.
Como ejemplo, considere este código:
List list = new ArrayList();
Este es un código pregenérico, y una vez que se introdujeron los genéricos, esto se interpretó como un tipo genérico sin procesar equivalente a:
List<?> list = new ArrayList<>();
Porque el ? no tiene una palabra clave extends
o super
después de ella, se transforma en esto:
List<Object> list = new ArrayList<>();
La versión de List
que se usaba antes de los genéricos usaba un Object
para referirse a un elemento de lista, y esta versión también usa un Object
para referirse a un elemento de lista, por lo que se conserva la compatibilidad con versiones anteriores.