Por que não consigo criar tipos de matriz genéricos em Java?

273

Qual é o motivo pelo qual o Java não nos permite fazer

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

Eu pude entender que o .NET não nos permitiu fazer isso, pois no .NET você tem tipos de valor que em tempo de execução podem ter tamanhos diferentes, mas em Java todos os tipos de T serão referências a objetos, tendo o mesmo tamanho ( Corrija-me se eu estiver errado).

Qual é a razão?

elísio devorado
fonte
29
Do que você está falando? Você pode absolutamente fazer isso no .NET. - Estou aqui tentando descobrir por que não consigo fazer isso em Java.
precisa saber é o seguinte
@ BrainSlugs83 - adicione um link para algum exemplo de código ou tutorial que mostra isso.
MasterJoe2 03/04
Também veja - stackoverflow.com/questions/21577493/…
MasterJoe2
1
@ MasterJoe2 o código acima na pergunta do OP é o que estou me referindo. Funciona bem em C #, mas não em Java. - A pergunta afirma que não funciona em nenhum dos dois, o que está incorreto. - Não tenho certeza se vale a pena discutir mais a fundo.
BrainSlugs83 6/04

Respostas:

204

Isso ocorre porque as matrizes de Java (ao contrário dos genéricos) contêm, em tempo de execução, informações sobre seu tipo de componente. Portanto, você deve conhecer o tipo de componente ao criar a matriz. Como você não sabe o que Testá em tempo de execução, não é possível criar a matriz.

newacct
fonte
29
Mas e a eliminação? Por que isso não se aplica?
Qix - MONICA FOI ERRADA EM
14
Como faz ArrayList <SomeType>isso então?
Thumbz
10
@Thumbz: Você quer dizer new ArrayList<SomeType>()? Tipos genéricos não contêm o parâmetro type em tempo de execução. O parâmetro type não é usado na criação. Não há diferença no código gerado por new ArrayList<SomeType>()ou new ArrayList<String>()ou de forma new ArrayList()alguma.
newacct
8
Eu estava perguntando mais sobre como ArrayList<T>funciona com o seu ' private T[] myArray. Em algum lugar do código, ele deve ter uma matriz do tipo T genérico, então como?
Thumbz
21
@ Thumbz: Ele não possui uma matriz do tipo de tempo de execução T[]. Ele possui uma matriz do tipo de tempo de execução Object[]e 1) o código fonte contém uma variável de Object[](é assim que está na fonte Java Oracle mais recente); ou 2) o código fonte contém uma variável do tipo T[], o que é uma mentira, mas não causa problemas devido à Texclusão no escopo da classe.
newacct
137

Citar:

Matrizes de tipos genéricos não são permitidas porque não são válidas. O problema é devido à interação de matrizes Java, que não são estaticamente válidas, mas são verificadas dinamicamente, com genéricos, que são estaticamente válidas e não verificadas dinamicamente. Aqui está como você pode explorar a brecha:

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!
    }
}

Propusemos resolver esse problema usando arrays estaticamente seguros (também conhecidos como Variance), que foram rejeitados pelo Tiger.

- gafter

(Eu acredito que é Neal Gafter , mas não tenho certeza)

Veja no contexto aqui: http://forums.sun.com/thread.jspa?threadID=457033&forumID=316

Bart Kiers
fonte
3
Note que eu fiz um CW, pois a resposta não é minha.
Bart Kiers
10
Isso explica por que pode não ser seguro. Porém, problemas de segurança de tipo podem ser avisados ​​pelo compilador. O fato é que nem mesmo é possível fazê-lo, quase pela mesma razão pela qual você não pode fazer new T(). Cada matriz em Java, por design, armazena o tipo de componente (ou seja T.class) dentro dela; portanto, você precisa da classe T em tempo de execução para criar essa matriz.
Newacct
2
Você ainda pode usar new Box<?>[n], o que às vezes é suficiente, embora não ajude no seu exemplo.
Bartosz Klimek
1
@ BartKiers Eu não entendi ... isso ainda não compila (java-8): Box<String>[] bsa = new Box<String>[3];alguma coisa mudou no java-8 e acima eu assumo?
Eugene
1
@Eugene, matrizes de tipos genéricos específicos simplesmente não são permitidas, pois podem levar à perda da segurança do tipo, conforme demonstrado na amostra. Não é permitido em nenhuma versão do Java. Os resposta começa como "Matrizes de tipos genéricos não são permitidas porque eles não são de som."
granada
47

Ao não fornecer uma solução decente, você acaba com algo pior no IMHO.

O trabalho comum é o seguinte.

T[] ts = new T[n];

é substituído por (assumindo que T estende Object e não outra classe)

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

Eu prefiro o primeiro exemplo, no entanto, mais tipos acadêmicos parecem preferir o segundo, ou simplesmente preferem não pensar nisso.

A maioria dos exemplos de por que você não pode simplesmente usar um Object [] se aplica igualmente a List ou Collection (que são suportados), então eu os vejo como argumentos muito ruins.

Nota: esse é um dos motivos pelos quais a própria coleção Collections não compila sem avisos. Se esse caso de uso não puder ser suportado sem avisos, algo será fundamentalmente quebrado com o modelo de genéricos IMHO.

Peter Lawrey
fonte
6
Você tem que ter cuidado com o segundo. Se você retornar a matriz criada dessa maneira para alguém que espera, digamos, a String[](ou se você a armazena em um campo publicamente acessível do tipo T[]e alguém a recupera), elas receberão uma ClassCastException.
Newacct
4
Votei esta resposta para baixo, porque o seu exemplo preferido não é permitido em Java e seu segundo exemplo pode lançar uma ClassCastException
José Roberto Araújo Júnior
5
@ JoséRobertoAraújoJúnior É bastante claro que o primeiro exemplo precisa ser substituído pelo segundo exemplo. Seria mais útil você explicar por que o segundo exemplo pode gerar uma ClassCastException, pois isso não seria óbvio para todos.
Peter Lawrey
3
@PeterLawrey eu criei uma pergunta mostrando auto-respondidas por isso T[] ts = (T[]) new Object[n];é uma má idéia: stackoverflow.com/questions/21577493/...
José Roberto Araújo Júnior
1
@MarkoTopolnik Eu deveria receber uma medalha por responder a todos os seus comentários para explicar a mesma coisa que eu já disse, a única coisa que mudou da minha razão original é que eu pensei que ele disse que T[] ts = new T[n];era um exemplo válido. Vou manter o voto, porque sua resposta pode causar problemas e confusões a outros desenvolvedores e também é fora de tópico. Além disso, vou parar de comentar sobre isso.
José Roberto Araújo Júnior
38

Matrizes são covariantes

Diz-se que matrizes são covariantes, o que basicamente significa que, dadas as regras de subtipagem de Java, uma matriz de tipo T[]pode conter elementos do tipo Tou qualquer subtipo de T. Por exemplo

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

Mas não é só isso, as regras de subtipagem de Java também afirmam que uma matriz S[]é um subtipo da matriz T[]se Sfor um subtipo de T, portanto, algo como isso também é válido:

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

Porque de acordo com as regras de subtipagem em Java, uma matriz Integer[]é um subtipo de matriz Number[]porque Integer é um subtipo de Number.

Mas essa regra de subtipagem pode levar a uma pergunta interessante: o que aconteceria se tentássemos fazer isso?

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

Essa última linha seria compilada muito bem, mas se executássemos esse código, obteríamos um ArrayStoreExceptionporque estamos tentando colocar um duplo em uma matriz inteira. O fato de estarmos acessando a matriz por meio de uma referência Number é irrelevante aqui, o que importa é que a matriz é uma matriz de números inteiros.

Isso significa que podemos enganar o compilador, mas não podemos enganar o sistema do tipo tempo de execução. E é assim porque matrizes são o que chamamos de tipo reificável. Isso significa que, em tempo de execução, Java sabe que essa matriz foi realmente instanciada como uma matriz de números inteiros que simplesmente é acessada por meio de uma referência do tipo Number[].

Então, como podemos ver, uma coisa é o tipo real do objeto, outra coisa é o tipo de referência que usamos para acessá-lo, certo?

O problema com Java Generics

Agora, o problema com tipos genéricos em Java é que as informações de tipo para os parâmetros de tipo são descartadas pelo compilador após a compilação do código; portanto, essas informações de tipo não estão disponíveis no tempo de execução. Esse processo é chamado de apagamento de tipo . Existem boas razões para implementar genéricos como este em Java, mas isso é uma longa história e tem a ver com compatibilidade binária com código pré-existente.

O ponto importante aqui é que, como em tempo de execução, não há informações de tipo, não há como garantir que não estamos cometendo poluição de pilha.

Vamos considerar agora o seguinte código não seguro:

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

Se o compilador Java não nos impedir de fazer isso, o sistema do tipo tempo de execução também não poderá nos impedir, porque não há como, em tempo de execução, determinar que essa lista deveria ser apenas uma lista de números inteiros. O tempo de execução do Java nos permite colocar o que queremos nesta lista, quando deve conter apenas números inteiros, porque quando foi criado, foi declarado como uma lista de números inteiros. É por isso que o compilador rejeita a linha número 4 porque é inseguro e, se permitido, pode quebrar as suposições do sistema de tipos.

Como tal, os designers de Java se certificaram de que não podemos enganar o compilador. Se não podemos enganar o compilador (como podemos fazer com matrizes), também não podemos enganar o sistema do tipo tempo de execução.

Como tal, dizemos que os tipos genéricos não são reificáveis, pois em tempo de execução não podemos determinar a verdadeira natureza do tipo genérico.

Eu pulei algumas partes destas respostas, você pode ler o artigo completo aqui: https://dzone.com/articles/covariance-and-contravariance

Humoyun Ahmad
fonte
32

A razão pela qual isso é impossível é que o Java implementa seus Genéricos puramente no nível do compilador e há apenas um arquivo de classe gerado para cada classe. Isso é chamado de apagamento de tipo .

No tempo de execução, a classe compilada precisa lidar com todos os seus usos com o mesmo bytecode. Portanto, new T[capacity]não teria absolutamente nenhuma idéia de que tipo precisa ser instanciado.

Durandal
fonte
17

A resposta já foi dada, mas se você já tiver uma Instância de T, poderá fazer o seguinte:

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 ajudar, Ferdi265

Ferdi265
fonte
1
Esta é uma boa solução. Mas isso receberá avisos não verificados (convertidos de Objeto para T []). Outra mas solução "mais lento" "sem aviso" seria: T[] ts = t.clone(); for (int i=0; i<ts.length; i++) ts[i] = null;.
midnite
1
Além disso, se o que mantivemos for T[] t, seria (T[]) Array.newInstance(t.getClass().getComponentType(), length);. Passei algumas vezes para descobrir getComponentType(). Espero que isso ajude os outros.
midnite 26/07/2013
1
@midnite t.clone()não retornará T[]. Porque tnão é a matriz nesta resposta.
xmen
6

O principal motivo é que matrizes em Java são covariantes.

Há uma boa visão geral aqui .

GaryF
fonte
Não vejo como você poderia suportar "novo T [5]", mesmo com matrizes invariantes.
Dimitris Andreou
2
@DimitrisAndreou Bem, a coisa toda é uma comédia de erros no design de Java. Tudo começou com covariância de array. Então, uma vez que você tem matriz covariância, você pode lançar String[]para Objecte armazenar um Integernele. Portanto, eles tiveram que adicionar uma verificação de tipo de tempo de execução para armazenamentos de matriz ( ArrayStoreException) porque o problema não pôde ser detectado no momento da compilação. (Caso contrário, um Integerrealmente poderia estar preso em um String[]e você receberia um erro ao tentar recuperá-lo, o que seria horrível.) ...
Radon Rosborough
2
@DimitrisAndreou… Então, depois de colocar uma verificação de tempo de execução no lugar de uma verificação em tempo de compilação mais sadia, você passa a apagar o tipo (também uma falha de design infeliz - incluída apenas para compatibilidade com versões anteriores). Eliminação de tipo significa que você não pode fazer verificações de tipo de tempo de execução para tipos genéricos. Portanto, para evitar o problema do tipo de armazenamento de matriz, você simplesmente não pode ter matrizes genéricas. Se eles tivessem simplesmente tornado as matrizes invariáveis ​​em primeiro lugar, poderíamos apenas fazer verificações de tipo em tempo de compilação sem sofrer apagamentos.
Radon Rosborough
… Acabei de descobrir o período de edição de cinco minutos para comentários. Objectdeveria estar Object[]no meu primeiro comentário.
Radon Rosborough
3

Eu gosto da resposta indiretamente dada por Gafter . No entanto, proponho que está errado. Mudei um pouco o código de Gafter. Ele compila e funciona por um tempo e depois bombardeia onde Gafter previu que seria

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();
        }
    }
}

A saída é

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)

Então, parece-me que você pode criar tipos de matriz genéricos em java. Eu entendi mal a pergunta?

emory
fonte
Seu exemplo é diferente do que eu pedi. O que você descreveu são os perigos da covariância de array. Confira (para .NET: blogs.msdn.com/b/ericlippert/archive/2007/10/17/… )
elysium devorado
Espero que você receba um aviso de segurança de tipo do compilador, sim?
Matt McHenry
1
Sim, recebo um aviso de segurança de tipo. Sim, vejo que meu exemplo não responde à pergunta.
Emory
Na verdade, você recebe vários avisos devido à inicialização incorreta de a, b, c. Além disso, isso é bem conhecido e afeta a biblioteca principal, por exemplo, <T> java.util.Arrays.asList (T ...). Se você passar um tipo não-reificável para T, você receberá um aviso (porque a matriz criada tem um tipo menos preciso do que o código pretende), e é super feio. Seria melhor que o autor desse método recebesse o aviso, em vez de emiti-lo no site de uso, uma vez que o próprio método é seguro, ele não expõe a matriz ao usuário.
Dimitris Andreou
1
Você não criou uma matriz genérica aqui. O compilador criou uma matriz (não genérica) para você.
Newacct
2

Sei que estou um pouco atrasado para a festa aqui, mas achei que poderia ajudar futuros googlers, pois nenhuma dessas respostas resolveu meu problema. A resposta de Ferdi265 ajudou imensamente.

Estou tentando criar minha própria lista vinculada, portanto, o seguinte código foi o que funcionou para mim:

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;
    }  

No método toArray (), está o caminho para criar uma matriz de um tipo genérico para mim:

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

No meu caso, eu simplesmente queria uma variedade de pilhas, algo como isto:

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

Como isso não foi possível, usei o seguinte como solução alternativa:

  1. Criou uma classe de invólucro não genérica em torno da Stack (por exemplo, MyStack)
  2. MyStack [] stacks = novo MyStack [2] funcionou perfeitamente bem

Feio, mas Java é feliz.

Nota: conforme mencionado por BrainSlugs83 no comentário à pergunta, é totalmente possível ter matrizes de genéricos no .NET

David Airapetyan
fonte
2

No tutorial do Oracle :

Você não pode criar matrizes de tipos com parâmetros. Por exemplo, o código a seguir não compila:

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

O código a seguir ilustra o que acontece quando diferentes tipos são inseridos em uma matriz:

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

Se você tentar a mesma coisa com uma lista genérica, haverá um 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.

Se matrizes de listas parametrizadas fossem permitidas, o código anterior falharia ao lançar a ArrayStoreException desejada.

Para mim, parece muito fraco. Acho que qualquer pessoa com um entendimento suficiente de genéricos estaria perfeitamente bem, e até esperaria, que a ArrayStoredException não seja lançada nesse caso.

Stick Hero
fonte
0

Certamente deve haver uma boa maneira de contornar isso (talvez usando reflexão), porque me parece que é exatamente ArrayList.toArray(T[] a)isso que faz. Eu cito:

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

Retorna uma matriz contendo todos os elementos nesta lista na ordem correta; o tipo de tempo de execução da matriz retornada é o da matriz especificada. Se a lista couber na matriz especificada, ela será retornada nela. Caso contrário, uma nova matriz é alocada com o tipo de tempo de execução da matriz especificada e o tamanho dessa lista.

Portanto, uma maneira de contornar seria usar essa função, ou seja, criar um ArrayListdos objetos que você deseja na matriz e, em seguida, usar toArray(T[] a)para criar a matriz real. Não seria rápido, mas você não mencionou seus requisitos.

Então, alguém sabe como toArray(T[] a)é implementado?

Adão
fonte
3
List.toArray (T []) funciona porque você está essencialmente fornecendo o tipo de componente T em tempo de execução (está fornecendo uma instância do tipo de matriz desejado, a partir do qual é possível obter a classe de matriz e, em seguida, a classe de componente T ) Com o tipo de componente real em tempo de execução, você sempre pode criar uma matriz desse tipo de tempo de execução usando Array.newInstance(). Você encontrará isso mencionado em muitas perguntas que perguntam como criar uma matriz com um tipo desconhecido em tempo de compilação. Mas o OP estava pedindo especificamente por que você não pode usar a new T[]sintaxe, que é uma questão diferente
newacct
0

É porque os genéricos foram adicionados ao java depois que eles o criaram, então é meio desajeitado porque os criadores originais do java pensavam que, ao criar uma matriz, o tipo seria especificado na criação. Portanto, como isso não funciona com genéricos, é necessário fazer E [] array = (E []) new Object [15]; Isso compila, mas dá um aviso.

Alvin
fonte
0

Se não podemos instanciar matrizes genéricas, por que o idioma possui tipos de matriz genéricos? Qual é o sentido de ter um tipo sem objetos?

A única razão pela qual consigo pensar é em varargs - foo(T...). Caso contrário, eles poderiam ter eliminado completamente os tipos de matriz genérica. (Bem, eles realmente não precisaram usar array para varargs, pois os varargs não existiam antes da versão 1.5 . Isso provavelmente é outro erro.)

Portanto, é uma mentira, você pode instanciar matrizes genéricas, através de varargs!

Obviamente, os problemas com matrizes genéricas ainda são reais, por exemplo,

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 exemplo para realmente demonstrar o perigo da matriz genérica .

Por outro lado, usamos varargs genéricos há uma década e o céu ainda não está caindo. Então, podemos argumentar que os problemas estão sendo exagerados; não é grande coisa. Se a criação de matriz genérica explícita for permitida, teremos erros aqui e ali; mas estamos acostumados com os problemas de apagamento e podemos conviver com isso.

E podemos apontar para foo2refutar a afirmação de que as especificações nos mantêm longe dos problemas que eles afirmam nos impedir. Se a Sun tivesse mais tempo e recursos para o 1.5 , acredito que eles poderiam ter alcançado uma resolução mais satisfatória.

ZhongYu
fonte
0

Como outros já mencionados, é claro que você pode criar através de alguns truques .

Mas não é recomendado.

Como a eliminação de tipo e, mais importante, a covariancematriz in, que apenas permite uma matriz de subtipo, pode ser atribuída a uma matriz de supertipo, o que força você a usar a conversão explícita de tipo ao tentar recuperar o valor, causando o tempo de execução, ClassCastExceptionque é um dos principais objetivos que os genéricos tentam eliminar: Verificações mais fortes em tempo de compilação .

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

Um exemplo mais direto pode ser encontrado em Java Efetivo: Item 25 .


covariância : uma matriz do tipo S [] é um subtipo de T [] se S é um subtipo de T

Hearen
fonte
0

Se a classe usar como um tipo parametrizado, poderá declarar uma matriz do tipo T [], mas não poderá instanciar diretamente essa matriz. Em vez disso, uma abordagem comum é instanciar uma matriz do tipo Object [] e, em seguida, fazer uma conversão estreita para o tipo T [], conforme mostrado a seguir:

  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
fonte
-2

Tente o seguinte:

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