Our principal goal as programmers is to reduce complexity. At any point in a program, we can reason about the state of a constant trivially -- its state is the state upon assignment. By contrast, the state of a variable can change endlessly.
With judicious use, immutable objects can lead to quicker execution speed and lower memory usage. The hash value of a String
, the query parameters of an immutable URL, and the distance traced by an immutable sequence of points are all immutable as well. We can cache their values for later use instead of recompute them on every access. We can also share an immutable object with clients without requiring those clients to defensively copy it.
An immutable value is safe in many ways. For example, String
is immutable and so its hashCode
value is immutable. Consequently, it is safe to use as a String
as a key in a Map
: The lower order bits of that hash will never change, and so an inserted key will never reside in the wrong bucket. An immutable value is also thread-safe, because a client must acquire a lock only to read data that another client can concurrently modify. An immutable value renders that moot.
A good approach for creating immutable types is to define types that are simply data, and which do not have behavior. For example, consider the SecondsWatched
class, which tracks the seconds watched values in a video:
public class SecondsWatched {
public final long lastSecondWatched;
public final long totalSecondsWatched;
public SecondsWatched(long lastSecondWatched, long totalSecondsWatched) {
/* Preconditions checks here... */
this.lastSecondWatched = lastSecondWatched;
this.totalSecondsWatched = totalSecondsWatched;
}
/* Method equals, hashCode, and toString follow here. */
}
Note that this instance is immutable because its lastSecondsWatched
and totalSecondsWatched
fields are final
, and hence cannot be reassigned.
The VideoUserProgress
instance composes a SecondsWatched
instance of this class:
public final class VideoUserProgress extends ContentItemUserProgress {
public final SecondsWatched secondsWatched;
public final Optional<Date> lastWatchedDate;
/* Constructor, equals, hashCode, and toString follow here. */
}
Again, SecondsWatched
is immutable. So is an instance of the Date
class. A constructed Optional
cannot be reassigned to refer to another (possibly null
) value, and so it is immutable as well. By virtue of the VideoUserProgress
composing instances of immutable classes, and making those fields final
, then each VideoUserProgress
instance is immutable as well.
Note that for such immutable classes, we do not provide getter methods for each field. Instead, these fields are have public
visibility. This is because getter methods are typically paired with corresponding setter methods for access control, but immutable values can by definition not be set.
From the Google Guava web page:
The Guava project contains several of Google's core libraries that we rely on in our Java-based projects: collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, and so forth.
Some of the packages are indispensable for disciplined Java development:
The Preconditions
class is found in the com.google.common.base
package. Use it in every constructor you define to ensure that a client is constructing a valid instance. By catching such invalid data when the instance is created, we can determine the source of the invalid parameters by navigating the stacktrace. If we checked for validity upon use, we not only put the burden on every client to ensure that the data is valid, and this precludes finding where the invalid data comes from.
The checkNotNull
method accepts a parameter and throws an NullPointerException
if it is null
. It returns that parameter if it is not. Use it for objects:
public final class ApiClient {
public final ContentApi contentApi;
public final UserApi userApi;
public ApiClient(ContentApi contentApi, UserApi userApi) {
this.contentApi = Preconditions.checkNotNull(contentApi);
this.userApi = Preconditions.checkNotNull(userApi);
}
}
Therefore a client of an ApiClient
instance can be ensured that it has contentApi
and userApi
fields that are not null
. It does not need to defend itself against these conditions.
The checkArgument
method asserts that its expression is true
. If not, it throws an IllegalArgumentException
. It's best to provide a second parameter that provides a more detailed message for the IllegalArgumentException
. This message should include any parameter in the expression that evaluated as false
. This makes debugging easier. For example:
public final class SecondsWatched {
public final long lastSecondWatched;
public final long totalSecondsWatched;
public SecondsWatched(long lastSecondWatched, long totalSecondsWatched) {
Preconditions.checkArgument(lastSecondWatched >= 0,
"lastSecondWatched cannot be negative: " + lastSecondWatched);
Preconditions.checkArgument(totalSecondsWatched >= 0,
"totalSecondsWatched cannot be negative: " + totalSecondsWatched);
this.lastSecondWatched = lastSecondWatched;
this.totalSecondsWatched = totalSecondsWatched;
}
}
For immutable objects that are just data and define no behavior, it's useful to implement equals
, hashCode
, and toString
.
The template for implementing equals
is:
public final class VideoSubtitle {
public final long timeMillis;
public final String text;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof VideoSubtitle)) return false;
VideoSubtitle that = (VideoSubtitle) o;
return (timeMillis == that.timeMillis
&& text.equals(that.text));
}
}
As shown above, do not forget to use the equals
method to compare instance fields that are objects (i.e. extend Object
). Accidentally using ==
will test for identity, or whether both operands refer to the same object (i.e. the same instance at the same address in memory).
Defining an equals
method is especially useful for testing, as it allow us to leverage the assertEquals
method:
long expectedTimeMillis = 123;
String expectedText = "Let's count to three."
VideoSubtitle expectedSubtitle =
new VideoSubtitle(expectedTimeMillis, expectedText);
assertEquals(expectedSubtitle, actualSubtitle);
If these values are not equal, then assertEquals
prints both arguments in the thrown assertion. (Which is only helpful if toString
is implemented, as described below!)
If there is a base class, then the derived class equals
implementation should call the base class equals
implementation. For example, consider the abstract base class ContentItemUserProgress
:
public abstract class ContentItemUserProgress {
public final ContentItemIdentifier contentItemIdentifier;
public final UserProgressLevel progressLevel;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ContentItemUserProgress)) return false;
ContentItemUserProgress that = (ContentItemUserProgress) o;
return (contentItemIdentifier.equals(that.contentItemIdentifier) &&
progressLevel == that.progressLevel);
}
}
The subclass VideoUserProgress
tests that the instance fields in the base class are equal before testing that its own instance fields are equal:
public final class VideoUserProgress extends ContentItemUserProgress {
public final SecondsWatched secondsWatched;
public final Optional<Date> lastWatchedDate;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof VideoUserProgress)) return false;
if (!super.equals(o)) return false;
VideoUserProgress that = (VideoUserProgress) o;
return (secondsWatched.equals(that.secondsWatched) &&
lastWatchedDate.equals(that.lastWatchedDate));
}
}
The Objects
method from the com.google.common.base
package contains a hashCode
method that computes a hash value for a list of arguments. Use it to compute the hash code of all the instance fields of a class:
@Override
public int hashCode() {
return Objects.hashCode(lastSecondWatched, totalSecondsWatched);
}
If there is a base class, then the derived class hashCode
implementation should call the base class hashCode
implementation. For example, again consider the base class ContentItemUserProgress
:
public final class VideoUserProgress extends ContentItemUserProgress {
public final SecondsWatched secondsWatched;
public final Optional<Date> lastWatchedDate;
@Override
public int hashCode() {
return Objects.hashCode(contentItemIdentifier, progressLevel);
}
}
The subclass VideoUserProgress
mixes in the superclass hashCode
value into the hashCode
value that it returns:
public final class VideoUserProgress extends ContentItemUserProgress {
public final SecondsWatched secondsWatched;
public final Optional<Date> lastWatchedDate;
@Override
public int hashCode() {
return Objects.hashCode(super.hashCode(), secondsWatched, lastWatchedDate);
}
}
The MoreObjects
class from the com.google.common.base
package has a useful toStringHelper
method that, as its name implies, helps construct a toString
value. Again, consider the SecondsWatched
class:
public class SecondsWatched {
public final long lastSecondWatched;
public final long totalSecondsWatched;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("lastSecondWatched", lastSecondWatched)
.add("totalSecondsWatched", totalSecondsWatched)
.toString();
}
}
If SecondsWatched
is instantiated with a lastSecondWatched
value of 5
and a totalSecondsWatched
value of 10
, then invoking its toString
method will return:
SecondsWatched={lastSecondWatched=5, totalSecondsWatched=10}
If there is a base class, it may be helpful for it to provide a protected
factory method that creates a ToStringHelper
instance and adds to it the base class fields. The derived class implementation of toString
can then invoke this base class factory method and add to it the derived class fields before returning. For example, again consider the base class ContentItemUserProgress
:
public abstract class ContentItemUserProgress {
public final ContentItemIdentifier contentItemIdentifier;
public final UserProgressLevel progressLevel;
protected MoreObjects.ToStringHelper getToStringHelper() {
return MoreObjects.toStringHelper(this)
.add("contentItemIdentifier", contentItemIdentifier)
.add("kind", getItemKind())
.add("progressLevel", progressLevel);
}
}
The subclass VideoUserProgress
then adds its own fields to the ToStringHelper
before returning:
public final class VideoUserProgress extends ContentItemUserProgress {
public final SecondsWatched secondsWatched;
public final Optional<Date> lastWatchedDate;
@Override
public String toString() {
return getToStringHelper()
.add("secondsWatched", secondsWatched)
.add("lastWatchedDate", lastWatchedDate)
.toString();
}
}
TODO(mgp)