Java 25 — Primitive Types in Patterns & instanceof
Java 25 finalizes primitive type patterns — use int, long, double, and boolean in instanceof checks and switch expressions. Practical examples and migration tips.
Java 25 finalizes primitive type patterns (JEP 488). You can now use int, long, double, boolean, and other primitive types in instanceof and switch expressions — just like reference types.
This was preview in Java 23 and 24. It’s stable in 25.
The problem before Java 25
Before this feature, instanceof only worked with reference types:
// This worked
if (obj instanceof String s) {
System.out.println(s.length());
}
// This did NOT compile before Java 25
if (obj instanceof int i) {
System.out.println(i + 1);
}
You also couldn’t use primitives in switch pattern matching. If you had a Number and wanted to branch on whether it was an int or long, you had to use wrapper types and manual casting.
Primitive patterns in instanceof
Now you can pattern match directly to primitive types:
Object value = 42;
if (value instanceof int i) {
System.out.println("It's an int: " + i);
} else if (value instanceof long l) {
System.out.println("It's a long: " + l);
} else if (value instanceof double d) {
System.out.println("It's a double: " + d);
}
The compiler handles unboxing automatically. Integer matches int, Long matches long, and so on.
Safe narrowing conversions
Primitive patterns also handle narrowing — but only when the value fits:
long big = 42L;
if (big instanceof int i) {
// Only matches if the long value fits in an int
System.out.println("Fits in int: " + i);
}
long tooBig = 3_000_000_000L;
if (tooBig instanceof int i) {
// Does NOT match — value exceeds Integer.MAX_VALUE
System.out.println("This won't print");
} else {
System.out.println("Value doesn't fit in int");
}
This replaces manual range-checking code:
// Before Java 25
if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) {
int i = (int) longValue;
// use i
}
// Java 25
if (longValue instanceof int i) {
// use i — guaranteed to fit
}
Cleaner and less error-prone.
Primitive patterns in switch
The bigger win is switch. You can now switch on any primitive type with patterns:
Switching on long
long statusCode = getStatusCode();
String message = switch (statusCode) {
case 200L -> "OK";
case 301L -> "Moved Permanently";
case 404L -> "Not Found";
case 500L -> "Internal Server Error";
default -> "Unknown: " + statusCode;
};
Before Java 25, switch didn’t support long at all. You had to use if-else chains.
Switching on boolean
boolean enabled = isFeatureEnabled();
String label = switch (enabled) {
case true -> "Feature ON";
case false -> "Feature OFF";
};
Yes, if-else works here too. But in pattern-heavy code, consistency matters.
Switching on float and double
double score = calculateScore();
String grade = switch (score) {
case double d when d >= 90.0 -> "A";
case double d when d >= 80.0 -> "B";
case double d when d >= 70.0 -> "C";
case double d when d >= 60.0 -> "D";
default -> "F";
};
The when guard combines with the primitive pattern to give you range matching.
Combining with record patterns
Primitive patterns compose naturally with record patterns from Java 21:
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
};
}
The double r in Circle(double r) is a primitive pattern inside a record pattern. Before Java 25, you had to use var or reference types here.
Nested extraction with guards
record Measurement(String unit, double value) {}
String classify(Measurement m) {
return switch (m) {
case Measurement(String u, double v) when u.equals("celsius") && v > 37.5 ->
"Fever";
case Measurement(String u, double v) when u.equals("celsius") && v >= 36.0 ->
"Normal";
case Measurement(String u, double v) when u.equals("celsius") ->
"Hypothermia";
case Measurement(String u, double v) ->
"Unknown unit: " + u + " = " + v;
};
}
Practical example: JSON-like data processing
When working with untyped data (JSON parsing, dynamic configs), primitive patterns clean up type dispatching:
Object jsonValue = parseValue(input);
String formatted = switch (jsonValue) {
case null -> "null";
case int i -> String.valueOf(i);
case long l -> String.valueOf(l);
case double d -> String.format("%.2f", d);
case boolean b -> b ? "true" : "false";
case String s -> "\"" + s + "\"";
default -> jsonValue.toString();
};
Before Java 25, you’d need Integer, Long, Double, Boolean wrapper types here. The primitive patterns handle the unboxing.
Exhaustiveness checking
The compiler checks exhaustiveness for primitive switches when combined with sealed types. For standalone primitive switches, you still need a default case since primitive types have too many possible values.
// Compiler requires default — int has ~4 billion values
int code = getCode();
String result = switch (code) {
case 0 -> "zero";
case 1 -> "one";
default -> "other";
};
Migration tips
Start with switch on long and boolean. These were impossible before and remove the most if-else chains.
Use primitive record patterns in existing switches. If you already use record patterns with var, switch to explicit primitive types for clarity.
Replace manual range checks with instanceof. The long instanceof int pattern is safer than manual boundary comparisons.
Don’t overuse boolean switch. For a simple true/false branch, if-else is still more readable. Use boolean switch when it’s part of a larger pattern-matching block.
Before and after
Parsing a config value
// Before Java 25
Object raw = config.get("timeout");
int timeout;
if (raw instanceof Integer i) {
timeout = i;
} else if (raw instanceof Long l) {
if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) {
timeout = l.intValue();
} else {
throw new IllegalArgumentException("Timeout too large");
}
} else if (raw instanceof String s) {
timeout = Integer.parseInt(s);
} else {
throw new IllegalArgumentException("Invalid timeout type");
}
// Java 25
Object raw = config.get("timeout");
int timeout = switch (raw) {
case int i -> i;
case long l when l instanceof int i -> i;
case long l -> throw new IllegalArgumentException("Timeout too large: " + l);
case String s -> Integer.parseInt(s);
case null -> throw new IllegalArgumentException("Timeout is null");
default -> throw new IllegalArgumentException("Invalid timeout type: " + raw.getClass());
};
Primitive patterns in Java 25 fill the last major gap in pattern matching. Combined with sealed types and record patterns from Java 21, you now have a complete pattern-matching system.