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:
- Hidden boolean.
- 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 OverflowI 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:
- Hidden boolean.
- 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.
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.
[BUG] javax Nonnull on Record Fails to Create Null-Check
java - Avoiding null attributes in records - Stack Overflow
option type - Java records with nullable components - Stack Overflow
Nullable not detected for Java records when ParametersAreNonnullByDefault is set on declaration
Videos
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.
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.
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.
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.
public record ServiceMaintenancePriceMatrixRequest(
@NotBlank String name,
Optional<@FutureOrPresent LocalDate> validFrom,
Optional<LocalDate> validTo) {}Is it a good practice to have optional record fields? Or just remove optional and have null checks?