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

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 de java.util.Map .

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

V get(Object key)

ao invés de

V get(K key)

e estou me perguntando por que (mesma coisa para remove, containsKey, containsValue ).

    Como mencionado por outros, o motivo pelo qual get() , etc. não é genérico porque a chave da input que você está recuperando não precisa ser do mesmo tipo que o object que você passa para get() ; a especificação do método requer apenas que sejam iguais. Isso decorre de como o método equals() aceita um parâmetro Object, não apenas o mesmo tipo do object.

    Embora seja comum que muitas classs tenham equals() definidas para que seus objects só possam ser iguais a objects de sua própria class, existem muitos lugares em Java onde isso não é o caso. Por exemplo, a especificação para List.equals() diz que dois objects List são iguais se eles forem Lists 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 e para mim chamar get() com um LinkedList como argumento, 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.

    Um incrível codificador de Java do Google, Kevin Bourrillion, escreveu exatamente sobre esse assunto em um post de blog há pouco tempo (reconhecidamente no contexto de Set vez de Map ). A sentença mais relevante:

    Uniformemente, os methods do Java Collections Framework (e da Biblioteca de Coleções do Google também) nunca restringem os tipos de seus parâmetros, exceto quando é necessário impedir que a coleção seja quebrada.

    Eu não tenho certeza se concordo com isso como um princípio – o .NET parece estar bem, exigindo o tipo certo de chave, por exemplo -, mas vale a pena seguir o raciocínio no post do blog. (Tendo mencionado o .NET, vale a pena explicar que parte do motivo pelo qual não é um problema no .NET é que existe o maior problema no .NET de variação mais limitada …)

    O contrato é expresso assim:

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

    (minha ênfase)

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

    É uma aplicação da Lei de Postel, “seja conservador no que você faz, seja liberal no que você aceita dos outros.”

    Verificações de igualdade podem ser realizadas independentemente do tipo; o método equals é definido na class Object e aceita qualquer Object como um parâmetro. Portanto, faz sentido que a equivalência de chave e as operações baseadas na equivalência de chave aceitem qualquer tipo de Object .

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

    Eu acho que esta seção do Generics Tutorial explica a situação (minha ênfase):

    “Você precisa ter certeza de que a API genérica não é indevidamente restritiva; ela deve continuar a suportar o contrato original da API. Considere novamente alguns exemplos de java.util.Collection. A API pré-genérica é semelhante a:

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

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

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

    Embora isso seja certamente seguro, ele não corresponde ao contrato original da API. O método containsAll () funciona com qualquer tipo de coleta de input. Ele só terá sucesso se a coleção recebida realmente contiver apenas instâncias de E, mas:

    • O tipo estático da coleção de input pode ser diferente, talvez porque o chamador não saiba o tipo preciso da coleção sendo transmitida, ou talvez porque seja uma Coleção , em que S é um subtipo de E.
    • É perfeitamente legítimo chamar containsAll () com uma coleção de um tipo diferente. A rotina deve funcionar, retornando falsa “.

    O motivo é que a contenção é determinada por equals e hashCode que são methods em Object e ambos usam um parâmetro Object . Essa foi uma falha inicial no design das bibliotecas padrão do Java. Juntamente com as limitações no sistema de tipos do Java, ele força qualquer coisa que dependa de equals e hashCode a pegar Object .

    A única maneira de ter tabelas hash seguras contra tipos e igualdade em Java é evitar Object.equals e Object.hashCode e usar um substituto genérico. O Java funcional vem com classs de tipos apenas para esse propósito: Hash e Equal . Um wrapper para HashMap é fornecido e usa Hash e Equal em seu construtor. Os methods get e contains dessa class, portanto, usam um argumento genérico do tipo K

    Exemplo:

     HashMap h = new HashMap(Equal.stringEqual, Hash.stringHash); h.add("one", 1); h.get("one"); // All good h.get(Integer.valueOf(1)); // Compiler error 

    Há mais um motivo importante, não pode ser feito tecnicamente, porque ele trafega o Mapa.

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

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

    Eu estava olhando para isso e pensando por que eles fizeram isso dessa maneira. Eu não acho que nenhuma das respostas existentes explique por que eles não poderiam simplesmente fazer a nova interface genérica aceitar apenas o tipo apropriado para a chave. A razão real é que, embora tenham introduzido genéricos, NÃO criaram uma nova interface. A interface Map é o mesmo mapa não genérico que 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 por um Map e ele ainda funcionará. Ao mesmo tempo, o contrato para get aceita Object para que a nova interface também suporte esse contrato.

    Na minha opinião, eles deveriam ter adicionado uma nova interface e implementado ambos na coleção existente, mas eles decidiram em favor de interfaces compatíveis, mesmo que isso signifique pior design para o método get. Observe que as collections em si seriam compatíveis com os methods existentes apenas as interfaces não seriam.

    Compatibilidade.

    Antes dos genéricos estarem disponíveis, era só pegar (Objecto).

    Se eles tivessem mudado esse método para obter ( o), ele teria forçado potencialmente a manutenção massiva de código para usuários java apenas para compilar o código de trabalho novamente.

    Eles poderiam ter introduzido um método adicional , digamos, get_checked ( o) e depreciar o método get () antigo para que houvesse um caminho de transição mais suave. Mas, por algum motivo, 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 () eo tipo de chave declarada do mapa.)

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

    Estamos fazendo grandes refatorações agora e estávamos perdendo esse get () fortemente tipado para verificar se não perdemos alguns get () com o tipo antigo.

    Mas eu encontrei um truque de workaround / feio para verificação de tempo de compilation: crie uma interface de mapa com get, containsKey, remove … e coloque-o no pacote java.util do seu projeto.

    Você receberá erros de compilation apenas para chamar get (), … com tipos errados, tudo o que parece bom para o compilador (pelo menos dentro do eclipse kepler).

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