Part 6 of 12

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-preview at 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

ContextBefore Java 25Java 25 (preview)
instanceof int i❌ Compile error✅ Works
switch(int) with type patterns❌ Only constants✅ Type + guard patterns
Range dispatchVerbose if/else ifcase int i when i > X
Mixed primitive + reference switchNot possible✅ Same switch block
Narrowing patterns (longint)Cast manuallycase 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 →