Search code examples
javaconstructorrecordjava-17

rewrite a class with two fields and a constructor with a single parameter as a record class


I would like to know how to rewrite this class

public class ClassA {
    private final String foo;
    private final String bar;

    public ClassA(String foo) {
        this.foo = foo;
        this.bar = foo.toUpperCase();
    }

    // getters...
}

as a record class.
The best I've managed to do is this

public record ClassA(String foo, String bar) {
    public ClassA(String foo) {
        this(foo, foo.toUpperCase());
    }
}

The problem is that this solution creates two constructors while I want only one which accepts the string foo


Solution

  • Record classes always have a so-called canonical constructor, which expects arguments for all the field you've declared. This constructor would be generated by the compiler by default, but you can provide your own one, the key point: a canonical constructor is available for every record at runtime.

    Here's a quote from the Java Language Specification:

    To ensure proper initialization of its record components, a record class does not implicitly declare a default constructor (§8.8.9). Instead, a record class has a canonical constructor, declared explicitly or implicitly, that initializes all the component fields of the record class.

    All non-canonical constructors, which are constructors with signatures that differ from the canonical, like your case constructor that expects a single argument ClassA(String), should delegate the call to the canonical constructor using so-called explicit constructor invocation, i.e. using this() (precisely as you've done), otherwise such constructor would not compile.

    A record declaration may contain declarations of constructors that are not canonical constructors. The body of every non-canonical constructor in a record declaration must start with an alternate constructor invocation (§8.8.7.1), or a compile-time error occurs.

    Conclusion: since you've declared a record that has two fields, and you also need a non-canonical constructor expecting one argument, there would be two constructors: canonical and a non-canonical. There are no workarounds.


    In addition, as @Brian Goetz has rightfully pointed out, it's worth to override the canonical constructor as well. Otherwise, there would be an ambiguity in regard whether the second argument of the record is an uppercase version of the first or not because of the possibility to use two constructors with the different logic.

    If we define the record as follows:

    public record ClassA(String foo, String bar) {
        public ClassA(String foo) {
            this(foo, foo.toUpperCase(Locale.ROOT));
        }
    }
    

    The code below:

    System.out.println(new ClassA("foo", "arbitrary string which not FOO"));
    System.out.println(new ClassA("foo"));
    

    Will produce the output:

    ClassA[foo=foo, bar=arbitrary string which not FOO] // unexpected result
    ClassA[foo=foo, bar=FOO]                            // desired result
    

    To ensure that the second argument at any circumstances would be equal to the first argument turned to uppercase, we have to override the canonical constructor:

    public record ClassA(String foo, String bar) {
        public ClassA(String foo) {
            this(foo, foo); // doesn't matter what would be provided as the second argument
        }
        
        public ClassA(String foo, String bar) {
            this.foo = foo;
            this.bar = foo.toUpperCase(Locale.ROOT);
        }
    }