Java 26 Primitive Types in Patterns — The Interview Trap
Java 26 ships JEP 530 — the fourth preview of primitive types in patterns — and with it comes one of the sneakiest interview topics in modern Java. You can now write instanceof int, instanceof byte, and switch on long or double directly. Seniors who have not read the JEP carefully assume this is trivial. It is not. The exactness model, the boxing behaviour, the null edge case, the dominance tightening in JEP 530 specifically — each one produces code that compiles, surprises, and explains why this feature has been in preview since JDK 23.
What the world looked like before JEP 530
Before this JEP, Java’s pattern matching was a reference-type-only party. instanceof required a reference type on the right-hand side; feeding it a primitive caused a compile error. Switch expressions accepted the primitive types that switch had always supported — int, short, byte, char, and their boxed forms, plus String and enums — but notably not long, float, double, or boolean. Pattern cases in switch only worked with reference types.
This created an awkward mismatch. You could pattern-match on Integer but not on int. You could switch on an int constant but not on a long. You needed manual range checks and explicit casts to convert an int to a byte safely, writing boilerplate that the compiler could — and as of JEP 530, does — eliminate automatically. As the JEP text explains, the prior state imposed friction with no corresponding safety benefit: if you omitted an unsafe cast against a reference type, you got a ClassCastException at runtime. If you omitted a range check before a narrowing primitive cast, you got silently wrong data with no exception at all.
This is still a preview feature in JDK 26Compile with
javac --release 26 --enable-preview MyClass.javaand run withjava --enable-preview MyClass. Without--enable-preview,instanceof intand primitive pattern cases in switch are compile errors. JEP 532 (fifth preview) has also been proposed for JDK 27, meaning GA is likely JDK 27 or JDK 28.
What instanceof int actually means: exactness
The critical concept behind JEP 530 is exactness. Classical instanceof asks: “Is this reference pointing to an object whose runtime type is compatible with this reference type?” For primitive patterns, the question is different: “Can this value be converted to the target primitive type without any loss of information?”
A conversion is unconditionally exact if it always preserves information regardless of the specific input value. Widening integral conversions fit this definition: byte to int is always exact because every representable byte value fits in an int. Narrowing conversions, by contrast, may or may not be exact depending on the actual value at runtime. int to byte is exact when the value falls in the range −128 to 127, and inexact — meaning information-losing — outside that range.
This is the heart of what JEP 530 provides: a compile-time and runtime safe narrowing path. The snippet below is the canonical example from the JEP text itself. Before JEP 530, you had to write the manual range check; with JEP 530, the same semantics are expressed as a pattern.
// Before JEP 530: manual range check required to narrow safely
if (i >= -128 && i <= 127) {
byte b = (byte) i;
// ... use b ...
}
// After JEP 530: the instanceof checks exactness at runtime for you
// The binding variable b is only in scope when the conversion was exact
if (i instanceof byte b) {
// b == (byte) i, and no information was lost — guaranteed by the pattern
System.out.println("byte-exact value: " + b);
}
// int to long is unconditionally exact — this compiles and always matches
// (because every int fits in a long, no runtime check needed)
if (i instanceof long l) {
System.out.println("always matches, l == (long) i: " + l);
}
// Object to primitive: unboxing + exactness check in one expression
// If o holds Integer(200), (o instanceof byte b) is FALSE — 200 is out of byte range
// If o holds Integer(42), (o instanceof byte b) is TRUE — 42 fits in byte
Object o = Integer.valueOf(200);
if (o instanceof byte b) {
System.out.println("never reached for 200");
} else {
System.out.println("200 does not fit in byte — pattern did not match");
}
The key insight — and the first interview trap — is that instanceof int against a variable already typed as int is an unconditionally exact identity conversion. It always matches and is essentially a no-op check. But instanceof byte against an int is a value-dependent check that the runtime evaluates on every execution. These look syntactically parallel but behave very differently.
The exactness table: what the compiler allows and what it rejects
JEP 530 introduces a precise table of which primitive type conversions are permitted in pattern contexts. This table — derived directly from the JEP specification — is what interviewers test against, often without showing it to candidates first.
| Source type | Pattern type: byte | Pattern type: short | Pattern type: int | Pattern type: long | Pattern type: float | Pattern type: double |
|---|---|---|---|---|---|---|
| byte | Identity ε | Exact ε | Exact ε | Exact ε | Exact ε | Exact ε |
| short | Value-dep. η | Identity ε | Exact ε | Exact ε | Exact ε | Exact ε |
| int | Value-dep. η | Value-dep. η | Identity ε | Exact ε | Value-dep. η | Exact ε |
| long | Value-dep. η | Value-dep. η | Value-dep. η | Identity ε | Value-dep. η | Value-dep. η |
| float | Value-dep. η | Value-dep. η | Value-dep. η | Value-dep. η | Identity ε | Exact ε |
| double | Value-dep. η | Value-dep. η | Value-dep. η | Value-dep. η | Value-dep. η | Identity ε |
Two entries in this table are the source of most interview confusion. First, int to float is value-dependent, not unconditionally exact — even though float can represent a wider range of magnitudes than int. This surprises candidates who associate “wider type” with “always safe.” The reason is that float has only 23 bits of mantissa, whereas int has 32 bits of precision. Large integers like 200_000_007 cannot be represented exactly in a float, so the conversion is not exact for those values. Second, int to double is unconditionally exact, because double has 52 bits of mantissa — enough to represent every 32-bit integer precisely.
How boxing changes at the bytecode level
When you write instanceof int against a reference type (an Object, say), the JVM must first check the runtime type of the reference, unbox it if it is the right wrapper, and then check whether the unboxed value satisfies the exactness condition. This is meaningfully different from classic instanceof Integer, and understanding the difference is what separates a senior who has thought about it from one who has not.
With classic instanceof Integer, the bytecode is a single instanceof instruction followed by a checkcast when the binding variable is used. The entire check is “is this reference pointing to an Integer object?” With instanceof int b against an Object, the compiled output does more: it checks that the reference is an Integer, unboxes it via Integer.intValue(), and — for value-dependent narrowing like instanceof byte b against an Integer — additionally emits a range check. The binding variable is only bound if every step succeeds.
// Source: what you write
// Requires --enable-preview on JDK 26
Object o = Integer.valueOf(42);
if (o instanceof byte b) {
System.out.println("fits in byte: " + b);
}
// Conceptual desugaring — what the compiler effectively does:
// 1. Check o is an Integer (reference instanceof check)
// 2. Unbox: int temp = ((Integer) o).intValue()
// 3. Range check for byte exactness: temp >= -128 && temp <= 127
// 4. Only if all three pass: byte b = (byte) temp
// and execute the branch body
//
// For 42: step 3 passes → b = 42, branch executes
// For 200: step 3 fails → branch not taken, no exception thrown
// The critical NULL behaviour: if o is null, step 1 fails silently.
// (o instanceof byte b) returns false for null — no NullPointerException.
// This matches standard instanceof semantics for reference types.
Object nullRef = null;
if (nullRef instanceof byte b) { // false — no NPE
System.out.println("never reached");
}
Gotcha #1
int instanceof float: the widening that isn’t always exact
The most common senior-level mistake is assuming that widening to a floating-point type is always exact because floats can represent “bigger” values. They cannot represent more distinct integer values. int i = 200_000_007: the nearest representable float is 200000000.0f, which is not equal to the original integer. Therefore i instanceof float f returns false for this value — even though the cast (float) 200_000_007 succeeds and produces a value. The pattern is stricter than the cast: it only matches when the conversion is lossless.
The interview trap is code like: int i = 200_000_007; System.out.println(i instanceof float f); — the answer is false, and candidates who answer true (because float is “wider”) reveal that they have not understood exactness.
Gotcha #2
Tighter dominance in JEP 530: switch constructs that compiled in JDK 25 now reject in JDK 26
JEP 530 introduced tighter dominance checks compared to JEP 507 (JDK 25). Dominance means that a wider pattern case must not appear before a narrower one that it would swallow entirely — the compiler rejects switch blocks where an earlier case makes a later case unreachable. JEP 530’s enhancement means that some switches that were legal in JDK 25 with --enable-preview are now compile errors in JDK 26.
Specifically, an unconditionally exact wider pattern now dominates a narrower constant or pattern case. In the example below, case int i dominates case 42 when the selector is of type int, because the int pattern is always exact and therefore always matches — leaving the constant case dead.
// This compiled in JDK 25 with --enable-preview.
// It is a compile-time ERROR in JDK 26 due to tighter dominance checks.
// "case 42" is dominated by "case int i" — it can never be reached.
int x = getStatus();
String result = switch (x) {
case int i when i < 0 -> "negative";
case int i -> "non-negative: " + i; // ? dominates everything below
case 42 -> "the answer"; // ? COMPILE ERROR in JDK 26
};
// Fix: place specific constant cases BEFORE the catch-all type pattern
String fixed = switch (x) {
case int i when i < 0 -> "negative";
case 42 -> "the answer"; // constant before the catch-all — correct
case int i -> "other: " + i;
};
Gotcha #3
Null and primitives: the asymmetry that catches everyone once
When the selector of a switch or the left-hand side of an instanceof is a reference type, null never matches a primitive pattern — null instanceof int is false, consistent with how null instanceof Integer is false. No NullPointerException is thrown, and no match occurs. This is the safe and expected behaviour.
The trap is in switch. When you switch on a reference-typed expression using primitive pattern cases, and the value is null, the switch falls through to the default case — if one exists. If no default and no case null is present, a NullPointerException is thrown, exactly as it was before JEP 530. The mistake seniors make is assuming that because primitive patterns handle null gracefully in instanceof, they also do so in switch. They do not. Switch’s null-hostility is unchanged unless you explicitly add case null.
// o is a reference type — could be Integer, null, or something else
Object o = getValueFromSomewhere(); // returns null at runtime
// instanceof handles null safely — always false, never NPE
if (o instanceof int i) {
System.out.println(i); // not reached when o is null
}
// Switch WITHOUT case null — NullPointerException when o is null!
switch (o) {
case int i -> System.out.println("int: " + i);
default -> System.out.println("other");
// ^ default does NOT catch null — throws NPE when o is null
}
// Correct: add case null explicitly to handle null gracefully
switch (o) {
case null -> System.out.println("null value"); // must come before type patterns
case int i -> System.out.println("int: " + i);
default -> System.out.println("other reference type");
}
Conversion exactness across primitive pairs — which instanceof checks can fail at runtime

Five interview questions — with annotated model answers
Interview Question 1 — Warm-up
What does 42 instanceof int evaluate to in Java 26 with preview enabled? What about 42 instanceof byte?
Model Answer 42 instanceof int evaluates to true. The conversion from int to int is the identity conversion — unconditionally exact by definition. The pattern always matches any int value.
42 instanceof byte also evaluates to true, because 42 is in the range −128 to 127 — the exact range of byte. The runtime performs a value-dependent check and finds that 42 fits. If the value were 200, the result would be false — 200 is outside the byte range, so the conversion is not exact for that value.
The trap: candidates who say “you can’t use instanceof with primitives” are citing pre-JEP 530 behaviour. Candidates who say both always return true are missing the value-dependency of narrowing patterns. The correct answer demonstrates understanding of the two distinct modes: unconditional exactness (identity and widening) vs. value-dependent exactness (narrowing).
Interview Question 2 — The float trap
What does the following print? int i = 200_000_007; System.out.println(i instanceof float f);
Common wrong answer: true, because float can hold larger values than int.
Correct answer: false. This is gotcha #1. float has only 23 bits of mantissa (plus 1 implicit), giving it roughly 7 significant decimal digits of precision. 200_000_007 has 9 significant digits. The nearest representable float is 200_000_000.0f — and since (int)(200_000_000.0f) != 200_000_007, the exactness check fails and the pattern does not match.
Follow-up: i instanceof double d for the same value prints true, because double‘s 52-bit mantissa can represent every 32-bit integer exactly. This asymmetry between float and double is the precise reason the JEP table marks int→float as value-dependent while marking int→double as unconditionally exact.
Interview Question 3 — Null behaviour
What happens when you pass null to this switch? Does it throw? Does it match a case?switch (obj) { case int i -> "int: " + i; default -> "other"; }
Common wrong answer: it matches the default case and prints “other”.
Correct answer: it throws NullPointerException. The default case in a pattern switch does not catch null. This is gotcha #3. Switch’s historical null-hostility is unchanged by JEP 530. When obj is null and there is no explicit case null, the JVM throws NPE before evaluating any case label.
Scoring distinction: a senior who answers “NPE” without hesitation passes. A senior who confidently says “default catches it” reveals they have conflated instanceof null-safety (which does return false for null without NPE) with switch null-safety (which does not). A candidate who correctly adds case null -> ... to fix it earns bonus points for knowing the idiom.
Interview Question 4 — Dominance (JEP 530 specific)
This code compiled with --enable-preview on JDK 25. Does it still compile on JDK 26?switch (x) { case int i -> "any int"; case 42 -> "the answer"; }
Common wrong answer: yes, it still compiles — switch handles the specific constant first.
Correct answer: no, it is a compile-time error in JDK 26. This is gotcha #2. JEP 530 introduced tighter dominance checks compared to JEP 507. case int i with no guard is an unconditionally exact pattern — it matches every int value. Because it appears before case 42, it dominates the constant case, making case 42 unreachable. JDK 26 rejects this at compile time, whereas JDK 25 accepted it (the tighter check is one of JEP 530’s two specific changes over its predecessor).
The fix is to place the specific constant before the type pattern: case 42 -> "the answer"; case int i -> "any int";.
Interview Question 5 — Bytecode and semantics
What is the difference between obj instanceof Integer i and obj instanceof int i when obj holds Integer.valueOf(300)?
Model Answer: both expressions match when obj holds Integer.valueOf(300), but the binding variable types and what the compiler generates are different.
obj instanceof Integer i is a reference pattern. It checks whether obj‘s runtime type is Integer (or a subtype), and if so binds i as type Integer. No unboxing occurs; i is still a boxed reference. A single instanceof bytecode instruction does the work.
obj instanceof int i is a primitive pattern. The compiler conceptually checks: (1) is obj an Integer? (2) unbox it to get a temporary int; (3) since int→int is the identity conversion, skip the range check. Bind i as type int with the unboxed value. The binding variable is an int, not an Integer. For the same value 300, both patterns match, but if obj instanceof byte b were used instead, it would not match because 300 is outside byte range — something that obj instanceof Integer i plus a manual cast could not express safely without explicit range checking.
The trap variation: what if obj holds a Long? obj instanceof Integer i returns false (Long is not an Integer). obj instanceof int i also returns false — the unboxing path expects an Integer, not a Long. Neither throws; both simply do not match.
What we learned
JEP 530 in Java 26 is a fourth preview of a feature that has been carefully refined across four JDK releases precisely because its semantics are non-trivial. The core concept — exactness — is what makes primitive patterns safe where explicit casts were not: a pattern like instanceof byte b only binds the variable when the conversion is lossless, giving you compile-time safety against forgotten range checks. Unconditionally exact conversions (byte→int, int→long, int→double) always match. Value-dependent conversions (int→byte, int→float, long→int) may or may not match depending on the runtime value.
The three gotchas that consistently trip senior engineers in interviews are: (1) int instanceof float can return false because float lacks the precision to represent all 32-bit integer values exactly — the float’s wider range does not imply wider precision; (2) JEP 530 introduced tighter dominance checks that make some switch expressions from JDK 25 compile-time errors in JDK 26 — specifically, an unguarded type pattern now dominates constant cases that follow it; and (3) switch’s null-hostility is unchanged — default does not catch null, only an explicit case null label does, and NullPointerException is thrown for any null selector without one. Knowing these three points, and being able to explain them against the JEP text, is what distinguishes engineers who have genuinely worked with the feature from those who have only heard about it.


