Skip to content

Instantly share code, notes, and snippets.

@nemoinho
Last active February 25, 2021 23:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nemoinho/e7529d8616a7b768b8fe5d67431fdc98 to your computer and use it in GitHub Desktop.
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.
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");
}
}
}
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");
}
}
}
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");
}
}
}
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