2012-05-26 11 views
16

suponer que el perro clase extiende clase Animal: por qué no se permite esta declaración polimórfica:¿Por qué el polimorfismo no trata las colecciones genéricas y las matrices simples de la misma manera?

List<Animal> myList = new ArrayList<Dog>(); 

Sin embargo, se permitió con matrices de civil:

Animal[] x=new Dog[3]; 
+6

Hay quienes sostienen que permitir que los arrays de hacerlo era una mala idea. – Jeffrey

+3

Tipo de borrado y genéricos, no colecciones, son la razón. – duffymo

+1

La respuesta corta es "los contenedores genéricos no son matrices". La respuesta más larga, como Duffymo ya sugirió, es "borrados": http://code.stephenmorley.org/articles/java-generics-type-erasure/ – paulsm4

Respuesta

21

Las razones para esto se basan en cómo implementa Java los genéricos.

Un matrices Ejemplo

Con los arreglos que se puede hacer esto (las matrices de covarianza son como otros han explicado)

Integer[] myInts = {1,2,3,4}; 
Number[] myNumber = myInts; 

Pero, ¿qué pasaría si intenta hacer esto?

Number[0] = 3.14; //attempt of heap pollution 

Esta última línea se compilará bien, pero si se ejecuta este código, se podría obtener una ArrayStoreException. Porque intenta poner un doble en una matriz de enteros (independientemente de que se acceda a través de una referencia numérica).

Esto significa que puede engañar al compilador, pero no puede engañar al sistema del tipo de tiempo de ejecución. Y esto es así porque las matrices son lo que llamamos tipos confirmables. Esto significa que, en tiempo de ejecución, Java sabe que esta matriz se instancia realmente como una matriz de enteros a los que simplemente se accede mediante una referencia de tipo Number[].

Entonces, como puede ver, una cosa es el tipo real del objeto, otra cosa es el tipo de referencia que utiliza para acceder a ella, ¿no?

El problema con Java Generics

Ahora, el problema con los tipos genéricos de Java es que la información de tipo se descarta por el compilador y no está disponible en tiempo de ejecución. Este proceso se llama type erasure. Hay buenas razones para implementar genéricos como este en Java, pero esa es una historia larga, y tiene que ver con la compatibilidad binaria con el código preexistente.

Pero el punto importante aquí es que, dado que, en el tiempo de ejecución no hay información de tipo, no hay forma de garantizar que no estamos cometiendo contaminación acumulativa.

Por ejemplo,

List<Integer> myInts = new ArrayList<Integer>(); 
myInts.add(1); 
myInts.add(2); 

List<Number> myNums = myInts; //compiler error 
myNums.add(3.14); //heap polution 

Si el compilador Java no le impide hacer esto, el sistema de tipos en tiempo de ejecución no se puede parar o bien, porque no hay manera, en tiempo de ejecución, para determinar que esta lista era se supone que es una lista de enteros solamente. El tiempo de ejecución de Java le permite colocar lo que desee en esta lista, cuando solo debe contener enteros, porque cuando se creó, se declaró como una lista de enteros.

Como tal, los diseñadores de Java se aseguraron de que no se puede engañar al compilador. Si no puede engañar al compilador (como podemos hacer con las matrices) tampoco puede engañar al sistema de tipo de tiempo de ejecución.

Como tal, decimos que los tipos genéricos son no no reificables.

Evidentemente, esto obstaculizaría el polimorfismo.Consideremos el siguiente ejemplo:

static long sum(Number[] numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Ahora se puede utilizar de esta manera:

Integer[] myInts = {1,2,3,4,5}; 
Long[] myLongs = {1L, 2L, 3L, 4L, 5L}; 
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0}; 

System.out.println(sum(myInts)); 
System.out.println(sum(myLongs)); 
System.out.println(sum(myDoubles)); 

Pero si se intenta poner en práctica el mismo código con colecciones genéricas, que no tendrá éxito:

static long sum(List<Number> numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Obtendrá errores de compilación si intenta ...

List<Integer> myInts = asList(1,2,3,4,5); 
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L); 
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0); 

System.out.println(sum(myInts)); //compiler error 
System.out.println(sum(myLongs)); //compiler error 
System.out.println(sum(myDoubles)); //compiler error 

La solución es aprender a usar dos poderosas características de los genéricos de Java conocidos como covarianza y contravarianza.

covarianza

Con covarianza se puede leer los artículos en una estructura, pero no se puede escribir nada en él. Todas estas son declaraciones válidas.

List<? extends Number> myNums = new ArrayList<Integer>(); 
List<? extends Number> myNums = new ArrayList<Float>() 
List<? extends Number> myNums = new ArrayList<Double>() 

Y se puede leer desde myNums:

Number n = myNums.get(0); 

Debido a que usted puede estar seguro de que cualquiera que sea la lista real contiene, se puede upcasted a un número (después de todo, cualquier cosa que se extiende número es un número , ¿no?)

Sin embargo, no está permitido poner nada en una estructura covariante.

myNumst.add(45L); //compiler error 

Esto no se permitiría, porque Java no puede garantizar cuál es el tipo real del objeto en la estructura genérica. Puede ser cualquier cosa que extienda Number, pero el compilador no puede estar seguro. Para que pueda leer, pero no escribir.

contravarianza

Con contravarianza puede hacer lo contrario. Puede poner las cosas en una estructura genérica, pero no puede leer desde allí.

List<Object> myObjs = new List<Object(); 
myObjs.add("Luke"); 
myObjs.add("Obi-wan"); 

List<? super Number> myNums = myObjs; 
myNums.add(10); 
myNums.add(3.14); 

En este caso, la naturaleza real del objeto es una lista de objetos y, a través de contravarianza, puede poner los números en ella, básicamente porque todos los números tienen como objeto su ancestro común. Como tal, todos los números son objetos, y por lo tanto esto es válido.

Sin embargo, no puede leer con seguridad nada de esta estructura contravariante suponiendo que obtendrá un número.

Number myNum = myNums.get(0); //compiler-error 

Como se puede ver, si el compilador permitió a escribir esta línea, se llega a un ClassCastException en tiempo de ejecución.

Obtener/Poner Principio

Como tal, el uso de covarianza cuando sólo se van a tomar valores genéricos de una estructura, utilice contravarianza cuando sólo se van a poner los valores genéricos en una estructura y el uso de los genéricos exacta escriba cuando tenga la intención de hacer ambas cosas.

El mejor ejemplo que tengo es el siguiente que copia cualquier tipo de números de una lista a otra. Solo obtiene elementos de la fuente, y solo pone elementos en el destino.

public static void copy(List<? extends Number> source, List<? super Number> destiny) { 
    for(Number number : source) { 
     destiny.add(number); 
    } 
} 

Gracias a los poderes de la covarianza y contravarianza Esto funciona para un caso como este:

List<Integer> myInts = asList(1,2,3,4); 
List<Double> myDoubles = asList(3.14, 6.28); 
List<Object> myObjs = new ArrayList<Object>(); 

copy(myInts, myObjs); 
copy(myDoubles, myObjs); 
1

Eso es muy interesante. No puedo decir la respuesta, pero esto funciona si usted quiere poner una lista de los perros en la lista de animales:

List<Animal> myList = new ArrayList<Animal>(); 
myList.addAll(new ArrayList<Dog>()); 
+1

Eso funciona porque 'addAll()' pasa por encima de la colección que se pasa y llama a 'List.add()' en cada elemento de la colección. Y una 'List ' absolutamente puede tener elementos que sean cualquier subclase de 'Animal'. – QuantumMechanic

+0

@QuantumMechanic Sí, sabía por qué funciona, acabo de publicarlo en caso de que se atascara en una solución alternativa. –

1

La manera de codificar la versión colecciones por lo que compila es:

List<? extends Animal> myList = new ArrayList<Dog>(); 

La razón por la que no necesita esto con las matrices se debe al borrado de tipos: las matrices de las no primitivas son todas Object[] y las matrices de Java no son una clase tipada (como las colecciones). El lenguaje nunca fue diseñado para atenderlo.

Las matrices y los genéricos no se mezclan.

+0

Esto me suena al revés, pero tal vez esté malinterpretando lo que dices. Usted dice que "las matrices de no primitivos son todas Objeto [] y las matrices Java no son una clase tipada (como las colecciones)", pero ¿no es todo lo contrario? Las matrices de Java mantienen su tipo en tiempo de ejecución, pero las colecciones no se deben a borrado de tipo. –

5

Las matrices difieren de los tipos genéricos de dos maneras importantes. Primero, las matrices son covariantes. Esta palabra de sonido aterrador significa simplemente que si Sub es un subtipo de Super, entonces el tipo de matriz Sub [] es un subtipo de Super []. Los genéricos, por el contrario, son invariables: para dos tipos distintos Type1 y Type2, List <Type1> no es un subtipo ni un supertipo de la lista <Type2>.

[..] La segunda gran diferencia entre matrices y genéricos es que las matrices están reificadas [JLS, 4.7]. Esto significa que las matrices conocen y aplican sus tipos de elementos en el tiempo de ejecución .

[..] Los genéricos, por el contrario, se implementan mediante el borrado [JLS, 4.6]. Esto significa que aplican sus restricciones de tipo solo al compilar el tiempo y descartar (o borrar) la información de su tipo de elemento en el tiempo de ejecución. Erasure es lo que permite que los tipos genéricos interactúen libremente con el código heredado que no usa los genéricos (artículo 23). Debido a estas diferencias fundamentales, los arreglos y los genéricos no mezclan bien . Por ejemplo, es ilegal crear una matriz de un tipo genérico, un tipo parametrizado de o un parámetro de tipo. Ninguna de estas expresiones de creación de matriz es legal: nueva Lista <E> [], nueva Lista <Cadena> [], nueva E []. Todo dará como resultado errores genéricos de creación de matriz en tiempo de compilación.]

Prentice Hall - A partir de Java 2ª Edición

1
List<Animal> myList = new ArrayList<Dog>(); 

no es posible porque en ese caso se puede poner gatos en perros:

private void example() { 
    List<Animal> dogs = new ArrayList<Dog>(); 
    addCat(dogs); 
    // oops, cat in dogs here 
} 

private void addCat(List<Animal> animals) { 
    animals.add(new Cat()); 
} 

Por otro lado

List<? extends Animal> myList = new ArrayList<Dog>(); 

es posible, pero en ese caso no se pueden utilizar métodos con paramteres genéricos (sólo se acepta nula):

private void addCat(List<? extends Animal> animals) { 
    animals.add(null);  // it's ok 
    animals.add(new Cat()); // compilation error here 
} 
1

La última respuesta es que es así porque Java se especifica de esa manera. Más precisamente, porque esa es la forma en que evolucionó la especificación Java *.

no podemos decir cuál era el pensamiento actual de los diseñadores de Java, pero considera esto:

List<Animal> myList = new ArrayList<Dog>(); 
myList.add(new Cat()); // compilation error 

frente

Animal[] x = new Dog[3]; 
x[0] = new Cat();  // runtime error 

El error de ejecución que será lanzado aquí es ArrayStoreException. Esto podría ser lanzado en cualquier asignación a cualquier conjunto de no primitivos.

Uno podría argumentar que el manejo de Java de los tipos de matriz es incorrecto ... debido a ejemplos como el anterior.

* Tenga en cuenta que el tipado de las matrices de Java se especificó antes de Java 1.0, pero los tipos genéricos solo se agregaron en Java 1.5. El lenguaje Java tiene un meta-requisito general de compatibilidad con versiones anteriores; es decir, las extensiones de idioma no deben romper el código anterior. Entre otras cosas, eso significa que no es posible corregir errores históricos, como la forma en que funciona la matriz de tipos. (Suponiendo que se acepta que fue un error ...)


Por el lado de tipo genérico, escriba el borrado des no explica el error de compilación. El error de compilación se está produciendo realmente debido a la comprobación del tipo de compilación que utiliza los tipos genéricos no borrados.

Y, de hecho, puede subvertir el error de compilación utilizando un desencheque de tipocast (ignore la advertencia) y termine en una situación en la que su ArrayList<Dog> realmente contiene Cat objetos en tiempo de ejecución. (¡Eso es una consecuencia del borrado de tipos!) Pero tenga en cuenta que su subversión de los errores de compilación usando una conversión no comprobada puede ocasionar errores en el tiempo de ejecución en lugares inesperados ... si se equivoca. Es por eso que es una mala idea.

0

En los días anteriores a los genéricos, escribir una rutina que pudiera ordenar matrices de tipo arbitrario hubiera requerido ya sea (1) crear matrices de solo lectura en forma covariante e intercambiar o reorganizar elementos de forma independiente del tipo, o (2) crear matrices de lectura y escritura de forma covariante que se puedan leer de forma segura, y que se puedan escribir de forma segura con elementos que se leyeron previamente de la misma matriz o (3) que las matrices proporcionen algunos medios independientes de tipo para comparar elementos.Si se han incluido interfaces genéricas covariantes y contravariantes en el lenguaje desde el principio, el primer enfoque podría haber sido el mejor, ya que habría evitado la necesidad de realizar la verificación de tipo en tiempo de ejecución, así como la posibilidad de que dichos controles de tipo podría fallar No obstante, dado que tal soporte genérico no existía, no había nada a lo que se pudiera aplicar sensiblemente una matriz de tipo derivada distinta de una matriz de tipo base.

Cuestiones relacionadas