일단 자바 17에서는 하단 코드는 에러가 발생한다.
Object obj = ...
switch (obj) {
case String s -> System.out.println(s.toLowerCase());
case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
...
}
왜냐하면 모든 케이스가 첫번째 case에 흡수되기 때문에 2번째 case는 도달할 수 없기 때문이다. 하지만
Object obj = ...
switch (obj) {
case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
case "foobar" -> System.out.println("baz");
...
}
이렇게 guarded pattern과 상수를 조합한 경우엔 여전히 두번째 케이스에 도달하는 경우가 없음에도 에러가 발생하지 않았다. 하지만 자바18에선 에러가 발생하도록 개선됐다.
예를 들어 하단과 같은 sealed 클래스가 있다면
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
아래 코드는 컴파일되지 않을것이다.
I<Integer> i = ...
switch (i) {
case A<Integer> a -> System.out.println("It's an A"); // not compilable
case B<Integer> b -> System.out.println("It's a B");
}
왜냐하면 A클래스는 String타입만을 취하는 클래스이기 때문이다. 그래서 이렇게 고치면
I<Integer> i = ...
switch (i) {
case B<Integer> b -> System.out.println("It's a B");
}
the switch statement does not cover all possible input values
라는 경고 문구가 발생했다. 이것은 버그였기 때문에 수정됐다.
사전 할당된 HashMap을 만드는 새로운 메서드 제공
기존 사전 할당된 데이터 구조를 만드려면 인자로 int를 넘겨주면 됐다.
List<String> list = new ArrayList<>(120);
그럼 사전에 120개의 공간이 할당된 ArrayList가 생성된다. HashMap의 경우
Map<String, Integer> map = new HashMap<>(120);
이렇게 하면 될 것 같지만, HashMap의 기본 적재 계수(default load factor) 는 0.75 라는 것을 알아야 한다.
이 말은 HashMap이 75%만큼 차면 2배의 크기로 재생성(재해싱) 된다는 의미이다. 이렇게 함으로써 모든 요소가 공평하게 HashMap의 버킷에 들어가고 가급적 적은 버킷에 1개 이상의 요소를 넣을 수 있기 때문이다. 그래서 120을 인자로 넘겨주면 90개밖에 맵핑할 수 없다. 120개를 맵핑하려면 160개를 넣어줘야 한다.
그래서 이걸 자동으로 계산해주는 메서드를 제공한다.
Map<String, Integer> map = HashMap.newHashMap(120);
//내부 코드
public static <K, V> HashMap<K, V> newHashMap(int numMappings) {
return new HashMap<>(calculateHashMapCapacity(numMappings));
}
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static int calculateHashMapCapacity(int numMappings) {
return (int) Math.ceil(numMappings / (double) DEFAULT_LOAD_FACTOR);
}
스위치 패턴 매칭 (써드 프리뷰)
자바 17의 guarded pattern
은 &&
연산자를 사용하여 정의했다.
switch (obj) {
case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
case String s -> System.out.println(s.toLowerCase());
case Integer i -> System.out.println(i * i);
default -> {}
}
이걸 맥락에 맞게 when으로 고쳤다. (&& 연산자는 삭제됌)
switch (obj) {
case String s when s.length() > 5 -> System.out.println(s.toUpperCase());
case String s -> System.out.println(s.toLowerCase());
case Integer i -> System.out.println(i * i);
default -> {}
}
레코드패턴 (프리뷰)
기존 페코드 선언 방법은 아래와 같다.
public record Position(int x, int y) {}
선언 후 다음과 같이 사용한다.
private void print(Object object) {
if (object instanceof Position position) {
System.out.println("object is a position, x = " + position.x()
+ ", y = " + position.y());
}
// else ...
}
이것마저 줄여서, instanceof
뒤에 바로 선언할 수 있게 변경됐다.
private void print(Object object) {
if (object instanceof Position(int x, int y)) {
System.out.println("object is a position, x = " + x + ", y = " + y);
}
// else ...
}
스위치 문도 사용할 수 있게 변경됐다.
//기존 방식에서
private void print(Object object) {
switch (object) {
case Position position
-> System.out.println("object is a position, x = " + position.x()
+ ", y = " + position.y());
// other cases ...
}
}
//바로 선언 가능
private void print(Object object) {
switch (object) {
case Position(int x, int y)
-> System.out.println("object is a position, x = " + x + ", y = " + y);
// other cases ...
}
}
중첩 레코드 패턴도 구현할 수 있다. 아래는 인자로 레코드를 넘겨주는 레코드다.
public record Path(Position from, Position to) {}
이 또한 instanceof와 스위치문에서 사용할 수 있다.
private void print(Object object) {
if (object instanceof Path(Position(int x1, int y1), Position(int x2, int y2))) {
System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1
+ ", x2 = " + x2 + ", y2 = " + y2);
}
// else ...
}
private void print(Object object) {
switch (object) {
case Path(Position(int x1, int y1), Position(int x2, int y2))
-> System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1
+ ", x2 = " + x2 + ", y2 = " + y2);
// other cases ...
}
}
레코드패턴 (세컨드 프리뷰)
instanceof 구문 제네릭 레코드 패턴의 타입 인자 추론
설명하기 위해, 제네릭 인터페이스와 이를 구현한 2개의 레코드가 있다고 하자.
interface Multi<T> {}
record Tuple<T>(T t1, T t2) implements Multi<T> {}
record Triple<T>(T t1, T t2, T t3) implements Multi<T> {}
기존에는, 인터페이스 객체에 따라 개별로 인스턴스를 체크해준 뒤에 기능을 구현해줘야 했다.
Multi<String> multi = ...
if (multi instanceof Tuple<String>(var s1, var s2)) {
System.out.println("Tuple: " + s1 + ", " + s2);
} else if (multi instanceof Triple<String>(var s1, var s2, var s3)) {
System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3);
}
자바20에서는 컴파일러가 알아서 가능한 타입인지 추론해주기 때문에 바로 레코드를 정의해줄 수 있다.
if (multi instanceof Tuple(var s1, var s2)) {
System.out.println("Tuple: " + s1 + ", " + s2);
} else if (multi instanceof Triple(var s1, var s2, var s3)) {
System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3);
}
다만 이 경우와 같은 원시유형(raw types)은 컴파일러가 타입 정보를 무시하기 때문에 좀 더 일관적인 구문을 위해 아래와 같이 서언해주기도 한다.
if (multi instanceof Tuple<>(var s1, var s2)) {
System.out.println("Tuple: " + s1 + ", " + s2);
} else if (multi instanceof Triple<>(var s1, var s2, var s3)) {
System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3);
}
for 루프 레코드 패턴
기존 방식은 다음과 같다.
List<Position> positions = ...
for (Position p : positions) {
System.out.printf("(%d, %d)%n", p.x(), p.y());
}
자바20에서는 매개변수를 바로 선언해줘서 각 매개변수에 직접 접근할 수 있다.
for (Position(int x, int y) : positions) {
System.out.printf("(%d, %d)%n", x, y);
}
이름있는 레코드 패턴 지원 삭제
Object object = new Position(4, 3);
// 1. Pattern Matching for instanceof
if (object instanceof Position p) {
System.out.println("object is a position, p.x = " + p.x() + ", p.y = " + p.y());
}
// 2. Record Pattern
if (object instanceof Position(int x, int y)) {
System.out.println("object is a position, x = " + x + ", y = " + y);
}
// 3. Named Record Pattern <- 필요 없기 때문에 삭제함
if (object instanceof Position(int x, int y) p) {
System.out.println("object is a position, p.x = " + p.x() + ", p.y = " + p.y()
+ ", x = " + x + ", y = " + y);
}
스위치 패턴 매칭(포스 프리뷰)
런타임에 스위치가 모든 경우의 수를 포함하고 있을 경우 IncompatibleClassChangeError
보다 MatchException
을 던진다.
예를 들어, Shapre 인터페이스를 선언하고 이를 상속하는 레코드를 만든 다음 스위치문으로 모두 구현했다고 하자.
public sealed interface Shape permits Rectangle, Circle {}
public record Rectangle(Position topLeft, Position bottomRight) implements Shape {}
public record Circle(Position center, int radius) implements Shape {}
public class ShapeDebugger {
public static void debug(Shape shape) {
switch (shape) {
case Rectangle r -> System.out.println(
"Rectangle: top left = " + r.topLeft() + "; bottom right = " + r.bottomRight());
case Circle c -> System.out.println(
"Circle: center = " + c.center() + "; radius = " + c.radius());
}
}
}
컴파일러는 sealed 인터페이스인 Shape에서 허용한 모든 경우의 수를 스위치문에서 구현했다는걸 인식할 수 있다. 이 스위치 식을 exhaustive(완전한) 하다고 한다.
만약 이 상태에서, Oval이라는 레코드를 추가하면 어떻게 되는지 확인해보자.
public sealed interface Shape permits Rectangle, Circle, Oval {}
public record Oval(Position center, int width, int height) implements Shape {}
public class Main {
public static void main(String[] args) {
var rectangle = new Rectangle(new Position(10, 10), new Position(50, 50));
ShapeDebugger.debug(rectangle);
var circle = new Circle(new Position(30, 30), 10);
ShapeDebugger.debug(circle);
var oval = new Oval(new Position(60, 60), 20, 10);
ShapeDebugger.debug(oval);
}
}
IDE를 사용해서 프로그래밍 한다면 스위치식에서 Oval 케이스를 처리하지 못한다고 즉시 알려줄 것이다.
하지만 IDE 없이 프로그램을 실행한다면, 스위치 식은 Oval을 처리하지 않은 상태이므로 MatchException
을 던진다.
기존 스위치 문에서는 제네릭 레코드를 다음과 같이 사용했다.
Multi<String> multi = ...
switch(multi) {
case Tuple<String>(var s1, var s2) -> System.out.println(
"Tuple: " + s1 + ", " + s2);
case Triple<String>(var s1, var s2, var s3) -> System.out.println(
"Triple: " + s1 + ", " + s2 + ", " + s3);
...
}
자바20에서는 구문을 보고 알아서 타입을 추론해주기 대문에 생략할 수 있다.
switch(multi) {
case Tuple(var s1, var s2) -> System.out.println(
"Tuple: " + s1 + ", " + s2);
case Triple(var s1, var s2, var s3) -> System.out.println(
"Triple: " + s1 + ", " + s2 + ", " + s3);
...
}