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.

1. Introduction

Pattern matching has become one of the most important language improvements in modern Java. It simplifies how we test values, extract data, and express conditional logic. Before looking at primitive patterns in Java 25, it is useful to first understand what pattern matching means in Java and how it has evolved.

1.1. What Is Pattern Matching in Java?

In simple terms, Pattern matching is a language feature that allows us to test a value.
If the test succeeds, it lets us extract data safely and in a readable way. Traditionally, we had to write separate type checks, casts, and assignments. With pattern matching, these steps are combined into a single construct.

In practice, pattern matching in Java is most commonly used with instanceof and switch. As a result, it reduces boilerplate code and makes the program’s intent clearer. When a pattern matches, Java automatically binds the matched value to a variable that can be used safely within a defined scope.

At a high level, the goal of pattern matching is to make code easier to read and less error-prone. More importantly, it allows developers to focus on what they are checking rather than how the check is performed.

1.2 Primitive Patterns in Java 25 (JEP 507)

Over the last few Java releases, pattern matching has evolved steadily. It began with pattern matching for instanceof (JEP 305), which allowed developers to test a type and bind a variable in a single step. Later, pattern matching for switch introduced more expressive case labels. After that, record patterns enabled concise and readable data deconstruction.

JEP 507 is the next step in this evolution.

Starting with Java 25, primitive types fully participate in pattern matching, instanceof, and switch. This support is available as a preview feature.

Until now, pattern matching worked mainly with reference types such as String, records, and user-defined classes. Because of this, primitive types like int, long, double, and even boolean were not first-class citizens in the pattern-matching model.

With JEP 507, this limitation is removed. Pattern matching becomes uniform, predictable, and complete, whether we are working with reference types or primitive values.

It is also worth noting that primitive patterns have gone through multiple preview iterations. They were first introduced in Java 23, refined in Java 24, and appeared again in Java 25 as a third preview with no major changes. These repeated previews show that the Java team is refining the feature carefully using real-world feedback before finalising it.

In essence, JEP 507 brings Java closer to a model where primitive values can be safely matched and extracted using patterns. In addition, instanceof can now test and bind primitive types, and switch can operate uniformly across all primitive types. As a result, this unification reduces mental overhead, removes boilerplate range checks, and makes Java’s pattern matching feel natural and complete.

2. 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.

a. 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

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

b. Primitive Types Were Not Part of Pattern Matching

Before Java 25, pattern matching worked only with reference types. We could match objects, deconstruct records, and bind variables, but primitive values were excluded from the pattern model.

Because of this, primitives such as int, long, and double could not appear in patterns at all. For example, the following code was not allowed:

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. 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

The above approach was verbose and easy to get wrong. Primitive patterns remove this inconsistency by allowing primitive values to participate directly in pattern matching.

c. 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.

d. 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 used a small JSON-like model with two records: JsonPerson and JsonNumber. Since numeric values were stored as double, we first had to check whether the input was a JsonPerson and then extract the raw age value.

Before Java 25, we could not express “extract the age only if it fits into an int” directly inside the pattern. Instead, we had to add extra checks outside the pattern to validate the range, ensure there was no fractional part, and then cast the value manually. This made the code longer and harder to read.

Primitive patterns in Java 25 remove this complexity by allowing validation and conversion to happen directly within the pattern itself.

e. 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.

3. What JEP 507 brings

JEP 507 brings primitive types into Java’s pattern-matching model and makes them behave consistently with reference types in instanceof, switch, and nested patterns. Until now, primitives were treated as special cases with several limitations. With this JEP, Java completes its pattern-matching design and offers a more uniform and predictable system.

At a high level, JEP 507 introduces four key improvements:

  • Primitive type patterns, allowing primitives to be matched and safely bound using pattern syntax
  • Enhanced instanceof support, enabling safe narrowing and binding of primitive values
  • Expanded switch support for all primitive types, combined with patterns and guards
  • Use of primitive patterns in nested and record patterns, enabling validation and extraction in a single step

Together, these changes remove long-standing inconsistencies, eliminate the need for manual range checks and casts, and make pattern-based code clearer and safer.

Each of these enhancements is explained in detail in the sections that follow.

3.1. Primitive Type Patterns

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.

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.

a. 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) { ... }

b. 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.

c. The Pattern Match Succeeds

If the value fits and the pattern matches, then:

  • 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);
}

d. 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.

3.2. 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.

a. 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.

b. 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.

c. 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.

d. Examples of Useful Narrowing Checks

In real applications, numeric values often come from external systems, calculations, or user input. In such cases, values may or may not fit into a smaller primitive type. Primitive patterns allow these checks to be expressed clearly and safely, without cluttering the code.

Ex. Converting long to int safely

long id = fetchId();

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

Here, the pattern matches only when the long value can be safely represented as an int. The conversion and validation happen automatically, without explicit range checks or casts.

Primitive patterns are also useful when working with floating-point values that must be whole numbers.

Ex. Ensuring a double Has No Fractional Part

double temperature = readTemperature();

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

This pattern matches only when the value has no fractional part and fits into an int, making the intent of the code clear and explicit.

e. 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.

f. Benefits of Primitive Pattern with instanceof

  • 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.

3.3. 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.

a. 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.

b. 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.

c. 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.

d. 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.

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

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

e. 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

3.4. 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. 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.

Before Java 25 – verbose and manual

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

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

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.

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.

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.

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

4. 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.

5. 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.

6. 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

7. 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.

7.1. 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.

7.2. 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.

8. Conclusion

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.

Further Reading

For the official specification and detailed design discussion behind primitive patterns, see:

  • JEP 507: Primitive Patterns – OpenJDK
    This JEP explains how pattern matching has been extended to work directly with primitive types, covering safe conversions, dominance rules, and how primitives integrate with existing pattern matching constructs.

What’s Next

Primitive patterns make conditional logic safer and more expressive by eliminating unnecessary boxing and manual checks. They are a clear step toward cleaner and more predictable Java code.

In the next chapter, we shift focus from language semantics to code organisation.

Module Import Declarations (JEP 511) shows how Java 25 simplifies imports in modular applications by allowing entire module exports to be imported at once. This reduces boilerplate, improves readability, and keeps modular code clean—without changing how Java modules work internally.

Leave a Comment