août
2014
Stream.distinct(), gare à la javadoc !
L'API Stream fournit une méthode distinct()
permettant d'éliminer les éléments en doublon.
Sa Javadoc indique :
Returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.
Cette description laisse penser qu'il suffit que nos objets implémentent equals()
pour être correctement traités par distinct()
, ce qui est faux (ou du moins incomplet).
En effet, la classe DistinctOps
, qui gère l'opération de dédoublonnement du stream, s'appuie sur un LinkedHashSet
ou une ConcurrentHashMap
[1] (dans le cas d'un stream parallèle), lesquels utilisent également, comme leur nom l'indique, la méthode hashCode()
de nos objets.
Pour le prouver, il suffit de dédoublonner un jeu de données avec Stream.distinct()
, et de comparer le résultat avec un dédoublonnement "à la main" avec List.contains()
[2] qui se base uniquement sur equals()
.
public class Person { private final String name; public Person(String name) { this.name = name; } @Override public String toString() { return name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; return name.equals(((Person) o).name); } // Pas de redéfinition de hashCode // => un hashCode différent par instance en mémoire }
public class StreamDistinct { public static void main(String[] args) { Person[] daltons = { new Person("Joe Dalton"), new Person("Joe Dalton"), // Doublon ! new Person("Joe Dalton"), // Doublon ! new Person("Jack Dalton"), new Person("William Dalton"), new Person("Averell Dalton"), }; // Dédoublonnage via Stream.distinct List<Person> d1 = Stream.of(daltons).distinct().collect(Collectors.toList()); System.out.println("Stream.distinct : "+d1.size() + " Daltons"); // Dédoublonnage manuel via List.contains List<Person> d2 = new ArrayList<>(); for (Person dalton : daltons) { if (!d2.contains(dalton)) { d2.add(dalton); } } System.out.println("List.contains : "+d2.size() + " Daltons"); } }
Le résultat confirme la différence de comportement :
Stream.distinct : 6 Daltons List.contains : 4 Daltons
En revanche, dès que la méthode hashCode()
est correctement redéfinie dans la classe Person
, les deux méthodes donnent le même résultat (4 Daltons).
Conclusion : quitte à écrire une documentation, autant qu'elle soit techniquement exacte et exhaustive ! Un développeux naïf (mais pas vous, naturellement) pourrait croire en lisant la documentation de Stream.distinct()
que seule l'implémentation d'equals
est nécessaire, et s'exposerait à des bugs subtils. Dans cet exemple précis, il est possible de lire le code source pour comprendre le fonctionnement exact de la classe, mais ce n'est pas toujours le cas...
Commentaires
Sauf que tout le monde sait que redéfinir equals sans redéfinir hashCode est puni de mort dans la plupart des états.
Si tu fais ça, tu t’exposes à plus que des « bugs subtils ».
Comme le dit Tibi, hashCode doit être redéfinit si equals est surchargé ! C'est même la javadox qui le dit: http://docs.oracle.com/javase/8/doc...
"Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes."