-
switch를 위한 패턴 매칭공부/자바 2023. 7. 29. 18:27728x90
개별 작업이 할당된 다양한 패턴을 테스트하는 switch 구문을 허용한다.
목표
기존의 switch 표현식과 문장이 변경 없이 컴파일 되고 동일한 의미로 실행될 수 있도록 switch 표현식과 문장의 표현력과 적용 가능성을 확장하기 위해, case 라벨에 패턴을 사용할 수 있도록 한다.
또한, 필요한 경우 switch의 기존 null 방어성을 완화시킨다.
두 가지 새로운 패턴 유형을 도입한다: 먼저, guarded patterns로 임의의 부울 표현식을 사용하여 패턴 매칭 로직을 더 정교하게 할 수 있다. 또한 괄호로 둘러싼 패턴을 도입하여 몇 가지 구문 분석 모호성을 해결한다.
기존의 switch 표현식과 문장이 case 라벨이 패턴인 경우와 기존 상수인 경우에 대해 동일한 방식으로 동작하도록 보장한다.
전통적인 switch 구조와 별도로 패턴 매칭 성격의 새로운 switch와 유사한 표현식이나 문장을 도입하지 않는다.
패턴을 사용하는 경우와 기존의 상수를 사용하는 경우에 switch 표현식이나 문ㄴ장이 동작하는 방식을 다르게 만들지 않는다.동기
자바 16에서 _instanceof 연산자_를 타입 패턴을 사용하여 패턴 매칭을 수행할 수 있도록 확장하였다. 이 작은 확장으로 기존에 익숙한 _instance of 와 cast_를 사용한 문법이 간소화 되었다.
// Old code if (o instanceof String) { String s = (String)o; ... use s ... } // New code if (o instanceof String s) { ... use s ... }
자바에서는 종종 변수인 o를 여러 가지 대안과 비교하고 싶은 경우가 있다. 자바는 switch문과 자바 14부터 switch 표현식을 사용하여 다중 비교를 지원하지만, 유감스럽게도 switch는 매우 제한적이다.
switch문은 몇 가지 유형의 값(숫자형, enum, String)에 대해서만 사용할 수 있고, 상수에 대한 동등성만 테스트 할 수 있다.
따라서 다른 유형의 값을 테스트 할 때는 if... else 문을 사용해야 한다:static String formatter(Object o) { String formatted = "unknown"; if (o instanceof Integer i) { formatted = String.format("int %d", i); } else if (o instanceof Long l) { formatted = String.format("long %d", l); } else if (o instanceof Double d) { formatted = String.format("double %f", d); } else if (o instanceof String s) { formatted = String.format("String %s", s); } return formatted; }
이 코드는 instanceof 표현식을 사용함으로써 이점을 얻지만 완벽하지는 않다. 이 접근 방식은 코딩 오류가 숨어있을 수도 있다.
if..else문 체인의 각 분기에서 formmated에 할당하는 것을 원하지만 컴파일러가 이 식을 식별하고 확인할 방법이 없어, formatted에 할당하지 않을 경우 버그가 발생한다.또한 위 코드는 최적화 할 수 없다.switch문은 패턴 매칭에 완벽하게 맞는 구조로 모든 유형에 사용할 수 있도록 확장하고, case가 단순한 상수뿐만 아니라 패턴을 허용하면, 위의 코드를 더 명확하고 신뢰성 있게 다시 작성할 수 있다.
static String formatterPatternSwitch(Object o) { return switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; }
패턴이 있는 case 라벨은 선택자 표현식 o의 값과 패턴이 일치하는 경우 해당 case 라벨이 매치된다. 파라미터 o는 다음 조건들 중 최대 하나와 일치하고, 이를 확인하여 해당하는 블록을 실행한다. 이에 따라 최적화할 수 있다.
패턴 매칭 null
switch 문과 표현식은 선택자 표현식이 null을 평가하면 NullPointerException을 발생시켜, null을 테스트하기 위해서는 switch 밖에서 처리해야 한다.
static void testFruits(String s) { if (s == null) { System.out.println("NULL!"); return; } switch (s) { case "Peach", "WaterMelon" -> System.out.println("여름과일!!"); default -> System.out.println("Ok"); } }
switch가 몇 가지의 참조 유형만 지원할 때는 이러한 접근이 합리적이었지마, switch가 모든 유형의 선택 표현식을 허용하고 case 라벨이 타입 패턴을 가질 수 있으면, 독립적인 null테스트는 불필요하고, 실수 가능성을 늘려 내부에서 null테스트를 통합하는것이 나을 것이다.
static void testFruits(String s) { switch (s) { case null -> System.out.println("NULL!"); case "Peach", "WaterMelon" -> System.out.println("여름과일!!"); default -> System.out.println("Ok"); } }
선택자 표현식 값이 null인 경우 switch의 동작은 항상 case 라벨에 의해 결정된다. case null 이 있는 경우, switch는 해당 라벨과 관련된코드를 실행한다. case null 이 없는 경우, switch는 이전과 마찬가지로 NullPointerException을 발생시킨다. null을 다른 case 라벨과 동일하게 처리 가능하다.
static void testFruits(String s) { switch (s) { case null, "Peach", "WaterMelon" -> System.out.println("여름과일!!"); default -> System.out.println("Ok"); } }
스위치 패턴 정교화
class Fruits {} class SpringrFruits extends Fruits {} class SummerFruits extends Fruits { boolean favoriteFruits() { ... } } static void testFavFruits(Fruits f) { switch (f) { case null: break; case SummerFruits s: if (s.favoriteFruits()) { System.out.println("Favorite!!"); break; } default: System.out.println("It's a fruits"); } }
정교화 하기 위해 case 라벨의 기능을 확장하는 대신에, 패턴 언어 자체를 확장할 수 있다. p&&b라고 쓰는 guarded pattern이라는 새로운 패턴 유형을 추가한다. p를 임의의 부울 표현식 b로 정교화 할 수 있다.
switch문에서 fall-through를 사용하지 않도록 하여, 간결한 화살표 스타일 (->) 규칙을 사용할 수 있도록 한다.static void testFavFruits(Fruits f) { switch (f) { case SummerFruits s&&s.favorite -> System.out.println("Favorite!!"); case SummerFruits s -> System.out.println("Summer!"); default -> System.out.println("It's a fruits"); } }
설명
switch문과 표현식을 두가지 방식으로 강화한다.
- 상수뿐만 아니라 패턴을 포함하여 case 라벨을 확장
- 두 가지 새로운 패턴 유형을 도입
- guarded patterns
- parenthesized patterns
switch라벨에서 패턴 활용
패턴인 p를 가진 새로운 case p switch 라벨을 도입한다. 다른 점은 이제 패턴을 가진 case 라벨에 대해, 이 선택이 동등성 검사가 아닌 패턴 매칭에 의해 결정된다는 것이다.
static String formatterPatternSwitch(Object o) { return switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; }
case 라벨이 패턴을 가질 경우 4가지 문제가 있다.
- 향상된 타입 체크
- switch 표현식과 문장의 완전성
- 패턴 변수 선언의 범위
- null 처리
1. 향상된 타입 체크
- 선택자 표현식의 타입 지정
switch에서 패턴을 지원하는 것은 현재 선택자 표현식의 타입에 대한 제한을 완화할 수 있음을 의미한다. 현재 일반적인 switch의 선택자 표현식은 정수 기본 타입(char, byte, short, int)또는 해당 박싱된 형태(Character, Byte, Short, Integer), String 또는 enum 타입이어야 한다.
예를 들어, 다음과 같은 switch 패턴에서 선택자 표현식 o는 클래스 타입, enum 타입, 레코드 타입, 배열 타입과 매치된다.
record Point(int i, int j) {} enum Color { RED, GREEN, BLUE; } static void typeTester(Object o) { switch (o) { case null -> System.out.println("null"); case String s -> System.out.println("String"); case Color c -> System.out.println("Color with " + Color.values().length + " values"); case Point p -> System.out.println("Record class: " + p.toString()); case int[] ia -> System.out.println("Array of ints of length" + ia.length); default -> System.out.println("Something else"); } }
- 패턴 라벨의 우선순위
선택자 표현식이 switch 블록 내에서 여러 라벨과 일치 하는 경우가 가능하다.
static void error(Object o) { switch(o) { case CharSequence cs -> System.out.println("A sequence of length " + cs.length()); case String s -> // Error - pattern is dominated by previous pattern System.out.println("A string: " + s); default -> { break; } } }
첫 번째 패턴 라벨인 case CharSequence cs가 두번째 패턴 라벨인 case String s보다 우선 한다. 형태가 case p인 패턴 라벨이 있는데, 여기서 p는 선택자 표현식의 타입에 대한 전체 패턴일 경우, case null을 우선한다.
형태가 case p인 패턴 라벨은 case p&&e 라벨을 우선한다.
컴파일러는 모든 패턴 라벨을 검사한다. 스위치 블록 내 패턴 라벨이 이전의 패턴 라벨에 의해 우선된다면, 컴파일 타임 에러가 발생한다.
이러한 우선순위 요구 사항은 스위치 블록이 타입 패턴 case 라벨만을 포함하는 경우 이러한 라벨들이 하위 타입 순서로 나타나도록 보장한다.
우선순위 개념은 try문의 catch절에 대한. 조건과 유사하다.
또한 switch 블록에 두 개 이상의 match-all switch 라벨이 있는 경우에도 컴파일 타임 에러가 발생한다.2. switch 표현식과 문장의 완전성
switch 표현식은 선택자 표현식의 모든 값들이 스위치 블록에서 처리되어야 한다. 하지만 패턴 switch 표현식에서는 switch 블록의 타입 커버리지 개념을 정의한다. 이를 통해 switch 블록이 선택자 표현식의 모든 타입을 다루는지 확인한다.
static int coverage(Object o) { return switch (o) { // Error - incomplete case String s -> s.length(); }; }
스위치 블록에는 case String s 하나의 라벨만 있어, String의 하위 타입인 경우 어떤 값이든 매치한다. switch 블록의 타입 커버리지가 선택자 표현식의 타입을 포함하지 않았기 때문에 완전하지 않다.
static int coverage(Object o) { return switch (o) { // Error - incomplete case String s -> s.length(); case Integer i -> i; }; }
타입 커버리지는 모든 String의 하위 타입과 모든 Integerd의 하위 타입의 집합이지만, 선택자 표현식의 타입을 포함하지 않아 컴파일 타임 에러를 발생시킨다.
static int coverage(Object o) { return switch (o) { case String s -> s.length(); case Integer i -> i; default -> 0; }; }
default 타입 커버리지는 모든 타입을 포함하여, 이제 에러가 없다. 선택자 표현식의 타입이 sealed 클래스인 경우, 타입 커버리지 검사는 sealed 클래스의 permits 절을 고려하여 switch 블록이 완전한지 여부를 판단할 수 있다.
3. 패턴 변수 선언의 범위
패턴 변수는 패턴에 의해 선언되는 로컬 변숫이다. 패턴 변수의 선언은 특이한 점이 있으며, 그 범위는 플로우에 따라 달라진다.
static void test(Object o) { if ((o instanceof String s) && s.length() > 3) { System.out.println(s); } else { System.out.println("Not a string"); } }
변수 s 선언은 && 표현식의 오른쪽 피연산자와 "then" 블록에서 유효한 범위에 있다. 그러나 "else" 블록에서는 유효하지 않다. "else" 블록으로 제어가 전달되기 위해서는 패턴매칭이 실패해야 되는데 이 경우 패턴 변수가 초기화 되지 않을 수도 있다.
패턴 변수 선언에 대해 개념을 확장하여 두가지 새로운 규칙을 포함하는 case 라벨에서의 패턴 선언을 포괄한다.
- 패턴 변수 선언의 범위는 화살표 오른쪽에 나타나는 표현식, 블록 또는 throw문을 포함한다.
- 더 이상 switch 라벨이 없는 switch 라벨 문장 그룹 내에서 발생하는 case 라벨의 패턴 변수 선언은 해당 문장 그룹의 블록 문장을 포함한다.
static void test(Object o) { switch (o) { case Character c -> { if (c.charValue() == 7) { System.out.println("Ding!"); } System.out.println("Character"); } case Integer i -> throw new IllegalStateException("Invalid Integer argument of value " + i.intValue()); default -> { break; } } }
4. null 처리
switch는 선택자 표현식이 null로 평가되면 NullPointerException을 발생시킨다.
null 값에 의미가 있다고 가정할 때 switch 페턴을 더 null 친화적으로 만들기 위해 두 가지 변경사항이 있다.- 새로운 null 라벨을 도입한다.
- 선택자 표현식의 타입에 대해 전체 패턴인 경우 패턴 case 라벨에 나타나면, 해당 라벨은 선택자 표현식의 값이 null일 때도 일치한다.
static void test(Object o) { switch (o) { case null -> throw new NullPointerException(); case String s -> System.out.println("String: "+s); case Integer i -> System.out.println("Integer"); default -> System.out.println("default"); } }
Guarded and parenthesized patterns
static void test(Object o) { switch (o) { case String s && (s.length() == 1) -> ... case String s -> ... ... } }
패턴 매치가 성공한 후 종종 매치결과를 추가로 테스트 해야되는 경우가 있는데, 테스트 라벨을 추가하는 대신 guarded patten이라고 불리는 패턴과 불리언 표현식을 조합하는 패턴언어로 개선한다.
guarded pattern은 p&&e p가 패턴이고 e가 불리언 표현식인 형태이다. 서브 표현식에서 선언되지 않은 모든 로컬 변수, 형식 매개변수 또는 예외 매개변수는 fina이거나 effectively final이어야 한다. 패턴 변수의 합집합을 도입하여 String s&&(s.length()>1)과 같은 패턴을 사용할 수 있다.728x90반응형'공부 > 자바' 카테고리의 다른 글
자바 instanceof (0) 2023.07.29 외부함수와 메모리 API (0) 2023.07.29 17 preview version 안되는 오류 (0) 2023.07.29 JDK 내부의 강력한 캡슐화 (0) 2023.07.27 스프링부트 웹레이어에서 테스트 (0) 2023.07.27