23

I have a situation where I want record instances for a specific type to only be creatable using a factory method in a separate class within the same package. The reason for this is because before creating the record I need to perform a significant amount of validation.

The record is intended to be a dumb-data carrier of its validated fields but the validation cannot take place in the record's constructor because we require access to some elaborate validator objects to actually perform the validation.

Since passing the validator objects to the record constructor would mean they would form part of the record state it means we cannot use the record constructor to perform the record's validation.

And so I extracted the validation out into its own factory and coded up something like this (a factory class and a record in the same package):

package some.package;

// imports.....

@Component
class SomeRecordFactory {
    private final SomeValidator someValidator;
    private final SomeOtherValidator someOtherValidator;
    // Rest of the fields
    // ....

    // constructor  
    // ....


    public SomeRecord create(...) {
         someValidator.validate(....);
         someOtherValidator.validate(....);
         // .... other validation

         return new SomeRecord(...);
    }
}
package some.package;

public record SomeRecord(...) {
    /* package-private */ SomeRecord {
    }
}

For whatever reason the above does not work with IntelliJ complaining:

Compact constructor access level cannot be more restrictive than the record access level (public)

I can avoid the issue by using a normal class (which allows for a single package-private constructor) but would like to more accurately model the data as a record.

Why does this restriction exist for records? Are there any plans to remove this restriction in the future?

Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
vab2048
  • 1,065
  • 8
  • 21
  • What is `Compact`? What is `Record`? Please post all of your code. – CryptoFool Nov 19 '20 at 01:21
  • 1
    @Steve Perhaps I missed something in the question relating to a class named `Record`, but "records" are a new type of class added in Java 14 as a preview feature. See [JEP 359](https://openjdk.java.net/jeps/359). – Slaw Nov 19 '20 at 03:07
  • @Slaw - Thanks for pointing that out. I forgot all about that. I'm still stuck in the trenches of real life with Java 8. We're using 11 now, but I haven't really used much of the new stuff even in 11. Kotlin's where I've branched out. That's another reason I don't know more about the more modern Java stuff. – CryptoFool Nov 19 '20 at 05:19

3 Answers3

22

I asked the question on the amber mailing list (http://mail.openjdk.java.net/pipermail/amber-dev/2020-December.txt).

The question was posed:

What exactly is the reason that the canonical constructor must have the same access as the record?

And the answer given was (emphasis added mine):

Records are named tuples, they are defined only by their components, in a transparent manner i.e. no encapsulation. From a tuple, you can access to the value of each component and from all component values, you can create a tuple. The idea is that, in a method, if you are able to see a record, you can create it. Thus the canonical constructor has the same visibility as the record itself.

So the restriction exists to comply with the design goal and fact that if someone has an instance of a record they should be able to deconstruct it and then reconstruct it with the canonical constructor. And of course as a corollary this necessitates the canonical constructor having the same access as the record itself.

vab2048
  • 1,065
  • 8
  • 21
12

Q: Why does this restriction exist for records?

There isn't an explicit justification for that decision in JEP 359 or in the JLS, but I think it is implied by this excerpt from the JEP:

"Because records make the semantic claim of being transparent carriers for their data ..."

A "transparent carrier" means (to me1) that records are designed to have a minimal abstraction boundary. Restricting the access of a constructor implies (to me) an additional abstraction boundary.

In addition, I suspect that record constructors with more restrictive access modifiers could impede or complicate intended use-cases for records in future versions of Java.

Anyway, my take is that if you want fancy stuff like that you should be declaring a class rather than a record.

1 - Transparent is the opposite of opaque, and abstract data types are typically opaque by design. Obviously, this is just my take on what the JEP authors meant.


Q: Are there any plans to remove this restriction in the future?

I am not aware of any. There are no (public) open Java Bugs or RFEs about this.

Indeed, all of the JDK bugs relating to this topic were to ensure that the Java 15+ specifications made the restriction clear. There is no suggestion that the restriction happened by accident or oversight.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 5
    Your understanding of transparency is correct. Records give you various benefits; what you give up to get those benefits is the ability to _decouple the API from the representation_. That means you get the obvious public constructor, accessors, and eventually, deconstruction patterns. You can add to this API, but you can't subtract from it. – Brian Goetz Jan 18 '22 at 13:50
5

You can't do exactly what you want here - public record with hidden constructor (or more generally - a record whose constructor has more restrictive access - as you've already pointed out!). But if you tweak the requirements a bit you can achieve a similarly desired outcome.

The trick is to make the record itself hidden (private or package-private) and only expose the factory class:

// package-private
record SomeRecord(String key, String value) {}
public final class SomeRecordFactory {
  public SomeRecord create(String value) {
    return new SomeRecord("key", value);
  }
}

Then you're forced to create instances like:

SomeRecordFactory factory = new SomeRecordFactory();
var myObject = factory.create("my value");

NOTE the use of var - I can't use MyRecord because it is hidden (package-private in this case, could make it private by nesting it in the factory class).


If you want to expose a Type (e.g. in cases where you can't use var such as method/constructor args) you can provide a sealed interface representing the type and only permit the hidden record:

public sealed interface SomeType permits SomeRecord {
  String key();
  String value();
}
// package-private
record SomeRecord(String key, String value) implements SomeType {}
public final class SomeRecordFactory {
  public SomeType create(String value) {
    return new SomeRecord("key", value);
  }
}

Then you can create instances without var like:

SomeRecordFactory factory = new SomeRecordFactory();
SomeType myObject = factory.create("my value");

The downside to this approach is you won't have access to the records. So you can't use the new features like pattern matching (exhaustive switch on sealed interface + record deconstruction). You'd have to do that within your package (you could argue this is a good thing, as it'll be hidden from the users of your API!).

wilmol
  • 1,429
  • 16
  • 22