자바

jdk pattern matching for switch

알쓸개잡 2023. 8. 26.

jdk 17 이전 릴리즈 에서는 switch 문의 선택자 표현식은 숫자, 문자열, Enum 상수로만 평가되었고 case 레이블은 상수만 지원을 하였습니다. jdk 17 릴리즈 부터는 switch 문의 선택자 표현식은 모든 타입이 될 수 있으면 case 레이블에는 패턴이 지원되도록 개선 되었습니다.

따라서 switch statement(문) 혹은 switch expression(표현식) 의 case 레이블에 pattern matching 을 적용할 수 있게 되었습니다. 이번 포스팅에서는 jdk pattern matching 을 swich 문과 표현식에 적용하는 방법을 기술합니다.

 

jdk pattern matching for switch 이력

  • jdk 17 에서 preview 기능으로써 JEP 406 에서 제안됨.
  • jdk 18 에서 re-preview 기능으로써 JEP 420 에서 제안됨.
  • jdk 19 에서 re-preview 기능으로써 JEP 427 에서 제안됨.

 

 

jdk pattern matching for instanceof

jdk pattern matching 은 jdk 14 버전부터 preview 기능으로 처음 도입 되었습니다. 참고로 instanceof 연산자에 pattern matching 을 적용하는 방법은 아래 포스팅을 참고하시면 되겠습니다.

2023.08.26 - [자바] - jdk pattern matching for instanceof

우선 아래 코드는 instanceof 연산자에 pattern matching 을 적용한 코드 입니다.

public interface Shape {
    void print();
}
public class Rectangle implements Shape {}
public class Circle implements Shape {}
public class Triangle implements Shape {}
...

public void printShape(Shape shape) {
    if (shape instanceof Rectangle r) {
        r.print();
    } else if (shape instanceof Triangle t) {
        t.print();
    } else if (shape instanceof Circle c) {
        c.print();
    } else {
        throw new IllegalArgumentException();
    }
}

 

pattern matching for switch

위 코드를 switch 문의 pattern matching 을 적용하면 아래와 같은 코드가 됩니다.

아래 코드는 Intellij IDE 에서 jdk 17 의 language level 은 17(Preview) - Pattern matching for switch 셋팅으로 컴파일 하였습니다.

 

void printShapeSwitch1(Shape shape) {
    switch(shape) {
        case Rectangle r: {
            r.print();
            break;
        }
        case Triangle t:
            t.print();
            break;
        case Circle c:
            c.print();
            break;
        case null:
            System.out.println("shape is null");
            break;
        default:
            throw new IllegalArgumentException();
    }
}

void printShapeSwitch2(Shape shape) {
    switch (shape) {
        case Rectangle r -> r.print();
        case Triangle t -> t.print();
        case Circle c -> c.print();
        case null -> System.out.println("shape is null");
        default -> throw new IllegalArgumentException();
    }
}

printShapeSwitch1() 과 printShapeSwitch2() 는 동일한 기능을 수행합니다.

case 레이블에 null 도 넣을 수 있습니다. case 레이블에 null 대신 Object 를 넣어도 됩니다.

pattern matching 은 switch expression 에서도 적용할 수 있습니다.

public interface Shape {
	void print();
	default String shapeName() {
		return this.getClass().getSimpleName();
	}
}

public class Circle implements Shape{}
public class Rectangle implements Shape{}
public class Triangle implements Shape{}
....

Shape shape = new Rectangle();
String shapeName = shapeName(shape);
System.out.println("shape name: " + shapeName);

....

String shapeName(Shape shape) {
    return switch (shape) {
        case Rectangle r -> r.shapeName();
        case Triangle t -> t.shapeName();
        case Circle c -> c.shapeName();
        default -> throw new IllegalArgumentException();
    };
}

switch selector 표현식 타입에는 어떠한 타입도 가능합니다.

public interface Shape {
    void print();
    default String shapeName() {
    return this.getClass().getSimpleName();
    }
}

public class Circle implements Shape{}
public class Rectangle implements Shape{}
public class Triangle implements Shape{}
....
public enum Sports {
    BASEBALL("야구"),
    BASKETBALL("농구"),
    SOCCER("축구")
    ;

    private final String name;
    Sports(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}
....
@Test
void switch_selector_test() {
    Object obj = Sports.BASEBALL;
    String switchExpressionValue = objectName(obj);
    Assertions.assertThat(switchExpressionValue)
        .isEqualTo(Sports.BASEBALL.getName());

    Shape shape = new Circle();
    switchExpressionValue = objectName(shape);
    Assertions.assertThat(switchExpressionValue)
        .isEqualTo(shape.shapeName());

    String string = "switch selector";
    switchExpressionValue = objectName(string);
    Assertions.assertThat(switchExpressionValue)
        .isEqualTo(string.toUpperCase());
}
....

String objectName(Object object) {
    return switch (object) {
        case Sports sports -> sports.getName();
        case Rectangle r -> r.shapeName();
        case Circle c -> c.shapeName();
        case Triangle t -> t.shapeName();
        case String s -> s.toUpperCase();
        default -> "other type object";
    };
}

위 switch_selector_test() 테스트 코드의 테스트는 모두 정상적으로 통과 합니다.

 

pattern label Dominance (지배력)

case 레이블은 switch 블록에 표시되는 순서대로 테스트 되는데 이 때 여러 개의 패턴 레이블이 일치하는 경우 컴파일 오류가 발생합니다.

 

String objectName(Object object) {
    return switch (object) {
        case Sports sports -> sports.getName();
        //object 가 Shape 타입의 객체라면 무조건 여기서 패턴은 일치합니다.
        case Shape s -> s.shapeName();
        //상단 case 레이블에서 패턴이 일치 하기 때문에
        //this case label is dominated by a preceding case label 오류가 발생합니다!!
        case Rectangle r -> r.shapeName();
        //상단 case 레이블에서 패턴이 일치 하기 때문에
        //this case label is dominated by a preceding case label 오류가 발생합니다!!
        case Circle c -> c.shapeName();
        //상단 case 레이블에서 패턴이 일치 하기 때문에
        //this case label is dominated by a preceding case label 오류가 발생합니다!!
        case Triangle t -> t.shapeName();
        case String s -> s.toUpperCase();
        default -> "other type object";
    };
}

========================================================================

//아래 테스트 코드는 정상 통과 됩니다.
@Test
void switch_dominance_test() {
    Shape shape = new Circle();
    switchExpressionValue = objectName(shape);
    Assertions.assertThat(switchExpressionValue)
	    .isEqualTo(shape.shapeName());
}
        
String objectName(Object object) {
    return switch (object) {
        case Sports sports -> sports.getName();
        //object 가 Circle 객체인 경우 아래 case 레이블은 패턴이 일치하지 않습니다.
        case Rectangle r -> r.shapeName();
        //object 가 Circle 객체인 경우 아래 case 레이블은 패턴이 일치하지 않습니다.
        case Triangle t -> t.shapeName();
        //object 가 Circle 객체인 경우 Shape 의 구현체 이므로
        //아래 case 레이블은 패턴이 일치합니다.
        case Shape s -> s.shapeName();
        case String s -> s.toUpperCase();
        default -> "other type object";
    };
}

 

type coverage in switch Expression

switch expression(표현식) 은 모든 case 에 대해서 커버가 되어야 합니다. 아래 switch expression 코드는 모든 case 에 대해서 고려되지 않았기 때문에 컴파일 오류가 발생합니다.

@Test
void switch_expression_coverage_test() {
    String string = "switch expression coverage";
    Integer length = length(string);
    Assertions.assertThat(length).isEqualTo(string.length());
}

Integer length(Object object) {
    return switch(object) {
        case String s -> s.length();
        case Integer i -> i;
        //String 타입과 Integer 타입만 case 에서 고려되었기 때문에
        //the switch expression does not cover all possible input values
        //오류가 발생합니다.!!
    };
}

===============================================

Integer length(Object object) {
    return switch(object) {
        case String s -> s.length();
        case Integer i -> i;
        //default label 을 추가하여 모든 case에 고려가 되어 정상 동작합니다.
        default -> throw new IllegalArgumentException(); 
    };
}

===============================================

Integer length(Object object) {
    return switch(object) {
        case String s -> s.length();
        case Integer i -> i;
        //모든 객체는 Object 를 상속하므로 모든 case에 고려되어 정상 동작합니다.
        //단, case Object 가 case String, case Integer 보다 상단에 선언되면 
        //Dominance 컴파일 오류가 발생합니다!!
        case Object o -> 0;
    };
}

위의 코드에서 알 수 있듯이 switch expression 에는 default 문이 반드시 있어야 합니다. 단, Enum 과 같이 미리 정의된 상수가 있는 타입의 경우에는 컴파일러에서 default 문이 없는 경우 암묵적으로 default 를 추가해 줍니다. 이에 대한 내용은 아래 포스팅의 Exhaustiveness (완전성) 항목을 참고하시면 도움이 됩니다.

2023.08.20 - [자바] - java switch expression - from jdk 14

 

댓글

💲 추천 글