Primitive patterns in Java 25

  • Last Updated: December 12, 2025
  • By: javahandson
  • Series
img

Primitive patterns in Java 25

Primitive patterns in Java 25 (JEP 507) complete Java’s pattern matching model by allowing primitives in instanceof, switch, and record patterns. This article explains syntax, safe conversions, nested patterns, real-world use cases, best practices, and common pitfalls, helping us write cleaner, safer, and more expressive Java code.

 

Introduction

Java’s pattern-matching journey has been progressing steadily over the last few releases. First, we got pattern matching for instanceof (JEP 305), which lets us test a type and bind a variable in a single step. Then came pattern matching for switch, allowing more expressive case labels. After that, record patterns were introduced, enabling deep data deconstruction in a concise and readable way.

JEP 507 is the next step in this evolution.

In simple terms: Primitive types now fully participate in patterns, instanceof, and switch — as a preview feature in Java 25.

This change is significant because, until now, pattern matching worked mainly with reference types (String, Point, records, etc.). Primitives like int, long, double, and even boolean were not first-class citizens in the pattern-matching world.

With JEP 507, pattern matching becomes uniform, predictable, and complete — whether we are working with reference types or primitive values.

This is also the Third Preview of primitive patterns:

1. JEP 455 – First introduced in Java 23

2. JEP 488 – Second Preview in Java 24

3. JEP 507 – Third Preview in Java 25, with no major changes

The repeated previews show that the Java team is refining the feature through real-world feedback before finalising it.

In essence, JEP 507 brings Java closer to a world where:

1. We can safely match and extract primitive values using patterns,

2. instanceof can test and bind primitive types,

3. A switch can operate uniformly across all primitive types.

This unification reduces mental overhead, eliminates boilerplate range checks, and makes Java’s pattern matching feel natural and complete.

Why do we need primitive patterns?

As Java developers, we often work with both object types and primitive values. However, before Java 25, pattern matching treated these two worlds very differently. Patterns worked beautifully for reference types, but primitives did not enjoy the same capabilities. This left us writing extra boilerplate, manual checks, and conversions whenever we dealt with numbers or boolean values.

Let’s look at the gaps that motivated JEP 507.

1. Limited switch Support for Primitives – Traditional switch statements supported only a subset of primitive types – mainly byte, short, char, and int. Types like boolean, long, float, and double were excluded. This forced us to fall back to long chains of if/else blocks when we really wanted clean, expressive branching.

package com.javahandson.jep507;

public class Demo {

    public static void main(String[] args) {

        long id = 100l;

        switch (id) {                // Compilation error before Java 25
            case 100L -> System.out.println("Suraj");
            case 200L -> System.out.println("Shweta");
            default   -> System.out.println("User");
        }
    }
}
Output: java: constant label of type long is not compatible with switch selector type long

Because long was not allowed as a switch selector, we had to write:

package com.javahandson.jep507;

public class Demo {

    public static void main(String[] args) {

        long id = 200l;

        if (id == 100L) {
            System.out.println("Suraj");
        } else if (id == 200L) {
            System.out.println("Shweta");
        } else {
            System.out.println("User");
        }
    }
}
Output: "C:\Program Files\Java\jdk-17\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=64331" -Dfile.encoding=UTF-8 -classpath C:\my-space com.javahandson.jep507.Demo
Shweta

This is much harder to read, especially when the branching grows.

Another Example — a boolean could not be used in a switch

This was not allowed:

package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        boolean isActive = true;

        switch (isActive) {      // Not allowed before Java 25
            case true  -> System.out.println("Active");
            case false -> System.out.println("Inactive");
        }
    }
}
Output: java: constant label of type boolean is not compatible with switch selector type boolean

So we had to fall back to:

package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        boolean isActive = true;

        if (isActive) {
            System.out.println("Active");
        } else {
            System.out.println("Inactive");
        }
    }
}
Output: "C:\Program Files\Java\jdk-17\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=55658" -Dfile.encoding=UTF-8 -classpath C:\my-space com.javahandson.jep507.Demo
Active

2. No Primitive Patterns – Pattern matching allowed us to deconstruct reference types, match shapes, and even extract components from records. But primitives like int i, long l, or double d could not appear as patterns at all. We could not do:

package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        long value = 100l;

        if (value instanceof int i) {         // Not allowed before Java 25
            System.out.println("Fits in int: " + i);
        } else {
            System.out.println("Too large for int: " + value);
        }
    }
}
Output: java: unexpected type

This inconsistency made pattern matching feel incomplete.

The Old Way – Verbose, Manual Range Checks

We were forced to write the equivalent logic manually:

package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        long value = 100l;

        if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
            int i = (int) value;  // safe cast
            System.out.println("Fits in int: " + i);
        } else {
            System.out.println("Too large for int: " + value);
        }
    }
}
Output: "C:\Program Files\Java\jdk-17\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=58501" -Dfile.encoding=UTF-8 -classpath C:\my-space com.javahandson.jep507.Demo
Fits in int: 100

Problems:

  • We manually repeat the min/max checks everywhere
  • Risk of getting the range wrong
  • Business logic becomes harder to read
  • No uniformity with reference pattern matching

3. Manual, Error-Prone Range Checks – Whenever we needed to safely convert primitives (for example, from long to int, or int to byte), we had to write:

if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
    byte b = (byte) value;
}

This is repetitive, easy to get wrong, and clutters our business logic. With millions of virtual threads or high-performance tasks, such checks add unnecessary noise.

4. Record Patterns Could Not Handle Primitive Conversions – Record patterns let us elegantly extract data. If a record contained numbers stored in wider types (like double), we had no clean way to test whether the value could fit into a smaller primitive (like int) without loss.

This meant additional conditions and boilerplate outside the pattern itself.

Example: Handling double → int Without Primitive Patterns

package com.javahandson.jep507;

public class Demo {

    // Simple JSON-like model
    record JsonNumber(double value) {}
    record JsonPerson(String name, JsonNumber age) {}

    public static void main(String[] args) {
        Object value1 = new JsonPerson("Shweta", new JsonNumber(33.0));
        Object value2 = new JsonPerson("Suraj", new JsonNumber(35.5));
        Object value3 = new JsonPerson("Iqbal", new JsonNumber(1_000_000_000_000.0)); // too big for int
        Object value4 = "Not a person at all";

        process(value1);
        process(value2);
        process(value3);
        process(value4);
    }

    private static void process(Object value) {
        // We first have to check the shape using instanceof + record binding
        if (value instanceof JsonPerson person) {
            String name = person.name();
            JsonNumber ageNode = person.age();

            double ageRaw = ageNode.value();

            // Now we have to manually check:
            // 1. Is it within int range?
            // 2. Is it a whole number (no fractional part)?
            if (ageRaw >= Integer.MIN_VALUE && ageRaw <= Integer.MAX_VALUE && ageRaw % 1 == 0) {
                int age = (int) ageRaw; // safe cast
                System.out.println("Valid person: name = " + name + ", age (int) = " + age);
            } else {
                System.out.println("Person " + name
                        + " has invalid age for int: " + ageRaw);
            }
        } else {
            System.out.println("Value is not a JsonPerson: " + value);
        }
    }
}
Output:
Valid person: name = Shweta, age (int) = 33
Person Suraj has invalid age for int: 35.5
Person Iqbal has invalid age for int: 1.0E12
Value is not a JsonPerson: Not a person at all

In this example, we created a small JSON-like model using two records: JsonPerson for a person and JsonNumber for their age. Since all numeric values were stored as double, we first used instanceof to check whether the input was a JsonPerson and then extracted the raw age value. However, because primitive patterns were not available before Java 25, we could not express “extract age only if it fits into an int” inside the pattern itself.

Instead, we had to write additional logic outside the pattern: check whether the double value was within the int range, verify that it had no fractional part, and then manually cast it to int. This added unnecessary boilerplate and mixed data-validation logic with business logic, making the overall code harder to read and maintain. Primitive patterns in Java 25 remove this complexity by allowing such checks to be done directly within the pattern.

5. Inconsistent Pattern Matching Model – The biggest motivation was conceptual: pattern matching was becoming a central part of modern Java, but primitives were missing from the model. This inconsistency required developers to remember special rules, exceptions, and limitations.

We needed primitive patterns because they:

  • eliminate repetitive range checks
  • unify primitive and reference type handling
  • reduce boilerplate
  • make pattern matching expressive and predictable
  • allow safe, lossless conversions directly inside patterns
  • enable switch to handle all primitive types uniformly

JEP 507 fills these gaps and brings primitive values into the full pattern-matching ecosystem.

What JEP 507 brings – High-Level overview

JEP 507 brings primitive types into the pattern-matching world and makes them behave just like reference types in instanceof, switch, and nested patterns. Until now, primitives were treated as “special cases” with several restrictions. With this JEP, Java finally completes the pattern-matching model and gives us a uniform, predictable system.

At a high level, JEP 507 introduces four important changes:

1. Primitive Type Patterns – We can now write patterns using primitive types such as int, long, double, or boolean. For example:

if (value instanceof int i) {
    // i is safely converted int
}

This replaces manual range checks and unsafe casts.

2. instanceof Works with Primitive Types – The instanceof operator can now:

  • test whether a value fits into a specific primitive type
  • bind that value if the conversion is safe

This gives us expressive and safe narrowing logic directly inside the language.

3. switch works with all Primitive Types – switch was previously limited to a subset of primitives. With JEP 507, we can switch on:

  • boolean
  • long
  • float
  • double

along with patterns, guards, and deconstruction. This makes branching cleaner and removes the need for long if/else chains.

4. Nested and Record Patterns Can Use Primitive Patterns

Primitive patterns can now appear inside record patterns and nested structures:

if (obj instanceof JsonNumber(int age)) {
    // age is validated and safely converted
}

This allows us to validate and destructure data in one step, without writing external logic.

In Short, JEP 507 gives us:

  • Uniform pattern matching across primitives and reference types
  • Safer conversions (only lossless matches succeed)
  • Cleaner switch statements with full primitive support
  • More expressive and concise patterns inside complex data structures

It completes the missing pieces in Java’s pattern-matching design and makes everyday code more expressive, readable, and safe.

Primitive Type Patterns – Syntax and Basics

Primitive type patterns extend Java’s pattern-matching capabilities to all primitive types. Until Java 25, patterns worked only for reference types. With JEP 507, we can finally write patterns that match, test, and safely convert primitive values directly in the code.

At the most basic level, a primitive pattern looks like this: T variable

where T is any primitive type (byte, short, int, long, float, double, char, boolean).

This pattern checks whether the value can be safely converted to the target primitive type without loss of information. If the test succeeds, the value is bound to the variable.

1. Basic Syntax – The core form of a primitive type pattern is:

if (expression instanceof int i) {
    // use i
}

Here:

  • expression must evaluate to a numeric or boolean value
  • The pattern int i checks if the value fits exactly into an int
  • If it does, it becomes an int-bound variable

Example: long value = 42L;

if (value instanceof int i) {
    System.out.println("Fits in int: " + i);
}
Output: Fits in int: 42

This pattern matches because 42L can be represented as an int without loss.

2. Supported Primitive Types – We can use patterns with all primitive types:

  • byte
  • short
  • int
  • long
  • float
  • double
  • char
  • boolean

This means we can test and bind the value directly using whichever primitive type we need.

if (x instanceof byte b) { ... }
if (x instanceof double d) { ... }
if (flag instanceof boolean b) { ... }

3. Safe Conversion Is Built-In – A primitive pattern matches only when the conversion is lossless.

For example: long value = 300;

if (value instanceof byte b) {   // does NOT match
    ...
}

300 does not fit into a byte (-128 to 127), so the pattern fails. This turns pattern matching into a safe replacement for manual range checks.

4. When the Pattern Match Succeeds – If the value fits and the pattern matches:

  • The variable is automatically created
  • The value is converted safely
  • No casting is required

double digit = 98.0;

if (digit instanceof int t) {
    // Only runs if digit is a whole number fitting in int
    System.out.println("Whole number digit = " + t);
}

5. Primitive Patterns Inside Larger Patterns – Primitive patterns can appear as part of more complex patterns:

if (obj instanceof JsonNumber(int age)) {
    System.out.println("Age = " + age);
}

This allows us to perform validation and extraction in a single step.

In Short:

  • Primitive patterns let us test and bind primitive values.
  • They enforce safe, lossless conversions automatically.
  • They work in instanceof, switch, and nested patterns.
  • They eliminate the need for manual range checks and explicit casts.

Primitive type patterns make pattern matching complete and consistent across the language.

Using instanceof with Primitive types

Primitive type patterns become especially powerful when used with the instanceof operator. Until Java 25, instanceof worked only with reference types. With JEP 507, we can now use it to test and bind primitive values in a safe, expressive, and uniform way.

This allows us to replace manual range checks and unsafe casts with clear, pattern-based logic.

1. Basic Primitive Pattern with instanceof – The simplest form looks like this:

if (value instanceof int i) {
    System.out.println("Fits in int: " + i);
}

Here:

  • The pattern checks whether the value can be safely converted to an int.
  • If it can, the variable i is created and holds the converted int.
  • If it cannot, the pattern fails, and the block is skipped.

This gives us a concise and safe way to perform narrowing conversions.

2. Type-Test Only (Without Binding) – Sometimes we only want to check if a value fits into a primitive type, without capturing it:

if (value instanceof int) {
    // Only checks if narrowing to int is lossless
}

This is similar to reference-type instanceof, but now applies to primitive conversions as well.

3. Why This Is Safer Than Manual Checks – Before primitive patterns, we had to write:

if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
    int i = (int) value;
}

This approach:

  • adds boilerplate
  • is easy to get wrong
  • mixes business logic with validation

Primitive patterns take care of the range checking automatically and only match when the conversion is lossless.

4. Examples of Useful Narrowing Checks

Converting long to int safely

long id = fetchId();

if (id instanceof int i) {
    useIntId(i);
} else {
    handleLargeId(id);
}

Ensuring a double Has No Fractional Part

double temperature = readTemperature();

if (temperature instanceof int t) {
    System.out.println("Rounded temperature = " + t);
}

This matches only when the double is a whole number that fits in an int.

5. Works Seamlessly With Nested Patterns – We can combine primitive patterns with other patterns:

if (obj instanceof JsonNumber(int age)) {
    System.out.println("Age = " + age);
}

Here, the primitive pattern inside the record pattern ensures the double value can be safely interpreted as an int.

6. Key Benefits

  • Uniform syntax for both primitive and reference types
  • Automatic, lossless conversion checks
  • No need for lengthy range conditions
  • Clearer intent and fewer mistakes
  • Works naturally with nested and record patterns

Primitive patterns make instanceof a far more powerful and expressive tool when working with numeric data.

Using a switch with Primitive types

With JEP 507, switch becomes far more expressive and uniform. Until Java 25, the switch statement supported only a subset of primitive types. We could use byte, short, char, and int, but we could not switch on boolean, long, float, or double. This limitation forced us to write long if/else chains even for simple branching.

Primitive type patterns remove these restrictions and allow the switch to work naturally with all primitive types.

1. Switching on All Primitive Types – We can now use:

  • boolean
  • long
  • float
  • double

as switch selectors:

boolean flag = isFeatureEnabled();

switch (flag) {
    case true  -> System.out.println("Feature enabled");
    case false -> System.out.println("Feature disabled");
}

This was not possible before Java 25.

2. Primitive Type Patterns inside switch – Primitive patterns allow us to match values based on safe conversion rules directly in the switch:

package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        long value = 98L;

        switch (value) {
            case int i     -> System.out.println("Fits into int: " + i);
            case long l    -> System.out.println("Large value: " + l);
        }
    }
}
Output: "C:\Program Files\Java\jdk-25\bin\java.exe" --enable-preview "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2025.1.3\lib\idea_rt.jar=50729" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\my-space com.javahandson.jep507.Demo

Fits into int: 98

The first case matches only if the long fits into an int without losing information.

3. Mixing Constants, Patterns, and Guards – We can write expressive switch cases using:

  • constants
  • primitive patterns
  • guards (when clauses)
package com.javahandson.jep507;

public class Demo {
    public static void main(String[] args) {

        double score = 95.5;

        switch (score) {
            case 100.0 -> System.out.println("Perfect");
            case double d when d > 90 -> System.out.println("Excellent: " + d);
            case int i -> System.out.println("Good: " + i);
            case double d when d < 0 -> System.out.println("Invalid");
            default -> System.out.println("Other");
        }
    }
}
Output: Excellent: 95.5

This gives us a compact way to express complex rules.

4. Removing Verbose if/else Chains – Before primitive patterns, we would have written:

double salary = getSalary();

if (salary == 0.0) {
    ...
} else if (salary >= 50_000 && salary % 1 == 0) {
    int s = (int) salary;
    ...
} else if (salary < 0) {
    ...
} else {
    ...
}

Now the same logic becomes far cleaner inside the switch using primitive patterns and guards.

5. Example: Range Classification Using Primitive Patterns

package com.javahandson.jep507;

public class Demo {

    public static void main(String[] args) {

        long amount = 8000L;

        switch (amount) {
            case int i when i < 1000  -> System.out.println("Small amount: " + i);
            case int i when i < 10_000 -> System.out.println("Medium amount: " + i);
            case long l               -> System.out.println("Large amount: " + l);
        }
    }
}
Output: Medium amount: 8000

6. switch with a boolean

Java finally allows this:

boolean active = user.isActive();

switch (active) {
    case true  -> sendWelcomeMessage();
    case false -> sendReminder();
}

This removes awkward if/else blocks and lets us keep the logic readable.

7. Benefits of Primitive Patterns in switch

  • All primitive types are now supported
  • Safe conversions inside the switch
  • Cleaner, more expressive branching
  • Easier to read and maintain
  • Works well with guards
  • Consistent with reference-type pattern matching

Primitive Patterns Inside Record Patterns (Nested Patterns)

One of the most powerful features introduced by JEP 507 is the ability to use primitive patterns inside record patterns. In earlier Java versions, record patterns allowed us to destructure objects, but they could not validate or bind primitive values based on safe, lossless conversions. This often forced us to write additional logic outside the pattern, even when the structure itself already described the rules we wanted.

Primitive patterns remove this limitation and make nested pattern matching far more expressive and concise.

1. Why This Matters – Record patterns are designed to let us match and extract data in a single, declarative step. But before Java 25, numeric values were often required:

  • manual range checks
  • fractional checks for double → int conversions
  • explicit casting
  • extra if/else layers outside the pattern

This broke the flow and made pattern matching less uniform.

With primitive patterns, record patterns finally become complete. We can match the structure and the primitive value constraints at the same time.

2. A Simple Example – Suppose we have a JSON-like model:

record JsonNumber(double value) {}
record JsonPerson(String name, JsonNumber age) {}

Before Java 25 - verbose and manual

if (obj instanceof JsonPerson person) {
    double raw = person.age().value();

    if (raw >= Integer.MIN_VALUE && raw <= Integer.MAX_VALUE && raw % 1 == 0) {
        int age = (int) raw;
        System.out.println("Age: " + age);
    }
}

We had to:

  • Extract the number
  • Check its range
  • Check if it has no fraction
  • Then convert it

All outside the pattern.

3. After Java 25 – clean and expressive

With primitive patterns, we can write:

if (obj instanceof JsonPerson(String name, JsonNumber(int age))) {
    System.out.println("Name = " + name + ", Age = " + age);
}

Below is a working example:

package com.javahandson.jep507;

public class Demo {

    record JsonNumber(double value) {}
    record JsonPerson(String name, JsonNumber age) {}

    public static void main(String[] args) {
        Object value1 = new JsonPerson("Shweta", new JsonNumber(33.0));
        Object value2 = new JsonPerson("Suraj", new JsonNumber(35.5));
        Object value3 = new JsonPerson("Iqbal", new JsonNumber(1_000_000_000_000.0)); // too big for int
        Object value4 = "Not a person at all";

        process(value1);
        process(value2);
        process(value3);
        process(value4);
    }

    private static void process(Object value) {
        if (value instanceof JsonPerson(String name, JsonNumber(int age))) {
            System.out.println("Name = " + name + ", Age = " + age);
        }
        else {
            System.out.println("Value is not a JsonPerson: " + value);
        }
    }
}
Output:
Name = Shweta, Age = 33
Value is not a JsonPerson: JsonPerson[name=Suraj, age=JsonNumber[value=35.5]]
Value is not a JsonPerson: JsonPerson[name=Iqbal, age=JsonNumber[value=1.0E12]]
Value is not a JsonPerson: Not a person at all

This single pattern does all of the following automatically:

  • ensures the age value fits in an int
  • ensures the conversion is lossless
  • binds the converted value to age
  • destructures the entire object shape

All without extra checks or casting.

4. Nested Patterns Can Go Deeper

Primitive patterns behave like any other pattern – we can nest them further:

record Address(String city, JsonNumber(pincode)) {}
record Person(String name, Address address) {}

if (p instanceof Person(String n, Address(String c, JsonNumber(int pin)))) {
    System.out.println(n + " lives in " + c + " with pincode " + pin);
}

This makes it easy to validate deep structures with both reference and primitive values.

Below is a working example:

package com.javahandson.jep507;

public class Demo {

    // Our simple JSON-like model
    record JsonNumber(double value) {}
    record Address(String city, JsonNumber pincode) {}
    record Person(String name, Address address) {}

    public static void main(String[] args) {

        Object p1 = new Person(
                "Suraj",
                new Address("Hyderabad", new JsonNumber(560001.0))
        );

        Object p2 = new Person(
                "Shweta",
                new Address("Hyderabad", new JsonNumber(4000.5))  // invalid pincode (fraction)
        );

        Object p3 = new Person(
                "Amar",
                new Address("Aurangabad", new JsonNumber(10_00_00_00000.0)) // too large for int
        );

        Object p4 = "Not a person at all";

        process(p1);
        process(p2);
        process(p3);
        process(p4);
    }

    private static void process(Object p) {
        if (p instanceof Person(String name,
                                Address(String city, JsonNumber(int pin)))) {

            // This block runs ONLY when:
            // - p is a Person
            // - p.address is Address
            // - pincode.value is a double that fits losslessly into int (no fraction)
            System.out.println(name + " lives in " + city + " with pincode " + pin);

        } else {
            System.out.println("Not a valid match for primitive pattern: " + p);
        }
    }
}
Output:
Suraj lives in Hyderabad with pincode 560001
Not a valid match for primitive pattern: Person[name=Shweta, address=Address[city=Hyderabad, pincode=JsonNumber[value=4000.5]]]
Not a valid match for primitive pattern: Person[name=Amar, address=Address[city=Aurangabad, pincode=JsonNumber[value=1.0E10]]]
Not a valid match for primitive pattern: Not a person at all

This example clearly shows how:

  • record patterns destructure the object
  • primitive patterns validate and bind numeric values
  • Only lossless conversions are allowed (double → int)
  • nested patterns work seamlessly across levels

This is exactly the kind of capability that was impossible before Java 25.

5. Benefits of Primitive Patterns in Nested Contexts

  • Clean, declarative extraction of data
  • Automatic handling of narrowing conversions
  • No manual checks or casts
  • Better readability in deeply nested structures
  • Perfect symmetry between primitives and objects
  • Ideal for JSON/XML parsers, DTOs, and data transformation pipelines

How Primitive Patterns Ensure Safe Conversions

Primitive patterns are built on a strict rule: a pattern matches only when the conversion to the target primitive type is completely lossless. This means Java checks whether the value fits within the target type’s range and whether converting it would preserve the exact numeric information. If any part of the conversion would lose data – like truncating a decimal, overflowing a smaller type, or rounding a floating point value the pattern simply does not match. This avoids silent errors that might happen with manual casts.

This safety guarantee replaces the old approach where we manually wrote >=, <=, or fractional checks to ensure a value could be narrowed safely. With primitive patterns, Java performs these validations automatically, allowing us to handle narrowing conversions directly inside a pattern. This keeps our business logic clean while ensuring that values are only bound when they are truly safe to use.

Because these rules apply everywhere primitive patterns appear inside instanceof, in switch statements, or nested inside record patterns, our code becomes more predictable and uniform. We no longer switch between pattern matching for objects and manual validation for primitives. Safe conversions become a natural part of the language, improving readability and reducing error-prone boilerplate.

Comparison with Boxing/Unboxing and Old Approaches

Before primitive patterns arrived in Java 25, working with numeric values inside patterns often required us to rely on boxing, unboxing, and a mix of manual checks. This created unnecessary complexity and made pattern matching feel inconsistent whenever primitives were involved. Primitive patterns solve this by giving us a unified and safer way to test, match, and bind primitive values.

In older Java versions, one common workaround was to wrap primitives in their wrapper types (Integer, Long, Double) so we could use existing pattern-matching features. But this introduced hidden costs: extra allocations, possible NullPointerExceptions, and unpredictable behaviour when unboxing occurred. More importantly, the logic for validating safe conversions still had to be handled manually, so the benefits of pattern matching often got diluted.

Primitive patterns remove the need for such workarounds. Instead of checking ranges and performing casts ourselves or converting primitives to wrapper types, we can directly use patterns with int, long, double, and others. This makes our code more expressive and far easier to maintain. We get predictable behaviour, built-in safety, and a uniform pattern model that works the same way for both primitive and reference types.

How to Enable and Use JEP 507 (Preview in Java 25)

Primitive patterns are a preview feature in Java 25, which means they are available for us to use, but they are not enabled by default. To experiment with them, we must explicitly turn on preview mode during both compilation and execution. This ensures that we are consciously opting into features that may still evolve in future releases.

If preview features are new to us, we can follow this guide: How to Enable Preview Features in Java 25

Best Practices and Common Pitfalls

Primitive patterns give us expressive and powerful tools, but like any new language feature, they work best when used thoughtfully. Following a few simple best practices helps us write clean and predictable code, while avoiding the subtle pitfalls that may arise when mixing primitives, patterns, and narrowing conversions.

Best Practices

1. Keep patterns simple and readable – Primitive patterns are most helpful when they replace boilerplate checks. If the pattern becomes overly complex or deeply nested, it’s often better to extract the logic into a helper method.

2. Prefer explicit ordering in switch cases – Place narrower matches before broader ones. For example, put case int i before case long l so the switch behaves in an intuitive, structured manner.

3. Use guards (when) for additional conditions – If we need more nuanced matching—like ranges, negative checks, or custom rules—guards keep the logic clean.

4. Use primitive patterns for validation + extraction – When we want to verify that a value is safe to convert, primitive patterns are ideal. They combine checks and extraction in one place.

5. Keep business logic outside the pattern – Patterns should define structure and validation, not full business workflows. Once a pattern matches, it’s cleaner to call separate methods for business logic.

Common Pitfalls

1. Assuming fractional values will match integer patterns – A pattern like case int i matches only when the floating-point value is a whole number and fits into an int. Values like 95.5 will skip the int pattern entirely.

2. Forgetting that safe conversion rules are strict – A conversion that would normally compile with a cast may still fail in a pattern. Patterns enforce safety, while casts allow lossy behaviour.

3. Misordered switch branches – If case long l comes before case int i, the int case will never be reached because a long pattern always accepts the original value. Order matters.

4. Trying to use patterns for type coercion – Patterns are not for rounding or coercing values. They match only when the conversion is lossless. If we need rounding, we should do it explicitly.

5. Forgetting that this is a preview feature – Since JEP 507 is still in preview, syntax or behaviour may evolve. We should always compile and run with –enable-preview and remain prepared for minor changes in future releases.

Summary

Primitive patterns in Java 25 bring a major improvement to the pattern-matching model by allowing us to work with primitives in the same expressive way we already use patterns for objects. With JEP 507, we can now use primitive types inside instanceof, switch, and nested record patterns, making our code more uniform, predictable, and concise. The biggest win is that safety patterns match only when conversions are lossless, which eliminates manual range checks and prevents accidental truncation or rounding.

This feature completes an important part of Java’s pattern-matching journey. By supporting primitives directly, Java finally removes the long-standing gap between object and primitive handling. Our branching logic becomes cleaner, our validation steps become declarative, and nested structures can now be deconstructed with both reference and primitive components. As part of Java’s preview cycle, primitive patterns also continue the evolution of Project Amber toward a more modern, expressive Java.

Primitive patterns may look small at first glance, but they have a big impact on real projects—especially when dealing with data transformation, safe narrowing, record models, or complex pattern-matching pipelines. Together with other Java 25 features, they help us write code that is safer, clearer, and much closer to our intent.

Leave a Comment