Java records and default/derived finalized properties

844 Views Asked by At

I have this:

// the id-constructor-parameter should not exist 
// as the value should always be calculated within the constructor.
public record User(String id, String name) {

    public User {
        id = UUID.randomUUID();
    }

    public User(String name) {
        this(null, name);

    }
}

How can I remove the parameter id from the constructor of a java record?

The id will be always reassigned/computed within the constructor, so it should not be passed by the constructor (i.e. id is a generated/derived property). Something like this, unfortunately, does not work:

public record User(String name) {
    // Unfortunately, this doesn't work in Java record
    private final String id;
    public User {
        id = UUID.randomUUID();
    }
}
2

There are 2 best solutions below

4
rzwitserloot On BEST ANSWER

The semantic principle you want fundamentally means you don't actually have a record.

Records are a little more than just 'a class where I want java to take care of equals and hashCode and getter methods for me'. For example, records also get an automatic deconstructor, and will therefore automatically support with-blocks, but, and herein lies the key issue, that fundamentally doesn't work with your take on how such a class works.

Hence, not a record.

Now, deconstructors and with-blocks aren't in java. Yet. They are in advanced planning stages and will probably be in something along the lines of java 23 or so.

Deconstructor

A deconstructor does what you'd think: A constructor takes in a value for each field (well, parameter, but there's the notion, at least with records, that each parameter is just the value for each of the fields) and turns that into an object. A deconstructor does the reverse, taking in an instance of User and turning that into its constituent parts. It's not like a normal method, because normal methods can only return one thing, whereas a deconstructor returns a series of things (here, a UUID and a name).

with-blocks

with blocks is one of a few features (the primary other one being pattern matching) that requires deconstructors to be a thing in order to 'work'. A with-block looks like this:

User u = new User("Joe");
...

public User rename(User in, String newName) {
  return u with {
    name = newName;
  };
}

The syntax isn't important (not fleshed out in the proposals yet, this is normal, syntax is generally not all that interesting and the last thing to be figured out), the point is: Inside 'with', the object (u, here) is deconstructed, i.e. inside the braces all the fields (here, UUID id and String name) exist as local variables you can write to, and at the end of the block, those locals are used to reconstitute a new instance, which is what the whole expression resolves to.

For all this to work, it has to be possible for the java compiler to know how to go through the process of breaking an instance of User into its parts and then recreating a new user object based on those parts later, and that requires that it is possible to create a user with a provided UUID instead of having your system random up a new id for them.

0
Sweeper On

A record has to have a canonical constructor, and that constructor's signature must match the record components. Therefore, if you want the record to only have a single-parameter String constructor, the record must only have the name component.

Technically, it is possible to associate an id to each created User, but since it can't be a record component, it isn't considered when generating the hashCode, equals, and toString methods, so it's not much shorter than if you had just written a regular class. All it saves is writing the getters for each of the other record components.

Here's one way of doing this (note that this isn't thread safe) - the idea is to use a Map to store which id corresponds to which User.

Of course, we can't put User objects directly into the Map, because their hashCode and equals are based on name alone. If you did, then you can't have users with duplicate names.

So we introduce a Wrapper inner class, whose hashCode and equals compare the reference equality of their outer instance.

record User(String name) {

    private class Wrapper {

        private User getUser() { return User.this; }

        @Override
        public int hashCode() {
            return System.identityHashCode(User.this);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof Wrapper w) {
                return User.this == w.getUser();
            }
            return false;
        }
    }
    private static final Map<Wrapper, UUID> map = new HashMap<>();

    public User {
        map.put(new Wrapper(), UUID.randomUUID());
    }

    public UUID id() {
        return map.get(new Wrapper());
    }

    // Here you would write your hashCode, equals, toString implementation that takes id into account...
}