I would strongly suggest you stop using this pattern. It's got all sorts of problems:

Basic errors in the code

Your NULL_ID field isn't final which it clearly should be.

Null object vs. Empty object

There are 2 concepts that seem similar or even the same but they aren't.

There's the unknown / not found / not applicable concept. For example:

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");

What should name be if "foo" is not in the map?

Not so fast - before you answer: Well, maybe "" - that would lead to all sorts of problems. If you wrote code that mistakenly thinks the id used is definitely in this map, then it's a fait accompli: This code is bugged. Period. All we can do now is ensure that this bug is dealt with as 'nicely' as possible.

And saying that name is null here, is strictly superior: The bug will now be explicit, with a stack trace pointing at the offending code. Absence of stack trace is not proof of bug free code - not at all. If this code returns the empty string and then sends an email to a blank mail address with a body that contains an empty string where the name should be, that's much worse than the code throwing an NPE.

For such a value (not found / unknown / not applicable), nothing in java beats null as value.

However, what does occur quite often when working with APIs that are documented to perhaps return null (that is, APIs that may return 'not applicable', 'no value', or 'not found'), is that the caller wants to treat this the same as a known convenient object.

For example, if I always uppercase and trim the student name, and some ids are already mapped to 'not enrolled anymore' and this shows up as having been mapped to the empty string, then it can be really convenient for the caller to desire for this specific use case that not-found ought to be treated as empty string. Fortunately, the Map API caters to this:

String name = map.getOrDefault(key, "").toUpperCase().trim();
if (name.isEmpty()) return;
// do stuff here, knowing all is well.

The crucial tool that you, API designer, should provide, is an empty object.

Empty objects should be convenient. Yours is not.

So, now that we've established that a 'null object' is not what you want, but an 'empty object' is great to have, note that they should be convenient. The caller already decided on some specific behaviour they want; they explicitly opted into this. They don't want to then STILL have to deal with unique values that require special treatment, and having an Id instance whose id field is null fails the convenience test.

What you'd want is presumably an Id that is fast, immutable, easily accessible, and has an empty string for id. not null. Be like "", or like List.of(). "".length() works, and returns 0. someListIHave.retainAll(List.of()) works, and clears the list. That's the convenience at work. It is dangerous convenience (in that, if you weren't expecting a dummy object with certain well known behaviours, NOT erroring on the spot can hide bugs), but that's why the caller has to explicitly opt into it, e.g. by using getOrDefault(k, THE_DUMMY).

So, what should you write here?

Simple:

private static final Id EMPTY = new Id("");

It is possible you need for the EMPTY value to have certain specific behaviours. For example, sometimes you want the EMPTY object to also have the property that it is unique; that no other instance of Id can be considered equal to it.

You can solve that problem in two ways:

  1. Hidden boolean.
  2. by using EMPTY as an explicit identity.

I assume 'hidden boolean' is obvious enough. a private boolean field that a private constructor can initialize to true, and all publically accessible constructors set to false.

Using EMPTY as identity is a bit more tricky. It looks, for example, like this:

@Override public boolean equals(Object other) {
    if (other == null || !other.getClass() == Id.class) return false;
    if (other == this) return true;
    if (other == EMPTY || this == EMPTY) return false;
    return ((Id) other).id.equals(this.id);
}

Here, EMPTY.equals(new Id("")) is in fact false, but EMPTY.equals(EMPTY) is true.

If that's how you want it to work (questionable, but there are use cases where it makes sense to decree that the empty object is unique), have at it.

Answer from rzwitserloot on Stack Overflow
Top answer
1 of 2
5

I would strongly suggest you stop using this pattern. It's got all sorts of problems:

Basic errors in the code

Your NULL_ID field isn't final which it clearly should be.

Null object vs. Empty object

There are 2 concepts that seem similar or even the same but they aren't.

There's the unknown / not found / not applicable concept. For example:

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");

What should name be if "foo" is not in the map?

Not so fast - before you answer: Well, maybe "" - that would lead to all sorts of problems. If you wrote code that mistakenly thinks the id used is definitely in this map, then it's a fait accompli: This code is bugged. Period. All we can do now is ensure that this bug is dealt with as 'nicely' as possible.

And saying that name is null here, is strictly superior: The bug will now be explicit, with a stack trace pointing at the offending code. Absence of stack trace is not proof of bug free code - not at all. If this code returns the empty string and then sends an email to a blank mail address with a body that contains an empty string where the name should be, that's much worse than the code throwing an NPE.

For such a value (not found / unknown / not applicable), nothing in java beats null as value.

However, what does occur quite often when working with APIs that are documented to perhaps return null (that is, APIs that may return 'not applicable', 'no value', or 'not found'), is that the caller wants to treat this the same as a known convenient object.

For example, if I always uppercase and trim the student name, and some ids are already mapped to 'not enrolled anymore' and this shows up as having been mapped to the empty string, then it can be really convenient for the caller to desire for this specific use case that not-found ought to be treated as empty string. Fortunately, the Map API caters to this:

String name = map.getOrDefault(key, "").toUpperCase().trim();
if (name.isEmpty()) return;
// do stuff here, knowing all is well.

The crucial tool that you, API designer, should provide, is an empty object.

Empty objects should be convenient. Yours is not.

So, now that we've established that a 'null object' is not what you want, but an 'empty object' is great to have, note that they should be convenient. The caller already decided on some specific behaviour they want; they explicitly opted into this. They don't want to then STILL have to deal with unique values that require special treatment, and having an Id instance whose id field is null fails the convenience test.

What you'd want is presumably an Id that is fast, immutable, easily accessible, and has an empty string for id. not null. Be like "", or like List.of(). "".length() works, and returns 0. someListIHave.retainAll(List.of()) works, and clears the list. That's the convenience at work. It is dangerous convenience (in that, if you weren't expecting a dummy object with certain well known behaviours, NOT erroring on the spot can hide bugs), but that's why the caller has to explicitly opt into it, e.g. by using getOrDefault(k, THE_DUMMY).

So, what should you write here?

Simple:

private static final Id EMPTY = new Id("");

It is possible you need for the EMPTY value to have certain specific behaviours. For example, sometimes you want the EMPTY object to also have the property that it is unique; that no other instance of Id can be considered equal to it.

You can solve that problem in two ways:

  1. Hidden boolean.
  2. by using EMPTY as an explicit identity.

I assume 'hidden boolean' is obvious enough. a private boolean field that a private constructor can initialize to true, and all publically accessible constructors set to false.

Using EMPTY as identity is a bit more tricky. It looks, for example, like this:

@Override public boolean equals(Object other) {
    if (other == null || !other.getClass() == Id.class) return false;
    if (other == this) return true;
    if (other == EMPTY || this == EMPTY) return false;
    return ((Id) other).id.equals(this.id);
}

Here, EMPTY.equals(new Id("")) is in fact false, but EMPTY.equals(EMPTY) is true.

If that's how you want it to work (questionable, but there are use cases where it makes sense to decree that the empty object is unique), have at it.

2 of 2
4

No, what you want is not possible with the current definition of records in Java 14. Every record type has a single canonical constructor, either defined implicitly or explicitly. Every non-canonical constructor has to start with an invocation of another constructor of this record type. This basically means, that a call to any other constructor definitely results in a call to the canonical constructor. [8.10.4 Record Constructor Declarations in Java 14]

If this canonical constructor does the argument validation (which it should, because it's public), your options are limited. Either you follow one of the suggestions/workarounds already mentioned or you only allow your users to access the API through an interface. If you choose this last approach, you have to remove the argument validation from the record type and put it in the interface, like so:

public interface Id {
    Id NULL_ID = new IdImpl(null);

    String id();

    static Id newIdFrom(String id) {
        Objects.requireNonNull(id);
        return new IdImpl(id);
    }
}

record IdImpl(String id) implements Id {}

I don't know your use case, so that might not be an option for you. But again, what you want is not possible right now.

Regarding Java 15, I could only find the JavaDoc for Records in Java 15, which seems to not have changed. I couldn't find the actual specification, the link to it in the JavaDoc leads to a 404, so maybe they have already relaxed the rules, because some people complained about them.

🌐
Reddit
reddit.com › r/learnjava › non-null record class values
Non-Null record class values : r/learnjava
March 10, 2022 - It looks like records can have instance methods, too, so you could write a validation method if you wanted to encapsulate it in the record you define. ... Yeah, they are pretty nice. This is a good resource: https://docs.oracle.com/en/java/javase/17/language/records.html
Discussions

[BUG] javax Nonnull on Record Fails to Create Null-Check
Describe the bug Using javax.annotation.Nonnull annotation on a Record's field does not generate a null-check. More on github.com
🌐 github.com
1
July 26, 2023
java - Avoiding null attributes in records - Stack Overflow
I have the following record. But the collection cannot be null. Is there a way to default it to an empty list if it's null? public record Car( long id, String model, List<... More on stackoverflow.com
🌐 stackoverflow.com
option type - Java records with nullable components - Stack Overflow
I really like the addition of records in Java 14, at least as a preview feature, as it helps to reduce my need to use lombok for simple, immutable "data holders". But I'm having an issue with the implementation of nullable components. I'm trying to avoid returning null in my codebase to indicate ... More on stackoverflow.com
🌐 stackoverflow.com
Nullable not detected for Java records when ParametersAreNonnullByDefault is set on declaration
Operating system: Ubuntu 24.04 SonarLint plugin version: 10.14.0.80203 IntelliJ version: IntelliJ IDEA 2024.3.1 (Ultimate Edition) Programming language you’re coding in: Java 21 Is connected mode used: No And a thorough description of the problem / question: Rule java:S4449 is triggered for ... More on community.sonarsource.com
🌐 community.sonarsource.com
1
0
December 19, 2024
🌐
Medium
mike-diaz006.medium.com › what-i-learned-at-work-this-week-a-nullable-option-in-a-java-record-8e16900d6f7e
What I Learned at Work this Week: A Nullable Option in a Java Record | by Mike Diaz | Medium
July 27, 2024 - A null encryption value isn’t an “error,” but a valid way to construct an SftpSettings object. I’d be curious what an alternative solution would look like and whether it would be easier or harder to read and understand. This isn’t the best code I’ve ever written, but it does work! And that’s something we can all be happy about. The DTO Pattern, Baledung · Optional as a Record Parameter in Java, Baledung ·
🌐
Xebia
xebia.com › home › blog › how to use java records
Java Records: Default Values, Validation & Constructors | Xebia
2 weeks ago - Java records are immutable data carriers and do not support default values directly. To assign default values or validate inputs, you must use a canonical or compact constructor to replace null or invalid values before the record instance is created.
🌐
GitHub
github.com › projectlombok › lombok › issues › 3467
[BUG] javax Nonnull on Record Fails to Create Null-Check · Issue #3467 · projectlombok/lombok
July 26, 2023 - Describe the bug Using javax.annotation.Nonnull annotation on a Record's field does not generate a null-check. public record JavaxRecord(@Nonnull String shouldNotBeNull) { } To Reproduce Attached is JavaxNonnullRecordExample.java.txt whi...
Author   projectlombok
🌐
Baeldung
baeldung.com › home › java › core java › optional as a record parameter in java
Optional as a Record Parameter in Java | Baeldung
January 16, 2024 - Before discussing the relationship between Optional and records, let’s quickly recap the intended uses for Optional in Java. Typically, before Java 8, we used null to represent the empty state of an object. However, a null as a return value requires null-check validation from the caller code ...
🌐
Oracle
docs.oracle.com › en › java › javase › 14 › language › records.html
Java Language Updates
Its custom constructor calls Objects.requireNonNull(message), which specifies that if the message field is initialized with a null value, then a NullPointerException is thrown. (Custom record constructors still initialize their record's private fields.) record HelloWorld(String message) { public HelloWorld { java.util.Objects.requireNonNull(message); } } Restrictions on Records ·
Find elsewhere
Top answer
1 of 2
3

null is a bit of an odd beast in java; it means too many things if you take a wide survey across the community.

But that doesn't mean you should make the same mistake. null is best left as a thing you 'tighten' - it needs to mean something highly specific. And that specific thing is: "Unknown / irrelevant / not set".

Here's the difference:

If I hold my 2 hands behind my back with closed fists, and I ask you: "Are the object that I hold in my left hand, and the one in my right hand, equal?" - then you should ask me to show you the objects. If I refuse, the answer is not "They are equal". The answer is also not "They are different". The only right answer is "I cannot tell."

Hence, this java code:

Copypublic void whatever() {
  String a = null;
  String b = null;
  if (a.equals(b)) { ... }
}

Actually does the right thing: It throws an NPE which is what you want. Similarly, asking for the length of an unknown thing is best answered with 'I do not know'. The answer: "It has 0 length" is not appropriate.

By approaching null that way, i.e. by ensuring you never use it as a standin for 'empty' or 'default' or similar, the NullPointerException turns into an asset instead of a nuisance. You want that exception.

Hence, the right approach is in fact this:

Copypublic record Car(
        long id,
        String model,
        List<Wheel> wheels
) {
    public Car {
        Objects.requireNonNull(wheels, "wheels");
    }
}

The constructor of records is the right place for checking preconditions, and this is a precondition: The list is required to be non-null. You might also want to require that model isn't null either.

A clue you're doing it wrong is if you write if (x == null || x.isEmpty()) a lot. After all, if 'null' and 'empty' are treated as semantically equivalent pretty much all the time then why is null a thing in the first place? It's not hard to make an empty thing. In fact, for strings, it's easier: "" is shorter to write than null and takes, if anything, less memory (the empty string is interned, don't worry about that).

Your callers shouldn't be overly bothered, they can just pass List.of() which is not much to type and far easier to read. After all, this:

Copynew Car(0, null, null);
new Car(0, "", List.of());

The second one is more informative; at least you have some vague idea about the types of the arguments, and you also know clearly that the list is supposed to be empty.

null can still make sense, if the concept of 'irrelevant/unknown' is something your record needs to be capable of conveying. For example, if you want to be able to encode the notion of a 'car' that is currently in the garage, with the wheels removed. It still has wheels, we just don't know where they are right now. This is annoying, of course: All code that deals with Car instances needs to be capable of dealing with it. Hence, you only 'do' that (allow null) if it's strictly necessary.

A wheel-less car, that's no problem, and should be represented with List.of(). Not null.

The simple way to think about it is: null should mean something different vs. any other possible value imaginable. If that is true, null is good. If that is not true (for example, null means the same thing as List.of()), then you should throw an NPE as fast as you can (first line of a constructor is a great place).

Immutability

records don't need to be immutable but it tends to be expected. Did you intend for this record to allow the set of wheels to change during its lifetime? If yes, record might not be appropriate. If no, then new ArrayList is not right; you want List.of which guarantees that the list cannot change. It's a bit difficult to enforce that callers pass an immutable list, unfortunately. Best simply not do that (document it instead). Also, List.of() is shorter and (very rarely relevant) more efficient.

2 of 2
0

I like rzwitserloots answer in generel.
Read it before changing a null value to anything else.

If you really want to change the value of the input parameter, you have the option to do this in a static factoy method.

Copypublic record Car(
        long id,
        String model,
        List<Wheel> wheels
) {
    public Car {
        Objects.requireNonNull(wheels, "wheels");
    }
    public static Car createNonNullCar(
        long id,
        String model,
        List<Wheel> wheels
    ) {
        // feel free to change what you want
        if (wheels == null) {
            wheels = List.of();
        } else {
            wheels = wheels.stream().filter(Objects::nonNull).toList();
        }
        return new Car(id, model, wheels);
    }
}

The factory method helps to keep the concept of records clean, while providing a convenient way to create kind of normalized Cars.

Keep in mind that it is always possible to use the cannonical constructor,
because it is not possible to make it private.

Top answer
1 of 4
19

A record comprises attributes that primarily define its state. The derivation of the accessors, constructors, etc. is completely based on this state of the records.

Now in your example, the state of the attribute value is null, hence the access using the default implementation ends up providing the true state. To provide customized access to this attribute you are instead looking for an overridden API that wraps the actual state and further provides an Optional return type.

Of course, as you mentioned one of the ways to deal with it would be to have a custom implementation included in the record definition itself

record MyClass(String id, String value) {
    
    Optional<String> getValue() {
        return Optional.ofNullable(value());
    }
}

Alternatively, you could decouple the read and write APIs from the data carrier in a separate class and pass on the record instance to them for custom accesses.

The most relevant quote from JEP 384: Records that I found would be(formatting mine):

A record declares its state -- the group of variables -- and commits to an API that matches that state. This means that records give up a freedom that classes usually enjoy -- the ability to decouple a class's API from its internal representation -- but in return, records become significantly more concise.

2 of 4
6

Due to restrictions placed on records, namely that canonical constructor type needs to match accessor type, a pragmatic way to use Optional with records would be to define it as a property type:

record MyRecord (String id, Optional<String> value){
}

A point has been made that this is problematic due to the fact that null might be passed as a value to the constructor. This can be solved by forbidding such MyRecord invariants through canonical constructor:

record MyRecord(String id, Optional<String> value) {

    MyRecord(String id, Optional<String> value) {
        this.id = id;
        this.value = Objects.requireNonNull(value);
    }
}

In practice most common libraries or frameworks (e.g. Jackson, Spring) have support for recognizing Optional type and translating null into Optional.empty() automatically so whether this is an issue that needs to be tackled in your particular instance depends on context. I recommend researching support for Optional in your codebase before cluttering your code possibly unnecessary.

🌐
Ducmanhphan
ducmanhphan.github.io › 2020-02-01-Working-with-Nulls-in-Java
Working with Nulls in Java
February 1, 2020 - If Hibernate creates the table, it adds a not null constraint to the database column, the database is the one that checks if the value is not null when we insert or update a record. @NotNull is a part of the Bean Validation specification. It triggers a validation during an update or persist lifecycle event. It validates at the applicaltion level. If it fail, Hibernate will not execute any SQL statement. ... Project Lombok is a library that generates boilerplate methods such as getter/setter. Lombok works as an annotation post-processor. It reads the annotations during the complication process, ... @NonNull annnotation for parameters of methods and constructors.
🌐
Google Groups
groups.google.com › g › jooq-user › c › iN7TFQKBaJw
Generate Records with Java 8 Optionals for nullable fields
There's no workaround for the latter. SQL has NULL. Java has null. The two special values are just the best match for each other (even if they're not exactly the same).
🌐
Gunnar Morling
morling.dev › blog › enforcing-java-record-invariants-with-bean-validation
Enforcing Java Record Invariants With Bean Validation - Gunnar Morling
January 20, 2020 - manufacturer is a non-blank string · license plate is never null and has a length of 2 to 14 characters · seatCount is at least 2 · Class invariants like these are specific conditions or rules applying to the state of a class (as manifesting in its fields), which always are guaranteed to be satisfied for the lifetime of an instance of the class. The Bean Validation API defines a way for expressing and validating constraints using Java annotations.
🌐
Mike my bytes
mikemybytes.com › 2022 › 02 › 16 › java-records-and-compact-constructors
Java records & compact constructors | Mike my bytes
February 16, 2022 - For some “standard” validations (e.g. “not null”, “not blank”, “min X”) we may want to use JEE Bean Validation API (annotations) instead. You can find more details about this approach in this interesting post on Gunnar Morling’s blog. Java language maintainers had a clear vision of the new syntax responsibilities:
🌐
GitHub
github.com › FasterXML › jackson-databind › issues › 3439
Java Record `@JsonAnySetter` value is null after deserialization · Issue #3439 · FasterXML/jackson-databind
April 1, 2022 - Additional context The problem happens in com.fasterxml.jackson.databind.deser.SettableAnyProperty class in method set(...). The value of the @JsonAnySetter annotated field is null and therefore setting the property value is skipped. The suggested solution would be for Java Record to provide a new empty Map instance for the annotated field to gather the unmapped properties and this would then be provided to Record's constructor when the deserialization is concluded.
Author   FasterXML
🌐
Codementor
codementor.io › community › use records to simplify your java code
Use Records to Simplify Your Java Code | Codementor
September 20, 2024 - With this custom constructor, we ensure the record fields are not null. However, as you can notice the component list is repeated in the record header and the constructor.
Top answer
1 of 1
7

JEP 440 indeed says:

The null value does not match any record pattern.

But look carefully at what a record pattern is:

A record pattern consists of a record class type and a (possibly empty) pattern list [...]

The JEP also specified the syntax of a record pattern like this:

Pattern:
  TypePattern
  RecordPattern

TypePattern:
  LocalVariableDeclaration

RecordPattern:
  ReferenceType ( [ PatternList ] )

PatternList : 
  Pattern { , Pattern }

By these definitions, we can see that var x and var y are not record patterns. They are type patterns. The rule that "The null value does not match any record pattern" does not apply to var x and var y.

This rule does apply to the entire Point(var x, var y) pattern, which is a record pattern, so if obj is null, it will not match Point(var x, var y).

I can't find exactly where in the JEP says that type patterns in record patterns behave like this, but the preview spec does state this explicitly:

Consider, for example:

class Super {}
class Sub extends Super {}
record R(Super s) {}

We expect all non-null values of type R to match the pattern R(Super s), including the value resulting from evaluating the expression new R(null). (Even though the null value does not match the pattern Super s.) However, we would not expect this value to match the pattern R(Sub s) as the null value for the record component does not match the pattern Sub s.

The meaning of a pattern occurring in a nested pattern list is then determined with respect to the record declaration. Resolution replaces any type patterns appearing in a nested pattern list that should match all values including null with instances of the special any pattern. In our example above, the pattern R(Sub s) is resolved to the pattern R(Sub s), whereas the pattern R(Super s) is resolved to a record pattern with type R and a nested pattern list containing an any pattern.

In other words, Point(var x, var y) is resolved to Point(<any>, <any>). The <any> pattern here can match everything, including null.


Here's another example:

record Foo(Integer x, Integer y) {}

record Bar(Foo a, Foo b) {}
private static void printSum(Object obj) {
    if (obj instanceof Bar(Foo(Integer x1, Integer y1), Foo(var x2, var y2))) {
        System.out.println("matched!");
    }
}

Now, a new Bar(null, null) would not match the pattern Bar(Foo(Integer x1, Integer y1), Foo(var x2, var y2)), because Foo(Integer x1, Integer y1) is a record pattern, and null does not match it.

🌐
DEV Community
dev.to › agavrilov76 › java-records-constructor-validation-beyond-the-boilerplate-5h4i
Java Records Constructor Validation: Beyond the Boilerplate - DEV Community
September 21, 2025 - In the compact constructor, when you call validator.validate(this), the Record instance hasn't been fully constructed yet—the fields are not initialized with the parameter values. The validator sees default values (null, 0) instead of your actual parameters.
🌐
codestudy
codestudy.net › blog › default-values-for-record-properties
Java Record Properties: How to Set Default Values Instead of Null (Getters vs Constructor Override) — codestudy.net
Pick one approach per record. Document Defaults: Clearly document which fields have defaults (e.g., age: defaults to 18 if null). Combine with Validation: In constructors, validate inputs (e.g., if (age < 0) throw new IllegalArgumentException()) to enforce business rules. Java Records simplify immutable data modeling, but handling null requires intentional design.