null, and designing NullPointerException out
NullPointerException is the most common exception in Java by a wide margin. Its
inventor called null his "billion-dollar mistake", which is the sort of thing you
can say once you have retired.
This lesson is not about catching it. Catching a NullPointerException is
almost always wrong — it means something was null that your design did not
expect, and the fix belongs where the null came from. This lesson is about
designing so that it cannot happen.
Where nulls come from
Only four places, and knowing them tells you where to defend:
| Source | Example |
|---|---|
| A missing map entry | map.get(absentKey) |
| A method that returns null on failure | An old API, a JSON field that was absent |
| An uninitialised field | Declared, never assigned, read early |
An explicit null you passed |
Usually as a placeholder or a "no value" flag |
Everything else — arrays, locals, parameters — is null because one of those four put it there.
Defend at the boundary, not everywhere
The instinct after being bitten is to check everything:
if (customer != null && customer.getAddress() != null
&& customer.getAddress().getPincode() != null) { ... }
That is unreadable, and it does not actually fix anything: it just decides, silently, to do nothing when the data is wrong.
The alternative is to make null impossible past one point:
record Subscriber(String name, String pincode) {
Subscriber {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(pincode, "pincode must not be null");
}
}
name must not be null
Now every Subscriber in the system has a non-null name, and no code
downstream needs to check. One assertion at construction replaces a hundred
checks.
Objects.requireNonNull(x, "message") throws NullPointerException immediately,
with your message, at the line where the null arrived — rather than three method
calls later where it is used.
That is the principle: fail fast, at the boundary, with a message naming the field. The boundary is wherever data enters your code — a constructor, a parser, a controller.
The null-safe idioms
Worth memorising; each one removes a check.
| Instead of | Write | Gives |
|---|---|---|
s.equals("veg") |
"veg".equals(s) |
false, never throws |
a.equals(b) with both uncertain |
Objects.equals(a, b) |
false, never throws |
s.toString() |
String.valueOf(s) |
"null" |
if (s == null) s = "veg" |
Objects.requireNonNullElse(s, "veg") |
the default |
map.get(k) then a check |
map.getOrDefault(k, fallback) |
the fallback |
if (s == null) |
Objects.isNull(s) / nonNull(s) |
reads better in a stream |
| returning null | returning Optional or an empty collection |
nothing to check |
literal first : false
Objects.equals: false
String.valueOf: null
concat is safe: plan: null
requireNonNullElse: veg
One that surprises people: string concatenation is null-safe.
"plan: " + null gives "plan: null" rather than throwing. Useful in log
messages, and a reason a null can travel a long way before anything notices.
Return empty, not null
static List<String> stopsFor(String area) {
return ROUTES.getOrDefault(area, List.of());
}
2 stops for a known area
0 stops for an unknown one
A method returning a collection should never return null. Return an empty one. The caller then writes a loop that runs zero times instead of a null check, and the common case and the empty case are the same code.
The same applies to arrays — return new String[0] — and to strings, where an
empty string is usually better than null.
For a single value that may be absent, return Optional, as module 6 covered.
Never return Optional for a collection, because empty already says it.
Unboxing: the null you did not see
Map<String, Integer> counts = new HashMap<>();
int n = counts.get("Priya");
Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
Nothing on that line looks like it could throw. The compiler inserted
.intValue() to convert Integer to int, and that call is what failed.
Any assignment from a wrapper to a primitive is a hidden null check. The same
trap appears in arithmetic (total + map.get(k)), in ternaries mixing Integer
and int, and in for (int x : listOfIntegers).
The fix is getOrDefault, or keeping the variable as Integer and deciding what
null means.
switch on null
String plan = null;
String r = switch (plan) {
case "veg" -> "a";
default -> "b";
};
old-style switch on null threw NullPointerException
The default does not save you. Since Java 21, pattern-matching switch lets you
handle it explicitly:
String safe = switch (plan) {
case null -> "no plan chosen";
case "veg" -> "a";
default -> "b";
};
with case null: no plan chosen
case null is better than a null check three lines above, because it is
inside the construct that needs it and cannot be separated from it by a later
edit.
Nulls in collections
List<String> withNull = new ArrayList<>(Arrays.asList("Priya", null));
withNull.stream().map(String::toUpperCase).toList();
ArrayList allows it: [Priya, null]
List.of rejects it
and it blows up downstream: null
ArrayList and HashMap allow nulls; List.of, Set.of and Map.of reject
them at construction. Prefer the factories — failing where the null was
introduced is far better than failing in a stream two hundred lines away, where
the message is just null.
Note that last message. Not every NullPointerException gets a helpful
description; when the null is inside a data structure rather than on the
current line, you are back to reading the stack trace.
When a null is the right answer
Being fair to it:
- At a boundary with an API that expects null.
optional.orElse(null)handed to an older library is correct. - As a sentinel inside a tightly scoped algorithm where the meaning is obvious and the scope is ten lines.
- In a field that genuinely has no value yet, where
Optionalas a field is worse — which module 6 argued it is.
Outside those, null is a decision not to say what you mean.
A checklist
- Validate parameters at constructors and public entry points with
Objects.requireNonNull. - Never return null from a method returning a collection, array or string.
- Return
Optionalfor a single value that may be absent. - Use
getOrDefaultrather thangeton maps. - Put the literal first in
equals, or useObjects.equals. - Use
List.ofand friends so nulls are rejected at the source. - Handle
case nullin a pattern switch rather than checking beforehand. - Do not catch
NullPointerException. Fix where the null came from.
Check your work
Why is catching NullPointerException almost always wrong? It means the
design allowed a null somewhere it was not expected. The fix belongs where the
null was produced.
What are the four sources of null? A missing map entry; a method that returns null on failure; an uninitialised field; an explicit null you passed.
What does Objects.requireNonNull(x, "msg") buy you? Failure at the line the
null arrived, with your message, instead of somewhere downstream with none.
Is "plan: " + null an error? No — string concatenation is null-safe and
produces "plan: null". Which is why a null can travel a long way unnoticed.
What should a method returning a collection do when there is nothing? Return an empty collection, never null. The caller's loop then runs zero times.
Why does int n = map.get(k) throw for a missing key? The compiler inserts
.intValue() to unbox, and that call is made on null. The message names
intValue() even though you never wrote it.
Does a default branch protect a switch from a null selector? No. Only
case null in a pattern switch handles it.
Which collection factories reject nulls? List.of, Set.of, Map.of.
ArrayList and HashMap accept them.
Practice 3, the two failures. int n = counts.get("Priya") on an empty map
gives Cannot invoke "java.lang.Integer.intValue()" because the return value of
"java.util.Map.get(Object)" is null — which names the cause exactly.
withNull.stream().map(String::toUpperCase) gives an NPE whose message is just
null, because the null is inside the collection rather than on the failing
line. The first is a five-second fix; the second needs the stack trace. That is
the argument for List.of.
Practice 5, the chain of checks. The nested != null version and the
requireNonNull-in-the-constructor version handle the same bad input. The
difference is that the first silently does nothing and leaves the bad data in the
system, while the second rejects it at the door and names the field. Only one of
them leaves the rest of the codebase able to assume the data is good.
Practice
-
Use every idiom. Write one line each for
"veg".equals(s),Objects.equals,String.valueOf,requireNonNullElse,getOrDefaultandObjects.isNull, all with a null input. Confirm none of them throw. -
Fail fast. Add
Objects.requireNonNullwith messages to a record's compact constructor and try to build an invalid one. Read the message. -
Cause the two NPE shapes.
int n = emptyMap.get(k)for the helpful message, and a stream over a list containing null for the unhelpful one. Compare what each tells you. -
Return empty, not null. Write
stopsFor(area)returning null for unknown areas, then have a caller loop over the result. Fix it to returnList.of()and delete the caller's check. -
Compare two defences. Write the nested
!= nullchain for a three-level object graph, then rewrite withrequireNonNullat construction. Feed both the same bad input and describe what the system knows afterwards. -
Harder — make a whole flow null-free. Take the delivery-row parser from the custom exceptions lesson. Make it impossible for a
Rowwith a null field to exist; make every lookup return an empty collection or anOptional; usecase nullwhere a switch needs it. Then deliberately feed it a CSV with empty columns and confirm that you get validation messages naming the fields rather than a singleNullPointerExceptionfrom somewhere in the middle.
That is module seven. You can read a stack trace in seconds, tell checked from unchecked and argue about why, write an exception whose message someone can act on, release resources without losing the exception that mattered, and design so that null does not get in.
Next module: files, dates and JSON — where every one of these failures happens for real.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship