Pourquoi les collections Java ne suppriment-elles pas les méthodes génériques?


144

Pourquoi Collection.remove (Object o) n'est-il pas générique?

On dirait que Collection<E>j'aurais puboolean remove(E o);

Ensuite, lorsque vous essayez accidentellement de supprimer (par exemple) Set<String>au lieu de chaque chaîne individuelle de a Collection<String>, ce serait une erreur de compilation au lieu d'un problème de débogage plus tard.


13
Cela peut vraiment vous mordre lorsque vous le combinez avec la boxe automatique. Si vous essayez de supprimer quelque chose d'une liste et que vous lui passez un entier au lieu d'un int, il appelle la méthode remove (Object).
ScArcher2

2
Question similaire concernant la carte: stackoverflow.com/questions/857420/…
AlikElzin-kilaka

Réponses:


73

Josh Bloch et Bill Pugh font référence à ce problème dans Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone et Revenge of The Shift .

Josh Bloch dit (6:41) qu'ils ont tenté de générer la méthode get de Map, la méthode remove et une autre, mais "cela n'a tout simplement pas fonctionné".

Il y a trop de programmes raisonnables qui ne pourraient pas être générés si vous n'autorisez que le type générique de la collection comme type de paramètre. L'exemple qu'il donne est une intersection de a Listde Numbers et de a Listde Longs.


6
Pourquoi add () peut prendre un paramètre typé mais pas remove () est encore un peu au-delà de ma compréhension. Josh Bloch serait la référence définitive pour les questions relatives aux collections. C'est peut-être tout ce que j'obtiens sans essayer de faire une implémentation de collection similaire et voir par moi-même. :( Merci.
Chris Mazzola

2
Chris - lisez le PDF du didacticiel Java Generics, il vous expliquera pourquoi.
JeeBee

42
En fait, c'est très simple! Si add () prenait un mauvais objet, cela briserait la collection. Il contiendrait des choses qu'il n'est pas censé contenir! Ce n'est pas le cas pour remove () ou contains ().
Kevin Bourrillion

12
Incidemment, cette règle de base - utiliser des paramètres de type pour éviter d'endommager la collection uniquement - est suivie de manière absolument cohérente dans toute la bibliothèque.
Kevin Bourrillion

3
@KevinBourrillion: Je travaille avec des génériques depuis des années (à la fois en Java et en C #) sans jamais me rendre compte que la règle des "dommages réels" existe même ... mais maintenant que je l'ai vu directement, cela a un sens à 100%. Merci pour le poisson !!! Sauf que maintenant je me sens obligé de revenir en arrière et de regarder mes implémentations, pour voir si certaines méthodes peuvent et doivent donc être dégénérées. Soupir.
corlettk

74

remove()(in Mapainsi que in Collection) n'est pas générique car vous devriez pouvoir passer n'importe quel type d'objet à remove(). L'objet supprimé n'a pas besoin d'être du même type que l'objet auquel vous passez remove(); il faut seulement qu'ils soient égaux. De la spécification de remove(), remove(o)supprime l'objet etel qu'il (o==null ? e==null : o.equals(e))est true. Notez qu'il n'y a rien d'exiger oet ed'être du même type. Cela découle du fait que la equals()méthode prend un Objectparamètre as, pas seulement le même type que l'objet.

Bien qu'il puisse être communément vrai que de nombreuses classes se soient equals()définies de sorte que ses objets ne puissent être égaux qu'aux objets de sa propre classe, ce n'est certainement pas toujours le cas. Par exemple, la spécification de List.equals()indique que deux objets List sont égaux s'ils sont tous les deux des Lists et ont le même contenu, même s'il s'agit d'implémentations différentes de List. Donc, pour revenir à l'exemple de cette question, il est possible d'avoir un Map<ArrayList, Something>et pour moi d'appeler remove()avec un LinkedListcomme argument, et cela devrait supprimer la clé qui est une liste avec le même contenu. Cela ne serait pas possible s'il remove()était générique et restreint son type d'argument.


1
Mais si vous deviez définir la carte comme Map <List, Something> (au lieu de ArrayList), il aurait été possible de la supprimer en utilisant une LinkedList. Je pense que cette réponse est incorrecte.
AlikElzin-kilaka

3
La réponse semble correcte, mais incomplète. Cela se prête seulement à se demander pourquoi diable n'ont-ils pas générialisé la equals()méthode également? Je pourrais voir plus d'avantages à taper la sécurité au lieu de cette approche «libertaire». Je pense que la plupart des cas d'implémentation actuelle concernent des bogues qui entrent dans notre code plutôt que la joie de cette flexibilité remove()qu'apporte la méthode.
kellogs

2
@kellogs: Que voulez-vous dire par "générialiser la equals()méthode"?
newacct

5
@MattBall: "où T est la classe déclarante" Mais une telle syntaxe n'existe pas en Java. Tdoit être déclaré comme paramètre de type sur la classe et Objectn'a aucun paramètre de type. Il n'y a aucun moyen d'avoir un type qui fait référence à "la classe déclarante".
newacct

3
Je pense que Kellogs dit que se passerait-il si l'égalité était une interface générique Equality<T>avec equals(T other). Alors vous pourriez avoir remove(Equality<T> o)et on'est qu'un objet qui peut être comparé à un autre T.
weston le

11

Parce que si votre paramètre de type est un caractère générique, vous ne pouvez pas utiliser une méthode de suppression générique.

Il me semble me souvenir d'avoir rencontré cette question avec la méthode get (Object) de Map. La méthode get dans ce cas n'est pas générique, bien qu'elle devrait raisonnablement s'attendre à recevoir un objet du même type que le premier paramètre de type. J'ai réalisé que si vous transmettez Maps avec un caractère générique comme premier paramètre de type, il n'y a aucun moyen d'extraire un élément de la carte avec cette méthode, si cet argument était générique. Les arguments génériques ne peuvent pas vraiment être satisfaits, car le compilateur ne peut pas garantir que le type est correct. Je suppose que la raison pour laquelle ajouter est générique est que vous devez garantir que le type est correct avant de l'ajouter à la collection. Cependant, lors de la suppression d'un objet, si le type est incorrect, il ne correspondra à rien de toute façon.

Je ne l'ai probablement pas très bien expliqué, mais cela me semble assez logique.


1
Pourriez-vous nous en dire un peu plus?
Thomas Owens

6

En plus des autres réponses, il y a une autre raison pour laquelle la méthode devrait accepter un Object, qui est des prédicats. Considérez l'exemple suivant:

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

Le fait est que l'objet passé à la removeméthode est responsable de la définition de la equalsméthode. Construire des prédicats devient très simple de cette façon.


Investisseur? (Filler filler filler)
Matt R

3
La liste est implémentée comme yourObject.equals(developer), comme documenté dans l'API Collections: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly

13
Cela me semble être un abus
RAY

7
C'est un abus puisque votre objet prédicat rompt le contrat de la equalsméthode, à savoir la symétrie. La méthode remove n'est liée à sa spécification que tant que vos objets remplissent la spécification equals / hashCode, donc toute implémentation serait libre de faire la comparaison dans l'autre sens. De plus, votre objet prédicat n'implémente pas la .hashCode()méthode (ne peut pas implémenter de manière cohérente à égal à égal), donc l'appel de suppression ne fonctionnera jamais sur une collection basée sur Hash (comme HashSet ou HashMap.keys ()). Que cela fonctionne avec ArrayList est un pur hasard.
Paŭlo Ebermann

3
(Je ne parle pas de la question de type générique - cela a déjà été répondu -, seulement votre utilisation d'égaux pour les prédicats ici.) Bien sûr, HashMap et HashSet vérifient le code de hachage, et TreeSet / Map utilise l'ordre des éléments . Pourtant, ils mettent pleinement en œuvre Collection.remove, sans rompre son contrat (si la commande est cohérente à égale). Et une ArrayList variée (ou AbstractCollection, je pense) avec l'appel égal retourné implémenterait toujours correctement le contrat - c'est votre faute si cela ne fonctionne pas comme prévu, puisque vous rompez le equalscontrat.
Paŭlo Ebermann

5

Supposons que l' on a une collection de Cat, et quelques références d'objets de types Animal, Cat, SiameseCatet Dog. Demander à la collection si elle contient l'objet auquel se réfère la Catou la SiameseCatréférence semble raisonnable. Demander s'il contient l'objet auquel la Animalréférence fait référence peut sembler douteux, mais cela reste parfaitement raisonnable. L'objet en question pourrait, après tout, être unCat , et pourrait apparaître dans la collection.

De plus, même si l'objet se trouve être autre chose qu'un Cat, il n'y a aucun problème à dire s'il apparaît dans la collection - répondez simplement "non, ce n'est pas le cas". Une collection "lookup-style" d'un certain type devrait être capable d'accepter de manière significative la référence de n'importe quel supertype et de déterminer si l'objet existe dans la collection. Si la référence d'objet transmise est d'un type non lié, il n'y a aucun moyen que la collection puisse la contenir, donc la requête n'est pas significative dans un certain sens (elle répondra toujours "non"). Néanmoins, comme il n'y a aucun moyen de restreindre les paramètres à des sous-types ou des supertypes, il est plus pratique d'accepter simplement n'importe quel type et de répondre "non" pour tous les objets dont le type n'est pas lié à celui de la collection.


1
"Si la référence d'objet transmise est d'un type non lié, il n'y a aucun moyen que la collection puisse la contenir" Mauvaise. Il n'a qu'à contenir quelque chose d'égal à lui; et les objets de différentes classes peuvent être égaux.
newacct

"peut sembler douteux, mais c'est toujours parfaitement raisonnable" Est-ce que c'est? Considérez un monde dans lequel un objet d'un type ne peut pas toujours être vérifié pour l'égalité avec un objet d'un autre type, car le type auquel il peut être égal est paramétré (similaire à la façon dont Comparableest paramétré pour les types avec lesquels vous pouvez comparer). Il ne serait alors pas raisonnable de permettre aux gens de passer quelque chose d'un type sans rapport.
newacct

@newacct: Il y a une différence fondamentale entre la comparaison de magnitude et la comparaison d'égalité: des objets donnés Aet Bd'un type, et Xet Yd'un autre, tels que A> B, et X> Y. Soit A> Yet Y< A, soit X> Bet B< X. Ces relations ne peuvent exister que si les comparaisons de magnitude connaissent les deux types. En revanche, la méthode de comparaison d'égalité d'un objet peut simplement se déclarer inégal à tout autre type, sans avoir à savoir quoi que ce soit sur l'autre type en question. Un objet de type Catpeut ne pas savoir si c'est ...
supercat

... "supérieur à" ou "inférieur à" un objet de type FordMustang, mais il ne devrait avoir aucune difficulté à dire s'il est égal à un tel objet (la réponse, évidemment, étant "non").
supercat

4

J'ai toujours pensé que c'était parce que remove () n'avait aucune raison de se soucier du type d'objet que vous lui donniez. Il est assez facile, peu importe, de vérifier si cet objet est l'un de ceux que contient la collection, car il peut appeler equals () sur n'importe quoi. Il est nécessaire de vérifier le type sur add () pour s'assurer qu'il ne contient que des objets de ce type.


0

C'était un compromis. Les deux approches ont leur avantage:

  • remove(Object o)
    • est plus flexible. Par exemple, il permet de parcourir une liste de nombres et de les supprimer d'une liste de longs.
    • le code qui utilise cette flexibilité peut être plus facilement généré
  • remove(E e) apporte plus de sécurité de type à ce que la plupart des programmes veulent faire en détectant des bogues subtils au moment de la compilation, comme essayer par erreur de supprimer un entier d'une liste de courts-circuits.

La rétrocompatibilité a toujours été un objectif majeur lors de l'évolution de l'API Java, c'est pourquoi remove (Object o) a été choisi car il facilitait la génération du code existant. Si la rétrocompatibilité n'avait PAS été un problème, je suppose que les concepteurs auraient choisi remove (E e).


-1

Remove n'est pas une méthode générique de sorte que le code existant utilisant une collection non générique sera toujours compilé et aura toujours le même comportement.

Voir http://www.ibm.com/developerworks/java/library/j-jtp01255.html pour plus de détails.

Edit: un commentateur demande pourquoi la méthode add est générique. [... supprimé mon explication ...] Le deuxième commentateur a répondu à la question de firebird84 bien mieux que moi.


2
Alors pourquoi la méthode add est-elle générique?
Bob Gettys

@ firebird84 remove (Object) ignore les objets du mauvais type, mais remove (E) provoquerait une erreur de compilation. Cela changerait le comportement.
noah

: haussement d'épaules: - le comportement d'exécution n'est pas modifié; l'erreur de compilation n'est pas un comportement d' exécution . Le "comportement" de la méthode add change de cette manière.
Jason S

-2

Une autre raison est à cause des interfaces. Voici un exemple pour le montrer:

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 */
}

Vous montrez quelque chose avec lequel vous pouvez vous en tirer parce qu'il remove()n'est pas covariant. La question, cependant, est de savoir si cela devrait être autorisé. ArrayList#remove()fonctionne par le biais de l'égalité des valeurs et non de l'égalité de référence. Pourquoi vous attendez-vous à ce que a Bsoit égal à un A? Dans votre exemple, cela peut l' être, mais c'est une attente étrange. Je préférerais que vous fournissiez un MyClassargument ici.
seh

-3

Parce que cela briserait le code existant (pré-Java5). par exemple,

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

Maintenant, vous pourriez dire que le code ci-dessus est faux, mais supposons que o provienne d'un ensemble hétérogène d'objets (c'est-à-dire qu'il contenait des chaînes, des nombres, des objets, etc.). Vous voulez supprimer toutes les correspondances, ce qui était légal car remove ignorait simplement les non-chaînes car elles n'étaient pas égales. Mais si vous le supprimez (String o), cela ne fonctionne plus.


4
Si j'instancie une List <String>, je m'attendrais à ne pouvoir appeler List.remove (someString); Si je dois prendre en charge la compatibilité descendante, j'utiliserais une liste brute - List <?> Alors je peux appeler list.remove (someObject), non?
Chris Mazzola le

5
Si vous remplacez «supprimer» par «ajouter», alors ce code est tout aussi cassé par ce qui a été réellement fait en Java 5.
DJClayworth
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.