Question about type pattern inside record pattern
Brian Goetz
brian.goetz at oracle.com
Sun Feb 8 17:47:19 UTC 2026
There are two things here: dominance and null handling, Let’s take them separately.
Question 1 is about dominance. This is indeed confusing and I had to remind myself of why this compiles. The reason is that our dominance computation does not currently take into account the type of the selector, and so is excessively conservative in computing when cases are dead. For example:
switch (anObject) {
case Number n -> …
case Integer n -> …. // compile error, dominated case
}
disallows the last case because `Number n` dominates `Integer n`, but this is solely based on the two types involved, not the selector. Similarly, `Box(Number)` would dominate `Box(Integer)` for the same reason. But we don’t see that `Amount(Number)` covers all cases that would be covered by `Amount(Object)` because we don’t currently take into account the type of the selector. This is a known issue and something that we’re working through. (Exhaustiveness is hard; no language that I’ve seen has gotten it right on the first try, so I’m not too surprised.)
Question 2 is slightly misstated, but I think I know what you mean: you are asking why `case Amount(Number)` matches Amount(null), not matches null itself, right? The treatment of nullity in patterns is tricky. There are two intuitions that might help; some people are more compelled by one than the others, so I’ll give them both here. (Note to all: this is an explanation, not an invitation to reopen the topic, which was extensively debated, multiple times.)
Explanation #1: Patterns match null, but contexts get first crack at the candidate.
In this explanation, we see pattern-using constructs like `instanceof`, `switch`, etc, are in control of the operational semantics, and may or may not delegate to the pattern if it sees fit. A type pattern `Foo f` matches null, but the pattern may not always be consulted. Construct-specific rules include:
- instanceof always says `false` on null, doesn’t bother to consult the pattern
- switch always throws NPE on null, unless there is a `case null`, in which case that case is selected
- nested pattern contexts evaluate the pattern when the subpattern itself is exhaustive, but fail to match on null when the sub pattern is not exhaustive
Explanation #2: It’s about nullity inference.
In this explanation, we have non-denotable patterns `Foo! f` and `Foo? f` which have the obvious semantics, and for a type pattern `Foo f` we _infer_ which of these it really means, based on context:
- For top level in switch and instanceof, we infer `Foo! f`
- For nested context when the sub pattern is not exhaustive, we infer `Foo! f`
- For nested context when the sub pattern is exhaustive, we infer `Foo? f`
The two come out the same, but some people prefer one explanation to the other. But the motivating example why we picked these rules is that they actually make sense. Consider:
record Box(Object o) { }
switch (box) {
case Box(Chocolate c):
case Box(Frog f):
case Box(Object o): ….
}
Which, if any, should Box(null) match? There are three options:
- It matches the first one, because null matches all type patterns
- It matches none of them, because null matches no type patterns
- It matches the last one, because that is actually the only sensible thing to do :)
The first is worse than useless; the user who wrote `case Box(Chocolate)` is asking “does this box contain chocolate”, and will almost certainly be astonished when they find the box contains null. It will forever be a source of bugs, and even though Chocolate and Frog seem disjoint, reordering these two cases would not be safe. This is a lifetime subscription to bugs, so we can’t do this.
The second is even worse, leading to an unusable language. As much as some people hate nulls and seek at every opportunity to exclude them, `new Box(null)` is an _entirely valid instance of Box_. Excluding this “because we hate nulls” means that in order to cover the case of “any Box, including one that contains null” requires treating `case Box(Object o)` and `case Box(null)` separately. If Box has three components, this is eight cases! The resulting feature is basically unusable in a world where nulls are real. (I realize some people would like to live in the other world, but we don’t.).
The third is the only realistic option, and in reality makes a lot of sense after you think about it for a bit, but is indeed surprising at first. When we say `case Box(Chocolate)`, we’re saying “only the boxes that contain chocolate”; that’s an intrinsically partial query. But when we say `Box(Object x)` or `Box(var x)`, we are saying, in effect, “box containing any valid data”, and _null is valid data_.
Some people had a really hard time with this, though, and bargained endlessly with this reality. Among the terrible ideas proffered were treating `case Box(Object x)` and `case Box(var x)` differently (which of course violated the principle that `var` should mean exclusively type inference, not something else.) But this is all bargaining with the essential complexity of a language that has null, which Java does.
(Note that if Java ever has explicit nullity, then all this goes away because you can say `Bar! b` or `Bar? b`, and there is no ambiguity. In the meantime, we infer the thing that makes most sense given the context, which varies by syntactic location and exhaustiveness.)
All of which is to say: the reality is complicated, and the “easy” answers do not very well match the reality we live in. And also: some people hate null so much they will embrace terrible ideas to try to keep it in its “box”.
> On Feb 6, 2026, at 5:00 PM, Cay Horstmann <cay.horstmann at gmail.com> wrote:
>
> Consider this program:
>
> record Amount(Number n) {}
>
> Integer value(Amount p) {
> return switch (p) {
> case Amount(Integer value) -> value;
> case Amount(Number _) -> -1;
> case Amount(Object _) -> -2;
> };
> }
>
> void main() {
> IO.println(value(new Amount(null)));
> }
>
> It prints -1.
>
> I have two questions:
>
> 1. Why does it compile? The case Amount(Object _) does not seem to reachable.
> 2. Why does null match case Amount(Number _) and not one of the other?
>
> I tried applying JLS (for Java 25) §14.30, §15.28, and §14.11 but could not figure it out. Where should I look?
>
> Thanks,
>
> Cay
>
> --
>
> Cay S. Horstmann | https://horstmann.com
>
More information about the amber-dev
mailing list