본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
37장 : 선택 심화 — Java 25 프리뷰 기능

기본형 패턴과 switch

Java 25 세 번째 프리뷰 기본형 패턴으로 `byte` 적합성, `long`·`boolean` `switch`, `record` 구성 요소 변환을 실행하고 선행 축소 변환의 정보 손실을 재현합니다.

JEP 507은 Java 25에서 패턴의 기본형 타입, instanceof, switch를 세 번째 프리뷰로 제공합니다. int instanceof byte small은 형변환 가능 여부만 보는 것이 아니라 현재 값을 정보 손실 없이 byte로 변환할 수 있는지 검사하고 바인딩합니다. long·float·double·booleanswitch 선택자로 사용할 수 있습니다. 모든 소스는 프리뷰 컴파일·실행 플래그가 필요합니다.

패턴 검사와 강제 축소 변환

기본형 패턴의 목적은 안전하고 정확한 변환을 확인하는 것입니다. 아래 코드는 큰 long을 먼저 int로 형변환해 정보를 잃고 난 뒤 패턴 switch를 수행합니다. 4,294,967,297은 int 1로 잘려 출력이 wrong-positive=1이 됩니다.

preview/PrimitiveNarrowingBeforePatternBug.java
public final class PrimitiveNarrowingBeforePatternBug {
    static String classify(long original) {
        int narrowed = (int) original;
        return switch (narrowed) {
            case int value when value > 0 -> "positive:" + value;
            case int value -> "other:" + value;
        };
    }

    public static void main(String[] args) {
        long original = 4_294_967_297L;
        System.out.println("original=" + original);
        System.out.println("wrong-positive=" + classify(original).substring("positive:".length()));
    }
}

원칙은 원래 넓은 타입을 패턴 검사 대상으로 유지하고 instanceof int converted처럼 정확한 변환이 성공했을 때만 좁은 바인딩을 사용하거나 long 분기를 유지하는 것입니다.

instanceof 기본형 패턴

전통적으로 범위 확인 뒤 형변환을 별도 작성해야 했습니다. 프리뷰 패턴은 현재 intbyte로 정확히 표현될 때만 바인딩을 만듭니다.

preview/ExactBytePatternProbe.java
public final class ExactBytePatternProbe {
    static String encode(int value) {
        if (value instanceof byte exact) {
            return "byte:" + exact;
        }
        return "int:" + value;
    }

    public static void main(String[] args) {
        System.out.println(encode(127));
        System.out.println(encode(128));
        System.out.println(encode(-128));
        System.out.println(encode(-129));
    }
}

관찰 결과 127과 -128만 byte 분기를 사용합니다. instanceof byte 검사 형태도 가능하지만 바인딩 형태가 형변환 중복을 없앱니다. 정확성 규칙은 소스 타입, 대상 타입, 실행 값에 따라 달라지므로 경계값 표로 확인합니다.

javac --enable-preview --release 25 -Xlint:preview ExactBytePatternProbe.java
java --enable-preview ExactBytePatternProbe
# byte:127
# int:128
# byte:-128
# int:-129

long·boolean switch 분기

Java 25 프리뷰는 이전 switch 선택자 제한을 넓힙니다. 상수 레이블 타입은 선택자와 맞춰 0L처럼 씁니다. boolean switch는 삼항 연산자보다 여러 문장이 필요한 분기를 식으로 구성할 수 있습니다.

preview/ExpandedPrimitiveSwitch.java
public final class ExpandedPrimitiveSwitch {
    static String bucket(long value) {
        return switch (value) {
            case 0L -> "zero";
            case 1L -> "one";
            case long number when number > 1_000_000_000L -> "large";
            case long number when number > 0 -> "positive";
            case long ignored -> "negative";
        };
    }

    static String access(boolean loggedIn) {
        return switch (loggedIn) {
            case true -> "member";
            case false -> "guest";
        };
    }

    public static void main(String[] args) {
        System.out.println(bucket(0L));
        System.out.println(bucket(5_000_000_000L));
        System.out.println(bucket(-3L));
        System.out.println(access(true));
        System.out.println(access(false));
    }
}

일반 case long을 가드가 있는 case보다 앞에 두면 뒤 case에는 도달할 수 없습니다. 안정 패턴 switch에서 배운 위에서 아래 선택 규칙이 기본형 패턴에도 적용됩니다.

record 패턴의 변환 규칙

record 구성 요소가 double이어도 현재 값을 int로 정확히 변환할 수 있으면 int whole 패턴과 일치합니다. 소수부가 있거나 범위를 벗어나거나 표현 손실이 발생하는 값은 대체 경로인 double 패턴으로 갑니다.

preview/PrimitiveRecordPatternConversion.java
public final class PrimitiveRecordPatternConversion {
    private record Measurement(double value) {}

    static String render(Measurement measurement) {
        return switch (measurement) {
            case Measurement(int whole) -> "whole:" + whole;
            case Measurement(double decimal) -> "decimal:" + decimal;
        };
    }

    public static void main(String[] args) {
        System.out.println(render(new Measurement(25.0)));
        System.out.println(render(new Measurement(25.5)));
        System.out.println(render(new Measurement(3_000_000_000.0)));
    }
}

정확한 변환 실험표

기본형 변환은 단순 최솟값·최댓값 범위만이 아니라 부동소수점이 정수를 정확히 표현하는지, NaN·무한대·부호 있는 0을 어떻게 다루는지 확인해야 합니다. 운영 파서를 프리뷰 패턴 하나로 바꾸기 전 기존 입력 검사 의미와 비교합니다.

preview/PrimitivePatternBoundaryTable.java
public final class PrimitivePatternBoundaryTable {
    static String asInt(double value) {
        if (value instanceof int exact) {
            return "int:" + exact;
        }
        return "double:" + value;
    }

    public static void main(String[] args) {
        double[] values = {
            0.0,
            -0.0,
            42.0,
            42.25,
            Integer.MAX_VALUE,
            (double) Integer.MAX_VALUE + 1,
            Double.NaN,
            Double.POSITIVE_INFINITY
        };
        for (double value : values) {
            System.out.println(asInt(value));
        }
    }
}

예상 산출물에 부동소수점 텍스트 전체를 고정하기보다 어느 caseint인지 표로 기록합니다. 프리뷰 명세가 다음 릴리스에서 바뀌면 같은 점검 프로그램을 다시 실행합니다.

적용 판단

  • 수치 파서와 직렬화 경계의 명시적 입력 검사를 대체하기 전에 오류 메시지 요구 사항을 확인합니다.
  • 정확한 변환 분기가 기존 형변환의 절삭·반올림과 다른지 비교합니다.
  • long·boolean switch가 복잡한 if보다 실제로 읽기 좋은지 검토합니다.
  • 안정 산출물에는 프리뷰 문법을 넣지 않고 실험 결과만 설계 문서에 반영합니다.
  • JDK 26의 네 번째 프리뷰에서 변화한 명세와 마이그레이션을 다시 확인합니다.
instanceof 기본형 패턴이 일치하면 모든 수치 변환이 안전한가요?

해당 소스 값을 대상 타입으로 정확하게 변환할 수 있다는 뜻입니다. 업무 범위, 단위, 오버플로 이후 이미 손실된 값까지 보장하지 않습니다. 원본을 먼저 형변환하거나 계산에서 오버플로가 난 뒤 검사하면 패턴은 잃어버린 정보를 복구하지 못합니다.

연습 문제

0, 양수 int 범위, int를 넘는 양수, 음수를 분리하세요. 원본 long을 미리 형변환하지 않습니다.

해설 보기
preview/LongMinuteClassificationSolution.java
public final class LongMinuteClassificationSolution {
    static String classify(long minutes) {
        return switch (minutes) {
            case 0L -> "zero";
            case int exact when exact > 0 -> "compact:" + exact;
            case long value when value > 0 -> "large:" + value;
            case long value -> "invalid:" + value;
        };
    }

    public static void main(String[] args) {
        System.out.println(classify(45L));
        System.out.println(classify((long) Integer.MAX_VALUE + 1));
        System.out.println(classify(-1L));
    }
}

종료 기준은 compact:45, large:2147483648, invalid:-1이며 컴파일·실행 모두 프리뷰 플래그를 사용합니다.