Por que as matrizes são covariantes, mas os genéricos são invariantes?

160

Do Effective Java de Joshua Bloch,

  1. Matrizes diferem do tipo genérico de duas maneiras importantes. As primeiras matrizes são covariantes. Os genéricos são invariantes.
  2. Covariante significa simplesmente se X é subtipo de Y, então X [] também será subtipo de Y []. Matrizes são covariantes Como string é o subtipo de Object So

    String[] is subtype of Object[]

    Invariante significa simplesmente, independentemente de X ser subtipo de Y ou não,

     List<X> will not be subType of List<Y>.

Minha pergunta é por que a decisão de fazer matrizes covariantes em Java? Existem outras postagens de SO, como Por que as matrizes são invariáveis, mas as listas são covariantes? , mas eles parecem estar focados em Scala e eu não consigo acompanhar.

ansioso para aprender
fonte
1
Não é isso porque os genéricos foram adicionados mais tarde?
Sotirios Delimanolis
1
Eu acho que comparar entre matrizes e coleções é injusto, Coleções usam matrizes em segundo plano !!
Ahmed Adel Ismail
4
@ EL-conteDe-monteTereBentikh Nem todas as coleções, por exemplo LinkedList.
Paul Bellora
@PaulBellora Eu sei que o Maps é diferente dos implementadores do Collection, mas li no SCPJ6 que o Collections geralmente contava com matrizes !!
Ahmed Adel Ismail
Porque não há ArrayStoreException; ao inserir um elemento errado na coleção, onde o array possui. Portanto, a Collection pode encontrar isso apenas no momento da recuperação e também por causa da transmissão. Portanto, os genéricos garantirão a solução desse problema.
Kanagavelu Sugumar

Respostas:

150

Via wikipedia :

As versões anteriores do Java e C # não incluíam genéricos (também conhecido como polimorfismo paramétrico).

Nesse cenário, a criação de matrizes invariáveis ​​exclui programas polimórficos úteis. Por exemplo, considere escrever uma função para embaralhar uma matriz ou uma função que testa duas matrizes quanto à igualdade usando o Object.equalsmétodo nos elementos. A implementação não depende do tipo exato de elemento armazenado na matriz, portanto, deve ser possível escrever uma única função que funcione em todos os tipos de matrizes. É fácil implementar funções do tipo

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

No entanto, se os tipos de matriz fossem tratados como invariantes, só seria possível chamar essas funções em uma matriz exatamente do tipo Object[]. Não se pode, por exemplo, embaralhar uma série de strings.

Portanto, Java e C # tratam os tipos de matriz de forma covariável. Por exemplo, em C # string[]é um subtipo de object[]e em Java String[]é um subtipo de Object[].

Isso responde à pergunta "Por que as matrizes são covariantes?" Ou, mais precisamente, "Por que as matrizes foram feitas covariantes na época ?"

Quando os genéricos foram introduzidos, eles não foram propositalmente tornados covariantes pelas razões apontadas nesta resposta por Jon Skeet :

Não, a List<Dog>não é a List<Animal>. Considere o que você pode fazer com um List<Animal>- você pode adicionar qualquer animal a ele ... incluindo um gato. Agora, você pode adicionar logicamente um gato a uma ninhada de filhotes? Absolutamente não.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

De repente você tem um gato muito confuso.

A motivação original para tornar as matrizes covariantes descritas no artigo da wikipedia não se aplicava aos genéricos, porque os curingas tornaram possível a expressão de covariância (e contravariância), por exemplo:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Paul Bellora
fonte
3
sim, as matrizes permitem um comportamento polimórfico; no entanto, introduz exceções de tempo de execução (diferentemente das exceções em tempo de compilação com os genéricos). Por exemplo:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn
21
Está correto. As matrizes têm tipos reificáveis ​​e lançam ArrayStoreExceptions conforme necessário. Claramente, isso foi considerado um compromisso digno na época. Compare isso com hoje: muitos consideram a covariância de matriz um erro, em retrospecto.
Paul Bellora
1
Por que "muitos" consideram um erro? É muito mais útil do que não ter covariância de array. Quantas vezes você viu um ArrayStoreException; eles são bastante raros. A ironia aqui é imperdoável ... entre os piores erros já cometidos em Java é a variação do site de uso, também conhecida como curingas.
Scott
3
@ScottMcKinney: "Por que" muitos "consideram um erro?" AIUI, isso ocorre porque a covariância de matriz requer testes de tipo dinâmico em todas as operações de atribuição de matriz (embora as otimizações do compilador possam ajudar?), O que pode causar uma sobrecarga significativa no tempo de execução.
Dominique Devriese
Obrigado, Dominique, mas, pela minha observação, parece que a razão pela qual muitos consideram um erro está mais parecida com a repetição do que alguns outros disseram. Novamente, dando uma nova olhada na covariância de array, é muito mais útil do que prejudicial. Novamente, o grande erro que Java cometeu foi a variação genérica do site de uso por meio de curingas. Isso causou mais problemas do que eu acho que "muitos" querem admitir.
Scott
30

O motivo é que toda matriz conhece seu tipo de elemento durante o tempo de execução, enquanto a coleta genérica não sabe por causa do apagamento do tipo.

Por exemplo:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

Se isso foi permitido com coleções genéricas:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

Mas isso causaria problemas mais tarde, quando alguém tentaria acessar a lista:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Katona
fonte
Eu acho que a resposta de Paul Bellora é mais apropriada, pois ele comenta sobre por que matrizes são covariantes. Se matrizes foram feitas invariáveis, tudo bem. você teria o tipo de apagamento com ele. O principal motivo para a propriedade Tipo Erasure é a compatibilidade com versões anteriores correta?
procurando
@ user2708477, sim, o apagamento de tipo foi introduzido devido à compatibilidade com versões anteriores. E sim, minha resposta tenta responder à pergunta no título, por que os genéricos são invariantes.
Katona
O fato de as matrizes conhecerem seu tipo significa que, embora a covariância permita que o código solicite o armazenamento de algo em uma matriz em que não caiba - não significa que essa loja poderá ocorrer. Consequentemente, o nível de perigo introduzido por ter matrizes ser covariante é muito menor do que seria se eles não conhecessem seus tipos.
Supercat 6/09
@supercat, correta, o que eu queria salientar é que, para os genéricos com o tipo de apagamento no lugar, covariância não poderia ter sido implementado com a segurança mínima de tempo de execução verifica
Katona
1
Pessoalmente, acho que essa resposta fornece a explicação correta sobre por que as matrizes são covariantes quando as coleções não poderiam ser. Obrigado!
ASGs
22

Pode ser esta ajuda: -

Os genéricos não são covariantes

Matrizes na linguagem Java são covariantes - o que significa que, se Inteiro estender Number (o que ele faz), além de um Inteiro também ser um Número, um Inteiro [] também será um Number[], e você poderá passar ou atribuir um número inteiro. Integer[]onde a Number[]é chamado. (Mais formalmente, se Number é um supertipo de Inteiro, então Number[]é um supertipo de Integer[].) Você também pode pensar o mesmo para tipos genéricos - que List<Number>é um supertipo List<Integer>e que você pode passar para List<Integer>onde List<Number>é esperado. Infelizmente, não funciona dessa maneira.

Acontece que há uma boa razão para que não funcione dessa maneira: quebraria o tipo que os genéricos de segurança deveriam fornecer. Imagine que você poderia atribuir a List<Integer>a a List<Number>. Em seguida, o código a seguir permitiria colocar algo que não era um número inteiro em um List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Como ln é a List<Number>, adicionar um float a ele parece perfeitamente legal. Mas se ln estivesse com um alias li, isso quebraria a promessa de segurança de tipo implícita na definição de li - que é uma lista de números inteiros, e é por isso que tipos genéricos não podem ser covariantes.

Rahul Tripathi
fonte
3
Para matrizes, você obtém um ArrayStoreExceptionem tempo de execução.
Sotirios Delimanolis
4
minha pergunta é WHYse as matrizes são covariantes. como Sotirios mencionou, com Arrays seria possível obter ArrayStoreException em tempo de execução, se Array se tornasse invariável, poderíamos detectar esse erro no momento da compilação correto?
precisa saber é o seguinte
@eagertoLearn: Uma das principais fraquezas semânticas do Java é que nada em seu sistema de tipos pode distinguir "Matriz que não contém nada além de derivados de Animal, que não precisa aceitar nenhum item recebido de outro lugar" de "Matriz que não deve conter nada além de Animal, e deve estar disposto a aceitar referências fornecidas externamente a Animal. Código que precisa do primeiro deve aceitar uma matriz de Cat, mas código que precisa do último não. Se o compilador puder distinguir os dois tipos, poderá fornecer verificação em tempo de compilação. Infelizmente, a única coisa que os distingue ... #
688
... é se o código realmente tenta armazenar algo neles, e não há como saber isso até o tempo de execução.
supercat
3

As matrizes são covariantes por pelo menos dois motivos:

  • É útil para coleções que contêm informações que nunca serão alteradas para serem covariantes. Para que uma coleção de T seja covariante, sua loja de apoio também deve ser covariante. Embora se possa projetar uma Tcoleção imutável que não a use T[]como repositório (por exemplo, usando uma árvore ou uma lista vinculada), é improvável que essa coleção funcione tão bem quanto uma que é apoiada por uma matriz. Alguém poderia argumentar que uma maneira melhor de fornecer coleções imutáveis ​​covariantes seria definir um tipo de "matriz imutável covariante" que eles pudessem usar em uma loja de apoio, mas simplesmente permitir a covariância da matriz era provavelmente mais fácil.

  • As matrizes serão frequentemente alteradas por código que não sabe que tipo de coisa haverá nelas, mas não colocará na matriz nada que não tenha sido lido nessa mesma matriz. Um excelente exemplo disso é o código de classificação. Conceitualmente, pode ter sido possível para os tipos de matriz incluir métodos para trocar ou permutar elementos (esses métodos podem ser igualmente aplicáveis ​​a qualquer tipo de matriz) ou definir um objeto "manipulador de matriz" que faça referência a uma matriz e uma ou mais coisas que foram lidas a partir dele e podem incluir métodos para armazenar itens lidos anteriormente na matriz da qual eles vieram. Se as matrizes não fossem covariantes, o código do usuário não seria capaz de definir esse tipo, mas o tempo de execução poderia ter incluído alguns métodos especializados.

O fato de que as matrizes são covariantes pode ser visto como um truque feio, mas na maioria dos casos, facilita a criação de código funcional.

supercat
fonte
1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- ponto bom
eagertoLearn
3

Uma característica importante dos tipos paramétricos é a capacidade de escrever algoritmos polimórficos, ou seja, algoritmos que operam em uma estrutura de dados independentemente do seu valor de parâmetro, como Arrays.sort() .

Com genéricos, isso é feito com tipos curinga:

<E extends Comparable<E>> void sort(E[]);

Para ser realmente útil, os tipos de caracteres curinga requerem captura de caracteres curinga, e isso requer a noção de um parâmetro de tipo. Nada disso estava disponível no momento em que as matrizes foram adicionadas ao Java, e as matrizes de makes do tipo covariante de referência permitiram uma maneira muito mais simples de permitir algoritmos polimórficos:

void sort(Comparable[]);

No entanto, essa simplicidade abriu uma brecha no sistema de tipo estático:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

exigindo uma verificação de tempo de execução de todo acesso de gravação a uma matriz do tipo de referência.

Em poucas palavras, a abordagem mais recente incorporada pelos genéricos torna o sistema de tipos mais complexo, mas também mais estaticamente seguro, enquanto a abordagem mais antiga era mais simples e menos segura do tipo estaticamente. Os projetistas da linguagem optaram pela abordagem mais simples, tendo coisas mais importantes a fazer do que fechar uma pequena brecha no sistema de tipos que raramente causa problemas. Mais tarde, quando o Java foi estabelecido e as necessidades prementes foram atendidas, eles tinham os recursos necessários para fazer o correto para genéricos (mas alterá-lo para matrizes teria quebrado os programas Java existentes).

Meriton
fonte
2

Os genéricos são invariantes : da JSL 4.10 :

... A subtipagem não se estende por tipos genéricos: T <: U não implica que C<T><: C<U>...

e algumas linhas adiante, a JLS também explica que as
matrizes são covariantes (primeiro marcador):

4.10.3 Subtipagem entre tipos de matriz

insira a descrição da imagem aqui

alfasin
fonte
2

Eu acho que eles tomaram uma decisão errada em primeiro lugar que tornaram a matriz covariável. Ele quebra o tipo de segurança descrito aqui e eles ficaram presos por causa da compatibilidade com versões anteriores e depois tentaram não cometer o mesmo erro de genérico. E essa é uma das razões pelas quais Joshua Bloch prefere as listas ao item 25 do livro "Java eficaz (segunda edição)"

Arnold
fonte
Josh Block foi o autor da estrutura de coleções do Java (1.2) e o autor dos genéricos do Java (1.5). Então o cara que construiu os genéricos dos quais todos reclamam também é coincidentemente o cara que escreveu o livro dizendo que eles são o melhor caminho a percorrer? Não é uma grande surpresa!
Cpurdy
1

Minha opinião: quando o código está esperando uma matriz A [] e você fornece B [] onde B é uma subclasse de A, há apenas duas coisas com que se preocupar: o que acontece quando você lê um elemento da matriz e o que acontece se você escrever isto. Portanto, não é difícil escrever regras de linguagem para garantir que a segurança do tipo seja preservada em todos os casos (a regra principal é que um ArrayStoreExceptionpoderia ser lançado se você tentar inserir um A em um B []). Para um genérico, porém, quando você declara uma classe SomeClass<T>, pode haver várias maneiras de Tusá-lo no corpo da classe, e acho que é muito complicado elaborar todas as combinações possíveis para escrever regras sobre quando coisas são permitidas e quando não são.

ajb
fonte