Los genéricos de Java explicados brevemente con ejemplos

Consulte estas preguntas frecuentes para obtener más información:

http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html#SECTION1

Aquí, de forma resumida, algunos puntos importantes de estas preguntas frecuentes.

Ejemplo (de tipo genérico):

class Pair<X,Y>  { 
  private X first;
  private Y second;   public Pair(X a1, Y a2) {
    first  = a1;
    second = a2;
  }
  public X getFirst()  { return first; }
  public Y getSecond() { return second; }
  public void setFirst(X arg)  { first = arg; }
  public void setSecond(Y arg) { second = arg; }
}

la clase Par tiene dos parámetros de tipo incógnita y Y .  Se reemplazan por argumentos de tipo cuando el tipo genérico Par está instanciado. Por ejemplo, en la declaración Par<Cadena, Fecha> el parámetro de tipo incógnita se reemplaza por el argumento de tipo Cadena y Y is replaced by  Fecha .
El alcance de los identificadores. incógnita y Y es toda la definición de la clase.  En este ámbito los dos parámetros de tipo incógnita y Y se utilizan como si fueran tipos (con algunas restricciones). En el ejemplo anterior, los parámetros de tipo se utilizan como argumento y tipo de retorno de los métodos de instancia y los tipos de campos de instancia.
Los parámetros de tipo se pueden declarar con límites. Los límites dan acceso a métodos del tipo desconocido que representa el parámetro de tipo. En nuestro ejemplo, no invocamos ningún método de tipo desconocido. incógnita y Y . Por este motivo, los dos parámetros de tipo no están limitados.

Ejemplo (de un tipo parametrizado concreto):

par de impresión público vacío ( Par<Cadena,Largo> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<Cadena,Largo> límite = nuevo Par<Cadena,Largo> ("máximo",1024L);
imprimirPar(límite);

Ejemplo (de un tipo parametrizado con comodín):

par de impresión público vacío ( Par<?,?> par) {
  System.out.println("("+pair.getFirst()+","+pair.getSecond()+")");
} Par<?,?> límite = nuevo Par<Cadena,Largo> ("máximo",1024L);
imprimirPar(límite);

Todas las instancias de un tipo genérico comparten la misma representación del tipo de tiempo de ejecución, es decir, la representación del tipo sin formato. Por ejemplo, las instancias de un tipo genérico. Lista , como Lista<Fecha> , Lista<Cadena> , Lista<Long> , etc. tienen diferentes tipos estáticos en el momento de la compilación, pero el mismo tipo dinámico Lista en tiempo de ejecución.

Un elenco consta de dos partes:

  • una verificación de tipo estático realizada por el compilador en el momento de la compilación y
  • una verificación de tipo dinámica realizada por la máquina virtual en tiempo de ejecución.

La parte estática clasifica los lanzamientos sin sentido que no pueden tener éxito, como el lanzamiento de Cadena eso Fecha o de Lista<Cadena> eso Lista<Fecha> .

La parte dinámica utiliza la información del tipo de tiempo de ejecución y realiza una verificación de tipo en tiempo de ejecución.  Se plantea un Excepción ClassCast si el tipo dinámico del objeto no es el tipo de destino (o un subtipo del tipo de destino) de la conversión. Ejemplos de elencos con una parte dinámica son el elenco de Objeto eso Cadena o de Objeto eso Lista<Cadena> .  Estos son los llamados abatidos, desde un supertipo hasta un subtipo.

Ejemplos de tipos parametrizados concretos son Lista<Cadena> , Mapa<Cadena,Fecha> , pero no Lista<? extiende el número> o Mapa<String,?> .

A veces se espera que un Lista<Objeto> sería un supertipo de Lista<Cadena> , porque Objeto es un supertipo de Cadena .  This expectation stems from the fact that such a type relationship exists for arrays:  Objeto[] es un supertipo de Cadena[] , porque Objeto es un supertipo de Cadena . (Este tipo de relación se conoce como covarianza .) La relación de supersubtipo de los tipos de componentes se extiende a los tipos de matriz correspondientes. No existe tal relación de tipos para instancias de tipos genéricos. (Los tipos parametrizados son no covariante.)

La falta de una relación de supersubtipo entre instancias del mismo tipo genérico tiene varias consecuencias.  Aquí hay un ejemplo.

Ejemplo:

void printAll(ArrayList<Object> c) {
  for (Object o : c) 
    System.out.println(o);
} Lista ArrayList<String> = nueva ArrayList<String>();
... llenar lista ...
imprimirTodo(lista);   // error

A ArrayList<String> El objeto no se puede pasar como argumento a un método que solicita una ArrayList<Object> porque los dos tipos son instancias del mismo tipo genérico, pero para diferente argumentos de tipo, y por esta razón no son compatibles entre sí.

Los tipos parametrizados concretos son instancias concretas de un tipo genérico.  Son casi como tipos; sólo hay unas pocas restricciones.  NO se pueden utilizar para los siguientes fines:

  • para la creación de matrices
  • en el manejo de excepciones
  • en una clase literal
  • en un instancia de expresión

Las matrices que contienen elementos cuyo tipo es un tipo parametrizado concreto son ilegales.

Ejemplo (de tipo de matriz ilegal):

prueba de vacío estático() {
  Par<Entero,Entero>[] intPairArr = nuevo Par<Entero,Entero>[10] ; // error
  addElements(intPairArr); 
  Pair<Integer,Integer> pair = intPairArr[1];
  Integer i = pair.getFirst();
  pair.setSecond(i);
}
addElements vacío estático ( Objeto[] objetoArr) {
  objArr[0] = new Pair<Integer,Integer>(0,0);
  objArr[1] = new Pair<String,String>("","");      // should fail with ArrayStoreException
}

El compilador prohíbe la creación de matrices cuyo tipo de componente sea un tipo parametrizado concreto, como Par<Entero,Entero> en nuestro ejemplo.  En la entrada anterior analizamos por qué es razonable que el compilador califique un Par<Entero,Entero>[] como ilegal.  El problema clave es que el compilador y el sistema de ejecución deben garantizar que una matriz sea una homogéneo secuencia de elementos del mismo tipo.  Una de las comprobaciones de tipo, concretamente la comprobación de almacenamiento de matriz realizada por la máquina virtual en tiempo de ejecución, no detecta la inserción infractora de un elemento extraño. En el ejemplo, la segunda inserción en el agregarElementos El método debería fallar porque estaban agregando un par de cadenas a una matriz de valores integrales, pero no falla como se esperaba. Las razones fueron discutidas en la entrada anterior.

Si no podemos usar matrices que contengan elementos cuyo tipo sea un tipo parametrizado concreto, ¿qué usamos como solución alternativa?

Consideremos 3 posibles soluciones:

  • variedad de tipos crudos
  • matriz de tipo parametrizado con comodín ilimitado
  • colección en lugar de matriz

Si bien las matrices de tipos parametrizados concretos son ilegales, se permiten colecciones de tipos parametrizados concretos.

Un tipo con parámetros comodín es una instanciación de un tipo genérico donde al menos un argumento de tipo es un comodín.  Ejemplos de tipos parametrizados con comodines son Colección<?> , Lista<? extiende el número> , Comparador<? súper cuerda> y Par<Cadena,?> . Un tipo parametrizado con comodín denota una familia de tipos que comprende instancias concretas de un tipo genérico.  El tipo de comodín que se utiliza determina qué tipos parametrizados concretos pertenecen a la familia.  Por ejemplo, el tipo parametrizado comodín Colección<?> denota la familia de todas las instancias del Recopilación interfaz independientemente del tipo de argumento.  El tipo parametrizado comodín Lista<? extiende el número> denotes the family of all list types where the element type is a subtype of  Número .  El tipo parametrizado comodín Comparador<? súper cuerda> es la familia de todas las instancias del Comparador interfaz para tipos de argumentos de tipo que son supertipos de Cadena.

Un tipo parametrizado con comodín no es un tipo en el sentido normal (diferente de una clase/interfaz no parametrizada o un tipo sin formato).

Se pueden utilizar tipos parametrizados comodín para escribir (como clases e interfaces no parametrizadas):

  • como argumento y tipos de métodos de retorno
  • como tipo de campo o variable de referencia local
  • como tipo de componente de una matriz como argumento de tipo de otros tipos parametrizados
  • como tipo de objetivo en lanzamientos

No sólo los tipos pueden ser genéricos, sino que los métodos también pueden serlo. Los métodos estáticos y no estáticos, así como los constructores, pueden tener parámetros de tipo. La sintaxis para la declaración de parámetros de tipo formal es similar a la sintaxis para tipos genéricos. La sección de parámetro de tipo está delimitada por corchetes angulares y aparece antes del tipo de retorno del método. Su sintaxis y significado son idénticos a la lista de parámetros de tipo de un tipo genérico.

Aquí está el ejemplo de un genérico. máximo Método que calcula el mayor valor en una colección de elementos de un tipo desconocido. A .

Ejemplo (de un método genérico):

Colecciones de clase {
  public static <A amplía Comparable<A>> A max(Colección< A > xs) {
    Iterator< A > xi = xs.iterador();
    A w = xi.siguiente();
    while (xi.hasNext()) {
      A x = xi.siguiente();
      if (w.compareTo(x) < 0) w = x;
    }
    return w;
  }
} 

El máximo El método tiene un parámetro de tipo, llamado A .  Es un marcador de posición para el tipo de elemento de la colección en la que funciona el método. El parámetro de tipo tiene un límite; debe ser un tipo que sea un subtipo de Comparable<A>

Ejemplo (del mismo tipo genérico, esta vez con límites):

interfaz pública Comparable<T> {
  public int compareTo(T arg);
}
clase pública TreeMap<Key extiende Comparable<Key> ,Datos>{
  private static class Entry<K,V> {
     K key;
     V value;
     Entry<K,V> left = null;
     Entry<K,V> right = null;
     Entry<K,V> parent;
  }
  private transient Entry<Key,Data> root = null;
  ...
  private Entry<Key,Data> getEntry(Key key) {
     Entry<Key,Data> p = root;
     Key k = key;
     while (p != null) {
       int cmp = k. comparar con (tecla p.) ; 
       if (cmp == 0)
         return p;
       else if (cmp < 0)
         p = p.left;
       else 
         p = p.right; 
     }
     return null;
  }
  public boolean containsKey(Key key) {
     return getEntry(key) != null;
  }
  ...
}

En el ejemplo anterior, el parámetro de tipo Llave tiene el límite Comparable<Key> .  La especificación de un límite tiene dos efectos:

  • Da acceso a los métodos que el límite especifica. .  En el ejemplo, el límite Comparable<Key> da acceso a la comparar con método que queremos invocar en la implementación de nuestro ÁrbolMapa clase.
  • Sólo se pueden utilizar tipos “dentro de límites” para la creación de instancias del tipo genérico.   In the example, a parameterized type such as ÁrbolMapa<Número,Cadena> sería rechazado, porque el tipo Número no es un subtipo de Comparable<Número> .  Un tipo parametrizado como TreeMap<String,String> sería aceptado, porque el tipo String está dentro de los límites, es decir, es un subtipo de Comparable<String> .

Todas las clases, interfaces y tipos de enumeración se pueden utilizar como parámetros de tipo vinculados, incluidos los tipos anidados e internos.  Ni los tipos primitivos ni los tipos de matriz se utilizarán como límite de parámetro de tipo.

Ejemplos (de límites de parámetros de tipo):

clase X0 <T se extiende entero > { ... }      // error
clase X1 <T se extiende Objeto[] > { ... } // error
clase X2 <T se extiende Número > { ... }
clase X3 <T se extiende Cadena > { ... }
clase X4 <T se extiende Ejecutable > { ... }
clase X5 <T se extiende Hilo.Estado > { ... }
clase X6 <T se extiende Lista > { ... }
clase X7 <T se extiende Lista<Cadena> > { ... }
clase X8 <T se extiende Lista<? extiende el número> > { ... }
clase X9 <T se extiende ¿Comparable<? super número> > { ... }
la clase X10<T se extiende Mapa.Entrada<?,?> > { ... }

El ejemplo de código muestra que los tipos primitivos como entero y tipos de matrices como Objeto[] no están permitidos como límite de parámetro de tipo.

Tipos de clases, como Número o Cadena y tipos de interfaz, como Ejecutable , están permitidos como parámetro de tipo vinculado.

Tipos de enumeración, como Hilo.Estado También se permiten como límite de parámetro de tipo. Hilo.Estado es un ejemplo de un tipo anidado utilizado como límite de parámetro de tipo.  También se permiten tipos interiores no estáticos.

Los tipos sin formato se permiten como parámetro de tipo vinculado; Lista es un ejemplo.

Los tipos parametrizados se permiten como límite de parámetro de tipo, incluidos tipos parametrizados concretos como Lista<Cadena> , tipos parametrizados con comodines acotados, como Lista<? extiende el número> y ¿Comparable<? súper largo> y tipos parametrizados con comodines ilimitados, como Mapa.Entrada<?,?> .  Un límite que es un tipo parametrizado con comodín permite como argumento de tipo todos los tipos que pertenecen a la familia de tipos que denota el comodín.  El tipo parametrizado comodín solo proporciona acceso restringido a campos y métodos; las restricciones dependen del tipo de comodín.

Ejemplo (de tipo parametrizado con comodín como parámetro de tipo vinculado):

clase X< ¿T extiende List<? extiende el número> > {
  public void someMethod(T t) {
    t.add(new Long(0L));    // error
    Number n = t.remove(0);
  }
}
prueba de clase {
  public static void main(String[] args) {
     X<AArrayList< Largo >>   x1 = new X<ArrayList<Long>>(); 
     X<AArrayList< Cadena >> x2 = nuevo X<ArrayList<String>>(); // error
  }
}

Variables de referencia de tipo t (el parámetro de tipo) se tratan como variables de referencia de un tipo comodín (el parámetro de tipo vinculado).  En nuestro ejemplo, la consecuencia es que el compilador rechaza la invocación de métodos que toman un argumento del tipo “desconocido” que representa el parámetro de tipo, como por ejemplo Lista.añadir , porque el límite es un tipo parametrizado comodín con un límite superior.
Al mismo tiempo el obligado Lista<? extiende el número> Determina los tipos que se pueden utilizar como argumentos de tipo. El compilador acepta todos los argumentos de tipo que pertenecen a la familia de tipos. Lista<? extiende el número> , es decir, todos los subtipos de Lista con un argumento de tipo que es un subtipo de Número .

Un parámetro de tipo se puede utilizar como límite de otro parámetro de tipo.

Ejemplo (de un parámetro de tipo utilizado como parámetro de tipo vinculado):

clase triple <T> {
  private T fst, snd, trd;
  public < Ud. extiende T , V extiende T , W. extiende T > Triple(U arg1, V arg2, W arg3) {
    fst = arg1;
    snd = arg2;
    trd = arg3;
  }
}

En este ejemplo el parámetro de tipo t de la clase parametrizada se utiliza como límite de los parámetros de tipo Ud. ,  V y W. de un método de instancia parametrizado de esa clase.

Otras oportunidades para utilizar parámetros de tipo como límites de otros parámetros de tipo incluyen situaciones en las que se define un tipo anidado dentro de un tipo genérico o se define una clase local dentro de un método genérico.  Incluso está permitido utilizar un parámetro de tipo como límite de otro parámetro de tipo en la misma sección de parámetro de tipo.

Un límite que es una clase da acceso a todos sus miembros públicos, es decir, campos públicos, métodos y tipos anidados. Sólo los constructores no son accesibles, porque no hay garantía de que una subclase del límite tenga los mismos constructores que el límite.

Ejemplo (de una clase utilizada como límite de un parámetro de tipo):

clase publica Superclase {     // static members
    public enum EnumType {THIS, THAT}
    public static Object staticField;
    public static void staticMethod() { ... }

    // non-static members
    public class InnerClass { ... }
    public Object nonStaticField;
    public void nonStaticMethod() { ... }

    // constructors
    public SuperClass() { ... }

    // private members
    private Object privateField; 

    ...
}

la clase final pública SomeClass<T se extiende Superclase > {
    private T object;
    public SomeClass(T t) { object = t; } 

    public String toString() {
        return
         "static nested type    : "+T.EnumType.class+"\n" 
        +"static field          : "+T.staticField+"\n"
        +"static method         : "+T.staticMethod()+"\n"
        +"non-static nested type: "+T.InnerClass.class+"\n"
        +"non-static field      : "+object.nonStaticField+"\n"
        +"non-static method     : "+object.nonStaticMethod()+"\n"
        +"constructor           : "+(new T())+"\n"                    // error
        +"private member        : "+object.privateField+"\n"          // error 
        ;
    }
}

el atado Superclase da acceso a sus tipos anidados, campos y métodos estáticos y campos y métodos no estáticos.  Sólo el constructor no es accesible.  Esto se debe a que los constructores no se heredan. Cada subclase define sus propios constructores y no necesita soportar los constructores de su superclase.  Por lo tanto, no hay garantía de que una subclase de Superclase tendrá el mismo constructor que su superclase.

El contexto en el que Enum<E extiende Enum<E>> “Lo que aparece es la declaración del enumeración clase en paquete java.lang :

clase abstracta pública Enum<E extiende Enum<E>> {
  ...
}

el tipo enumeración es la clase base común de todos los tipos de enumeración.  En Java, un tipo de enumeración como Color se traduce en una clase Color que se extiende Enum<Color> . El propósito de la superclase. enumeración es proporcionar una funcionalidad que sea común a todos los tipos de enumeración.

Aquí hay un boceto de la clase. enumeración :

clase abstracta pública Enum< mi extiende Enum<E>> implementa Comparable< mi >, serializable {
  private final String name;
  public  final String name() { ... }   private final int ordinal;
  public  final int ordinal() { ... }

  protected Enum(String name, int ordinal) { ... }

  public String           toString() { ... }
  public final boolean    equals(Object other) { ... }
  public final int        hashCode() { ... }
  protected final Object  clone() throws CloneNotSupportedException { ... }
  public final int        compareTo( mi o) {...}

  public final Class< mi > getDeclaringClass() { ... }
  public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}

La característica sorprendente de la declaración “ Enum<E extiende Enum<E>> ” es el hecho de que la clase recién definida enumeración y su parámetro de tipo recién definido mi aparecen en el límite de ese mismo parámetro de tipo.  Significa que el enumeración Se debe crear una instancia del tipo para uno de sus subtipos.  Para entender por qué esto tiene sentido, considere que cada tipo de enumeración se traduce en un subtipo de enumeración .

Aquí está el tipo de enumeración artificial. Color :

enumeración Color {ROJO, AZUL, VERDE}

El compilador lo traduce a la siguiente clase:

clase final publica Color se extiende Enum<Color> {
  public static final Color[] values() { return (Color[])$VALUES.clone(); }
  public static Color valueOf(String name) { ... }   private Color(String s, int i) { super(s, i); }

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  private static final Color $VALUES[];

  static {
    RED = new Color("RED", 0);
    BLUE = new Color("BLUE", 1);
    GREEN = new Color("GREEN", 2);
    $VALUES = (new Color[] { RED, BLUE, GREEN });
  }
}

La herencia tiene por efecto que el Color El tipo hereda todos los métodos implementados en Enum<Color> .  Entre ellos está el comparar con método.  El Color. comparar con El método probablemente debería tomar un Color como argumento.  Para que esto suceda clase enumeración es genérico y el Enum.compareTo el método toma enumeración parámetro de tipo mi como argumento.  Como resultado, escriba Color derivado de Enum<Color> hereda un comparar con método que requiere un Color como y argumento, exactamente como debería.
Si analizamos la declaración “ Enum<E extiende Enum<E>> “Podemos ver que este patrón tiene varios aspectos.

En primer lugar, está el hecho de que el parámetro de tipo vinculado es el tipo mismo: “ enumeración <E se extiende enumeración <E>> “. Se asegura de que solo los subtipos de tipo enumeración están permitidos como argumentos de tipo. (Teóricamente, escriba enumeración podría ser instanciado en sí mismo, como en Enum<Enum> , pero ciertamente esto no es lo que se pretende y es difícil imaginar una situación en la que tal creación de instancias sería útil).

En segundo lugar, está el hecho de que el tipo vinculado al parámetro es el tipo parametrizado enumeración <E> , que utiliza el parámetro de tipo mi como argumento de tipo del límite. Esta declaración asegura que la relación de herencia entre un subtipo y una instanciación de enumeración es de la forma X extiende Enum<X> “. Un subtipo como “ X extiende Enum<Y> “no se puede declarar debido al argumento de tipo Y no estaría dentro de los límites; sólo subtipos de Enum<X> están dentro de los límites.

En tercer lugar, está el hecho de que enumeración es genérico en primer lugar.  Significa que algunos de los métodos de clase. enumeración tomar un argumento o devolver un valor de un tipo desconocido (o depender de un tipo desconocido). Como ya sabemos, este tipo desconocido luego será un subtipo incógnita de Enum<X> .  Por lo tanto, en el tipo parametrizado Enum<X> , estos métodos involucran el subtipo incógnita , y se heredan en el subtipo incógnita .  El comparar con El método es un ejemplo de tal método; se hereda de la superclase a cada subclase y tiene una firma específica de subclase en cada caso.

En resumen, la declaración “ Enum<E extiende Enum<E>> ” se puede descifrar como: enumeración es un tipo genérico del que solo se pueden crear instancias para sus subtipos, y esos subtipos heredarán algunos métodos útiles, algunos de los cuales toman argumentos específicos del subtipo (o dependen del subtipo).

Parámetros de clases de tipo de límite inferior

Los parámetros de tipo pueden tener varios límites, como en clase Box<T extiende Número> {...} . Pero un parámetro de tipo no puede tener límite inferior, es decir, una construcción como caja de clase<T excelente Número> {...} no está permitido.  ¿Por qué no?  La respuesta es: no tiene sentido porque no te compraría nada, si estuviera permitido.  Veamos por qué los parámetros de tipo de límite inferior de las clases son confusos explorando lo que significa un límite superior en un parámetro de tipo.

El límite superior de un parámetro de tipo tiene tres efectos:

    1. Creación de instancias restringida. El límite superior restringe el conjunto de tipos que se pueden utilizar para la creación de instancias del tipo genérico.  Si declaramos un clase Box<T extiende Número> {...} entonces el compilador se aseguraría de que solo los subtipos de Número se puede utilizar como argumento de tipo.  Es decir, un Caja<Número> o un Caja<Largo> está permitido, pero un Caja<Objeto> o Caja<Cadena> sería rechazado

Ejemplo (de creación de instancias restringida debido a un límite superior en un parámetro de tipo):
class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   ...
}  prueba de clase { 
   public static void main(String[] args) { 
       Box<Long>    boxOfLong   = new Box<Long>(0L);   // bien
       Box<String>  boxOfString = new Box<String>(""); // error: la cadena no está dentro de los límites
   }
}

  1. Acceso a miembros no estáticos. El límite superior da acceso a todos los métodos y campos públicos no estáticos del límite superior.  En la implementación de nuestra clase Box<T extiende Número> {...} podemos invocar todos los métodos públicos no estáticos definidos en la clase Número , como valorint() por ejemplo.  Sin el límite superior, el compilador rechazaría cualquier invocación de este tipo. Ejemplo (de acceso a miembros no estáticos debido a un límite superior en un parámetro de tipo):

class Box<T extends Number> { 
   private T value; 
   public Box(T t) { value = t; } 
   public int increment() { return value.intValue()+1; } // <= sería un error sin el número vinculado
   ...
} 

    1. Escriba Borrado. El límite superior más a la izquierda se utiliza para borrar tipos y reemplaza el parámetro de tipo en el código de bytes.  en nuestro clase Box<T extiende Número> {...} todas las ocurrencias de t sería reemplazado por el límite superior Número .  Por ejemplo, si la clase Caja tiene un campo privado de tipo t y un método conjunto vacío (contenido T) para configurar este campo privado, entonces el campo sería de tipo Número después del borrado de tipo y el método se traduciría a un método conjunto vacío (contenido numérico) .

Ejemplo (de uso del límite superior en un parámetro de tipo en el borrado de tipo – antes tipo borrado):
caja de clase< T extiende el número > { 
   privado t valor; 
   public Box( t t) { valor = t; } 
   ...
} 

      Ejemplo (de uso del límite superior en un parámetro de tipo en el borrado de tipo –

después

      tipo borrado):

 

caja de clase { 
   privado Número valor; 
   public Box( Número t) { valor = t; } 
   ...
}

    Además, el límite superior situado más a la izquierda aparece en otras ubicaciones, como modelos insertados automáticamente y métodos de puente.

Si se permitieran límites inferiores en los parámetros de tipo, ¿qué efectos secundarios tendrían o deberían tener? Si una construcción como caja de clase<T excelente Número> {...} estuvieran permitidos, ¿qué significaría?  ¿Qué significarían los 3 efectos secundarios de un límite de parámetro de tipo superior (creación de instancias restringida, acceso a miembros no estáticos, borrado de tipo) para un límite inferior?

Instancias restringidas. El compilador podría restringir el conjunto de tipos que se pueden utilizar para la creación de instancias del tipo genérico con un parámetro de tipo de límite inferior.  Por ejemplo, el compilador podría permitir instanciaciones como Caja<Número> y Caja<Objeto> de un Caja<T excelente Número> y rechazar instanciaciones como Caja<Largo> o Caja<Corto> .  Este sería un efecto acorde con el efecto secundario restrictivo descrito para los límites de parámetros de tipo superior.

Acceso a miembros no estáticos. Un límite de parámetro de tipo inferior no da acceso a ningún método particular más allá de los heredados de la clase. Objeto . En el ejemplo de Caja<T excelente Número> los supertipos de Número no tienen nada en común, excepto que son tipos de referencia y por lo tanto subtipos de Objeto .  El compilador no puede asumir que el campo de tipo t es de tipo Número o un subtipo del mismo.  En cambio, el campo de tipo t puede ser de cualquier supertipo de Número , como Serializable o Objeto .  La invocación de un método como valorint() ya no es seguro para tipos y el compilador tendría que rechazarlo. Como consecuencia, el límite de parámetro de tipo inferior no daría acceso a miembros no estáticos más allá de los definidos en la clase. Objeto y por tanto tiene el mismo efecto que “sin límites”.

Tipo de borrado .  Siguiendo esta línea de lógica, no tiene sentido reemplazar las apariciones del parámetro de tipo por su límite inferior más a la izquierda.  Declarar un método como el constructor Caja( t t) como constructor Caja( Número t) no tiene sentido, considerando que t es reemplazado por un supertipo de Número .  Un Objeto podría pasarse correctamente al constructor en una instanciación Caja<Objeto> y el constructor lo rechazaría.  Esto estaría completamente equivocado.   Entonces, el borrado de tipo reemplazaría todas las apariciones de la variable de tipo. t por tipo Objeto , y no por su límite inferior.  Una vez más, el límite inferior tendría el mismo efecto que “sin límite”.

¿Quieres saber qué significaría si ambos bajaran? _y_ ¿Se permitieron límites superiores?   Personalmente, no quiero ni pensar en ello y preferiría archivarlo como “no manejable”, si me lo permite.

La conclusión es: todo eso es un “ excelente “Lo que te compraría es la restricción de que sólo los supertipos de Número se pueden utilizar como argumentos de tipo.  E incluso eso a menudo se malinterpreta.  NO significaría que clase Box<T super Número> {...} contiene sólo instancias de supertipos de Número .  Todo lo contrario, ¡como lo demuestra el siguiente ejemplo!

Ejemplo (de uso del límite superior en un parámetro de tipo en el borrado de tipo – antes tipo borrado):

caja de clase< T súper número > {
   privado t valor;
   public Box( t t) { valor = t; }
   ...
}

Ejemplo (de uso del límite superior en un parámetro de tipo en el borrado de tipo – después tipo borrado):

caja de clase {
   privado Objeto valor;
   public Box( Objeto t) { valor = t; }
   ...
}

A caja de clase<T excelente Número> {...} sería traducido por borrado de tipo a un Caja que contiene un Objeto El campo y su constructor se traducirían a Caja (Objeto t) . Eso es fundamentalmente diferente de un caja de clase<T se extiende Número> {...} , que se traduciría a un Caja que contiene un Número El campo y su constructor se traducirían a Caja(Número t) . En consecuencia, un Caja<Número> instanciado desde un clase Box<T extiende Número> {...} sería diferente de un Caja<Número> instanciación de un clase Box<T super Número> {...} , lo que probablemente cause confusión.  Por este motivo, los límites inferiores no tienen sentido en los parámetros de tipo de las clases.

 


 

Parámetros de métodos de tipo de límite inferior

Junto con los métodos y sus tipos de argumentos, en ocasiones puede resultar útil un parámetro de tipo con un límite inferior.

Ejemplo (de un método que se beneficiaría de un parámetro de tipo con un límite inferior):

clase Par<X,Y> {
   private X first;
   private Y second;
   ...
   public < Una súper X , B súper Y > B addToMap(Map<A,B> map) { // error: el parámetro de tipo no puede tener un límite inferior
      return map.put(first, second);
   }
}
prueba de clase { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = pair.addToMap(map);
   }
}

El agregarToMap() El método agrega el contenido del par a un mapa.  Cualquier mapa que pueda contener supertipos de incógnita y Y haría.  el mapa poner() El método devuelve el valor encontrado en el mapa para la clave dada, si ya existe una entrada clave-valor para la clave en el mapa.  El valor de retorno del mapa. poner() El método será devuelto desde el agregarToMap() método.  En estas circunstancias, uno quisiera declarar el método como se muestra arriba: El mapa está parametrizado con supertipos de los parámetros de tipo del par y el agregarToMap() El tipo de retorno del método es el tipo de valor del mapa.

Dado que el compilador no permite límites inferiores en los parámetros de tipo, necesitamos una solución alternativa.

Una solución que me viene a la mente es el uso de un comodín, porque los comodines pueden tener un límite inferior.  A continuación se ofrece una solución alternativa mediante el uso de un comodín.

Ejemplo (de una solución alternativa para el ejemplo anterior usando comodines):

clase Par<X,Y> {
   private X first;
   private Y second;
   ...
   public Objeto addToMap(Mapa< ? Súper X , ? súper Y mapa >) { 
      return map.put(first, second);
   }
}
prueba de clase { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = (Número) par.addToMap(mapa);
   }
}

Funciona, excepto que no hay forma de declarar el tipo de devolución deseado.  Sería el supertipo de Y que el compilador captura del tipo de mapa, pero no hay sintaxis para especificarlo.  No debemos declarar el tipo de devolución a “ ? súper Y “, porque “ ? súper Y ” es un comodín y no un tipo y, por lo tanto, no está permitido como tipo de devolución. No tenemos otra opción y debemos usar Objeto en lugar de ello, como el tipo de retorno de nuestro método.  Este tipo de devolución bastante inespecífico obliga a su vez a quienes llaman al agregarToMap() método para convertir el valor de retorno hacia abajo desde Objeto a su tipo real.  Esto no es exactamente lo que teníamos en mente.

Otra solución es el uso de métodos estáticos.  A continuación se ofrece una solución alternativa con un método estático en lugar de uno no estático.

Ejemplo (de una solución alternativa para el ejemplo anterior utilizando un método estático):

clase Par<X,Y> {
   private X first;
   private Y second;
   ...
   public estático < A , B ,INCÓGNITA extiende A ,Y extiende B > B addToMap(Par<X,PAR Y>, Map< A , B mapa >) {
      return map.put(pair.first,pair.second);
   }
}
prueba de clase { 
   public static void main(String[] args) { 
     Pair<String,Long> pair = new Pair<>("ABC",42L);
     Map<CharSequence, Number> map = HashMap<CharSequence, Number>();
     Number number = Par .addToMap( par ,mapa);
   }
}

el generico agregarToMap() El método tiene cuatro parámetros de tipo: dos marcadores de posición. incógnita y Y para el tipo de par y dos marcadores de posición A y B para el tipo de mapa. A y B son supertipos de incógnita y Y , porque incógnita y Y   are declared with A y B como sus límites superiores. (Tenga en cuenta que los parámetros de tipo del método genérico incógnita y Y no tiene nada que ver con el Par clase incógnita y Y parámetros.  los nombres incógnita y Y se reutilizan para el método genérico para hacerlos fácilmente reconocibles como parámetros de tipo del par. Usando cuatro parámetros de tipo podemos declarar el tipo de retorno preciso como lo deseemos: es el mismo tipo que el tipo de valor del mapa.

Ejemplo (de uso ilegal de parámetro de tipo en el contexto estático de una clase genérica):

clase alguna clase <T> {
  // inicializador estático, campo estático, método estático
  estático {
    SomeClass< t Prueba > = nueva SomeClass< t >(); // error
  }
  privado estático t globalInfo;          // error
  public  estático t getGlobalInfo() {        // error
    return globalInfo;
  }
  // inicializador no estático, campo no estático, método no estático
  { 
    SomeClass< t Prueba > = nueva SomeClass< t >();
  }
  privado        t información local;
  public         t getLocalInfo() { 
    return localInfo;
  }
  // tipos anidados estáticos
  public estático clase El error extiende la excepción {
    private final t info;           // error
    public Failure( t t) { información = t; }      // error
    public t getInfo() { return info; }    // error
   }
  privado interfaz Copiable {
    t copy();                       // error
  }
  privado enumeración Estado {
    VALID, INVALID;
    privado t información; // error
    public void setInfo( t t) { información = t; } // error
    public t getInfo() { return info; }    // error
  }
  // tipos anidados no estáticos
  public clase Accesorio {
    public t getInfo() { return información local; }
  }
}

El ejemplo ilustra que el parámetro de tipo no se puede utilizar en el contexto estático de una clase genérica.  También muestra que las interfaces anidadas y los tipos de enumeración se consideran miembros de tipo estático de la clase.  Sólo las clases internas, es decir, las clases anidadas no estáticas, pueden utilizar el parámetro de tipo de la clase genérica adjunta.

El alcance del parámetro de tipo de una interfaz es la definición completa de la interfaz, excepto cualquier campo o tipo anidado.  Esto se debe a que los campos y tipos anidados definidos en una interfaz son implícitamente estáticos.

Ejemplo (de uso ilegal de parámetro de tipo en una interfaz genérica):

interfaz alguna interfaz <T> {
  // campo
  SomeClass< t Valor > = nuevo SomeClass< t >();  // error
  // tipo anidado
  clase Accesorio {
    public t getInfo() {            // error
      return value.getGlobalInfo();
    }
  }
  // metodos
  t obtenerValor();
}

El ejemplo muestra que los campos de una interfaz son implícitamente estáticos, por lo que el parámetro de tipo no se puede utilizar en ninguna parte de la declaración de un campo de una interfaz genérica.  De manera similar, la clase anidada se considera una clase anidada estática, no una clase interna, y por esta razón el uso del parámetro de tipo en cualquier parte de la clase anidada es ilegal.

Solución alternativa para clases e interfaces estáticas anidadas:

En el caso de clases e interfaces anidadas estáticas, esto no es una calamidad importante.  Como solución alternativa, podemos generar la clase estática o la interfaz misma.

Ejemplo (solución alternativa: generar el tipo estático anidado):

envoltura de clase <T> {
  private final t el objeto;

  public Wrapper( t t) { elObjeto = t; }
  public t getWrapper() { devolver elObjeto; }   public Immutable <T> hacerInmutable() {
        return new Immutable <T> (elObjeto);
  }
  public Mutable <T> hacerMutable() {
        return new Mutable <T> (elObjeto);
  }
  private static <A> A makeClone(A theObject) { ... }

  public static final class Immutable <A> { // ahora es una clase genérica
        private final A el objeto;

        public Immutable( A argumento) {
            theObject = makeClone(arg);
        }
        public A getWrappedItem() {
            return makeClone(theObject);
        }
  }
  public static class Mutable <A> {
        ... similar ...
  }
}

No existe tal solución para los tipos de enumeración anidados porque no pueden ser genéricos.

Solución alternativa para clases estáticas anidadas:

Si el tipo estático anidado es una clase, una solución alternativa sería convertir la clase estática en una clase interna no estática.

Ejemplo (solución alternativa: utilice una clase interna):

envoltura de clase <T> {
  private final t el objeto;

  public Wrapper( t t) { elObjeto = t; }
  public t getWrapper() { devolver elObjeto; }   public Mutable makeMutable() {
      return este. nuevo Mutable(elObjeto);
  }
  public Immutable makeImmutable() {
      return este. nuevo Inmutable (el Objeto);
  }
  private static <A> A makeClone(A theObject) { ... }

  public final class Immutable {  // ya no es una clase estática
      private final t el objeto;

      public Immutable( t argumento) {
          theObject = makeClone(arg);
      }
      public t getWrappedItem() {
          return makeClone(theObject);
      }
  }
  public class Mutable 
        ... similar ...
  }
}

Esta solución viene con una cierta cantidad de gastos generales porque todas las clases internas tienen una referencia oculta a una instancia del tipo externo, que en este ejemplo no necesitan ni aprovechan; la referencia oculta es sólo equipaje.  A menudo, las clases internas se combinan con interfaces para mantener la clase interna como un detalle de implementación privado de la clase adjunta.  Podemos hacer lo mismo aquí.

Ejemplo (la solución alternativa anterior se perfeccionó):

envoltura de clase <T> {
  private final t el objeto;

  public Wrapper( t t) { elObjeto = t; }
  public t getWrapper() { devolver elObjeto; }   public Mutable <T> hacerMutable() {
      return this.new Mutable(theObject);
  }
  public Immutable <T> hacerInmutable() {
      return this.new Immutable(theObject);
  }
  private static <A> A makeClone(A theObject) { ... }

  interfaz pública Inmutable<S> {
      S getWrappedItem();
  }
  interfaz pública Mutable<S> {
      S getWrappedItem();
      void setWrappedItem(S arg);
  }
  privado Implementación inmutable de clase final implementa Inmutable<T> { 

      private final T theObject;

      public ImmutableImplementation(T arg) {
          theObject = makeClone(arg);
      }
      public T getWrappedItem() {
          return makeClone(theObject);
      }
  }
  privado clase MutableImplementación implementa Mutable<T> { 
        ... similar ...
  }
}

Como puede ver, las interfaces públicas anidadas deben ser genéricas, mientras que las clases privadas internas pueden usar el parámetro de tipo de la clase adjunta.

 

 

 

 

 

Deja una respuesta

Descubra más en el blog de Velislav Krastev

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Continuar leyendo