Upload
bleporini
View
208
Download
0
Embed Size (px)
Citation preview
@blep#DevoxxFrEqHc
RÉVISION DES FONDAMENTAUX : EQUALS ET HASHCODE C'EST IMPORTANT
Brice LEPORINI @blep Indépendant http://the-babel-tower.github.io/
@blep#DevoxxFrEqHc
Pourquoi?
•Les devs sont rarement affûtés sur cette problématique
•La plupart des implémentations sont buggées
@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
@blep#DevoxxFrEqHc
java.lang.Object#equalsjava.lang.Object#equals = ==
@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
@blep#DevoxxFrEqHc
Redéfinir equals• Seul moyen de vérifier que deux instances distinctes sont
fonctionnellement équivalentes
@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
@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]]>
@blep#DevoxxFrEqHc
HashCode• Valeur entière représentant la réduction d’un objet
@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);
@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
@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…
@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
@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
@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…