Primitive Types in Patterns (JEP 507): Pattern Match Every Type
Note: JEP 507 is a preview feature in Java 25 (3rd preview). Enable it with
--enable-previewat compile and runtime. It is expected to finalize in Java 26 or 27.
The Asymmetry in Pattern Matching
Java 21 gave us pattern matching for switch and instanceof. It is a great feature — but it had a glaring asymmetry: it only worked with reference types.
Object obj = 42;
// Java 21: works fine
if (obj instanceof Integer i) {
System.out.println("Integer: " + i);
}
// Java 21: does NOT work
int value = 42;
if (value instanceof int i) { // ❌ compile error
System.out.println(i);
}
This made primitive-heavy code (numerical processing, protocol parsers, financial calculations) awkward. You either boxed everything into wrapper types (expensive) or used classic if/else if chains.
JEP 507: Primitives in All Pattern Contexts
JEP 507 closes this gap. In Java 25 (with --enable-preview), all primitive types can appear in patterns: byte, short, int, long, float, double, char, and boolean.
Primitive instanceof
// Java 25 preview
Object sensor = 98.6;
if (sensor instanceof double temp) {
System.out.println("Temperature reading: " + temp + "°F");
}
if (sensor instanceof float f) {
System.out.println("Float precision: " + f);
}
This also works in the other direction — matching a primitive value against a type:
int statusCode = 200;
if (statusCode instanceof int code) {
System.out.println("HTTP status: " + code);
}
Primitive switch Patterns
The biggest win is in switch. Before, switch on a numeric value used constants. Now it can use type patterns and guard patterns:
Before Java 25 (classic numeric switch)
int httpStatus = 404;
String message = switch (httpStatus) {
case 200 -> "OK";
case 201 -> "Created";
case 400 -> "Bad Request";
case 401 -> "Unauthorized";
case 403 -> "Forbidden";
case 404 -> "Not Found";
case 500 -> "Internal Server Error";
default -> "Unknown status: " + httpStatus;
};
This is fine for exact values — but what about ranges?
Before Java 25 (range checks — awkward)
String category;
if (httpStatus >= 100 && httpStatus < 200) category = "Informational";
else if (httpStatus >= 200 && httpStatus < 300) category = "Success";
else if (httpStatus >= 300 && httpStatus < 400) category = "Redirection";
else if (httpStatus >= 400 && httpStatus < 500) category = "Client Error";
else if (httpStatus >= 500 && httpStatus < 600) category = "Server Error";
else category = "Invalid";
After Java 25 — Guard Patterns on Primitives
int httpStatus = 404;
String category = switch (httpStatus) {
case int s when s >= 100 && s < 200 -> "Informational";
case int s when s >= 200 && s < 300 -> "Success";
case int s when s >= 300 && s < 400 -> "Redirection";
case int s when s >= 400 && s < 500 -> "Client Error";
case int s when s >= 500 && s < 600 -> "Server Error";
default -> "Invalid status code: " + httpStatus;
};
System.out.println(httpStatus + " → " + category);
// 404 → Client Error
Readable range dispatch with when guards — no if/else chain.
Mixed Primitive and Reference Patterns
When the switch selector is Object (or a sealed type that includes primitives via boxing), you can match both primitives and reference types in the same switch:
Object measurement = getSensorReading(); // returns Integer, Double, String, or null
String result = switch (measurement) {
case int i when i < 0 -> "Negative integer: " + i;
case int i when i == 0 -> "Zero";
case int i -> "Positive integer: " + i;
case double d when d < 0.0 -> "Negative double: " + d;
case double d -> "Double: " + d;
case String s when s.isBlank() -> "Empty string";
case String s -> "String: " + s;
case null -> "No reading";
default -> "Unknown: " + measurement;
};
Type Narrowing with Primitives
JEP 507 also supports narrowing patterns — matching a long value as an int when the value fits:
long bigNumber = 12345L;
String result = switch (bigNumber) {
case int i -> "Fits in int: " + i; // matches if value fits in int range
case long l -> "Needs long: " + l; // matches otherwise
};
System.out.println(result);
// Fits in int: 12345
And widening works too:
int value = 100;
// int can match against long pattern (widening)
String desc = switch (value) {
case long l when l > 1000 -> "large";
case long l -> "small: " + l;
};
Real-World Example: Sensor Data Parser
import module java.base;
sealed interface SensorReading permits TemperatureReading, PressureReading, ErrorReading {}
record TemperatureReading(double celsius) implements SensorReading {}
record PressureReading(int pascals) implements SensorReading {}
record ErrorReading(int code, String message) implements SensorReading {}
void processSensor(SensorReading reading) {
switch (reading) {
case TemperatureReading(double t) when t > 100.0 ->
System.out.println("ALERT: High temp " + t + "°C");
case TemperatureReading(double t) ->
System.out.println("Temp OK: " + t + "°C");
case PressureReading(int p) when p > 101325 ->
System.out.printf("High pressure: %,d Pa%n", p);
case PressureReading(int p) ->
System.out.printf("Normal pressure: %,d Pa%n", p);
case ErrorReading(int code, String msg) when code >= 500 ->
System.out.println("CRITICAL ERROR " + code + ": " + msg);
case ErrorReading(int code, String msg) ->
System.out.println("Error " + code + ": " + msg);
}
}
void main() {
processSensor(new TemperatureReading(23.5));
processSensor(new TemperatureReading(105.2));
processSensor(new PressureReading(101325));
processSensor(new ErrorReading(503, "Service Unavailable"));
}
Output:
Temp OK: 23.5°C
ALERT: High temp 105.2°C
Normal pressure: 101,325 Pa
CRITICAL ERROR 503: Service Unavailable
The int code and double celsius patterns in the record deconstruction are primitive patterns — enabled by JEP 507.
boolean Patterns
boolean works too, which enables switch on boolean values (not just if/else):
boolean featureEnabled = true;
String status = switch (featureEnabled) {
case true -> "Feature is ON";
case false -> "Feature is OFF";
};
This is more useful with guard patterns:
boolean isReady = checkSystem();
switch (isReady) {
case boolean b when b -> startProcessing();
case boolean b -> System.out.println("System not ready, waiting...");
}
Enabling in Gradle / Maven
This is a preview feature. Enable it or your code won’t compile:
Gradle (Kotlin DSL):
tasks.withType<JavaCompile>().configureEach {
options.release = 25
options.compilerArgs.add("--enable-preview")
}
tasks.withType<JavaExec>().configureEach { jvmArgs("--enable-preview") }
tasks.withType<Test>().configureEach { jvmArgs("--enable-preview") }
Maven:
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
Summary
| Context | Before Java 25 | Java 25 (preview) |
|---|---|---|
instanceof int i | ❌ Compile error | ✅ Works |
switch(int) with type patterns | ❌ Only constants | ✅ Type + guard patterns |
| Range dispatch | Verbose if/else if | case int i when i > X |
| Mixed primitive + reference switch | Not possible | ✅ Same switch block |
Narrowing patterns (long → int) | Cast manually | case int i with auto check |
Primitive patterns are still in preview in Java 25 — but if you’re writing data-heavy code, it’s worth trying today. The API is nearly finalized and the feature is highly unlikely to change significantly.
Next up: Scoped Values →