A concise way to define classes with immutable objects.
We want instances of this class to be immutable.
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public class Point {
// other stuff ...
public int x() {
return x;
}
public int y() {
return y;
}
}
toString
equals
hashCode
toString
not that greatnew Point(10, 20).toString() => "Point@133e16fd"
Would be nice to give our objects a human-readable String
format. like:
new Point(10, 20).toString() ⟹ "Point[x=10, y=20]"
toString
public class Point {
// other stuff ...
@Override public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
equals
If we want to consider two different Point
objects with the same x
and y
values to be equivalent, we want to override equals
.
In particular, if we want to be able to use Point
s as keys in a HashMap
, we need equivalent Point
s to be equals
.
equals
is hard to get rightThe equals
contract is actually kind of tricky to get right in ways we don’t talk about in CSA.
There are five characteristics that a correct equals
method must have.
x.equals(x)
, i.e. every object is equals
to itself.
x.equals(y)
if and only if y.equals(x)
.
If x.equals(y)
and y.equals(z)
, then x.equals(z)
.
Multiple invocations of x.equals(y)
return the same value, provided no information used in the equals
comparison on the objects is modified.
Defining equals
on mutable objects is a bit fraught.
For any non-null x
, x.equals(null)
should return false
.
(And if x
is null we’d get a NullPointerException
.)
The equals
method is used in lots of places (e.g. List.contains()
) and those places may depend on one or more aspects of this contract being fulfilled.
equals
implementationpubic class Point {
// other stuff ...
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Point other) {
return x == other.x && y == other.y;
} else {
return false;
}
}
}
P.S. Don’t try to override equals
in a subclass of Point
as you’ll almost certainly violate the symmetry requirement.
hashCode
In CSA you learned about equals
but not hashCode
.
But in practice they go together and whenever you override equals
you also need to override hashCode
.
But why?
HashMap
?A HashMap
gives almost constant time lookup of arbitrary keys, i.e. about as efficient as indexing into an array and potentially much faster than even a binary search.
How does it work?
We want to store n key/value pairs so we can look up a value by its key.
And we want equals
key objects to be considered the same key.
Suppose we could map every key to an int
index in the range [0, n).
Then we could just use an n-element array, storing each pair at the index for the key.
But if there are more than n keys, by the pigeonhole principle there are at least some not-equals
keys that would be assigned the same index.
Instead of storing the key/value pairs directly in the array, at each spot in the array store a list of key value pairs.
To look up a key in the table, use the key’s index to find the right spot in the array and then look through the list until we find the pair whose key is equals
to the one we are looking for.
As long as there aren’t too many collisions, this is about as fast as a simple array lookup.
If we end up with too many collisions, we can reduce the number of collisons by making a bigger array and moving every key/value pair to the correct bucket in the bigger array, based on the new remainder.
If we can assign a int
value to every key, lets call it h
, such that most objects have different h
s we can use Math.floorMod(h, array.length)
to get an index into an array where that key should be stored.
That’s what hashCode
is for; it returns the int
whose modulus we get based on the size of the array in the HashMap
.
hashCode
Main requirement is that it be “consistent with equals”, i.e. two objects that are equals
must return the same value from hashCode
.
But good hashCode
implementations also maximize the chance that objects that are not equals
will have different hash codes.
hashCode
A reasonable hashCode
method incorporates information from all the fields that are used in the equals
method, mushing up the bits to be fairly random seeming.
public class Point {
// other stuff ...
@Override public int hashCode() {
return 31 * x + y;
}
}
A proper immutable Point
class requires:
Instance variables
A constructor
Getters for all the instance variables
A toString
method
And correct equals
and hashCode
methods
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return x;
}
public int y() {
return y;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Point other) {
return x == other.x && y == other.y;
} else {
return false;
}
}
@Override public int hashCode() {
return 31 * x + y;
}
@Override public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
Forty-three lines.
public record Point(int x, int y) {}
This gives us everything we wrote above for free.
We can …
Add methods and constructors
Add static variables
Implement interfaces
public record Point(int x, int y)
implements Comparable<Point> {
public static final Point ORIGIN = new Point(0, 0);
public double distanceFrom(Point other) {
return Math.hypot(x - other.x, y - other.y);
}
public int compareTo(Point other) {
var byX = Integer.compare(x, other.x);
return byX != 0 ? byX : Integer.compare(y, other.y);
}
}
We automatically get a constructor whose signature matches the list of fields in the record. But we can augment it like this:
public record Zoo(List<Animal> animals) {
public Zoo {
var toSort = new ArrayList<>(animals);
Collections.sort(toSort);
animals = Collections.unmodifiableList(toSort);
// Actual assignment to field happens here
// Implicitly: this.animals = animals
}
}
Or we can add other constructors with different signatures.
public record Point(int x, int y)
implements Comparable<Point> {
public Point() { this(0, 0); }
// other stuff as before
}
Whenever you want immutable objects.
So, often.
However, records are mostly used to hold data and usually don’t have a ton of complex behaviors.
Records are also commonly defined as nested classes.
public class DB {
public record Book(String title, int pages, String authorName) {}
public record Author(String name, String birthday, boolean alive) {}
}
Define at least two record classes that are related in some way.
Populate collections of each kind of record.
Write some methods that return collections of records that match some criteria.
Write some methods that return collections of records from one collection based on criteria applied to related records from another collection, e.g. books by living authors.
You may want to use streams. But not required.
You way want to use some of the generic function interfaces.
You can also define local records within a method which may be useful for temporarily combining values from two different record classes for use in a stream pipeline.