xref: /aosp_15_r20/external/auto/value/userguide/records.md (revision 1c2bbba85eccddce6de79cbbf1645fda32e723f0)
1# AutoValue and Java Records
2
3
4Starting with Java 16,
5[records](https://docs.oracle.com/en/java/javase/19/language/records.html) are a
6standard feature of the language. If records are available to you, is there any
7reason to use AutoValue?
8
9## <a id="summary"></a>The short answer
10
11Generally, **use records** when you can. They have a very concise and readable
12syntax, they produce less code, and they don't need any special configuration or
13dependency. They are obviously a better choice when your class is just an
14aggregation of values, for example to allow a method to return multiple values
15or to combine values into a map key.
16
17(This was by design: the AutoValue authors were part of the
18[Project Amber](https://openjdk.org/projects/amber/) working group, where our
19goal was to make the records feature the best AutoValue replacement it could
20be.)
21
22If you have existing code that has AutoValue classes, you might want to migrate
23some or all of those classes to be records instead. In this document we will
24explain how to do this, and in what cases you might prefer not to.
25
26## <a id="notyet"></a>Can't use Java records yet?
27
28If you're creating new AutoValue classes for Java 15 or earlier, **follow this
29advice** to make sure your future conversion to records will be straightforward:
30
31*   Extend `Object` only (implementing interfaces is fine).
32*   Don't use JavaBeans-style prefixes: use `abstract int bar()`, not `abstract
33    int getBar()`.
34*   Don't declare any non-static fields of your own.
35*   Give the factory method and accessors the same visibility level as the
36    class.
37*   Avoid using [extensions](extensions.md).
38
39Adopting AutoValue at this time is still a good idea! There is no better way to
40make sure your code is as ready as possible to migrate to records later.
41
42## <a id="whynot"></a>Reasons to stick with AutoValue
43
44While records are usually better, there are some AutoValue features that have no
45simple equivalent with records. So you might prefer not to try migrating
46AutoValue classes that use those features, and you might even sometimes make new
47AutoValue classes even if records are available to you.
48
49### Extensions
50
51AutoValue has [extensions](extensions.md). Some are built in, like the
52[`@Memoized`](https://javadoc.io/static/com.google.auto.value/auto-value-annotations/1.10/com/google/auto/value/extension/memoized/Memoized.html),
53[`@ToPrettyString`](https://javadoc.io/static/com.google.auto.value/auto-value-annotations/1.10/com/google/auto/value/extension/toprettystring/ToPrettyString.html),
54and
55[`@SerializableAutoValue`](https://javadoc.io/static/com.google.auto.value/auto-value-annotations/1.10/com/google/auto/value/extension/serializable/SerializableAutoValue.html)
56extensions. Most extensions will have no real equivalent with records.
57
58### <a id="staticfactory"></a> Keeping the static factory method
59
60AutoValue has very few API-visible "quirks", but one is that it forces you to
61use a static factory method as your class's creation API. A record can have this
62too, but it can't prevent its constructor from *also* being visible, and
63exposing two ways to do the same thing can be dangerous.
64
65We think most users will be happy to switch to constructors and drop the factory
66methods, but you might want to keep a factory method in some records. Perhaps
67for compatibility reasons, or because you are normalizing input data to
68different types, such as from `List` to `ImmutableList`.
69
70In this event, you can still *discourage* calling the constructor by marking it
71deprecated. More on this [below](#deprecating).
72
73Clever ways do exist to make calling the constructor impossible, but it's
74probably simpler to keep using AutoValue.
75
76### Superclass
77
78The superclass of a record is always `java.lang.Record`. Occasionally the
79superclass of an AutoValue class is something other than `Object`, for example
80when two AutoValue classes share a subset of their properties.
81
82You might still be able to convert to records if you can convert these classes
83into interfaces.
84
85### Derived properties
86
87Records can't have instance fields (other than their properties). So it is hard
88to cache a derived property, for example. AutoValue makes this trivial with
89[`@Memoized`](https://javadoc.io/static/com.google.auto.value/auto-value-annotations/1.10/com/google/auto/value/extension/memoized/Memoized.html).
90
91We suggest ways to achieve the same effect with records [below](#derived), but
92it might be simpler to stick with AutoValue.
93
94### Primitive array properties
95
96AutoValue allows properties of primitive array types such as `byte[]` or `int[]`
97and it will implement `equals` and `hashCode` using the methods of
98`java.util.Arrays`. Records do not have any special treatment for primitive
99arrays, so by default they will use the `equals` and `hashCode` methods of the
100arrays. So two distinct arrays will never compare equal even if they have the
101same contents.
102
103The best way to avoid this problem is not to have properties with primitive
104array type, perhaps using alternatives such as
105[`ImmutableIntArray`](http://guava.dev/ImmutableIntArray). Alternatively you can
106define custom implementations of `equals` and `hashCode` as described in the
107[section](#eqhc) on that topic. But again, you might prefer to keep using
108AutoValue.
109
110(AutoValue doesn't allow properties of non-primitive array types.)
111
112## Translating an AutoValue class into a record
113
114Suppose you have existing AutoValue classes that you do want to translate into
115records, and the [above reasons](#whynot) not to don't apply. What does the
116translation look like?
117
118One important difference is that AutoValue does not allow properties to be
119`null` unless they are marked `@Nullable`. Records require explicit null checks
120to achieve the same effect, typically with
121[`Objects.requireNonNull`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Objects.html#requireNonNull\(T\)).
122
123This might also be a good time to start using a nullness-analysis tool on your
124code; see [NullAway](https://github.com/uber/NullAway) for example.
125
126The examples below show some before-and-after for various migration scenarios.
127For brevity, we've mostly omitted the javadoc comments that good code should
128have on its public classes and methods.
129
130### Basic example with only primitive properties
131
132Before:
133
134```java
135@AutoValue
136public abstract class Point {
137  public abstract int x();
138  public abstract int y();
139
140  public static Point of(int x, int y) {
141    return new AutoValue_Point(x, y);
142  }
143}
144```
145
146After:
147
148```java
149public record Point(int x, int y) {
150  /** @deprecated Call the constructor directly. */
151  @Deprecated
152  public static Point of(int x, int y) {
153    return new Point(x, y);
154  }
155}
156```
157
158The static factory method `of` is retained so clients of the `Point` class don't
159have to be updated. If possible, you should migrate clients to call `new
160Point(...)` instead. Then the record can be as simple as this:
161
162```java
163public record Point(int x, int y) {}
164```
165
166We've omitted the static factory methods from the other examples, but the
167general approach applies: keep the method initially but deprecate it and change
168its body so it just calls the constructor; migrate the callers so they call the
169constructor directly; delete the method. You might be able to use the
170[`InlineMe`](https://errorprone.info/docs/inlineme) mechanism from the Error
171Prone project to encourage this migration:
172
173```java
174package com.example.geometry;
175
176public record Point(int x, int y) {
177  /** @deprecated Call the constructor directly. */
178  @Deprecated
179  @InlineMe(replacement = "new Point(x, y)", imports = "com.example.geometry.Point")
180  public static Point of(int x, int y) {
181    return new Point(x, y);
182  }
183}
184```
185
186### Non-primitive properties that are not `@Nullable`
187
188Before:
189
190```java
191@AutoValue
192public abstract class Person {
193  public abstract String name();
194  public abstract int id();
195
196  public static Person create(String name, int id) {
197    return new AutoValue_Person(name, id);
198  }
199}
200```
201
202After:
203
204```java
205public record Person(String name, int id) {
206  public Person {
207    Objects.requireNonNull(name, "name");
208  }
209}
210```
211
212### Non-primitive properties that are all `@Nullable`
213
214Before:
215
216```java
217@AutoValue
218public abstract class Person {
219  public abstract @Nullable String name();
220  public abstract int id();
221
222  public static Person create(@Nullable String name, int id) {
223    return new AutoValue_Person(name, id);
224  }
225}
226```
227
228After:
229
230```java
231public record Person(@Nullable String name, int id) {}
232```
233
234### Validation
235
236Before:
237
238```java
239@AutoValue
240public abstract class Person {
241  public abstract String name();
242  public abstract int id();
243
244  public static Person create(String name, int id) {
245    if (id <= 0) {
246      throw new IllegalArgumentException("Id must be positive: " + id);
247    }
248    return new AutoValue_Person(name, id);
249  }
250}
251```
252
253After:
254
255```java
256public record Person(String name, int id) {
257  public Person {
258    Objects.requireNonNull(name, "name");
259    if (id <= 0) {
260      throw new IllegalArgumentException("Id must be positive: " + id);
261    }
262  }
263}
264```
265
266### Normalization
267
268With records, you can rewrite the constructor parameters to apply normalization
269or canonicalization rules.
270
271In this example we have two `int` values, but we don't care which order they are
272supplied in. Therefore we have to put them in a standard order, or else `equals`
273won't behave as expected.
274
275Before:
276
277```java
278@AutoValue
279public abstract class UnorderedPair {
280  public abstract int left();
281  public abstract int right();
282
283  public static UnorderedPair of(int left, int right) {
284    int min = Math.min(left, right);
285    int max = Math.max(left, right);
286    return new AutoValue_UnorderedPair(min, max);
287  }
288}
289```
290
291After:
292
293```java
294public record UnorderedPair(int left, int right) {
295  public UnorderedPair {
296    int min = Math.min(left, right);
297    int max = Math.max(left, right);
298    left = min;
299    right = max;
300  }
301}
302```
303
304If your normalization results in different types (or more or fewer separate
305fields) than the parameters, you will need to keep the static factory method. On
306a more subtle note, the user of this record might be surprised that what they
307passed in as `left` doesn't always come out as `left()`; keeping the static
308factory method would also allow the parameters to be named differently. See the
309section on the [static factory](#staticfactory) method.
310
311### <a id="beans"></a> JavaBeans prefixes (`getFoo()`)
312
313AutoValue allows you to prefix every property getter with `get`, but records
314don't have any special treatment here. Imagine you have a class like this:
315
316```java
317@AutoValue
318public abstract class Person {
319  public abstract String getName();
320  public abstract int getId();
321
322  public static Person create(String name, int id) {
323    return new AutoValue_Person(name, id);
324  }
325}
326```
327
328The names of the fields in `Person`, and the names in its `toString()`, don't
329have the `get` prefix:
330
331```
332jshell> Person.create("Priz", 6)
333$1 ==> Person{name=Priz, id=6}
334jshell> $1.getName()
335$2 ==> Priz
336jshell> List<String> showFields(Class<?> c) {
337   ...>   return Arrays.stream(c.getDeclaredFields()).map(Field::getName).toList();
338   ...> }
339jshell> showFields($1.getClass())
340$3 ==> [name, id]
341```
342
343You can translate this directly to a record if you don't mind a slightly strange
344`toString()`, and strange field names from reflection and debuggers:
345
346```java
347public record Person(String getName, int getId) {
348  public Person {
349    Objects.requireNonNull(getName);
350  }
351}
352```
353
354```
355jshell> Person.create("Priz", 6)
356$1 ==> Person[getName=Priz, getId=6]
357jshell> $1.getName()
358$2 ==> Priz
359jshell> showFields($1.getClass())
360$3 ==> [getName, getId]
361```
362
363Alternatively, you can alias `Person.getName()` to be `Person.name()`, etc.:
364
365```java
366public record Person(String name, int id) {
367  public Person {
368    Objects.requireNonNull(name);
369  }
370
371  public String getName() {
372    return name();
373  }
374
375  public int getId() {
376    return id();
377  }
378}
379```
380
381So both `Person.getName()` and `Person.name()` are allowed. You might want to
382deprecate the `get-` methods so you can eventually remove them.
383
384### <a id="derived"></a> Caching derived properties
385
386A record has an instance field for each of its properties, but cannot have other
387instance fields. That means in particular that it is not easy to cache derived
388properties, as you can with AutoValue and [`@Memoized`](howto.md#memoize).
389
390Records *can* have static fields, so one way to cache derived properties is to
391map from record instances to their derived properties.
392
393Before:
394
395```java
396@AutoValue
397public abstract class Person {
398  public abstract String name();
399  public abstract int id();
400
401  @Memoized
402  public UUID derivedProperty() {
403    return expensiveFunction(this);
404  }
405
406  public static Person create(String name, int id) {
407    return new AutoValue_Person(name, id);
408  }
409}
410```
411
412After:
413
414```java
415public record Person(String name, int id) {
416  public Person {
417    Objects.requireNonNull(name);
418  }
419
420  private static final Map<Person, String> derivedPropertyCache = new WeakHashMap<>();
421
422  public UUID derivedProperty() {
423    synchronized (derivedPropertyCache) {
424      return derivedPropertyCache.computeIfAbsent(this, person -> expensiveFunction(person)));
425    }
426  }
427}
428```
429
430It's very important to use **`WeakHashMap`** (or similar) or you might suffer a
431memory leak. As usual with `WeakHashMap`, you have to be sure that the values in
432the map don't reference the keys. For more caching options, consider using
433[Caffeine](https://github.com/ben-manes/caffeine).
434
435You might decide that AutoValue with `@Memoized` is simpler than records for
436this case, though.
437
438### Builders
439
440Builders are still available when using records. Instead of
441`@AutoValue.Builder`, you use [`@AutoBuilder`](autobuilder.md).
442
443Before:
444
445```java
446@AutoValue
447public abstract class Person {
448  public abstract String name();
449  public abstract int id();
450
451  public static Builder builder() {
452    return new AutoValue_Person.Builder();
453  }
454
455  @AutoValue.Builder
456  public interface Builder {
457    Builder name(String name);
458    Builder id(int id);
459    Person build();
460  }
461}
462
463Person p = Person.builder().name("Priz").id(6).build();
464```
465
466After:
467
468```java
469public record Person(String name, int id) {
470  public static Builder builder() {
471    return new AutoBuilder_Person_Builder();
472  }
473
474  @AutoBuilder
475  public interface Builder {
476    Builder name(String name);
477    Builder id(int id);
478    Person build();
479  }
480}
481
482Person p = Person.builder().name("Priz").id(6).build();
483```
484
485#### <a id="deprecating"></a>Deprecating the constructor
486
487As mentioned [above](#staticfactory), the primary constructor is always visible.
488In the preceding example, the builder will enforce that the `name` property is
489not null (since it is not marked @Nullable), but someone calling the constructor
490will bypass that check. You could deprecate the constructor to discourage this:
491
492```java
493public record Person(String name, int id) {
494  /** @deprecated Obtain instances using the {@link #builder()} instead. */
495  @Deprecated
496  public Person {}
497
498  public static Builder builder() {
499    return new AutoBuilder_Person_Builder();
500  }
501
502  @AutoBuilder
503  public interface Builder {
504    Builder name(String name);
505    Builder id(int id);
506    Person build();
507  }
508}
509```
510
511### Custom `toString()`
512
513A record can define its own `toString()` in exactly the same way as an AutoValue
514class.
515
516### <a id="eqhc"></a> Custom `equals` and `hashCode`
517
518As with AutoValue, it's unusual to want to change the default implementations of
519these methods, and if you do you run the risk of making subtle mistakes. Anyway,
520the idea is the same with both AutoValue and records.
521
522Before:
523
524```java
525@AutoValue
526public abstract class Person {
527  ...
528
529  @Override public boolean equals(Object o) {
530    return o instanceof Person that
531        && Ascii.equalsIgnoreCase(this.name(), that.name())
532        && this.id() == that.id();
533  }
534
535  @Override public int hashCode() {
536    return Objects.hash(Ascii.toLowerCase(name()), id());
537  }
538}
539```
540
541After:
542
543```java
544public record Person(String name, int id) {
545  ...
546
547  @Override public boolean equals(Object o) {
548    return o instanceof Person that
549        && Ascii.equalsIgnoreCase(this.name, that.name)
550        && this.id == that.id;
551  }
552
553  @Override public int hashCode() {
554    return Objects.hash(Ascii.toLowerCase(name), id);
555  }
556}
557```
558
559With records, the methods can access fields directly or use the corresponding
560methods (`this.name` or `this.name()`).
561