Before you start: this is a preview

JEP 532 is a fifth preview in JDK 27. Compile and run with —enable-preview. Do not use preview bytecode in production.

You need a JDK 27 build. Confirm the toolchain first:

java --version
javac --version

Compile and run a single-file example with preview enabled on both commands:

javac --enable-preview --release 27 PrimitivePatternsDemo.java
java --enable-preview PrimitivePatternsDemo

Preview features can change or disappear, and class files compiled with them require the matching runtime preview support. Use the code in this post for experiments, migration planning, and feedback — not production releases.

The wrapper dance

Java’s pattern matching story has steadily removed casts from reference-type code. In part 1, sealed classes and pattern switch, a closed hierarchy gave the compiler enough information to check every subtype.

Primitive values still lived outside that smooth path. You could switch on an int, but not a long, float, double, or boolean. You could ask whether an Object was an Integer, but not whether a numeric value could become an int without losing information.

That pushed code toward wrappers and manual guards:

static void acceptWholeNumber(double value) {
    if (value >= Integer.MIN_VALUE
            && value <= Integer.MAX_VALUE
            && value == Math.rint(value)) {
        int whole = (int) value;
        System.out.println("accepted: " + whole);
    }
}

The range check, whole-number check, and cast all describe one intent: “continue only if this value is exactly representable as an int.”

With JEP 532:

static void acceptWholeNumber(double value) {
    if (value instanceof int whole) {
        System.out.println("accepted: " + whole);
    }
}

A primitive pattern combines an exact-conversion test, a conversion, and a binding. The branch runs only when no information would be lost.

What JEP 532 adds

JEP 532 makes three related changes:

  1. Primitive types can appear in top-level type patterns, such as value instanceof int i.
  2. Plain instanceof type tests can name primitive types, such as value instanceof int.
  3. switch accepts every primitive type, adding long, float, double, and boolean to the existing integral selectors.

It also allows primitive patterns inside record patterns to convert safely instead of requiring the component type to be identical:

record Measurement(double value) {}

static String describe(Measurement measurement) {
    return switch (measurement) {
        case Measurement(int whole) -> "whole: " + whole;
        case Measurement(double value) -> "fractional or out of int range: " + value;
    };
}

The int whole case is conditional. The double value case is unconditional for a double component, so it catches the remainder.

Fifth preview: the short timeline

The feature began with JEP 455 in JDK 23, returned in JDK 24 and 25, and gained refined exactness and dominance rules in JEP 530 for JDK 26. JEP 532 previews that design for a fifth time in JDK 27, without change.

Five previews are a signal to evaluate carefully, not a promise that the syntax is production-ready.

instanceof int: test and bind

Start with a double whose source might be JSON, a spreadsheet, or a database driver:

static String asCount(double value) {
    if (value instanceof int count) {
        return "count=" + count;
    }
    return "not an exact int: " + value;
}

public static void main(String[] args) {
    System.out.println(asCount(42.0));
    System.out.println(asCount(42.5));
    System.out.println(asCount(3_000_000_000.0));
}

Expected result:

count=42
not an exact int: 42.5
not an exact int: 3.0E9

42.0 has an exact conversion to int. 42.5 would lose its fractional part. Three billion is outside the int range. The latter two patterns simply do not match.

You can also test without binding: value instanceof int answers whether an int cast would be safe. If you need the converted value, prefer the binding form so the test and conversion cannot drift apart:

if (value instanceof int exact) {
    use(exact);
}

Exactness, not “fits after truncation”

A Java primitive cast is allowed to discard information:

double price = 19.99;
int unsafe = (int) price; // 19: fractional information vanished

long audience = 4_294_967_296L;
int wrapped = (int) audience; // 0: high-order bits vanished

Neither cast throws. Both produce a legal value that can travel through the program looking trustworthy.

A primitive pattern asks a stricter question: Can this runtime value be converted with no loss of information?

static void inspect(double value) {
    if (value instanceof int i) {
        System.out.println("exact int: " + i);
    } else {
        System.out.println("conversion would lose information");
    }
}

Some conversions are exact for every source value:

byte small = 42;
System.out.println(small instanceof int);    // true

int count = 42;
System.out.println(count instanceof double); // true

Others depend on the runtime value:

int a = 16_777_216;
int b = 16_777_217;

System.out.println(a instanceof float); // true
System.out.println(b instanceof float); // false: float cannot preserve it

The surprising case is int to float. Java calls it a widening primitive conversion, but a float has only 24 bits of precision. “Widening” does not always mean “information preserving.”

Note: JEP 532 does not add new numeric conversions or change assignment and cast behavior. It uses conversions already permitted in a casting context, then makes exactness the condition for a match.

The compiler calls conversions such as byte to int and int to double unconditionally exact because every source value is preserved. Conversions such as long to int, double to int, and int to float are value-dependent. This distinction also drives switch exhaustiveness and case dominance. A boolean-to-numeric pattern is illegal because Java defines no such cast.

Primitive patterns in switch

A pattern switch can classify by exact representation:

static String classify(double value) {
    return switch (value) {
        case byte b -> "byte-sized integer: " + b;
        case int i -> "other int: " + i;
        case double d -> "other double: " + d;
    };
}

Order matters. byte b handles the narrowest exact values first. int i handles remaining exact ints. double d is unconditional and makes the switch exhaustive.

Reversing broad and narrow patterns can make a later case unreachable:

static String broken(int value) {
    return switch (value) {
        case long l -> "always matches";
        case int i -> "compile-time error: dominated";
    };
}

Every int converts exactly to long, so case long l consumes the entire input domain. The compiler rejects the dead int case.

Put conditional, narrow matches before unconditional, broad matches.

Switch directly on long

Large IDs no longer need an if before an int switch:

static String shard(long accountId) {
    return switch (accountId) {
        case 10_000_000_000L -> "legacy-import";
        case 20_000_000_000L -> "partner-import";
        case long id when id < 0L -> "invalid";
        case long id -> "standard-" + Math.floorMod(id, 16);
    };
}

For a long selector, constant labels must be long constants — note the L suffix. The final long id pattern is unconditional.

Switch directly on float and double

Floating-point switches are useful when the values are protocol constants, sentinel values, or deliberately quantized measurements:

static String throttle(float ratio) {
    return switch (ratio) {
        case 0.0f -> "stopped";
        case 1.0f -> "full speed";
        case float r when r > 0.0f && r < 1.0f -> "throttled";
        case float r -> "invalid or special: " + r;
    };
}

The constants must have the selector’s type. For float, write 0.0f, not the double literal 0.0 or the int literal 0.

A double switch follows the same shape:

static String temperature(double celsius) {
    return switch (celsius) {
        case -273.15 -> "absolute zero";
        case 0.0 -> "water freezes";
        case 100.0 -> "water boils at standard pressure";
        case double value -> "other: " + value;
    };
}

Floating-point case labels use representation equivalence. Two source literals that round to the same representation are duplicate labels and fail compilation. Use floating-point switching for exact machine values, not approximate measurement comparisons; use guards with a tolerance when that is your real intent.

Switch directly on boolean

A boolean switch is an expression-friendly alternative to ?: when one arm needs a block:

static String accessMessage(boolean loggedIn) {
    return switch (loggedIn) {
        case true -> "Welcome back";
        case false -> {
            auditAnonymousAttempt();
            yield "Please sign in";
        }
    };
}

true plus false is exhaustive. Adding default after both is a compile-time error because nothing remains. A ternary is still clearer for two tiny expressions; use boolean switch when statement blocks or yield improve the code.

Nested primitive record patterns

Primitive patterns become especially useful at typed data boundaries:

sealed interface JsonValue permits JsonNumber, JsonString {}
record JsonNumber(double value) implements JsonValue {}
record JsonString(String value) implements JsonValue {}

static String render(JsonValue value) {
    return switch (value) {
        case JsonNumber(int i) -> "integer " + i;
        case JsonNumber(double d) -> "number " + d;
        case JsonString(String s) -> "text " + s;
    };
}

The record still stores a double. The first case matches only values that can become int exactly; the second catches every remaining double.

This continues the sealed-switch story from part 1: sealed types make the reference variants exhaustive, while primitive patterns refine the values inside those variants.

Gotchas

  • Exact does not mean valid. An exact int can still be negative when your domain requires a positive quantity; add a guard such as input instanceof int quantity && quantity > 0.
  • Pattern order matters. An unconditional conversion catches every selector value, so put it after narrower cases.
  • Do not box just to switch. Switch on long, float, double, or boolean directly; wrappers add nullable semantics and hide the primitive intent.
  • Preview must be enabled everywhere. Compiler, runtime, tests, IDE, and build tool must all agree on JDK 27 preview support.

Cheat sheet

JDK 27 / JEP 532: fifth preview
Compile: javac --enable-preview --release 27 Demo.java
Run:     java --enable-preview Demo

value instanceof int           // test exact convertibility
value instanceof int i         // test + convert + bind

switch supports:
byte, short, char, int, long, float, double, boolean
and corresponding wrapper types

Narrow/value-dependent cases first
Unconditional catch-all pattern last
Exact conversion != domain validation
Preview experiments only; not production

Do:

  • Use a binding pattern when you need the converted value.
  • Let failed matches handle out-of-range or precision-losing input.
  • Add guards for business rules after exact conversion succeeds.
  • Keep float constants suffixed with f and long constants with L.

Don’t:

  • Replace a lossy cast with a pattern and assume every input will match.
  • Put an unconditional primitive pattern before narrower cases.
  • Use floating-point case constants for approximate comparisons.
  • Ship JDK 27 preview-dependent class files as production artifacts.

Wrap-up

JEP 532 lets instanceof answer the question primitive casts never answered safely: “Will this specific conversion preserve the value?” The binding form gives you the converted primitive only when the answer is yes.

The same exactness rules power primitive patterns in switch, while expanded selectors remove the wrapper dance around long, float, double, and boolean. The result is less range-check boilerplate, fewer silent truncation bugs, and control flow that states its numeric intent.

For now, keep it behind --enable-preview in a JDK 27 sandbox. Use sealed classes and pattern switch for the production-ready reference-type foundation, then evaluate primitive patterns as the next step — not as a reason to move preview bytecode into production.