Primitive patterns in Java 25
-
Last Updated: December 12, 2025
-
By: javahandson
-
Series
Learn Java in a easy way
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.
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.
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:
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:
JEP 507 fills these gaps and brings primitive values into the full pattern-matching ecosystem.
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:
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:
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:
It completes the missing pieces in Java’s pattern-matching design and makes everyday code more expressive, readable, and safe.
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:
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:
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:
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 type patterns make pattern matching complete and consistent across the language.
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:
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:
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
Primitive patterns make instanceof a far more powerful and expressive tool when working with numeric data.
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:
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:
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
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:
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:
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:
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:
This is exactly the kind of capability that was impossible before Java 25.
5. Benefits of Primitive Patterns in Nested Contexts
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.
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.
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
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.
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.
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.
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.