Starting with Java 16, records are a standard feature of the language. If records are available to you, is there any reason to use AutoValue?
Generally, use records when you can. They have a very concise and readable syntax, they produce less code, and they don't need any special configuration or dependency. They are obviously a better choice when your class is just an aggregation of values, for example to allow a method to return multiple values or to combine values into a map key.
(This was by design: the AutoValue authors were part of the Project Amber working group, where our goal was to make the records feature the best AutoValue replacement it could be.)
If you have existing code that has AutoValue classes, you might want to migrate some or all of those classes to be records instead. In this document we will explain how to do this, and in what cases you might prefer not to.
If you're creating new AutoValue classes for Java 15 or earlier, follow this advice to make sure your future conversion to records will be straightforward:
- Extend
Object
only (implementing interfaces is fine). - Don't use JavaBeans-style prefixes: use
abstract int bar()
, notabstract int getBar()
. - Don't declare any non-static fields of your own.
- Give the factory method and accessors the same visibility level as the class.
- Avoid using extensions.
Adopting AutoValue at this time is still a good idea! There is no better way to make sure your code is as ready as possible to migrate to records later.
While records are usually better, there are some AutoValue features that have no simple equivalent with records. So you might prefer not to try migrating AutoValue classes that use those features, and you might even sometimes make new AutoValue classes even if records are available to you.
AutoValue has extensions. Some are built in, like the
@Memoized
,
@ToPrettyString
,
and
@SerializableAutoValue
extensions. Most extensions will have no real equivalent with records.
AutoValue has very few API-visible "quirks", but one is that it forces you to use a static factory method as your class's creation API. A record can have this too, but it can't prevent its constructor from also being visible, and exposing two ways to do the same thing can be dangerous.
We think most users will be happy to switch to constructors and drop the factory
methods, but you might want to keep a factory method in some records. Perhaps
for compatibility reasons, or because you are normalizing input data to
different types, such as from List
to ImmutableList
.
If you don't want users to call your constructor at all, records are not a good fit. You probably want to continue using AutoValue in that case.
The superclass of a record is always java.lang.Record
. Occasionally the
superclass of an AutoValue class is something other than Object
, for example
when two AutoValue classes share a subset of their properties.
You might still be able to convert to records if you can convert these classes into interfaces.
Records can't have instance fields (other than their properties). So it is hard
to cache a derived property, for example. AutoValue makes this trivial with
@Memoized
.
We suggest ways to achieve the same effect with records below, but it might be simpler to stick with AutoValue.
AutoValue allows properties of primitive array types such as byte[]
or int[]
and it will implement equals
and hashCode
using the methods of
java.util.Arrays
. Records do not have any special treatment for primitive
arrays, so by default they will use the equals
and hashCode
methods of the
arrays. So two distinct arrays will never compare equal even if they have the
same contents.
The best way to avoid this problem is not to have properties with primitive
array type, perhaps using alternatives such as
ImmutableIntArray
. Alternatively you can
define custom implementations of equals
and hashCode
as described in the
section on that topic. But again, you might prefer to keep using
AutoValue.
(AutoValue doesn't allow properties of non-primitive array types.)
Suppose you have existing AutoValue classes that you do want to translate into records, and the above reasons not to don't apply. What does the translation look like?
One important difference is that AutoValue does not allow properties to be
null
unless they are marked @Nullable
. Records require explicit null checks
to achieve the same effect, typically with
Objects.requireNonNull
.
This might also be a good time to start using a nullness-analysis tool on your code; see NullAway for example.
The examples below show some before-and-after for various migration scenarios. For brevity, we've mostly omitted the javadoc comments that good code should have on its public classes and methods.
Before:
@AutoValue
public abstract class Point {
public abstract int x();
public abstract int y();
public static Point of(int x, int y) {
return new AutoValue_Point(x, y);
}
}
After:
public record Point(int x, int y) {
/** @deprecated Call the constructor directly. */
@Deprecated
public static Point of(int x, int y) {
return new Point(x, y);
}
}
The static factory method of
is retained so clients of the Point
class don't
have to be updated. If possible, you should migrate clients to call new Point(...)
instead. Then the record can be as simple as this:
public record Point(int x, int y) {}
We've omitted the static factory methods from the other examples, but the
general approach applies: keep the method initially but deprecate it and change
its body so it just calls the constructor; migrate the callers so they call the
constructor directly; delete the method. You might be able to use the
InlineMe
mechanism from the Error
Prone project to encourage this migration:
package com.example.geometry;
public record Point(int x, int y) {
/** @deprecated Call the constructor directly. */
@Deprecated
@InlineMe(replacement = "new Point(x, y)", imports = "com.example.geometry.Point")
public static Point of(int x, int y) {
return new Point(x, y);
}
}
Before:
@AutoValue
public abstract class Person {
public abstract String name();
public abstract int id();
public static Person create(String name, int id) {
return new AutoValue_Person(name, id);
}
}
After:
public record Person(String name, int id) {
public Person {
Objects.requireNonNull(name, "name");
}
}
Before:
@AutoValue
public abstract class Person {
public abstract @Nullable String name();
public abstract int id();
public static Person create(@Nullable String name, int id) {
return new AutoValue_Person(name, id);
}
}
After:
public record Person(@Nullable String name, int id) {}
Before:
@AutoValue
public abstract class Person {
public abstract String name();
public abstract int id();
public static Person create(String name, int id) {
if (id <= 0) {
throw new IllegalArgumentException("Id must be positive: " + id);
}
return new AutoValue_Person(name, id);
}
}
After:
public record Person(String name, int id) {
public Person {
Objects.requireNonNull(name, "name");
if (id <= 0) {
throw new IllegalArgumentException("Id must be positive: " + id);
}
}
}
With records, you can rewrite the constructor parameters to apply normalization or canonicalization rules.
In this example we have two int
values, but we don't care which order they are
supplied in. Therefore we have to put them in a standard order, or else equals
won't behave as expected.
Before:
@AutoValue
public abstract class UnorderedPair {
public abstract int left();
public abstract int right();
public static UnorderedPair of(int left, int right) {
int min = Math.min(left, right);
int max = Math.max(left, right);
return new AutoValue_UnorderedPair(min, max);
}
}
After:
public record UnorderedPair(int left, int right) {
public UnorderedPair {
int min = Math.min(left, right);
int max = Math.max(left, right);
left = min;
right = max;
}
}
If your normalization results in different types (or more or fewer separate
fields) than the parameters, you will need to keep the static factory method. On
a more subtle note, the user of this record might be surprised that what they
passed in as left
doesn't always come out as left()
; keeping the static
factory method would also allow the parameters to be named differently. See the
section on the static factory method.
AutoValue allows you to prefix every property getter with get
, but records
don't have any special treatment here. Imagine you have a class like this:
@AutoValue
public abstract class Person {
public abstract String getName();
public abstract int getId();
public static Person create(String name, int id) {
return new AutoValue_Person(name, id);
}
}
The names of the fields in Person
, and the names in its toString()
, don't
have the get
prefix:
jshell> Person.create("Priz", 6)
$1 ==> Person{name=Priz, id=6}
jshell> $1.getName()
$2 ==> Priz
jshell> List<String> showFields(Class<?> c) {
...> return Arrays.stream(c.getDeclaredFields()).map(Field::getName).toList();
...> }
jshell> showFields($1.getClass())
$3 ==> [name, id]
You can translate this directly to a record if you don't mind a slightly strange
toString()
, and strange field names from reflection and debuggers:
public record Person(String getName, int getId) {
public Person {
Objects.requireNonNull(getName);
}
}
jshell> Person.create("Priz", 6)
$1 ==> Person[getName=Priz, getId=6]
jshell> $1.getName()
$2 ==> Priz
jshell> showFields($1.getClass())
$3 ==> [getName, getId]
Alternatively, you can alias Person.getName()
to be Person.name()
, etc.:
public record Person(String name, int id) {
public Person {
Objects.requireNonNull(name);
}
public String getName() {
return name();
}
public int getId() {
return id();
}
}
So both Person.getName()
and Person.name()
are allowed. You might want to
deprecate the get-
methods so you can eventually remove them.
A record has an instance field for each of its properties, but cannot have other
instance fields. That means in particular that it is not easy to cache derived
properties, as you can with AutoValue and @Memoized
.
Records can have static fields, so one way to cache derived properties is to map from record instances to their derived properties.
Before:
@AutoValue
public abstract class Person {
public abstract String name();
public abstract int id();
@Memoized
public UUID derivedProperty() {
return expensiveFunction(this);
}
public static Person create(String name, int id) {
return new AutoValue_Person(name, id);
}
}
After:
public record Person(String name, int id) {
public Person {
Objects.requireNonNull(name);
}
private static final Map<Person, String> derivedPropertyCache = new WeakHashMap<>();
public UUID derivedProperty() {
synchronized (derivedPropertyCache) {
return derivedPropertyCache.computeIfAbsent(this, person -> expensiveFunction(person)));
}
}
}
It's very important to use WeakHashMap
(or similar) or you might suffer a
memory leak. As usual with WeakHashMap
, you have to be sure that the values in
the map don't reference the keys. For more caching options, consider using
Caffeine.
You might decide that AutoValue with @Memoized
is simpler than records for
this case, though.
Builders are still available when using records. Instead of
@AutoValue.Builder
, you use @AutoBuilder
.
Before:
@AutoValue
public abstract class Person {
public abstract String name();
public abstract int id();
public static Builder builder() {
return new AutoValue_Person.Builder();
}
@AutoValue.Builder
public interface Builder {
Builder name(String name);
Builder id(int id);
Person build();
}
}
Person p = Person.builder().name("Priz").id(6).build();
After:
public record Person(String name, int id) {
public static Builder builder() {
return new AutoBuilder_Person_Builder();
}
@AutoBuilder
public interface Builder {
Builder name(String name);
Builder id(int id);
Person build();
}
}
Person p = Person.builder().name("Priz").id(6).build();
As mentioned above, the primary constructor is always visible. So you cannot assume that instances of your record will always be built with the builder. Any data validation should be performed in the constructor.
A record can define its own toString()
in exactly the same way as an AutoValue
class.
As with AutoValue, it's unusual to want to change the default implementations of these methods, and if you do you run the risk of making subtle mistakes. Anyway, the idea is the same with both AutoValue and records.
Before:
@AutoValue
public abstract class Person {
...
@Override public boolean equals(Object o) {
return o instanceof Person that
&& Ascii.equalsIgnoreCase(this.name(), that.name())
&& this.id() == that.id();
}
@Override public int hashCode() {
return Objects.hash(Ascii.toLowerCase(name()), id());
}
}
After:
public record Person(String name, int id) {
...
@Override public boolean equals(Object o) {
return o instanceof Person that
&& Ascii.equalsIgnoreCase(this.name, that.name)
&& this.id == that.id;
}
@Override public int hashCode() {
return Objects.hash(Ascii.toLowerCase(name), id);
}
}
With records, the methods can access fields directly or use the corresponding
methods (this.name
or this.name()
).