Pregunta ¿Cómo crear una matriz genérica en Java?


Debido a la implementación de los genéricos de Java, no puede tener un código como este:

public class GenSet<E> {
    private E a[];

    public GenSet() {
        a = new E[INITIAL_ARRAY_LENGTH]; // error: generic array creation
    }
}

¿Cómo puedo implementar esto manteniendo la seguridad del tipo?

Vi una solución en los foros de Java que dice así:

import java.lang.reflect.Array;

class Stack<T> {
    public Stack(Class<T> clazz, int capacity) {
        array = (T[])Array.newInstance(clazz, capacity);
    }

    private final T[] array;
}

Pero realmente no entiendo lo que está pasando.


880
2018-02-09 17:30


origen


Respuestas:


Debo hacer una pregunta a cambio: es su GenSet "marcado" o "desactivado"? Qué significa eso?

  • Comprobado: tipificación fuerte. GenSet sabe explícitamente qué tipo de objetos contiene (es decir, su constructor fue llamado explícitamente con un Class<E> argumento, y los métodos arrojarán una excepción cuando se pasen argumentos que no son de tipo E. Ver Collections.checkedCollection.

    -> en ese caso, debes escribir:

    public class GenSet<E> {
    
        private E[] a;
    
        public GenSet(Class<E> c, int s) {
            // Use Array native method to create array
            // of a type only known at run time
            @SuppressWarnings("unchecked")
            final E[] a = (E[]) Array.newInstance(c, s);
            this.a = a;
        }
    
        E get(int i) {
            return a[i];
        }
    }
    
  • Desenfrenado: tipeo débil. No se realiza ninguna verificación de tipo en ninguno de los objetos pasados ​​como argumento.

    -> en ese caso, deberías escribir

    public class GenSet<E> {
    
        private Object[] a;
    
        public GenSet(int s) {
            a = new Object[s];
        }
    
        E get(int i) {
            @SuppressWarnings("unchecked")
            final E e = (E) a[i];
            return e;
        }
    }
    

    Tenga en cuenta que el tipo de componente de la matriz debe ser el borradura del tipo de parámetro:

    public class GenSet<E extends Foo> { // E has an upper bound of Foo
    
        private Foo[] a; // E erases to Foo, so use Foo[]
    
        public GenSet(int s) {
            a = new Foo[s];
        }
    
        ...
    }
    

Todo esto resulta de una debilidad conocida y deliberada de los genéricos en Java: se implementó mediante el borrado, por lo que las clases "genéricas" no saben con qué tipo de argumento se crearon en el tiempo de ejecución y, por lo tanto, no pueden proporcionar seguridad a menos que se implemente algún mecanismo explícito (verificación de tipo).


579
2018-02-09 22:19



Siempre puedes hacer esto:

E[] arr = (E[])new Object[INITIAL_ARRAY_LENGTH];

Esta es una de las formas sugeridas de implementar una colección genérica en Java efectivo; Artículo 26. No hay errores de tipo, no es necesario lanzar el conjunto repetidamente. sin embargo esto activa una advertencia porque es potencialmente peligroso y debe usarse con precaución. Como se detalla en los comentarios, esto Object[] ahora se hace pasar por nuestro E[] tipo, y puede causar errores inesperados o ClassCastExceptions si se usa de manera insegura.

Como regla general, este comportamiento es seguro siempre que la matriz de lanzamiento se use internamente (por ejemplo, para respaldar una estructura de datos) y no se devuelva ni se exponga al código del cliente. Si necesita devolver una matriz de un tipo genérico a otro código, el reflejo Array la clase que mencionas es el camino correcto a seguir.


Vale la pena mencionar que siempre que sea posible, tendrás un tiempo mucho más feliz trabajando con Lists en lugar de matrices si estás usando genéricos. Ciertamente, a veces no tienes opción, pero usar el marco de colecciones es mucho más sólido.


160
2018-05-27 20:00



A continuación, se indica cómo utilizar los genéricos para obtener una matriz del tipo que está buscando, al mismo tiempo que se preserva la seguridad del tipo (a diferencia de las otras respuestas, que le devolverán una Object array o resultado en advertencias en tiempo de compilación):

import java.lang.reflect.Array;  

public class GenSet<E> {  
    private E[] a;  

    public GenSet(Class<E[]> clazz, int length) {  
        a = clazz.cast(Array.newInstance(clazz.getComponentType(), length));  
    }  

    public static void main(String[] args) {  
        GenSet<String> foo = new GenSet<String>(String[].class, 1);  
        String[] bar = foo.a;  
        foo.a[0] = "xyzzy";  
        String baz = foo.a[0];  
    }  
}

Eso compila sin advertencias, y como puedes ver en main, para cualquier tipo que declare una instancia de GenSet como, puedes asignar a a una matriz de ese tipo, y puede asignar un elemento de a a una variable de ese tipo, lo que significa que la matriz y los valores en la matriz son del tipo correcto.

Funciona mediante el uso de literales de clase como tokens de tipo de tiempo de ejecución, como se discutió en el Tutoriales de Java. Los literales de clase son tratados por el compilador como instancias de java.lang.Class. Para usar uno, simplemente sigue el nombre de una clase con .class. Asi que, String.class actúa como un Class objeto que representa la clase String. Esto también funciona para interfaces, enumeraciones, matrices de cualquier dimensión (p. String[].class), primitivas (p. int.class) y la palabra clave void (es decir. void.class)

Class en sí mismo es genérico (declarado como Class<T>, dónde T representa el tipo que el Class objeto está representando), lo que significa que el tipo de String.class es Class<String>.

Entonces, cada vez que llamas al constructor para GenSet, se pasa un literal de clase para el primer argumento que representa una matriz del GenSet el tipo declarado de la instancia (p. String[].class para GenSet<String>) Tenga en cuenta que no podrá obtener una matriz de primitivas, ya que las primitivas no se pueden usar para variables de tipo.

Dentro del constructor, llamando al método cast devuelve el pasado Object argumento emitido a la clase representada por el Class objeto en el que se llamó el método. Llamar al método estático newInstance en java.lang.reflect.Array regresa como un Object una matriz del tipo representado por el Class objeto pasado como el primer argumento y de la longitud especificada por el int pasado como el segundo argumento. Llamar al método getComponentType devuelve un Class objeto que representa el tipo de componente de la matriz representada por el Class objeto en el que se llamó el método (p. String.class para String[].class, null Si el Class objeto no representa una matriz).

Esa última oración no es del todo exacta. Vocación String[].class.getComponentType() devuelve un Class objeto que representa la clase String, pero su tipo es Class<?>no Class<String>, por eso no puedes hacer algo como lo siguiente.

String foo = String[].class.getComponentType().cast("bar"); // won't compile

Lo mismo ocurre con todos los métodos en Class que devuelve un Class objeto.

En cuanto al comentario de Joachim Sauer sobre esta respuesta (No tengo la reputación suficiente para comentarlo yo mismo), el ejemplo usando el molde para T[] dará lugar a una advertencia porque el compilador no puede garantizar la seguridad del tipo en ese caso.


Editar con respecto a los comentarios de Ingo:

public static <T> T[] newArray(Class<T[]> type, int size) {
   return type.cast(Array.newInstance(type.getComponentType(), size));
}

53
2017-11-19 03:30



Esta es la única respuesta que es segura

E[] a;

a = newArray(size);

@SafeVarargs
static <E> E[] newArray(int length, E... array)
{
    return Arrays.copyOf(array, length);
}

34
2017-11-08 15:28



Para extender a más dimensiones, solo agrega []y parámetros de dimensión para newInstance() (T es un parámetro de tipo, cls es un Class<T>, d1 mediante d5 son enteros):

T[] array = (T[])Array.newInstance(cls, d1);
T[][] array = (T[][])Array.newInstance(cls, d1, d2);
T[][][] array = (T[][][])Array.newInstance(cls, d1, d2, d3);
T[][][][] array = (T[][][][])Array.newInstance(cls, d1, d2, d3, d4);
T[][][][][] array = (T[][][][][])Array.newInstance(cls, d1, d2, d3, d4, d5);

Ver Array.newInstance() para detalles.


25
2017-08-15 13:47



En Java 8, podemos hacer una especie de creación de matriz genérica utilizando una referencia de lambda o método. Esto es similar al enfoque reflexivo (que pasa una Class), pero aquí no estamos usando la reflexión.

@FunctionalInterface
interface ArraySupplier<E> {
    E[] get(int length);
}

class GenericSet<E> {
    private final ArraySupplier<E> supplier;
    private E[] array;

    GenericSet(ArraySupplier<E> supplier) {
        this.supplier = supplier;
        this.array    = supplier.get(10);
    }

    public static void main(String[] args) {
        GenericSet<String> ofString =
            new GenericSet<>(String[]::new);
        GenericSet<Double> ofDouble =
            new GenericSet<>(Double[]::new);
    }
}

Por ejemplo, esto es utilizado por <A> A[] Stream.toArray(IntFunction<A[]>).

Esta podría también se puede hacer antes de Java 8 usando clases anónimas, pero es más engorroso.


11
2018-03-05 14:14



Esto está cubierto en el Capítulo 5 (Genéricos) de Effective Java, 2nd Edition, artículo 25 ...Prefiere listas a matrices

Su código funcionará, aunque generará una advertencia no verificada (que puede suprimir con la siguiente anotación:

@SuppressWarnings({"unchecked"})

Sin embargo, probablemente sería mejor usar una lista en lugar de una matriz.

Hay una discusión interesante de este error / función en el sitio del proyecto OpenJDK.


10
2018-02-09 18:50