Last active
February 25, 2021 23:21
-
-
Save nemoinho/e7529d8616a7b768b8fe5d67431fdc98 to your computer and use it in GitHub Desktop.
`equals()` and `hasCode()` are special in functions and their contract is easy to violate as this example shows.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.HashSet; | |
import java.util.Set; | |
import lombok.Data; | |
/** | |
* This is a simple demo how to implement equals and hashcode wrong, just because we don't understand it good enough | |
* It's is a very common mistake and easy to make wrong when you use lombok. | |
*/ | |
@Data | |
public class EqualsAndHashCodeDemo { | |
private Long id; | |
public static void main(String[] args) { | |
EqualsAndHashCodeDemo demo = new EqualsAndHashCodeDemo(); | |
Set<EqualsAndHashCodeDemo> set = new HashSet<>(); | |
if (set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here since the demo is not in the set, yet"); | |
} | |
set.add(demo); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here either because the demo is in the set, now"); | |
} | |
demo.setId(123L); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("This fails, because hashCode has changed, therefore the demo is not part of the HashSet anymore"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.HashSet; | |
import java.util.Objects; | |
import java.util.Set; | |
import lombok.Getter; | |
import lombok.Setter; | |
import lombok.ToString; | |
@Getter | |
@Setter | |
@ToString | |
public class EqualsAndHashCodeDemo_Better { | |
private Long id; | |
/** | |
* This is a fairly straight and regular implementation of equals(). | |
* Nothing special | |
*/ | |
@Override | |
public boolean equals(Object o) { | |
if (o == null) return false; | |
if (o == this) return true; | |
if (o instanceof EqualsAndHashCodeDemo_Better other) { | |
return Objects.equals(id, other.id); | |
} | |
return false; | |
} | |
/** | |
* However the only valid solution for an entity where EVERY field is volatile is to return a static hashCode! | |
*/ | |
@Override | |
public int hashCode() { | |
return 123; | |
} | |
public static void main(String[] args) { | |
EqualsAndHashCodeDemo_Better demo = new EqualsAndHashCodeDemo_Better(); | |
Set<EqualsAndHashCodeDemo_Better> set = new HashSet<>(); | |
if (set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here since the demo is not in the set, yet"); | |
} | |
set.add(demo); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here either because the demo is in the set, now"); | |
} | |
demo.setId(123L); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here because hashCode has NOT changed"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.HashSet; | |
import java.util.Set; | |
import lombok.Getter; | |
import lombok.Setter; | |
import lombok.ToString; | |
import lombok.EqualsAndHashCode; | |
@Getter | |
@ToString | |
@EqualsAndHashCode(onlyExplicitlyIncluded = true) | |
public class EqualsAndHashCodeDemo_WithNaturalId { | |
@Setter | |
private Long id; | |
/** | |
* This is pseudo-final (not final, but not setters or other mutations) as a demo of how to handle this. | |
* It's required this way by some librariies/frameworks/environments, e.g. JPA | |
*/ | |
@EqualsAndHashCode.Include | |
private String naturalUniqueIdentifier; | |
public EqualsAndHashCodeDemo_WithNaturalId(String naturalUniqueIdentifier) { | |
this.naturalUniqueIdentifier = naturalUniqueIdentifier; | |
} | |
public static void main(String[] args) { | |
EqualsAndHashCodeDemo_WithNaturalId demo = new EqualsAndHashCodeDemo_WithNaturalId("Some uniq String"); | |
Set<EqualsAndHashCodeDemo_WithNaturalId> set = new HashSet<>(); | |
if (set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here since the demo is not in the set, yet"); | |
} | |
set.add(demo); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here either because the demo is in the set, now"); | |
} | |
demo.setId(123L); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here because hashCode has NOT changed"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.HashSet; | |
import java.util.Set; | |
import java.util.UUID; | |
import lombok.Getter; | |
import lombok.Setter; | |
import lombok.ToString; | |
import lombok.EqualsAndHashCode; | |
@Getter | |
@ToString | |
@EqualsAndHashCode(onlyExplicitlyIncluded = true) | |
public class EqualsAndHashCodeDemo_WithNonVolatileId { | |
/** | |
* This is pseudo-final (not final, but not setters or other mutations) as a demo of how to handle this. | |
* It's required this way by some librariies/frameworks/environments, e.g. JPA | |
*/ | |
@EqualsAndHashCode.Include | |
private UUID id; | |
/** | |
* The constructor is not accessable from outside of the class, but protected to allow access for | |
* libraries such as Spring Data | |
*/ | |
protected EqualsAndHashCodeDemo_WithNonVolatileId(UUID id) { | |
this.id = id; | |
} | |
/** | |
* We'll use this static factory instead of the new keyword to create an instance of the class. | |
* This is a good abstraction anyway because it brings us a lot of opportunities with basically no extra costs. | |
*/ | |
public static EqualsAndHashCodeDemo_WithNonVolatileId of() { | |
return new EqualsAndHashCodeDemo_WithNonVolatileId(UUID.randomUUID()); | |
} | |
public static void main(String[] args) { | |
EqualsAndHashCodeDemo_WithNonVolatileId demo = EqualsAndHashCodeDemo_WithNonVolatileId.of(); | |
Set<EqualsAndHashCodeDemo_WithNonVolatileId> set = new HashSet<>(); | |
if (set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here since the demo is not in the set, yet"); | |
} | |
set.add(demo); | |
if (!set.contains(demo)) { | |
throw new RuntimeException("We shouldn't end here either because the demo is in the set, now"); | |
} | |
// Since we can't even change the id the last check is now nonsense! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment