Skip to content

Latest commit

 

History

History
535 lines (412 loc) · 15.4 KB

records.md

File metadata and controls

535 lines (412 loc) · 15.4 KB

AutoValue and Java Records

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?

The short answer

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.

Can't use Java records yet?

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(), not abstract 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.

Reasons to stick with AutoValue

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.

Extensions

AutoValue has extensions. Some are built in, like the @Memoized, @ToPrettyString, and @SerializableAutoValue extensions. Most extensions will have no real equivalent with records.

Keeping the static factory method

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.

Superclass

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.

Derived properties

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.

Primitive array properties

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.)

Translating an AutoValue class into a record

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.

Basic example with only primitive properties

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);
  }
}

Non-primitive properties that are not @Nullable

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");
  }
}

Non-primitive properties that are all @Nullable

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) {}

Validation

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);
    }
  }
}

Normalization

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.

JavaBeans prefixes (getFoo())

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.

Caching derived properties

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

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.

Custom toString()

A record can define its own toString() in exactly the same way as an AutoValue class.

Custom equals and hashCode

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()).