Java Language
Genéricos
Buscar..
Introducción
Los genéricos son una instalación de programación genérica que extienden el sistema de tipos de Java para permitir que un tipo o método opere en objetos de varios tipos mientras proporciona seguridad de tipo de tiempo de compilación. En particular, el marco de colecciones Java es compatible con los genéricos para especificar el tipo de objetos almacenados en una instancia de colección.
Sintaxis
- class ArrayList <E> {} // una clase genérica con parámetro de tipo E
- clase HashMap <K, V> {} // una clase genérica con dos parámetros de tipo K y V
- <E> void print (elemento E) {} // un método genérico con el parámetro de tipo E
- ArrayList <String> nombres; // declaración de una clase genérica
- ArrayList <?> Objetos; // declaración de una clase genérica con un parámetro de tipo desconocido
- nueva ArrayList <String> () // creación de instancias de una clase genérica
- nueva ArrayList <> () // creación de instancias con inferencia de tipo "diamante" (Java 7 o posterior)
Observaciones
Los genéricos se implementan en Java a través del borrado de tipos, lo que significa que durante el tiempo de ejecución la información de tipo especificada en la creación de instancias de una clase genérica no está disponible. Por ejemplo, la declaración List<String> names = new ArrayList<>();
produce un objeto de lista desde el cual el tipo de elemento String
no se puede recuperar en tiempo de ejecución. Sin embargo, si la lista se almacena en un campo de tipo List<String>
, o se pasa a un parámetro de método / constructor de este mismo tipo, o se devuelve desde un método de ese tipo de retorno, entonces la información de tipo completo se puede recuperar en tiempo de ejecución a través de la API de reflexión de Java.
Esto también significa que cuando se realiza la conversión a un tipo genérico (por ejemplo: (List<String>) list
), la conversión es una conversión sin verificar . Debido a que el parámetro <String>
se borra, la JVM no puede verificar si una conversión de una List<?>
una List<String>
es correcta; La JVM solo ve un reparto de List
a List
en tiempo de ejecución.
Creando una clase genérica
Los genéricos permiten que las clases, interfaces y métodos tomen otras clases e interfaces como parámetros de tipo.
Este ejemplo utiliza la clase genérica Param
para tomar un solo tipo de parámetro T
, delimitado por corchetes angulares ( <>
):
public class Param<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Para crear una instancia de esta clase, proporcione un argumento de tipo en lugar de T
Por ejemplo, Integer
:
Param<Integer> integerParam = new Param<Integer>();
El argumento de tipo puede ser cualquier tipo de referencia, incluidas las matrices y otros tipos genéricos:
Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;
En Java SE 7 y versiones posteriores, el argumento de tipo se puede reemplazar con un conjunto vacío de argumentos de tipo ( <>
) llamado diamante :
Param<Integer> integerParam = new Param<>();
A diferencia de otros identificadores, los parámetros de tipo no tienen restricciones de denominación. Sin embargo, sus nombres suelen ser la primera letra de su propósito en mayúsculas. (Esto es cierto incluso en los JavaDocs oficiales).
Los ejemplos incluyen T
para "tipo" , E
para "elemento" y K
/ V
para "clave" / "valor" .
Extendiendo una clase genérica
public abstract class AbstractParam<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
AbstractParam
es una clase abstracta declarada con un parámetro de tipo de T
Al extender esta clase, ese parámetro de tipo puede reemplazarse por un argumento de tipo escrito dentro de <>
, o el parámetro de tipo puede permanecer sin cambios. En el primer y segundo ejemplo a continuación, String
y Integer
reemplazan el parámetro de tipo. En el tercer ejemplo, el parámetro de tipo permanece sin cambios. El cuarto ejemplo no usa genéricos en absoluto, por lo que es similar a si la clase tuviera un parámetro Object
. El compilador advertirá que AbstractParam
es un tipo sin ObjectParam
, pero compilará la clase ObjectParam
. El quinto ejemplo tiene 2 parámetros de tipo (consulte "parámetros de tipo múltiple" a continuación), seleccionando el segundo parámetro como el parámetro de tipo pasado a la superclase.
public class Email extends AbstractParam<String> {
// ...
}
public class Age extends AbstractParam<Integer> {
// ...
}
public class Height<T> extends AbstractParam<T> {
// ...
}
public class ObjectParam extends AbstractParam {
// ...
}
public class MultiParam<T, E> extends AbstractParam<E> {
// ...
}
El siguiente es el uso:
Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();
Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();
Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);
Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);
MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);
Observe que en la clase de Email
, el método T getValue()
actúa como si tuviera una firma de String getValue()
, y el método void setValue(T)
actúa como si fuera declarado void setValue(String)
.
También es posible crear una instancia con una clase interna anónima con llaves vacías ( {}
):
AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);
Tenga en cuenta que no se permite usar el diamante con clases internas anónimas.
Parámetros de tipo múltiple
Java ofrece la posibilidad de utilizar más de un parámetro de tipo en una clase o interfaz genérica. Se pueden usar múltiples parámetros de tipo en una clase o interfaz colocando una lista de tipos separados por comas entre los corchetes angulares. Ejemplo:
public class MultiGenericParam<T, S> {
private T firstParam;
private S secondParam;
public MultiGenericParam(T firstParam, S secondParam) {
this.firstParam = firstParam;
this.secondParam = secondParam;
}
public T getFirstParam() {
return firstParam;
}
public void setFirstParam(T firstParam) {
this.firstParam = firstParam;
}
public S getSecondParam() {
return secondParam;
}
public void setSecondParam(S secondParam) {
this.secondParam = secondParam;
}
}
El uso se puede hacer de la siguiente manera:
MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);
Declarar un método genérico
Los métodos también pueden tener parámetros de tipo genérico .
public class Example {
// The type parameter T is scoped to the method
// and is independent of type parameters of other methods.
public <T> List<T> makeList(T t1, T t2) {
List<T> result = new ArrayList<T>();
result.add(t1);
result.add(t2);
return result;
}
public void usage() {
List<String> listString = makeList("Jeff", "Atwood");
List<Integer> listInteger = makeList(1, 2);
}
}
Tenga en cuenta que no tenemos que pasar un argumento de tipo real a un método genérico. El compilador infiere el argumento de tipo para nosotros, basado en el tipo de destino (por ejemplo, la variable a la que asignamos el resultado), o en los tipos de los argumentos reales. Por lo general, inferirá el argumento de tipo más específico que hará que la llamada sea correcta.
A veces, aunque rara vez, puede ser necesario anular esta inferencia de tipo con argumentos de tipo explícito:
void usage() {
consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}
void consumeObjects(Stream<Object> stream) { ... }
Es necesario en este ejemplo porque el compilador no puede "mirar hacia adelante" para ver que el Object
se desea para T
después de llamar a stream()
y, de lo contrario, makeList
String
función de los argumentos de la lista de elementos. Tenga en cuenta que el lenguaje Java no admite omitir la clase u objeto en el que se llama al método ( this
en el ejemplo anterior) cuando se proporcionan explícitamente argumentos de tipo.
El diamante
Java 7 introdujo el Diamond 1 para eliminar algunas placas de calderas en torno a la creación de instancias de clase genérica. Con Java 7+ puedes escribir:
List<String> list = new LinkedList<>();
Donde tenías que escribir en versiones anteriores, esto:
List<String> list = new LinkedList<String>();
Una limitación es para las clases anónimas , donde aún debe proporcionar el parámetro de tipo en la creación de instancias:
// This will compile:
Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
// But this will not:
Comparator<String> caseInsensitiveComparator = new Comparator<>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
Aunque el uso de Diamond con Anonymous Inner Classes no es compatible con Java 7 y 8, se incluirá como una nueva función en Java 9 .
Nota:
1 - Algunas personas llaman al uso <>
el " operador de diamante". Esto es incorrecto. El diamante no se comporta como un operador, y no se describe ni se enumera en ningún lugar en el JLS o en los Tutoriales de Java (oficiales) como operador. De hecho, <>
ni siquiera es un token de Java distinto. Más bien, es un <
token seguido de un >
token, y es legal (aunque mal estilo) tener espacios en blanco o comentarios entre los dos. El JLS y los Tutoriales se refieren constantemente a <>
como "el diamante", y ese es el término correcto para ello.
Requerir múltiples límites superiores ("extiende A y B")
Puede requerir un tipo genérico para extender varios límites superiores.
Ejemplo: queremos ordenar una lista de números, pero Number
no implementa Comparable
.
public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
Collections.sort( n );
}
En este ejemplo, T
debe extender el Number
e implementar Comparable<T>
que debe ajustarse a todas las implementaciones de números incorporados "normales" como Integer
o BigDecimal
pero no se ajusta a las más exóticas como Striped64
.
Dado que no se permite la herencia múltiple, puede usar como máximo una clase como límite y debe ser la primera en la lista. Por ejemplo, <T extends Comparable<T> & Number>
no está permitido porque Comparable es una interfaz y no una clase.
Creando una clase genérica limitada
Puede restringir los tipos válidos utilizados en una clase genérica al delimitar ese tipo en la definición de clase. Dada la siguiente jerarquía de tipos simple:
public abstract class Animal {
public abstract String getSound();
}
public class Cat extends Animal {
public String getSound() {
return "Meow";
}
}
public class Dog extends Animal {
public String getSound() {
return "Woof";
}
}
Sin genéricos limitados , no podemos crear una clase de contenedor que sea genérica y que sepa que cada elemento es un animal:
public class AnimalContainer<T> {
private Collection<T> col;
public AnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Illegal, type T doesn't have makeSound()
// it is used as an java.lang.Object here
System.out.println(t.makeSound());
}
}
}
Con el límite genérico en la definición de clase, esto ahora es posible.
public class BoundedAnimalContainer<T extends Animal> { // Note bound here.
private Collection<T> col;
public BoundedAnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Now works because T is extending Animal
System.out.println(t.makeSound());
}
}
}
Esto también restringe las instancias válidas del tipo genérico:
// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();
// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();
// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();
Decidir entre 'T', `? super T`, y `? extiende T`
La sintaxis de los comodines delimitados de los genéricos de Java, que representa el tipo desconocido por ?
es:
? extends T
representa un comodín acotado superior. El tipo desconocido representa un tipo que debe ser un subtipo de T o el mismo tipo de T.? super T
representa un comodín acotado más bajo. El tipo desconocido representa un tipo que debe ser un supertipo de T o el mismo tipo de T.
Como regla general, debes usar
-
? extends T
si solo necesita acceso de "lectura" ("entrada") -
? super T
si necesita acceso de "escritura" ("salida") -
T
si necesita ambos ("modificar")
El uso de extends
o super
es generalmente mejor porque hace que su código sea más flexible (como en: permite el uso de subtipos y supertipos), como verá a continuación.
class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}
public class FruitHelper {
public void eatAll(Collection<? extends Fruit> fruits) {}
public void addApple(Collection<? super Apple> apples) {}
}
El compilador ahora podrá detectar cierto mal uso:
public class GenericsTest {
public static void main(String[] args){
FruitHelper fruitHelper = new FruitHelper() ;
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Apple()); // Allowed, as Apple is a Fruit
fruits.add(new Banana()); // Allowed, as Banana is a Fruit
fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
fruitHelper.eatAll(fruits); // Allowed
Collection<Banana> bananas = new ArrayList<>();
bananas.add(new Banana()); // Allowed
//fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits
Collection<Apple> apples = new ArrayList<>();
fruitHelper.addApple(apples); // Allowed
apples.add(new GrannySmith()); // Allowed, as this is an Apple
fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
Collection<GrannySmith> grannySmithApples = new ArrayList<>();
fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
// GrannySmith is not a supertype of Apple
apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit
Collection<Object> objects = new ArrayList<>();
fruitHelper.addApple(objects); // Allowed, as Object super Apple
objects.add(new Shoe()); // Not a fruit
objects.add(new IPhone()); // Not a fruit
//fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}
La elección de la derecha T
, ? super T
o ? extends T
es necesario para permitir el uso con subtipos. El compilador puede entonces garantizar la seguridad del tipo; no debería tener que emitir (que no es de tipo seguro y puede causar errores de programación) si los usa correctamente.
Si no es fácil de entender, recuerde la regla de PECS :
P roducer usa " E xtends" y C onsumer usa " S uper".
(El productor solo tiene acceso de escritura y el consumidor solo tiene acceso de lectura)
Beneficios de la clase genérica y la interfaz
El código que utiliza genéricos tiene muchos beneficios sobre el código no genérico. A continuación se presentan los principales beneficios.
Controles de tipo más fuertes en tiempo de compilación
Un compilador de Java aplica una comprobación de tipo segura al código genérico y emite errores si el código viola la seguridad del tipo. Reparar errores de tiempo de compilación es más fácil que corregir errores de tiempo de ejecución, que pueden ser difíciles de encontrar.
Eliminación de moldes
El siguiente fragmento de código sin genéricos requiere el lanzamiento:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Cuando se vuelve a escribir para usar genéricos , el código no requiere conversión:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast
Permitiendo a los programadores implementar algoritmos genéricos
Mediante el uso de genéricos, los programadores pueden implementar algoritmos genéricos que funcionan en colecciones de diferentes tipos, pueden personalizarse, son seguros y más fáciles de leer.
Vinculación de parámetro genérico a más de 1 tipo
Los parámetros genéricos también pueden vincularse a más de un tipo utilizando la sintaxis T extends Type1 & Type2 & ...
Digamos que desea crear una clase cuyo tipo debe implementar tanto Genérico Flushable
y Closeable
, se puede escribir
class ExampleClass<T extends Flushable & Closeable> {
}
Ahora, el ExampleClass
sólo acepta como parámetros genéricos, tipos que implementan tanto Flushable
y Closeable
.
ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable
ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable
ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.
Los métodos de clase pueden optar por deducir argumentos de tipo genérico como tampoco Closeable
o Flushable
.
class ExampleClass<T extends Flushable & Closeable> {
/* Assign it to a valid type as you want. */
public void test (T param) {
Flushable arg1 = param; // Works
Closeable arg2 = param; // Works too.
}
/* You can even invoke the methods of any valid type directly. */
public void test2 (T param) {
param.flush(); // Method of Flushable called on T and works fine.
param.close(); // Method of Closeable called on T and works fine too.
}
}
Nota:
No puede enlazar el parámetro genérico a ninguno de los tipos usando la cláusula OR ( |
). Solo se admite la cláusula AND ( &
). El tipo genérico puede extender solo una clase y muchas interfaces. La clase debe colocarse al principio de la lista.
Creando un tipo genérico
Debido al borrado de tipo no funcionará lo siguiente:
public <T> void genericMethod() {
T t = new T(); // Can not instantiate the type T.
}
El tipo T
se borra. Dado que, en el tiempo de ejecución, la JVM no sabe qué era T
originalmente, no sabe a qué constructor llamar.
Soluciones
Pasando la clase de
T
al llamar agenericMethod
:public <T> void genericMethod(Class<T> cls) { try { T t = cls.newInstance(); } catch (InstantiationException | IllegalAccessException e) { System.err.println("Could not instantiate: " + cls.getName()); } }
genericMethod(String.class);
Lo que genera excepciones, ya que no hay forma de saber si la clase pasada tiene un constructor predeterminado accesible.
Pasando una referencia al constructor de
T
:public <T> void genericMethod(Supplier<T> cons) { T t = cons.get(); }
genericMethod(String::new);
Refiriéndose al tipo genérico declarado dentro de su propia declaración.
¿Cómo se utiliza una instancia de un tipo genérico heredado (posiblemente más) dentro de una declaración de método en el tipo genérico que se está declarando? Este es uno de los problemas a los que se enfrentará cuando profundice un poco en los genéricos, pero sigue siendo bastante común.
Supongamos que tenemos un tipo DataSeries<T>
(interfaz aquí), que define una serie de datos genéricos que contienen valores de tipo T
Es complicado trabajar con este tipo directamente cuando queremos realizar muchas operaciones con, por ejemplo, valores dobles, por lo que definimos DoubleSeries extends DataSeries<Double>
. Ahora supongamos que el tipo original DataSeries<T>
tiene un método add(values)
que agrega otra serie de la misma longitud y devuelve una nueva. ¿Cómo DoubleSeries
el tipo de values
y el tipo de retorno para que sea DoubleSeries
lugar de DataSeries<Double>
en nuestra clase derivada?
El problema se puede resolver agregando un parámetro de tipo genérico que se refiera y extienda el tipo que se está declarando (aplicado aquí a una interfaz, pero lo mismo significa clases):
public interface DataSeries<T, DS extends DataSeries<T, DS>> {
DS add(DS values);
List<T> data();
}
Aquí T
representa el tipo de datos que contiene la serie, por ejemplo, Double
y DS
la propia serie. Un tipo (o tipos) heredado ahora se puede implementar fácilmente sustituyendo el parámetro mencionado anteriormente por un tipo derivado correspondiente, dando así una definición concreta de Double
base de la forma:
public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
static DoubleSeries instance(Collection<Double> data) {
return new DoubleSeriesImpl(data);
}
}
En este momento, incluso un IDE implementará la interfaz anterior con los tipos correctos en su lugar, que, después de un poco de llenado de contenido, pueden tener este aspecto:
class DoubleSeriesImpl implements DoubleSeries {
private final List<Double> data;
DoubleSeriesImpl(Collection<Double> data) {
this.data = new ArrayList<>(data);
}
@Override
public DoubleSeries add(DoubleSeries values) {
List<Double> incoming = values != null ? values.data() : null;
if (incoming == null || incoming.size() != data.size()) {
throw new IllegalArgumentException("bad series");
}
List<Double> newdata = new ArrayList<>(data.size());
for (int i = 0; i < data.size(); i++) {
newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
}
return DoubleSeries.instance(newdata);
}
@Override
public List<Double> data() {
return Collections.unmodifiableList(data);
}
}
Como puede ver, el método de add
se declara como DoubleSeries add(DoubleSeries values)
y el compilador está contento.
El patrón se puede anidar más si es necesario.
Uso de instanceof con Genéricos.
Usando genéricos para definir el tipo en instancia de
Considere el siguiente Example
clase genérica declarado con el parámetro formal <T>
:
class Example<T> {
public boolean isTypeAString(String s) {
return s instanceof T; // Compilation error, cannot use T as class type here
}
}
Esto siempre dará un error de compilación porque en cuanto el compilador compila la fuente Java en el código de bytes de Java , aplica un proceso conocido como borrado de tipo , que convierte todo el código genérico en código no genérico, lo que hace imposible distinguir entre los tipos T en tiempo de ejecución. El tipo utilizado con instanceof
tiene que ser verificable , lo que significa que toda la información sobre el tipo debe estar disponible en tiempo de ejecución, y esto no suele ser el caso para los tipos genéricos.
La siguiente clase representa el aspecto de dos clases diferentes de Example
, Example<String>
y Example<Number>
, después de que los genéricos se hayan eliminado por borrado de tipo :
class Example { // formal parameter is gone
public boolean isTypeAString(String s) {
return s instanceof Object; // Both <String> and <Number> are now Object
}
}
Dado que los tipos han desaparecido, no es posible que la JVM sepa qué tipo es T
Excepción a la regla anterior.
Siempre puede usar un comodín ilimitado (?) Para especificar un tipo en el instanceof
siguiente manera:
public boolean isAList(Object obj) {
return obj instanceof List<?>;
}
Esto puede ser útil para evaluar si una instancia obj
es una List
o no:
System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true
De hecho, el comodín ilimitado se considera un tipo confiable.
Usando una instancia genérica con instanceof
El otro lado de la moneda es que usar una instancia t
de T
con instanceof
es legal, como se muestra en el siguiente ejemplo:
class Example<T> {
public boolean isTypeAString(T t) {
return t instanceof String; // No compilation error this time
}
}
porque después del borrado de tipo, la clase tendrá el siguiente aspecto:
class Example { // formal parameter is gone
public boolean isTypeAString(Object t) {
return t instanceof String; // No compilation error this time
}
}
Dado que, incluso si el borrado de tipo ocurre de todos modos, ahora la JVM puede distinguir entre diferentes tipos en la memoria, incluso si usan el mismo tipo de referencia ( Object
), como muestra el siguiente fragmento de código:
Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String
Diferentes formas de implementar una interfaz genérica (o extender una clase genérica)
Supongamos que la siguiente interfaz genérica ha sido declarada:
public interface MyGenericInterface<T> {
public void foo(T t);
}
A continuación se enumeran las posibles formas de implementarlo.
Implementación de clase no genérica con un tipo específico
Elija un tipo específico para reemplazar el parámetro de tipo formal <T>
de MyGenericClass
e implementarlo, como lo hace el siguiente ejemplo:
public class NonGenericClass implements MyGenericInterface<String> {
public void foo(String t) { } // type T has been replaced by String
}
Esta clase solo trata con String
, y esto significa que el uso de MyGenericInterface
con diferentes parámetros (por ejemplo, Integer
, Object
, etc.) no se compilará, como muestra el siguiente fragmento de código:
NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile
Implementación genérica de clase.
Declare otra interfaz genérica con el parámetro de tipo formal <T>
que implementa MyGenericInterface
, de la siguiente manera:
public class MyGenericSubclass<T> implements MyGenericInterface<T> {
public void foo(T t) { } // type T is still the same
// other methods...
}
Tenga en cuenta que se puede haber utilizado un parámetro de tipo formal diferente, de la siguiente manera:
public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
public void foo(U t) { }
// other methods...
}
Implementación de clase de tipo raw
Declare una clase no genérica que implementa MyGenericInteface
como un tipo sin MyGenericInteface
(sin usar genérico en absoluto), como sigue:
public class MyGenericSubclass implements MyGenericInterface {
public void foo(Object t) { } // type T has been replaced by Object
// other possible methods
}
De esta forma no se recomienda, ya que no es 100% seguro en el tiempo de ejecución porque mezcla el tipo sin formato (de la subclase) con los genéricos (de la interfaz) y también es confuso. Los compiladores de Java modernos generarán una advertencia con este tipo de implementación; sin embargo, el código, por razones de compatibilidad con JVM más antiguos (1.4 o anteriores), se compilará.
También se permiten todas las formas enumeradas anteriormente cuando se utiliza una clase genérica como supertipo en lugar de una interfaz genérica.
Usando genéricos para auto-cast
Con los genéricos, es posible devolver lo que la persona que llama espera:
private Map<String, Object> data;
public <T> T get(String key) {
return (T) data.get(key);
}
El método se compilará con una advertencia. El código es en realidad más seguro de lo que parece porque el tiempo de ejecución de Java se convertirá en una conversión cuando lo uses:
Bar bar = foo.get("bar");
Es menos seguro cuando usas tipos genéricos:
List<Bar> bars = foo.get("bars");
Aquí, la conversión funcionará cuando el tipo devuelto sea cualquier tipo de List
(es decir, devolver la List<String>
no ClassCastException
una ClassCastException
; finalmente la obtendría al eliminar elementos de la lista).
Para solucionar este problema, puede crear una API que use claves escritas:
public final static Key<List<Bar>> BARS = new Key<>("BARS");
junto con este método put()
:
public <T> T put(Key<T> key, T value);
Con este enfoque, no puede poner el tipo incorrecto en el mapa, por lo que el resultado siempre será correcto (a menos que cree accidentalmente dos claves con el mismo nombre pero tipos diferentes).
Relacionado:
Obtenga una clase que satisfaga el parámetro genérico en tiempo de ejecución
Muchos parámetros genéricos no vinculados, como los que se utilizan en un método estático, no se pueden recuperar en tiempo de ejecución (consulte Otros subprocesos en Erasure ). Sin embargo, hay una estrategia común empleada para acceder al tipo que satisface un parámetro genérico en una clase en tiempo de ejecución. Esto permite un código genérico que depende del acceso al tipo sin tener que pasar información de tipo a través de cada llamada.
Fondo
La parametrización genérica en una clase se puede inspeccionar creando una clase interna anónima. Esta clase capturará la información de tipo. En general, este mecanismo se conoce como tokens de tipo super , que se detallan en la publicación del blog de Neal Gafter .
Implementaciones
Tres implementaciones comunes en Java son:
Ejemplo de uso
public class DataService<MODEL_TYPE> {
private final DataDao dataDao = new DataDao();
private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
(getClass()){}.getRawType();
public List<MODEL_TYPE> getAll() {
return dataDao.getAllOfType(type);
}
}
// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be
// recovered at runtime
public class UserService extends DataService<User> {}
public class Main {
public static void main(String[] args) {
UserService service = new UserService();
List<User> users = service.getAll();
}
}