Java 26: Primitive Types in Patterns (JEP 530 — Preview)

Pattern matching in Java is now complete. JEP 530 extends patterns to support primitive types in switch, instanceof, and record patterns — closing the last gap in Java’s pattern matching story.

Status: Fourth Preview — requires --enable-preview

What Changed

Before this feature, pattern matching only worked with reference types. Now you can write patterns like case int i, case double d, case boolean b, etc., including inside record destructuring patterns and with when guards.

Key Capabilities

  1. Primitive type patterns in switch
  2. Primitive instanceof
  3. Guard patterns with primitives (case int i when i > 0)
  4. Primitive patterns inside record destructuring
  5. Narrowing conversions in patterns (match long against int)
  6. Exhaustive boolean switch (no default needed)

Demo Highlights

1. Primitive Type Patterns in switch(Object)

When autoboxed values are stored in an Object, you can now match them back to their primitive types:

Object[] values = { 42, 3.14, true, "hello", 'A', 100L };

for (Object value : values) {
    String description = switch (value) {
        case Integer i   -> "int: " + i;
        case Double d    -> "double: " + d;
        case Boolean b   -> "boolean: " + b;
        case Long l      -> "long: " + l;
        case Character c -> "char: '" + c + "'";
        case String s    -> "String: \"" + s + "\"";
        default          -> "other: " + value;
    };
}

2. Guarded Primitive Patterns

Combine primitive patterns with when guards for clean range-based matching — much better than if-else chains:

static String classifyTemperature(int temp) {
    return switch ((Object) temp) {
        case int t when t < 0  -> "🥶 Freezing";
        case int t when t < 10 -> "❄️ Cold";
        case int t when t < 20 -> "🌤️ Cool";
        case int t when t < 30 -> "☀️ Warm";
        case int t when t < 40 -> "🔥 Hot";
        case int t             -> "🌋 Extreme heat!";
        default                -> "Unknown";
    };
}

3. Record Destructuring with Primitive Patterns

Records with primitive fields can be destructured with primitive patterns, including nested records and guards:

record Point(int x, int y) {}
record Circle(Point center, double radius) {}

String desc = switch (shape) {
    case Point(int x, int y) when x == 0 && y == 0
        -> "Origin point";
    case Point(int x, int y)
        -> "Point at (" + x + ", " + y + "), distance: "
           + String.format("%.2f", Math.sqrt(x * x + y * y));
    case Circle(Point(int cx, int cy), double r) when r < 1.0
        -> "Tiny circle at (" + cx + "," + cy + ") r=" + r;
    case Circle(Point(int cx, int cy), double r)
        -> "Circle at (" + cx + "," + cy + ") r=" + r
           + ", area=" + String.format("%.2f", Math.PI * r * r);
    default -> "Unknown shape";
};

4. Primitive Narrowing in Patterns

When switching on a wider type like long, you can match against int. The pattern only matches if the value can be losslessly narrowed:

long[] values = { 42L, 100_000L, Long.MAX_VALUE };

for (long value : values) {
    String desc = switch ((Object) value) {
        case int i  -> "Fits in int: " + i;
        case long l -> "Only fits in long: " + l;
        default     -> "other";
    };
}
// 42L           → "Fits in int: 42"
// 100_000L      → "Fits in int: 100000"
// Long.MAX_VALUE → "Only fits in long: 9223372036854775807"

5. Exhaustive Boolean Switch

No default clause needed — the compiler knows boolean is exhaustive:

for (boolean b : new boolean[]{ true, false }) {
    String desc = switch (b) {
        case true  -> "✅ true — affirmative";
        case false -> "❌ false — negative";
        // No default needed!
    };
}

Real-World Use Cases

The companion PrimitivePatternsRealWorldExamples class demonstrates eight production scenarios:

HTTP Status Code Classifier

Replace nested if-else range checks with declarative guarded patterns. Specific cases (like 429 Too Many Requests) naturally come first; ranges follow:

record HttpAction(String category, String logLevel, boolean shouldRetry, String message) {}

static HttpAction classifyHttpStatus(int status) {
    return switch ((Object) status) {
        case int s when s >= 200 && s < 300
            -> new HttpAction("SUCCESS", "DEBUG", false, "Request succeeded");
        case int s when s == 429
            -> new HttpAction("RATE_LIMITED", "WARN", true, "Back off and retry after delay");
        case int s when s == 401 || s == 403
            -> new HttpAction("AUTH_FAILURE", "WARN", false, "Authentication failed");
        case int s when s >= 400 && s < 500
            -> new HttpAction("CLIENT_ERROR", "WARN", false, "Fix the request");
        case int s when s == 502 || s == 503 || s == 504
            -> new HttpAction("INFRA_ERROR", "ERROR", true, "Infrastructure issue — retry");
        case int s when s >= 500
            -> new HttpAction("SERVER_ERROR", "ERROR", true, "Server error — retry");
        default
            -> new HttpAction("UNKNOWN", "WARN", false, "Unexpected status code");
    };
}

Progressive Tax Bracket Calculator

Each income bracket is a self-contained case — easy to read and audit:

String bracket = switch ((Object) income) {
    case double d when d <= 11_600  -> "10%";
    case double d when d <= 47_150  -> "12%";
    case double d when d <= 100_525 -> "22%";
    case double d when d <= 191_950 -> "24%";
    case double d when d <= 243_725 -> "32%";
    case double d when d <= 609_350 -> "35%";
    case double d                   -> "37%";
    default                         -> "unknown";
};

IoT Sensor Alert System

Record destructuring with primitive guards makes multi-field classification declarative:

record SensorReading(String deviceId, double temperature, double humidity, double batteryVoltage) {}

static Alert classifySensorReading(SensorReading reading) {
    return switch (reading) {
        case SensorReading(var id, double t, double h, double bv) when bv < 3.0
            -> new Alert("🔴 CRITICAL", "Battery critically low (" + bv + "V)");
        case SensorReading(var id, double t, double h, double bv) when t > 50.0
            -> new Alert("🔴 CRITICAL", "Extreme temperature (" + t + "C)");
        case SensorReading(var id, double t, double h, double bv) when t < 0
            -> new Alert("🟠 WARNING", "Freezing conditions (" + t + "C)");
        case SensorReading(var id, double t, double h, double bv) when t > 35.0 && h > 70.0
            -> new Alert("🟠 WARNING", "Heat + humidity combo (" + t + "C, " + h + "%)");
        case SensorReading(var id, double t, double h, double bv) when h > 90.0
            -> new Alert("🟡 CAUTION", "Very high humidity (" + h + "%)");
        case SensorReading(var id, double t, double h, double bv)
            -> new Alert("🟢 OK", "All readings normal");
    };
}

Game Achievement Engine

Score thresholds mapping to ranks — each tier is self-documenting:

record PlayerScore(String name, int score) {}
record Achievement(String rank, String badge, int bonusCoins) {}

static Achievement awardAchievement(PlayerScore player) {
    return switch (player) {
        case PlayerScore(var name, int s) when s >= 10_000 -> new Achievement("Legend",  "🏆", 5000);
        case PlayerScore(var name, int s) when s >= 5_000  -> new Achievement("Master",  "⭐", 2000);
        case PlayerScore(var name, int s) when s >= 2_000  -> new Achievement("Expert",  "🥇", 1000);
        case PlayerScore(var name, int s) when s >= 500    -> new Achievement("Skilled", "🥈",  500);
        case PlayerScore(var name, int s) when s >= 100    -> new Achievement("Novice",  "🥉",  100);
        case PlayerScore(var name, int s)                  -> new Achievement("Beginner","🎮",   10);
    };
}

Financial Risk Scoring

Multi-field record patterns make loan decisions auditable by compliance teams:

record LoanApplication(String applicant, int creditScore, double dti, double loanAmount) {}

static RiskAssessment assessRisk(LoanApplication app) {
    return switch (app) {
        case LoanApplication(var n, int cs, double dti, double amt) when cs < 580
            -> new RiskAssessment("🔴 HIGH", "DECLINE", "Credit score below minimum");
        case LoanApplication(var n, int cs, double dti, double amt) when cs < 670 && dti > 0.43
            -> new RiskAssessment("🔴 HIGH", "DECLINE", "Low credit + high debt-to-income");
        case LoanApplication(var n, int cs, double dti, double amt) when dti > 0.50
            -> new RiskAssessment("🟠 MEDIUM", "MANUAL REVIEW", "DTI exceeds 50%");
        case LoanApplication(var n, int cs, double dti, double amt) when cs >= 700 && dti <= 0.43
            -> new RiskAssessment("🟢 LOW", "AUTO-APPROVE", "Strong credit profile");
        case LoanApplication(var n, int cs, double dti, double amt)
            -> new RiskAssessment("🟡 MODERATE", "MANUAL REVIEW", "Doesn't fit standard criteria");
    };
}

Responsive Layout Breakpoints

Server-side rendering picks a layout based on viewport width. Also demonstrates exhaustive boolean switch for RTL detection:

int cols = switch (vp) {
    case Viewport(int w, boolean rtl) when w < 576  -> 1;
    case Viewport(int w, boolean rtl) when w < 768  -> 2;
    case Viewport(int w, boolean rtl) when w < 1200 -> 3;
    case Viewport(int w, boolean rtl)               -> 4;
};

// Exhaustive boolean switch — no default needed!
String direction = switch (vp.isRtl()) {
    case true  -> "right-to-left";
    case false -> "left-to-right";
};

Other Use Cases in the Demo

  • Dynamic config parser — route Object-typed JSON/YAML values by runtime type (int, long, double, boolean, String)
  • Network packet classifier — destructure packet records with port/protocol/size guards for firewall rules

Running the Demo

mvn compile exec:exec \
  -Dexec.mainClass=org.example.preview.PrimitivePatternsDemo

Check out the full source on GitHub.