Jon Skeet a récemment soulevé un sujet de programmation intéressant sur son blog: "Il y a un trou dans mon abstraction, chère Liza, chère Liza" (emphase ajoutée):
J'ai un ensemble - un
HashSet
, en fait. Je souhaite en supprimer certains éléments… et bon nombre de ces éléments pourraient bien ne pas exister. En fait, dans notre cas de test, aucun des articles de la collection "déménagements" seront dans l'ensemble d'origine. Cela semble - et en effet c'est - extrêmement facile à coder. Après tout, nous avonsSet<T>.removeAll
pour nous aider, non?Nous spécifions la taille de l'ensemble "source" et la taille de la collection "removals" sur la ligne de commande, et les construisons tous les deux. L'ensemble source ne contient que des entiers non négatifs; l'ensemble de suppressions ne contient que des entiers négatifs. Nous mesurons le temps qu'il faut pour supprimer tous les éléments à l'aide de
System.currentTimeMillis()
, qui n'est pas le chronomètre le plus précis au monde mais qui est plus que suffisant dans ce cas, comme vous le verrez. Voici le code:import Java.util.*; public class Test { public static void main(String[] args) { int sourceSize = Integer.parseInt(args[0]); int removalsSize = Integer.parseInt(args[1]); Set<Integer> source = new HashSet<Integer>(); Collection<Integer> removals = new ArrayList<Integer>(); for (int i = 0; i < sourceSize; i++) { source.add(i); } for (int i = 1; i <= removalsSize; i++) { removals.add(-i); } long start = System.currentTimeMillis(); source.removeAll(removals); long end = System.currentTimeMillis(); System.out.println("Time taken: " + (end - start) + "ms"); } }
Commençons par lui donner un travail facile: un ensemble source de 100 éléments, et 100 à supprimer:
c:UsersJonTest>Java Test 100 100 Time taken: 1ms
D'accord, donc nous ne nous attendions pas à ce que ce soit lent… clairement, nous pouvons accélérer un peu les choses. Que diriez-vous d'une source d'un million d'articles et de 300 000 articles à supprimer?
c:UsersJonTest>Java Test 1000000 300000 Time taken: 38ms
Hmm. Cela semble encore assez rapide. Maintenant, je sens que j'ai été un peu cruel, lui demandant de faire tout ce retrait. Rendons les choses un peu plus faciles - 300 000 éléments sources et 300 000 suppressions:
c:UsersJonTest>Java Test 300000 300000 Time taken: 178131ms
Pardon? Près de trois - minutes? Oui! Il devrait sûrement être plus facile de supprimer des éléments d'une collection plus petite que celle que nous avons gérée en 38 ms?
Quelqu'un peut-il expliquer pourquoi cela se produit? Pourquoi la méthode HashSet<T>.removeAll
Est-elle si lente?
Le comportement est (quelque peu) documenté dans le javadoc :
Cette implémentation détermine laquelle est la plus petite de cet ensemble et de la collection spécifiée, en appelant la méthode size sur chacun. Si cet ensemble contient moins d'éléments, l'implémentation parcourt cet ensemble, vérifiant chaque élément renvoyé par l'itérateur dans tournez pour voir si elle est contenue dans la collection spécifiée. S'il est ainsi contenu, il est supprimé de cet ensemble avec la méthode remove de l'itérateur. Si la collection spécifiée contient moins d'éléments, l'implémentation parcourt la collection spécifiée, supprimant de cet ensemble chaque élément renvoyé par l'itérateur, en utilisant la méthode remove de cet ensemble.
Ce que cela signifie en pratique, lorsque vous appelez source.removeAll(removals);
:
si la collection removals
est d'une taille inférieure à source
, la méthode remove
de HashSet
est appelée, ce qui est rapide.
si la collection removals
est de taille égale ou supérieure à celle de source
, alors removals.contains
est appelé, ce qui est lent pour une ArrayList.
Solution rapide:
Collection<Integer> removals = new HashSet<Integer>();
Notez qu'il y a n bogue ouvert qui est très similaire à ce que vous décrivez. L'essentiel semble être que c'est probablement un mauvais choix mais ne peut pas être changé car il est documenté dans le javadoc.
Pour référence, voici le code de removeAll
(en Java 8 - je n'ai pas vérifié les autres versions):
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}