¿Por qué no puedo crear tipos de matriz genéricos en Java?

273

¿Cuál es la razón por la cual Java no nos permite hacer

private T[] elements = new T[initialCapacity];

Podría entender que .NET no nos permitió hacer eso, ya que en .NET tiene tipos de valor que en tiempo de ejecución pueden tener diferentes tamaños, pero en Java todo tipo de T serán referencias de objeto, por lo tanto tienen el mismo tamaño ( corrígeme si me equivoco).

¿Cuál es la razón?

elysium devorado
fuente
29
¿De qué estás hablando? Absolutamente puede hacer esto en .NET. - Estoy aquí tratando de descubrir por qué no puedo hacerlo en Java.
BrainSlugs83
@ BrainSlugs83: agregue un enlace a algún ejemplo de código o tutorial que lo demuestre.
MasterJoe2
También vea - stackoverflow.com/questions/21577493/…
MasterJoe2
1
@ MasterJoe2 el código anterior en la pregunta del OP es a qué me refiero. Funciona bien en C #, pero no en Java. - La pregunta dice que no funciona en ninguno de los dos, lo cual es incorrecto. - No estoy seguro de que haya valor en discutirlo más a fondo.
BrainSlugs83

Respuestas:

204

Se debe a que las matrices de Java (a diferencia de los genéricos) contienen, en tiempo de ejecución, información sobre su tipo de componente. Por lo tanto, debe conocer el tipo de componente cuando crea la matriz. Como no sabe qué Thay en tiempo de ejecución, no puede crear la matriz.

newacct
fuente
29
¿Pero qué pasa con el borrado? ¿Por qué no se aplica eso?
Qix - MONICA FUE MALTRATADA el
14
¿Cómo ArrayList <SomeType>lo hace entonces?
Thumbz
10
@Thumbz: ¿Quieres decir new ArrayList<SomeType>()? Los tipos genéricos no contienen el parámetro de tipo en tiempo de ejecución. El parámetro tipo no se usa en la creación. No hay diferencia en el código generado por new ArrayList<SomeType>()o new ArrayList<String>(), o new ArrayList()en absoluto.
newacct
8
Estaba preguntando más sobre cómo ArrayList<T>funciona con su ' private T[] myArray. En algún lugar del código, debe tener una matriz de tipo genérico T, entonces, ¿cómo?
Thumbz
21
@Thumbz: No tiene una matriz de tipo de tiempo de ejecución T[]. Tiene una matriz de tipo de tiempo de ejecución Object[], y 1) el código fuente contiene una variable de Object[](así es en la última fuente de Oracle Java); o 2) el código fuente contiene una variable de tipo T[], lo cual es una mentira, pero no causa problemas debido a que Tse borra dentro del alcance de la clase.
newacct
137

Citar:

Las matrices de tipos genéricos no están permitidas porque no son sólidas. El problema se debe a la interacción de las matrices de Java, que no son estáticamente sólidas, pero se comprueban dinámicamente, con genéricos, que son estáticamente sólidas y no se comprueban dinámicamente. Así es como podría explotar la escapatoria:

class Box<T> {
    final T x;
    Box(T x) {
        this.x = x;
    }
}

class Loophole {
    public static void main(String[] args) {
        Box<String>[] bsa = new Box<String>[3];
        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3); // error not caught by array store check
        String s = bsa[0].x; // BOOM!
    }
}

Habíamos propuesto resolver este problema utilizando matrices estáticamente seguras (también conocidas como Variance) que fueron rechazadas por Tiger.

- gafter

(Creo que es Neal Gafter , pero no estoy seguro)

Véalo en contexto aquí: http://forums.sun.com/thread.jspa?threadID=457033&forumID=316

revs Bart Kiers
fuente
3
Tenga en cuenta que lo hice un CW ya que la respuesta no es mía.
Bart Kiers
10
Esto explica por qué podría no ser seguro para escribir. Pero el compilador podría advertir los problemas de seguridad de tipos. El hecho es que ni siquiera es posible hacerlo, por casi la misma razón por la que no puedes hacerlo new T(). Cada matriz en Java, por diseño, almacena el tipo de componente (es decir T.class) dentro de ella; por lo tanto, necesita la clase de T en tiempo de ejecución para crear dicha matriz.
newacct
2
Todavía puede usar new Box<?>[n], lo que a veces puede ser suficiente, aunque no ayudaría en su ejemplo.
Bartosz Klimek
1
@BartKiers No lo entiendo ... esto todavía no se compila (java-8): Box<String>[] bsa = new Box<String>[3];¿supongo que algo cambió en java-8 en adelante?
Eugene
1
@Eugene, las matrices de tipos genéricos específicos simplemente no están permitidas porque pueden conducir a una pérdida de seguridad de tipo como se demuestra en la muestra. No está permitido en ninguna versión de Java. La respuesta comienza como "Las matrices de tipos genéricos no están permitidas porque no son sólidas"
Granate
47

Al no proporcionar una solución decente, terminas con algo peor en mi humilde opinión.

La solución común es la siguiente.

T[] ts = new T[n];

se reemplaza con (suponiendo que T extiende Object y no otra clase)

T[] ts = (T[]) new Object[n];

Prefiero el primer ejemplo, sin embargo, más tipos académicos parecen preferir el segundo, o simplemente prefieren no pensar en ello.

La mayoría de los ejemplos de por qué no puede usar un Object [] se aplican igualmente a List o Collection (que son compatibles), por lo que los veo como argumentos muy pobres.

Nota: esta es una de las razones por las que la biblioteca de Colecciones en sí no se compila sin advertencias. Si este caso de uso no puede ser soportado sin advertencias, algo se rompe fundamentalmente con el modelo genérico en mi humilde opinión.

Peter Lawrey
fuente
66
Tienes que tener cuidado con el segundo. Si devuelve la matriz creada de tal manera a alguien que espera, por ejemplo, a String[](o si la almacena en un campo de tipo de acceso público T[]y alguien la recupera), obtendrá una ClassCastException.
newacct
44
Voté esta respuesta porque su ejemplo preferido no está permitido en Java y su segundo ejemplo puede lanzar una ClassCastException
José Roberto Araújo Júnior
55
@ JoséRobertoAraújoJúnior Está bastante claro que el primer ejemplo necesita ser reemplazado por el segundo ejemplo. Sería más útil para usted explicar por qué el segundo ejemplo puede arrojar una ClassCastException ya que no sería obvio para todos.
Peter Lawrey 01 de
3
@PeterLawrey Creé una pregunta con respuesta propia que muestra por qué T[] ts = (T[]) new Object[n];es una mala idea: stackoverflow.com/questions/21577493/…
José Roberto Araújo Júnior
1
@MarkoTopolnik Debería recibir una medalla por responder a todos sus comentarios para explicar lo mismo que ya dije, lo único que cambió de mi razón original es que pensé que dijo que T[] ts = new T[n];era un ejemplo válido. Mantendré el voto porque su respuesta puede causar problemas y confusiones a otros desarrolladores y también está fuera de tema. Además, dejaré de comentar sobre esto.
José Roberto Araújo Júnior
38

Las matrices son covariantes

Se dice que las matrices son covariantes, lo que básicamente significa que, dadas las reglas de subtipo de Java, una matriz de tipo T[]puede contener elementos de tipo To cualquier subtipo de T. Por ejemplo

Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);

Pero no solo eso, las reglas de subtipo de Java también establecen que una matriz S[]es un subtipo de la matriz T[]si Ses un subtipo de T, por lo tanto, algo como esto también es válido:

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

Porque de acuerdo con las reglas de subtipo en Java, una matriz Integer[]es un subtipo de una matriz Number[]porque Integer es un subtipo de Número.

Pero esta regla de subtipo puede llevar a una pregunta interesante: ¿qué pasaría si intentamos hacer esto?

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

Esta última línea se compilaría bien, pero si ejecutamos este código, obtendríamos un ArrayStoreExceptionporque estamos tratando de poner un doble en una matriz entera. El hecho de que estemos accediendo a la matriz a través de una referencia de Número es irrelevante aquí, lo que importa es que la matriz es una matriz de enteros.

Esto significa que podemos engañar al compilador, pero no podemos engañar al sistema de tipo de tiempo de ejecución. Y esto es así porque las matrices son lo que llamamos un tipo reificable. Esto significa que en tiempo de ejecución Java sabe que esta matriz en realidad se instancia como una matriz de enteros a los que simplemente se accede a través de una referencia de tipo Number[].

Entonces, como podemos ver, una cosa es el tipo real del objeto, otra cosa es el tipo de referencia que usamos para acceder a él, ¿verdad?

El problema con los genéricos de Java

Ahora, el problema con los tipos genéricos en Java es que la información de tipo para los parámetros de tipo es descartada por el compilador después de la compilación del código; por lo tanto, este tipo de información no está disponible en tiempo de ejecución. Este proceso se llama borrado de tipo . Hay buenas razones para implementar genéricos como este en Java, pero esa es una larga historia, y tiene que ver con la compatibilidad binaria con el código preexistente.

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

Consideremos ahora el siguiente código inseguro:

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

Si el compilador de Java no nos impide hacer esto, el sistema de tipo de tiempo de ejecución tampoco puede detenernos, porque no hay forma, en tiempo de ejecución, de determinar que esta lista se suponía que era solo una lista de enteros. El tiempo de ejecución de Java nos permitiría incluir lo que queramos en esta lista, cuando solo debería contener enteros, porque cuando se creó, se declaró como una lista de enteros. Es por eso que el compilador rechaza la línea número 4 porque no es segura y, si se permite, podría romper los supuestos del sistema de tipos.

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

Como tal, decimos que los tipos genéricos no son reificables, ya que en tiempo de ejecución no podemos determinar la verdadera naturaleza del tipo genérico.

Me salteé algunas partes de estas respuestas, puede leer el artículo completo aquí: https://dzone.com/articles/covariance-and-contravariance

Humoyun Ahmad
fuente
32

La razón por la que esto es imposible es que Java implementa sus Genéricos únicamente en el nivel del compilador, y solo se genera un archivo de clase para cada clase. Esto se llama borrado de tipo .

En tiempo de ejecución, la clase compilada necesita manejar todos sus usos con el mismo código de bytes. Por new T[capacity]lo tanto, no tendría ni idea de qué tipo necesita ser instanciado.

Durandal
fuente
17

La respuesta ya se dio, pero si ya tiene una Instancia de T, puede hacer esto:

T t; //Assuming you already have this object instantiated or given by parameter.
int length;
T[] ts = (T[]) Array.newInstance(t.getClass(), length);

Espero poder ayudar, Ferdi265

Ferdi265
fuente
1
Esta es una buena solución. Pero esto obtendrá advertencias no verificadas (lanzado de Objeto a T []). Otra solución, pero "más lento", "libre de alerta" sería la siguiente: T[] ts = t.clone(); for (int i=0; i<ts.length; i++) ts[i] = null;.
midnite
1
Además, si lo que guardamos es T[] t, entonces lo sería (T[]) Array.newInstance(t.getClass().getComponentType(), length);. Pasé algunas veces para averiguar getComponentType(). Espero que esto ayude a otros.
midnite
1
@midnite t.clone()no volverá T[]. Porque tno es Array en esta respuesta.
xmen
6

La razón principal se debe al hecho de que las matrices en Java son covariantes.

Hay una buena descripción general aquí .

GaryF
fuente
No veo cómo podría soportar "nueva T [5]" incluso con matrices invariantes.
Dimitris Andreou
2
@DimitrisAndreou Bueno, todo es más bien una comedia de errores en el diseño de Java. Todo comenzó con la covarianza de la matriz. Entonces, una vez que tenga matriz de covarianza, puede convertir String[]a Objecty almacenar una Integeren el mismo. Entonces, tuvieron que agregar una verificación de tipo de tiempo de ejecución para los almacenes de matrices ( ArrayStoreException) porque el problema no se pudo detectar en tiempo de compilación. (De lo contrario, un en Integerrealidad podría estar atascado en un String[], y obtendría un error cuando intentara recuperarlo, lo que sería horrible.) ...
Radon Rosborough
2
@DimitrisAndreou ... Luego, una vez que haya puesto una verificación de tiempo de ejecución en lugar de una verificación de tiempo de compilación mucho más sólida, se encuentra con el borrado de tipo (también un defecto de diseño desafortunado, incluido solo por compatibilidad con versiones anteriores). El borrado de tipos significa que no puede hacer verificaciones de tipos en tiempo de ejecución para tipos genéricos. Por lo tanto, para evitar el problema del tipo de almacenamiento de matriz, simplemente no puede tener matrices genéricas. Si simplemente hubieran hecho que las matrices fueran invariables en primer lugar, podríamos hacer verificaciones de tipo en tiempo de compilación sin correr el riesgo de borrado.
Radon Rosborough
... Acabo de descubrir el período de edición de cinco minutos para comentarios. Objectdebería haber estado Object[]en mi primer comentario.
Radon Rosborough
3

Me gusta la respuesta indirectamente dada por Gafter . Sin embargo, propongo que esté mal. Cambié un poco el código de Gafter. Se compila y funciona durante un tiempo, luego bombardea donde Gafter predijo que lo haría

class Box<T> {

    final T x;

    Box(T x) {
        this.x = x;
    }
}

class Loophole {

    public static <T> T[] array(final T... values) {
        return (values);
    }

    public static void main(String[] args) {

        Box<String> a = new Box("Hello");
        Box<String> b = new Box("World");
        Box<String> c = new Box("!!!!!!!!!!!");
        Box<String>[] bsa = array(a, b, c);
        System.out.println("I created an array of generics.");

        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3);
        System.out.println("error not caught by array store check");

        try {
            String s = bsa[0].x;
        } catch (ClassCastException cause) {
            System.out.println("BOOM!");
            cause.printStackTrace();
        }
    }
}

La salida es

I created an array of generics.
error not caught by array store check
BOOM!
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Loophole.main(Box.java:26)

Entonces, me parece que puede crear tipos de matriz genéricos en Java. ¿Entendí mal la pregunta?

emory
fuente
Tu ejemplo es diferente de lo que te he pedido. Lo que describiste son los peligros de la covarianza de matriz. Compruébalo (para .NET: blogs.msdn.com/b/ericlippert/archive/2007/10/17/… )
elysium devorado
Espero que reciba una advertencia de seguridad de tipo del compilador, ¿sí?
Matt McHenry
1
Sí, recibo una advertencia de seguridad de tipo. Sí, veo que mi ejemplo no responde a la pregunta.
emory
En realidad, recibe múltiples advertencias debido a la inicialización descuidada de a, b, c. Además, esto es bien conocido y afecta a la biblioteca principal, por ejemplo, <T> java.util.Arrays.asList (T ...). Si pasa cualquier tipo no reificable para T, recibirá una advertencia (porque la matriz creada tiene un tipo menos preciso de lo que pretende el código), y es súper feo. Sería mejor si el autor de este método recibiera la advertencia, en lugar de emitirla en el sitio de uso, dado que el método en sí es seguro, no expone la matriz al usuario.
Dimitris Andreou
1
No creaste una matriz genérica aquí. El compilador creó una matriz (no genérica) para usted.
newacct
2

Sé que llego un poco tarde a la fiesta aquí, pero pensé que podría ayudar a los futuros googlers ya que ninguna de estas respuestas solucionó mi problema. Sin embargo, la respuesta de Ferdi265 ayudó inmensamente.

Estoy tratando de crear mi propia lista de Linked, por lo que el siguiente código es lo que funcionó para mí:

package myList;
import java.lang.reflect.Array;

public class MyList<TYPE>  {

    private Node<TYPE> header = null;

    public void clear() {   header = null;  }

    public void add(TYPE t) {   header = new Node<TYPE>(t,header);    }

    public TYPE get(int position) {  return getNode(position).getObject();  }

    @SuppressWarnings("unchecked")
    public TYPE[] toArray() {       
        TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());        
        for(int i=0 ; i<size() ; i++)   result[i] = get(i); 
        return result;
    }


    public int size(){
         int i = 0;   
         Node<TYPE> current = header;
         while(current != null) {   
           current = current.getNext();
           i++;
        }
        return i;
    }  

En el método toArray () se encuentra la forma de crear una matriz de un tipo genérico para mí:

TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());    
Derek Ziemba
fuente
2

En mi caso, simplemente quería una serie de pilas, algo como esto:

Stack<SomeType>[] stacks = new Stack<SomeType>[2];

Como esto no era posible, utilicé lo siguiente como solución alternativa:

  1. Creó una clase de contenedor no genérico alrededor de Stack (digamos MyStack)
  2. MyStack [] stacks = new MyStack [2] funcionó perfectamente bien

Feo, pero Java es feliz.

Nota: como mencionó BrainSlugs83 en el comentario a la pregunta, es totalmente posible tener matrices de genéricos en .NET

David Airapetyan
fuente
2

Del tutorial de Oracle :

No puede crear matrices de tipos parametrizados. Por ejemplo, el siguiente código no se compila:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

El siguiente código ilustra lo que sucede cuando se insertan diferentes tipos en una matriz:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

Si intenta lo mismo con una lista genérica, habría un problema:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

Si se permitieran matrices de listas parametrizadas, el código anterior no arrojaría la ArrayStoreException deseada.

Para mí, suena muy débil. Creo que cualquiera con una comprensión suficiente de los genéricos, estaría perfectamente bien, e incluso esperaría, que ArrayStoredException no se arroje en tal caso.

Stick Hero
fuente
0

Seguramente debe haber una buena forma de evitarlo (tal vez usando la reflexión), porque me parece que eso es exactamente lo que ArrayList.toArray(T[] a)hace. Yo cito:

public <T> T[] toArray(T[] a)

Devuelve una matriz que contiene todos los elementos de esta lista en el orden correcto; El tipo de tiempo de ejecución de la matriz devuelta es el de la matriz especificada. Si la lista se ajusta a la matriz especificada, se devuelve en ella. De lo contrario, se asigna una nueva matriz con el tipo de tiempo de ejecución de la matriz especificada y el tamaño de esta lista.

Entonces, una forma de evitarlo sería usar esta función, es decir, crear uno ArrayListde los objetos que desea en la matriz, luego usar toArray(T[] a)para crear la matriz real. No sería rápido, pero no mencionaste tus requisitos.

Entonces, ¿alguien sabe cómo toArray(T[] a)se implementa?

Adán
fuente
3
List.toArray (T []) funciona porque esencialmente le está dando el tipo de componente T en tiempo de ejecución (le está dando una instancia del tipo de matriz deseado, del cual puede obtener la clase de matriz, y luego, la clase de componente T ) Con el tipo de componente real en tiempo de ejecución, siempre puede crear una matriz de ese tipo de tiempo de ejecución utilizando Array.newInstance(). Encontrará eso mencionado en muchas preguntas que preguntan cómo crear una matriz con un tipo desconocido en el momento de la compilación. Pero el OP preguntaba específicamente por qué no se puede usar la new T[]sintaxis, que es una pregunta diferente
2011
0

Esto se debe a que los genéricos se agregaron a Java después de que lo hicieron, por lo que es un poco torpe porque los fabricantes originales de Java pensaron que al hacer una matriz, el tipo se especificaría en su fabricación. Entonces, eso no funciona con los genéricos, por lo que debe hacer E [] array = (E []) new Object [15]; Esto compila pero da una advertencia.

Alvin
fuente
0

Si no podemos crear instancias de matrices genéricas, ¿por qué el lenguaje tiene tipos de matrices genéricas? ¿Cuál es el punto de tener un tipo sin objetos?

La única razón por la que puedo pensar es varargs - foo(T...). De lo contrario, podrían haber eliminado completamente los tipos de matriz genéricos. (Bueno, en realidad no tenían que usar array para varargs, ya que los varargs no existían antes de 1.5 . Ese es probablemente otro error).

Es mentira, ¡ puedes crear instancias de matrices genéricas a través de varargs!

Por supuesto, los problemas con las matrices genéricas siguen siendo reales, p. Ej.

static <T> T[] foo(T... args){
    return args;
}
static <T> T[] foo2(T a1, T a2){
    return foo(a1, a2);
}

public static void main(String[] args){
    String[] x2 = foo2("a", "b"); // heap pollution!
}

Podemos usar este ejemplo para demostrar realmente el peligro de una matriz genérica .

Por otro lado, hemos estado usando varargs genéricos durante una década, y el cielo aún no se está cayendo. Entonces podemos argumentar que los problemas están siendo exagerados; no es un gran problema. Si se permite la creación explícita de matrices genéricas, tendremos errores aquí y allá; pero estamos acostumbrados a los problemas de borrado, y podemos vivir con eso.

Y podemos apuntar a foo2refutar la afirmación de que la especificación nos mantiene alejados de los problemas de los que dicen alejarnos. Si Sun tuviera más tiempo y recursos para 1.5 , creo que podrían haber alcanzado una resolución más satisfactoria.

ZhongYu
fuente
0

Como otros ya mencionaron, por supuesto puedes crear a través de algunos trucos .

Pero no es recomendable.

Debido a que el borrado de tipo y, lo que es más importante, la covariancematriz in que solo permite una matriz de subtipo se puede asignar a una matriz de supertipo, lo que le obliga a utilizar la conversión de tipo explícito al intentar recuperar el valor causando tiempo de ejecución, ClassCastExceptionque es uno de los objetivos principales que los genéricos intentan eliminar: verificaciones de tipo más fuertes en tiempo de compilación .

Object[] stringArray = { "hi", "me" };
stringArray[1] = 1;
String aString = (String) stringArray[1]; // boom! the TypeCastException

Un ejemplo más directo se puede encontrar en Effective Java: Item 25 .


covarianza : una matriz de tipo S [] es un subtipo de T [] si S es un subtipo de T

Hearen
fuente
0

Si la clase se usa como un tipo parametrizado, puede declarar una matriz de tipo T [], pero no puede instanciar directamente dicha matriz. En cambio, un enfoque común es crear una instancia de una matriz de tipo Object [], y luego hacer una conversión estrecha al tipo T [], como se muestra a continuación:

  public class Portfolio<T> {
  T[] data;
 public Portfolio(int capacity) {
   data = new T[capacity];                 // illegal; compiler error
   data = (T[]) new Object[capacity];      // legal, but compiler warning
 }
 public T get(int index) { return data[index]; }
 public void set(int index, T element) { data[index] = element; }
}
DeV
fuente
-2

Prueba esto:

List<?>[] arrayOfLists = new List<?>[4];
Jorge Washington
fuente