Por que o Java Collections não remove métodos genéricos?

144

Por que Collection.remove (Object o) não é genérico?

Parece que Collection<E>poderia terboolean remove(E o);

Em seguida, quando você tenta remover acidentalmente (por exemplo) em Set<String>vez de cada String individual de um Collection<String>, seria um erro de tempo de compilação em vez de um problema de depuração posteriormente.

Chris Mazzola
fonte
13
Isso pode realmente te incomodar quando você o combina com a caixa automática. Se você tentar remover algo de uma lista e passar um número inteiro em vez de um int, ele chama o método remove (Object).
ScArcher2 19/09/08
2
Pergunta semelhante sobre o mapa: stackoverflow.com/questions/857420/…
AlikElzin-kilaka 4/12

Respostas:

73

Josh Bloch e Bill Pugh se referem a esta edição em Java Puzzlers IV: A ameaça fantasma de referência, Ataque do clone e A vingança da mudança .

Josh Bloch diz (6:41) que eles tentaram gerar o método get do Map, remover o método e outros, mas "simplesmente não funcionou".

Existem muitos programas razoáveis ​​que não poderiam ser gerados se você permitir apenas o tipo genérico da coleção como tipo de parâmetro. O exemplo dado por ele é uma interseção de a Listde Numbers e a Listde Longs.

dmeister
fonte
6
Por que add () pode receber um parâmetro digitado, mas remove () não pode ainda estar um pouco além da minha compreensão. Josh Bloch seria a referência definitiva para as perguntas de coleções. Pode ser tudo o que recebo sem tentar fazer uma implementação de coleção semelhante e ver por mim mesmo. :( Obrigado.
Chris Mazzola
2
Chris - leia o PDF do Java Generics Tutorial, ele explicará o porquê.
21710 JeeBee
42
Na verdade, é muito simples! Se add () pegasse um objeto errado, ele quebraria a coleção. Ele conteria coisas que não deveria! Esse não é o caso de remove () ou contém ().
Kevin Bourrillion
12
Aliás, essa regra básica - usando parâmetros de tipo para impedir danos reais apenas à coleção - é seguida de maneira absolutamente consistente em toda a biblioteca.
Kevin Bourrillion
3
@KevinBourrillion: Eu trabalho com genéricos há anos (em Java e C #) sem nunca perceber que a regra de "dano real" ainda existe ... mas agora que eu a vi declarada diretamente faz todo o sentido. Obrigado pelo peixe !!! Exceto agora, sinto-me compelido a voltar e examinar minhas implementações, para ver se alguns métodos podem e, portanto, devem ser degenerados. Suspiro.
corlettk
74

remove()(in Mape in Collection) não é genérico porque você deve poder passar qualquer tipo de objeto para remove(). O objeto removido não precisa ser do mesmo tipo que o objeto para o qual você passa remove(); requer apenas que sejam iguais. A partir da especificação de remove(), remove(o)remove o objeto ecomo (o==null ? e==null : o.equals(e))está true. Observe que não há nada que exija oe eseja do mesmo tipo. Isso decorre do fato de o equals()método receber um Objectparâmetro as, não apenas do mesmo tipo que o objeto.

Embora possa ser verdade que muitas classes tenham sido equals()definidas para que seus objetos possam ser iguais apenas aos objetos de sua própria classe, esse nem sempre é 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 pergunta, é possível ter um Map<ArrayList, Something>e para mim chamar remove()com um LinkedListargumento as e deve remover a chave que é uma lista com o mesmo conteúdo. Isso não seria possível se remove()fosse genérico e restringisse seu tipo de argumento.

newacct
fonte
1
Mas se você definisse o Mapa como Mapa <Lista, Algo> (em vez de ArrayList), seria possível remover usando um LinkedList. Eu acho que esta resposta está incorreta.
AlikElzin-kilaka 4/12/12
3
A resposta parece estar correta, mas incompleta. Apenas se presta a perguntar por que diabos eles não generam o equals()método também? Pude ver mais benefícios ao digitar segurança em vez dessa abordagem "libertária". Eu acho que a maioria dos casos com a implementação atual é para erros que entram no nosso código, e não sobre a alegria dessa flexibilidade que o remove()método traz.
Kellogs
2
@ kellogs: O que você quer dizer com "genérico do equals()método"?
newacct
5
@ MattBall: "onde T é a classe declarante" Mas não existe essa sintaxe em Java. Tdeve ser declarado como um parâmetro de tipo na classe e Objectnão possui nenhum parâmetro de tipo. Não há como ter um tipo que se refere à "classe declarante".
newacct
3
Acho kellogs está dizendo que se a igualdade fosse uma interface genérica Equality<T>com equals(T other). Então você poderia ter remove(Equality<T> o)e oé apenas um objeto que pode ser comparado a outro T.
weston
11

Como se o seu parâmetro type for um curinga, você não poderá usar um método de remoção genérico.

Parece-me que me lembro de me deparar com essa questão com o método get (Object) do Map. O método get, nesse caso, não é genérico, embora deva esperar razoavelmente receber um objeto do mesmo tipo que o primeiro parâmetro de tipo. Percebi que se você está passando o Maps com um curinga como o primeiro parâmetro de tipo, não há como extrair um elemento do Map com esse método, se esse argumento for genérico. Os argumentos curinga não podem ser realmente satisfeitos, porque o compilador não pode garantir que o tipo esteja correto. Especulo que o motivo pelo qual adicionar é genérico é que você deve garantir que o tipo esteja correto antes de adicioná-lo à coleção. No entanto, ao remover um objeto, se o tipo estiver incorreto, ele não corresponderá a nada.

Provavelmente não expliquei muito bem, mas me parece lógico o suficiente.

Bob Gettys
fonte
1
Você poderia elaborar um pouco sobre isso?
Thomas Owens
6

Além das outras respostas, há outro motivo pelo qual o método deve aceitar um Object, que é predicado. Considere o seguinte exemplo:

class Person {
    public String name;
    // override equals()
}
class Employee extends Person {
    public String company;
    // override equals()
}
class Developer extends Employee {
    public int yearsOfExperience;
    // override equals()
}

class Test {
    public static void main(String[] args) {
        Collection<? extends Person> people = new ArrayList<Employee>();
        // ...

        // to remove the first employee with a specific name:
        people.remove(new Person(someName1));

        // to remove the first developer that matches some criteria:
        people.remove(new Developer(someName2, someCompany, 10));

        // to remove the first employee who is either
        // a developer or an employee of someCompany:
        people.remove(new Object() {
            public boolean equals(Object employee) {
                return employee instanceof Developer
                    || ((Employee) employee).company.equals(someCompany);
        }});
    }
}

O ponto é que o objeto que está sendo passado para o removemétodo é responsável por definir o equalsmétodo. A construção de predicados se torna muito simples dessa maneira.

Hosam Aly
fonte
Investidor? (Enchimento de enchimento de enchimento)
Matt R
3
A lista é implementado como yourObject.equals(developer), conforme documentado na API Collections: java.sun.com/javase/6/docs/api/java/util/...
Hosam Aly
13
Isso parece um abuso para mim
RAY
7
É abuso, já que seu objeto predicado quebra o contrato do equalsmétodo, ou seja, simetria. O método remove está vinculado apenas à sua especificação, desde que seus objetos atendam à especificação equals / hashCode, portanto, qualquer implementação estará livre para fazer a comparação do contrário. Além disso, seu objeto predicado não implementa o .hashCode()método (não pode ser implementado de forma consistente com iguais), portanto, a chamada de remoção nunca funcionará em uma coleção baseada em Hash (como HashSet ou HashMap.keys ()). O fato de funcionar com ArrayList é pura sorte.
Pa Elo Ebermann 28/02
3
(Não estou discutindo a questão do tipo genérico - isso já foi respondido antes -, apenas o uso de iguais para predicados aqui.) É claro que o HashMap e o HashSet estão verificando o código de hash e o TreeSet / Map está usando a ordem dos elementos . Ainda assim, eles implementam completamente Collection.remove, sem quebrar seu contrato (se a ordem for consistente com igual). E uma ArrayList variada (ou AbstractCollection, eu acho) com a chamada de igual ativada ainda implementaria corretamente o contrato - a culpa é sua se ela não funcionar conforme o esperado, pois você está quebrando o equalscontrato.
Paŭlo Ebermann
5

Suponha que tem uma coleção de Cat, e algumas referências de objetos de tipos Animal, Cat, SiameseCat, e Dog. Perguntar à coleção se ela contém o objeto referido pela referência Catou SiameseCatparece razoável. Perguntar se ele contém o objeto referido pela Animalreferência pode parecer desonesto, mas ainda é perfeitamente razoável. O objeto em questão pode, afinal, ser um Cate pode aparecer na coleção.

Além disso, mesmo que o objeto seja algo diferente de a Cat, não há problema em dizer se ele aparece na coleção - simplesmente responda "não, não aparece". Uma coleção "de estilo de pesquisa" de algum tipo deve ser capaz de aceitar significativamente a referência de qualquer supertipo e determinar se o objeto existe na coleção. Se a referência do objeto passado for de um tipo não relacionado, não há como a coleção a conter, de modo que a consulta não é significativa (de certo modo, ela sempre responderá "não"). No entanto, como não há como restringir os parâmetros a subtipos ou supertipos, é mais prático simplesmente aceitar qualquer tipo e responder "não" a objetos cujo tipo não esteja relacionado ao da coleção.

supercat
fonte
1
"Se a referência do objeto transmitido for de um tipo não relacionado, não há como a coleção possivelmente a conter" Errado. Ele só precisa conter algo igual a ele; e objetos de diferentes classes podem ser iguais.
Newacct
"pode ​​parecer desonesto, mas ainda é perfeitamente razoável" É? Considere um mundo em que um objeto de um tipo nem sempre possa ser verificado quanto à igualdade com um objeto de outro tipo, porque qual tipo pode ser igual é parametrizado (semelhante a como Comparableé parametrizado para os tipos com os quais você pode comparar). Então não seria razoável permitir que as pessoas passassem algo de um tipo não relacionado.
Newacct
@ newacct: Há uma diferença fundamental entre comparação de magnitude e comparação de igualdade: objetos dados Ae Bde um tipo Xe Yde outro, tais que A> B, e X> Y. Ou A> Ye Y< Aou X> Be B< X. Esses relacionamentos só podem existir se as comparações de magnitude conhecerem os dois tipos. Por outro lado, o método de comparação de igualdade de um objeto pode simplesmente se declarar desigual a qualquer coisa de qualquer outro tipo, sem ter que saber nada sobre o outro tipo em questão. Um objeto do tipo Catpode não ter idéia de se é ... #
318
... "maior que" ou "menor que" um objeto do tipo FordMustang, mas não deve ter dificuldade em dizer se é igual a esse objeto (a resposta, obviamente, é "não").
Supercat
4

Eu sempre achei que isso acontecia porque remove () não tem motivos para se importar com o tipo de objeto que você atribui a ele. Independentemente disso, é fácil verificar se esse objeto é um dos que a coleção contém, pois pode chamar igual () em qualquer coisa. É necessário verificar o tipo em add () para garantir que ele contenha apenas objetos desse tipo.

ColinD
fonte
0

Foi um compromisso. Ambas as abordagens têm sua vantagem:

  • remove(Object o)
    • é mais flexível. Por exemplo, ele permite percorrer uma lista de números e removê-los de uma lista de longos.
    • código que usa essa flexibilidade pode ser mais facilmente gerado
  • remove(E e) traz mais segurança de tipo ao que a maioria dos programas deseja, detectando erros sutis no tempo de compilação, como tentar erroneamente remover um número inteiro de uma lista de curtos.

A compatibilidade com versões anteriores sempre foi um dos principais objetivos ao desenvolver a API Java; portanto, o remove (Object o) foi escolhido porque facilitava a geração do código existente. Se a compatibilidade com versões anteriores NÃO tivesse sido um problema, acho que os designers escolheriam remover (E e).

Stefan Feuerhahn
fonte
-1

Remove não é um método genérico, de modo que o código existente usando uma coleção não genérica ainda será compilado e ainda terá o mesmo comportamento.

Consulte http://www.ibm.com/developerworks/java/library/j-jtp01255.html para obter detalhes.

Edit: Um comentarista pergunta por que o método add é genérico. [... removeu minha explicação ...] O segundo comentarista respondeu à pergunta do firebird84 muito melhor do que eu.

Jeff C
fonte
2
Então, por que o método add é genérico?
Bob Gettys 19/09/08
@ firebird84 remove (Object) ignora objetos do tipo errado, mas remove (E) causaria um erro de compilação. Isso mudaria o comportamento.
noah
: shrug: - o comportamento do tempo de execução não é alterado; erro de compilação não é comportamento de tempo de execução . O "comportamento" do método add é alterado dessa maneira.
21413 Jason S
-2

Outro motivo é por causa das interfaces. Aqui está um exemplo para mostrá-lo:

public interface A {}

public interface B {}

public class MyClass implements A, B {}

public static void main(String[] args) {
   Collection<A> collection = new ArrayList<>();
   MyClass item = new MyClass();
   collection.add(item);  // works fine
   B b = item; // valid
   collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}
Patrick
fonte
Você está mostrando algo com o qual pode se safar porque remove()não é covariante. A questão, porém, é se isso deve ser permitido. ArrayList#remove()funciona por meio de igualdade de valor, não de igualdade de referência. Por que você esperaria que a Bfosse igual a um A? No seu exemplo, pode ser, mas é uma expectativa estranha. Prefiro ver você fornecer um MyClassargumento aqui.
seh
-3

Porque isso quebraria o código existente (pré-Java5). por exemplo,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

Agora, você pode dizer que o código acima está errado, mas suponha que o tenha vindo de um conjunto heterogêneo de objetos (ou seja, continha cadeias, número, objetos etc.). Você deseja remover todas as correspondências, o que era legal porque remove iria ignorar as não-seqüências de caracteres porque eram diferentes. Mas se você remover (String o), isso não funcionará mais.

Noé
fonte
4
Se eu instanciar uma lista <>, esperaria poder chamar apenas List.remove (someString); Se eu precisar dar suporte à compatibilidade com versões anteriores, eu usaria uma Lista - Lista <?> Bruta, então posso chamar list.remove (someObject), não?
22620 Chris Mazzola
5
Se você substituir "remover" com "add", em seguida, esse código é tão quebrada por que foi realmente feito em Java 5.
DJClayworth