15
@blep #DevoxxFrEqHc RÉVISION DES FONDAMENTAUX : EQUALS ET HASHCODE C'EST IMPORTANT Brice LEPORINI @blep Indépendant http://the-babel-tower.github.io/

Devoxx 15 equals hashcode

Embed Size (px)

Citation preview

Page 1: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

RÉVISION DES FONDAMENTAUX : EQUALS ET HASHCODE C'EST IMPORTANT

Brice LEPORINI @blep Indépendant http://the-babel-tower.github.io/

Page 2: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

Pourquoi?

•Les devs sont rarement affûtés sur cette problématique

•La plupart des implémentations sont buggées

Page 3: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

L’opérateur ==

•Permet de comparer des types primitifs

•Utiliser == sur les objets ne permet que de vérifier que deux références pointent sur la même instance

Page 4: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

java.lang.Object#equalsjava.lang.Object#equals = ==

Page 5: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Getter @Setterprivate static class User{ private String name; } @Testpublic void testEquals() { final User user1 = new User(); user1.setName("test"); final User user2 = new User(); user2.setName("test"); assertThat(user1.equals(user1)).isTrue(); //Reflexive assertThat(user2.equals(user2)).isTrue(); //Reflexive assertThat(user1.equals(null)).isFalse(); assertThat(user1.equals(user2)).isTrue(); // —> Fails}

java.lang.Object#equals

Page 6: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

Redéfinir equals• Seul moyen de vérifier que deux instances distinctes sont

fonctionnellement équivalentes

Page 7: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Getter @Setter @ToStringprivate static class User{ private String name; @Override public boolean equals(Object obj) { if (this == obj) return true; if( ! (obj instanceof User) ) return false; User other = (User) obj; return this.name !=null ? this.name.equals(other.name) : this.name == other.name; }}

@Testpublic void should_be_equals() { final User user1 = new User(); user1.setName("bali"); final User user2 = new User(); user2.setName("bali"); final User user3 = new User(); user3.setName("bali"); /* Reflexive */ assertThat(user1.equals(user1)).isTrue(); assertThat(user2.equals(user2)).isTrue(); assertThat(user3.equals(user3)).isTrue(); /* Symmetric */ assertThat(user1.equals(user2)).isTrue(); assertThat(user2.equals(user1)).isTrue(); /* Transitive */ assertThat(user2.equals(user3)).isTrue(); assertThat(user1.equals(user3)).isTrue(); assertThat(user1.equals(null)).isFalse(); assertThat(user2.equals(null)).isFalse(); assertThat(user3.equals(null)).isFalse();}

Redéfinir equals

Page 8: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Testpublic void testHashMap() { final User user1 = new User(); user1.setName("bali"); final User user2 = new User(); user2.setName("bali"); final Map<User, Integer> map = new HashMap<>(); map.put(user1, 2); assertThat(map).hasSize(1); assertThat(map.keySet().iterator().next()).isEqualTo(user2); assertThat(map.values().iterator().next()).isEqualTo(2); assertThat(map).containsEntry(user2, 2); // —> fails}

@Testpublic void testHashSet() { final User user1 = new User(); user1.setName("bali"); final User user2 = new User(); user2.setName("bali"); final Set<User> users = new HashSet<>(); users.add(user1); assertThat(users.iterator().next()).isEqualTo(user2); assertThat(users.contains(user2)).isTrue(); // -> fails}

@Testpublic void should_not_contain_doubles() { final User user1 = new User(); user1.setName("bali"); final User user2 = new User(); user2.setName("bali"); final Set<User> users = new HashSet<>(); users.add(user1); users.add(user2); assertThat(user1.equals(user2)).isTrue(); assertThat(users).hasSize(1); // —> fails}

Généralement —> systématiquement

java.lang.AssertionError: Expecting: <{_01BasicEquals.User(name=bali)=2}> to contain: <[MapEntry[key=_01BasicEquals.User(name=bali), value=2]]> but could not find: <[MapEntry[key=_01BasicEquals.User(name=bali), value=2]]>

Page 9: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

HashCode• Valeur entière représentant la réduction d’un objet

Page 10: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

Hash Collections

User1#hashCode

User1, 2

hashCodes

User1#hashCode

User1, ?

hashCodes

User2, ?

User2#hashCode

map.put(user1, 2); users.add(user1);users.add(user2);

Page 11: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Getter @Setter @ToStringprivate static class User{ private String name; @Override public boolean equals(Object obj) { if (this == obj) return true; if( ! (obj instanceof User) ) return false; User other = (User) obj; return this.name !=null ? this.name.equals(other.name) : this.name == other.name; } @Override public int hashCode() { return name == null? 0 : name.length(); // Ugly but correct! } }

hashCode

Page 12: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Getter @Setterprivate static class UserWithPassword extends User{ private String password; @Override public boolean equals(Object obj) { if (this == obj) return true; if(!super.equals(obj)) return false; if( ! (obj instanceof UserWithPassword) ) return false; UserWithPassword other = (UserWithPassword) obj; return this.password !=null ? this.password.equals(other.password) : this.password == other.password; } @Override public int hashCode() { return super.hashCode() + (password == null ? 0 : password.length()); }}

@Testpublic void should_be_equals(){ final User user1 = new User(); user1.setName("bali"); final UserWithPassword user2 = new UserWithPassword(); user2.setName("bali"); user2.setPassword("balo"); /* Reflexive */ assertThat(user1.equals(user1)).isTrue(); assertThat(user2.equals(user2)).isTrue(); /* Symmetric */ assertThat(user1.equals(user2)).isTrue(); assertThat(user2.equals(user1)).isTrue(); // -> fails} @Testpublic void hashCode_should_be_the_same(){ final User user1 = new User(); user1.setName("bali"); final UserWithPassword user2 = new UserWithPassword(); user2.setName("bali"); user2.setPassword("balo"); assertThat(user1.equals(user2)).isTrue(); /* contract with hashcode */ assertThat(user1.hashCode()).isEqualTo(user2.hashCode()); // --> fails}

Le problème de l’héritage

Toujours aussi nulle mon implémentation de hashCode…

Page 13: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Getter @Setterprivate static class User{ private String name; @Override public boolean equals(Object obj) { if (this == obj) return true; if( ! (obj instanceof User) ) return false; User other = (User) obj; if(!other.canEqual(this)) return false; return this.name !=null ? this.name.equals(other.name) : this.name == other.name; } protected boolean canEqual(Object o) { return o instanceof User; } @Override public int hashCode() { return name.length(); // Ugly but correct! } }

@Getter @Setterprivate static class UserWithPassword extends User{ private String password; @Override public boolean equals(Object obj) { if (this == obj) return true; if(!super.equals(obj)) return false; if( ! (obj instanceof UserWithPassword) ) return false; UserWithPassword other = (UserWithPassword) obj; if(!other.canEqual(this)) return false; return this.password !=null ? this.password.equals(other.password) : this.password == other.password; } @Override protected boolean canEqual(Object o) { return o instanceof UserWithPassword; } @Override public int hashCode() { return super.hashCode() + (password == null ? 0 : password.length()); }}

Le problème de l’héritage

Page 14: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

Pour finir: comment bien faire?• Pour une implémentation pertinente de hashCode: se référer à

l’item 9 de Effective Java (Josh Bloch)

• Plus facile : déléguer à Apache Commons Lang EqualsBuilder

et HashCodeBuilder

• Encore plus facile : demander à Lombok de les générer à votre

place

Page 15: Devoxx 15 equals hashcode

@blep#DevoxxFrEqHc

@Testpublic void mutability_fails_in_collections(){ final User user = new User(); user.setName("bali"); final Set<User> set = new HashSet<>(); set.add(user); assertThat(set).hasSize(1); assertThat(set.contains(user)).isTrue(); user.setName("bali balo"); assertThat(set).hasSize(1); assertThat(set.iterator().next()).isEqualTo(user); assertThat(set.contains(user)).isTrue(); // --> fails}

One more thing…