Friday, April 22, 2022

Bruce Eckel on Java records

The amount of boilerplate and errors eliminated by the addition of records to Java is quite significant. Records also make code much more readable.

JDK 16 finalized the addition of the record keyword, which defines classes designed to be data transfer objects (also called data carriers). Records automatically generate

Oracle Java, Java Records, Core Java, Java Skills, Java Jobs, Java Preparation, Oracle Java Learning, Oracle Java JDK 16

◉ Immutable fields

◉ A canonical constructor

◉ An accessor method for each element

◉ The equals() method

◉ The hashCode() method

◉ The toString() method

The following code shows each feature:

// collections/BasicRecord.java

// {NewFeature} Since JDK 16

import java.util.*;

record Employee(String name, int id) {}

public class BasicRecord {

  public static void main(String[] args) {

    var bob = new Employee("Bob Dobbs", 11);

    var dot = new Employee("Dorothy Gale", 9);

    // bob.id = 12; // Error:

    // id has private access in Employee

    System.out.println(bob.name()); // Accessor

    System.out.println(bob.id()); // Accessor

    System.out.println(bob); // toString()

    // Employee works as the key in a Map:

    var map = Map.of(bob, "A", dot, "B");

    System.out.println(map);

  }

}

/* Output:

Bob Dobbs

11

Employee[name=Bob Dobbs, id=11]

{Employee[name=Dorothy Gale, id=9]=B, Employee[name=Bob Dobbs, id=11]=A}

*/

(Note: The {NewFeature} comment tag excludes this example from the Gradle build that uses JDK 8.)

For most uses of record, you will just give a name and provide the parameters, and you won’t need anything in the body. This automatically creates the canonical constructor that you see called in the first two lines of main(). This usage also creates the internal private final fields name and id; the constructor initializes these fields from its argument list.

You cannot add fields to a record except by defining them in the header. However, static methods, fields, and initializers are allowed.

Each property defined via the argument list for the record automatically gets its own accessor, as seen in the calls to bob.name() and bob.id(). (I appreciate that the designers did not continue the outdated JavaBean practice of accessors called getName() and getId().)

From the output, you can see that a record also creates a nice toString() method. Because a record also creates properly defined hashCode() and equals() methods, Employee can be used as the key in a Map. When that Map is displayed, the toString() method produces readable results.

If you later decide to add, subtract, or change one of the fields in your record, Java ensures that the result will still work properly. This changeability is one of the things that makes record so valuable.

A record can define methods, but the methods can only read fields, which are automatically final, as in the following:

// collections/FinalFields.java

// {NewFeature} Since JDK 16

import java.util.*;

record FinalFields(int i) {

  int timesTen() { return i * 10; }

  // void tryToChange() { i++; } // Error:

  // cannot assign a value to final variable i

}

Records can be composed of other objects, including other records, as follows:

// collections/ComposedRecord.java

// {NewFeature} Since JDK 16

record Company(Employee[] e) {}

// class Conglomerate extends Company {}

// error: cannot inherit from final Company

You cannot inherit from a record because it is implicitly final (and cannot be abstract). In addition, a record cannot be inherited from another class. However, a record can implement an interface, as follows:

// collections/ImplementingRecord.java

// {NewFeature} Since JDK 16

interface Star {

  double brightness();

  double density();

}

record RedDwarf(double brightness) implements Star {

  @Override public double density() { return 100.0; }

}

The compiler forces you to provide a definition for density(), but it doesn’t complain about brightness(). That’s because the record automatically generates an accessor for its brightness argument, and that accessor fulfills the contract for brightness() in interface Star.

A record can be nested within a class or defined locally within a method, as shown in the following example:

// collections/NestedLocalRecords.java

// {NewFeature} Since JDK 16

public class NestedLocalRecords {

  record Nested(String s) {}

  void method() {

    record Local(String s) {}

  }

}

Both nested and local uses of record are implicitly static.

Although the canonical constructor is automatically created according to the record arguments, you can add constructor behavior using a compact constructor, which looks like a constructor but has no parameter list, as follows:

// collections/CompactConstructor.java

// {NewFeature} Since JDK 16

record Point(int x, int y) {

  void assertPositive(int val) {

    if(val < 0)

      throw new IllegalArgumentException("negative");

  }

  Point { // Compact constructor: No parameter list

    assertPositive(x);

    assertPositive(y);

  }

}

The compact constructor is typically used to validate the arguments. It’s also possible to use the compact constructor to modify the initialization values for the fields.

// collections/PlusTen.java

// {NewFeature} Since JDK 16

record PlusTen(int x) {

  PlusTen {

    x += 10;

  }

  // Adjustment to field can only happen in

  // the constructor. Still not legal:

  // void mutate() { x += 10; }

  public static void main(String[] args) {

    System.out.println(new PlusTen(10));

  }

}

/* Output:

PlusTen[x=20]

*/

Although this seems as if final values are being modified, they are not. Behind the scenes, the compiler is creating an intermediate placeholder for x and then performing a single assignment of the result to this.x at the end of the constructor.

If necessary, you can replace the canonical constructor using normal constructor syntax, as follows:

// collections/NormalConstructor.java

// {NewFeature} Since JDK 16

record Value(int x) {

  Value(int x) { // With the parameter list

    this.x = x; // Must explicitly initialize

  }

}

Yes, this looks a bit strange. The constructor must exactly duplicate the signature of the record including the identifier names; you can’t define it using Value(int initValue). In addition, record Value(int x) produces a final field named x that is not initialized when using a noncompact constructor, so you will get a compile-time error if the constructor does not initialize this.x. Fortunately you’ll only rarely use the normal constructor form with record; if you do write a constructor, it will almost always be the compact form, which takes care of field initialization for you.

To copy a record, you must explicitly pass all fields to the constructor.

// collections/CopyRecord.java

// {NewFeature} Since JDK 16

record R(int a, double b, char c) {}

public class CopyRecord {

  public static void main(String[] args) {

    var r1 = new R(11, 2.2, 'z');

    var r2 = new R(r1.a(), r1.b(), r1.c());

    System.out.println(r1.equals(r2));

  }

}

/* Output:

true

*/

Creating a record generates an equals() method that ensures a copy is equal to its original.

Source: oracle.com

Related Posts

0 comments:

Post a Comment