Quais são as razões pelas quais o Map.get (chave do objeto) não é (totalmente) genérico

405

Quais são as razões por trás da decisão de não ter um método get totalmente genérico na interface do java.util.Map<K, V>.

Para esclarecer a questão, a assinatura do método é

V get(Object key)

ao invés de

V get(K key)

e eu estou querendo saber o porquê (a mesma coisa para remove, containsKey, containsValue).

WMR
fonte
3
Pergunta semelhante sobre Coleção: stackoverflow.com/questions/104799/…
AlikElzin-kilaka 4/12
11
Surpreendente. Estou usando Java há mais de 20 anos e hoje percebo esse problema.
GhostCat 9/08/19

Respostas:

260

Conforme mencionado por outros, a razão pela qual get()etc. não é genérica porque a chave da entrada que você está recuperando não precisa ser do mesmo tipo que o objeto para o qual você passa get(); a especificação do método requer apenas que sejam iguais. Isso segue como o equals()método recebe um Objeto como parâmetro, não apenas o mesmo tipo que o objeto.

Embora possa ser verdade que muitas classes foram equals()definidas para que seus objetos possam ser iguais aos objetos de sua própria classe, há muitos lugares em Java onde esse não é o caso. Por exemplo, a especificação para List.equals()diz que dois objetos de Lista são iguais se forem Listas e tiverem o mesmo conteúdo, mesmo que sejam implementações diferentes de List. Então, voltando ao exemplo nesta questão, de acordo com a especificação do método, é possível ter um Map<ArrayList, Something>e para eu chamar get()com um LinkedListargumento as, e ele deve recuperar a chave que é uma lista com o mesmo conteúdo. Isso não seria possível se get()fosse genérico e restringisse seu tipo de argumento.

newacct
fonte
28
Então, por que está V Get(K k)em c #?
134
A questão é: se você deseja ligar m.get(linkedList), por que você não definiu mo tipo como Map<List,Something>? Não consigo pensar em um caso de caso em que fazer chamadas m.get(HappensToBeEqual)sem alterar o Maptipo para obter uma interface faça sentido.
Elazar Leibovich
58
Uau, falha de design séria. Você também não recebe nenhum aviso do compilador. Eu concordo com Elazar. Se este é realmente útil, o que duvido acontece muitas vezes, um getByEquals (-chave Object) soa mais razoável ...
mmm
37
Parece que essa decisão foi tomada com base na pureza teórica e não na praticidade. Para a maioria dos usos, os desenvolvedores preferem ver o argumento limitado pelo tipo de modelo, do que tê-lo ilimitado para suportar casos extremos como o mencionado por newacct em sua resposta. Deixar as assinaturas sem modelo cria mais problemas do que resolve.
Sam Goldberg
14
@ newacct: “perfeitamente tipo seguro” é uma forte reivindicação de uma construção que pode falhar imprevisivelmente em tempo de execução. Não restrinja sua visualização a mapas de hash que funcionem com isso. TreeMappode falhar quando você passa objetos do tipo errado para o getmétodo, mas pode passar ocasionalmente, por exemplo, quando o mapa está vazio. E ainda pior, no caso de um fornecimento, Comparatoro comparemétodo (que possui uma assinatura genérica!) Pode ser chamado com argumentos do tipo errado, sem nenhum aviso desmarcado. Este é um comportamento quebrado.
Holger
105

Um incrível codificador de Java do Google, Kevin Bourrillion, escreveu exatamente sobre esse problema em um post de blog há um tempo atrás (reconhecidamente no contexto de em Setvez de Map). A frase mais relevante:

De maneira uniforme, os métodos do Java Collections Framework (e também da Biblioteca de coleções do Google) nunca restringem os tipos de parâmetros, exceto quando é necessário impedir que a coleção seja interrompida.

Não tenho certeza absoluta de que concordo com isso como um princípio - o .NET parece bem exigindo o tipo de chave correto, por exemplo -, mas vale a pena seguir o raciocínio na postagem do blog. (Tendo mencionado o .NET, vale a pena explicar que parte do motivo pelo qual não é um problema no .NET é que há o maior problema no .NET de variação mais limitada ...)

Jon Skeet
fonte
4
Apocalisp: isso não é verdade, a situação ainda é a mesma.
Kevin Bourrillion
9
@ user102008 Não, a postagem não está errada. Mesmo que um Integere um Doublenunca sejam iguais, ainda é uma pergunta justa perguntar se a Set<? extends Number>contém o valor new Integer(5).
Kevin Bourrillion
33
Eu nunca quis verificar uma vez uma associação a Set<? extends Foo>. Eu mudei muito frequentemente o tipo de chave de um mapa e fiquei frustrado por o compilador não conseguir encontrar todos os locais onde o código precisava ser atualizado. Realmente não estou convencido de que essa seja a troca correta.
Porculus 21/09/12
4
@EarthEngine: sempre foi quebrado. Esse é o ponto - o código está quebrado, mas o compilador não pode pegá-lo.
Jon Skeet
11
E ainda está quebrado, e apenas nos causou um bug ... resposta incrível.
GhostCat 9/08/19
28

O contrato é expresso assim:

Mais formalmente, se este mapa contiver um mapeamento de uma chave k para um valor v tal que (key == null? K == null: key.equals (k) ), esse método retornará v; caso contrário, ele retornará nulo. (Pode haver no máximo um desses mapeamentos.)

(minha ênfase)

e, como tal, uma pesquisa de chave bem-sucedida depende da implementação do método de igualdade pela chave de entrada. Isso não é necessariamente dependente da classe de k.

Brian Agnew
fonte
4
Também é dependente hashCode(). Sem uma implementação adequada de hashCode (), um bem implementado equals()é bastante inútil neste caso.
31411 rudolfson
5
Eu acho que, em princípio, isso permitiria que você usasse um proxy leve para uma chave, se a recriação de toda a chave fosse impraticável - desde que equals () e hashCode () sejam implementados corretamente.
Bill Michell
5
@rudolfson: Até onde eu sei, apenas um HashMap depende do código de hash para encontrar o bucket correto. Um TreeMap, por exemplo, usa uma árvore de pesquisa binária e não se importa com hashCode ().
1300 Rob
4
A rigor, get()não é necessário usar um argumento do tipo Objectpara satisfazer o contato. Imagine que o método get estivesse restrito ao tipo de chave K- o contrato ainda seria válido. Obviamente, os usos em que o tipo de tempo de compilação não era uma subclasse de Kagora não seriam compilados, mas isso não invalida o contrato, pois os contratos discutem implicitamente o que acontece se o código for compilado.
BeeOnRope
16

É uma aplicação da Lei de Postel: "seja conservador no que faz, seja liberal no que aceita dos outros".

Verificações de igualdade podem ser realizadas independentemente do tipo; o equalsmétodo é definido na Objectclasse e aceita qualquer Objectcomo parâmetro. Portanto, faz sentido que a equivalência de chave e operações baseadas na equivalência de chave aceitem qualquer Objecttipo.

Quando um mapa retorna valores-chave, ele conserva o máximo possível de informações de tipo, usando o parâmetro type.

erickson
fonte
4
Então, por que está V Get(K k)em c #?
11
Está V Get(K k)em C # porque também faz sentido. A diferença entre as abordagens Java e .NET é realmente apenas quem bloqueia coisas não correspondentes. Em C # é o compilador, em Java é a coleção. Raiva I sobre classes de coleção inconsistentes do .NET vez em quando, mas Get()e Remove()aceitando apenas um tipo de correspondência certamente impede que você acidentalmente passando um valor errado.
Wormbo
26
É uma aplicação incorreta da lei de Postel. Seja liberal no que você aceita dos outros, mas não muito liberal. Essa API idiota significa que você não pode dizer a diferença entre "não está na coleção" e "você cometeu um erro de digitação estática". Muitos milhares de horas perdidas do programador poderiam ter sido evitadas com o get: K -> boolean.
Juiz Mental
11
Claro que deveria ter sido contains : K -> boolean.
Juiz Mental
4
Postel estava errado .
Alnitak
13

Acho que esta seção do Tutorial sobre genéricos explica a situação (minha ênfase):

"Você precisa garantir que a API genérica não seja indevidamente restritiva; ela deve continuar a oferecer suporte ao contrato original da API. Considere novamente alguns exemplos de java.util.Collection. A API pré-genérica se parece com:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Uma tentativa ingênua de gerá-lo é:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Embora esse seja certamente um tipo seguro, ele não cumpre o contrato original da API. O método containsAll () funciona com qualquer tipo de coleção recebida. Só terá êxito se a coleção recebida realmente contiver apenas instâncias de E, mas:

  • O tipo estático da coleção recebida pode ser diferente, talvez porque o chamador não saiba o tipo exato da coleção que está sendo transmitida ou talvez porque seja uma Coleção <S>, onde S é um subtipo de E.
  • É perfeitamente legítimo chamar containsAll () com uma coleção de um tipo diferente. A rotina deve funcionar, retornando false ".
Yardena
fonte
2
porque não containsAll( Collection< ? extends E > c )então?
Juiz Mental
11
@JudgeMental, embora não seja dado como exemplo acima, também é necessário permitir containsAllcom um Collection<S>onde Sé um supertipo de E. Isso não seria permitido se fosse containsAll( Collection< ? extends E > c ). Além disso, como é explicitamente declarado no exemplo, é legítimo passar uma coleção de um tipo diferente (com o valor de retorno então false).
davmac
Não deve ser necessário permitir containsAll com uma coleção de um supertipo de E. Argumento que é necessário proibir essa chamada com uma verificação de tipo estático para evitar um bug. É um contrato bobo, que eu acho que é o ponto da pergunta original.
Juiz Mental
6

O motivo é que a contenção é determinada por equalse hashCodequais são os métodos ativados Objecte ambos assumem um Objectparâmetro. Essa foi uma falha de design anterior nas bibliotecas padrão do Java. Juntamente com as limitações no sistema de tipos do Java, força qualquer coisa que dependa de iguais e hashCode a serem executadas Object.

A única maneira de ter tabelas de hash tipo seguro e igualdade em Java é evitar Object.equalse Object.hashCodee usar um substituto genérico. Java funcional vem com classes de tipo para esse fim: Hash<A>e Equal<A>. HashMap<K, V>É fornecido um wrapper que leva Hash<K>e Equal<K>em seu construtor. Portanto, essa classe gete containsmétodos usam um argumento genérico do tipo K.

Exemplo:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error
Apocalisp
fonte
4
Isso por si só não impede que o tipo de 'get' seja declarado como "V get (chave K)", porque 'Object' sempre é um ancestral de K; portanto, "key.hashCode ()" ainda seria válido.
finnw 21/09/09
11
Embora não o impeça, acho que explica. Se eles mudassem o método equals para forçar a igualdade de classe, certamente não poderiam dizer às pessoas que o mecanismo subjacente para localizar o objeto no mapa utiliza equals () e hashmap () quando os protótipos do método para esses métodos não são compatíveis.
12124
5

Compatibilidade.

Antes que os genéricos estivessem disponíveis, havia apenas get (Objeto o).

Se eles tivessem alterado esse método para obter (<K> o), isso teria potencialmente forçado a manutenção maciça de código para usuários java apenas para tornar o código de trabalho compilado novamente.

Eles poderiam ter introduzido um método adicional , digamos get_checked (<K> o) e reprovado o antigo método get (), para que houvesse um caminho de transição mais suave. Mas, por alguma razão, isso não foi feito. (A situação em que estamos agora é que você precisa instalar ferramentas como findBugs para verificar a compatibilidade de tipos entre o argumento get () e o tipo de chave declarada <K> do mapa.)

Os argumentos relativos à semântica de .equals () são falsos, eu acho. (Tecnicamente, eles estão corretos, mas ainda acho que são falsos. Nenhum designer em sã consciência jamais tornará o1.equals (o2) verdadeiro se o1 e o2 não tiverem nenhuma superclasse comum.)

Erwin Smout
fonte
4

Há mais uma razão pesada, isso não pode ser feito tecnicamente, porque quebra o Map.

Java tem construção genérica polimórfica como <? extends SomeClass>. Marcada como referência pode apontar para o tipo assinado <AnySubclassOfSomeClass>. Mas o genérico polimórfico torna essa referência somente leitura . O compilador permite que você use tipos genéricos apenas como tipo de método retornado (como getters simples), mas bloqueia o uso de métodos em que tipo genérico é argumento (como setters comuns). Isso significa que, se você escrever Map<? extends KeyType, ValueType>, o compilador não permitirá que você chame o método get(<? extends KeyType>)e o mapa será inútil. A única solução é fazer com que este método não genérico: get(Object).

Owheee
fonte
por que o método set é fortemente digitado então?
Sentenza
se você quer dizer 'put': o método put () altera o mapa e não estará disponível com genéricos como <? estende SomeClass>. Se você chamar, você tem uma exceção de compilação. Esse mapa será "somente leitura"
Owheee
1

Compatibilidade com versões anteriores, eu acho. Map(ou HashMap) ainda precisa apoiar get(Object).

Anton Gogolev
fonte
13
Mas o mesmo argumento pode ser feito para put(o que restringe os tipos genéricos). Você obtém compatibilidade com versões anteriores usando tipos brutos. Os genéricos são "aceitos".
Thilo
Pessoalmente, acho que o motivo mais provável para essa decisão de design é a compatibilidade com versões anteriores.
Geekdenz #
1

Eu estava olhando para isso e pensando por que eles fizeram dessa maneira. Não acho que nenhuma das respostas existentes explique por que elas não puderam apenas fazer a nova interface genérica aceitar apenas o tipo adequado para a chave. O motivo real é que, embora eles tenham introduzido genéricos, NÃO criaram uma nova interface. A interface do mapa é o mesmo mapa não genérico antigo, serve apenas como versão genérica e não genérica. Dessa forma, se você tiver um método que aceite um Mapa não genérico, poderá passá-lo Map<String, Customer>e ainda funcionará. Ao mesmo tempo, o contrato para get aceita Object, portanto a nova interface também deve suportar esse contrato.

Na minha opinião, eles deveriam ter adicionado uma nova interface e implementado tanto na coleção existente, mas decidiram a favor de interfaces compatíveis, mesmo que isso signifique pior design para o método get. Observe que as próprias coleções seriam compatíveis com os métodos existentes, apenas as interfaces não.

Stilgar
fonte
0

Estamos fazendo uma grande refatoração no momento e estávamos perdendo este get () fortemente tipado para verificar se não perdemos alguns get () com o tipo antigo.

Mas eu achei uma solução alternativa / feia para a verificação do tempo de compilação: crie uma interface de mapa com o get fortemente tipado, containsKey, remova ... e coloque-o no pacote java.util do seu projeto.

Você receberá erros de compilação apenas ao chamar get (), ... com tipos errados, tudo o que os outros parecem bem para o compilador (pelo menos dentro do eclipse kepler).

Não se esqueça de excluir essa interface após a verificação de sua compilação, pois não é isso que você deseja em tempo de execução.

henva
fonte