새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

9 분 소요

자바 12 버전부터 17버전까지 자바 feature를 정리했습니다.

자바 18 ~ 21 feature 정리Permalink

자바 21Permalink

record patternPermalink

record class(DTO)를 instanceof pattern matching과 함께 사용할 때 내부 필드에 바로 접근할 수 있는 기능입니다.

  • record class는 자바 16에 추가된 데이터 전송 전용 객체입니다.

    public record Member(
    	//component private final 기본 장착
        String name,
        int age
    ){
        static field;
        static method;
        instance method;
          
        //auto override
        toString,equals,hashCode;
              
        //get
    }
    
  • instanceof pattern matching : 형변환후 새 변수에 할당하는 과정을 생략할 수 있습니다.

    public void checkPerson(Person person){
        if(!(person instanceof Member member)){
            return;
        }
        // use member 
    }
    

record pattern을 사용하여 Member 객체에 바로 접근할 수 있습니다

public class Main {
    record Member(String name, int age) {
        public Member {
            if (age < 0) {
                throw new IllegalArgumentException("나이는 0보다 커야 합니다.");
            }
        }
    }
    public static void main(String[] args) {
        Member member = new Member("홍길동", 20);
        printMember(member);
    }
    public static void printMember(Object member){
        if (member instanceof Member(String name, int age)){
            System.out.println("name = " + name);
            System.out.println("age = " + age);
        }
    }
}

매개변수의 자료형을 간단하게 var로 표현할 수 있습니다.

public static void printMember(Object member){
    if (member instanceof Member(var name, var age)){
        System.out.println("name = " + name);
        System.out.println("age = " + age);
    }
}
중첩 record patternPermalink

내부 클래스에서도 적용이 가능합니다.

public class Main {
    record Address(String city, String street, int zipCode) {}

    record Member(String name, int age, Address address) {}

    record Family(Member father, Member mother) {}

    public static void main(String[] args) {
        // address create
        Address address1 = new Address("서울", "강남", 12345);
        Address address2 = new Address("잠실", "잠실51번길", 12345);

        Family family = new Family(new Member("아빠", 50,address1), new Member("엄마", 45,address2));
        printMember(family);
    }

    public static void printMember(Object member) {
        if (member instanceof Member(String name, int age, Address address)) {
            System.out.println("name = " + name);
            System.out.println("age = " + age);
            System.out.println("address = " + address);
        }
        if (member instanceof Family(
                Member(String fatherName, int fatherAge, Address(String city1, String street2, int zipCode2)),
                Member(String motherName, int motherAge, Address(String city2, String street3, int zipCode3))
        )) {
            System.out.println("fatherName = " + fatherName);
            System.out.println("fatherAge = " + fatherAge);
            System.out.println("city1 = " + city1);
            System.out.println("street2 = " + street2);
            System.out.println("zipCode3 = " + zipCode2);
  				:			:				:
        }
    }
}

위 코드를 보면 중첩안에 다시 중첩 클래스도 사용가능합니다.

switch pattern matchingPermalink

자바 14에 추가된 switch Expressionswitch문이 statement가 아니라 expression으로 변경되어 아래와 같이 작성이 가능했습니다.

private int calculateStudyCafeFee(int hours){
    return switch(hours){
        case 1,2 -> 2_000;
        case 3,4 -> 4_000;
        case 5,6 -> 5_000;
        default -> 7_000;
    };
}

switch문을 하나의 값으로 사용할 수 있게 되었고 -> 화살표를 통해서 간단하게 반환할 값을 선택할 수 있었습니다.

switch(selector): selector는 char,byte,short,int,String,Enum만 가능했습니다.

객체는 사용할 수 없고, selector는 분기를 칠 수 있는 값도 상수에 대해서만 가능했습니다

reference type 허용Permalink

자바 21부터 selector 안에 어떤 reference type도 들어갈 수 있습니다.

추가로 case 뒤에 패턴 매칭이 올 수 있습니다.

즉, case Member member 와 같은 구문도 사용이 가능하게 변화했습니다.

이렇게 변경된 이후로 if-else-if를 사용했던 instanceOf 분기 처리를 switch expression으로 변경할 수 있습니다.

public static void printMemberSwitch(Object obj) {
    switch(obj){
        case Member member -> {
            System.out.println("member = " + member);
        }
        case Family family -> {
            System.out.println("family = " + family);
        }
        default -> {
            System.out.println("default");
        }
    }
}

switch에 객체를 받아서 변수에 바로 값을 할당하여 사용할 수 있습니다.

sealed class 추가 기능Permalink

만약 들어오는 객체가 sealed class일 경우 default 라벨도 제거할 수 있습니다( 컴파일 시점에 클래스 상황을 알 수 있습니다.)

public sealed interface Computer permits DeskTop, Laptop {
    void boot();
    void shutdown();
}

//switch pattern matching example by Computer
public static void boot(Computer computer) {
    switch (computer) {
        case DeskTop deskTop -> System.out.println("데스크탑 부팅 : " );
        case Laptop laptop -> System.out.println("랩탑 부팅 : ");
    }
}
순서 주의Permalink

만약 스위치 문장에서 전혀 도달 할 수 없는 경우, 즉 아래 switch로 접근할 수 없는 경우 컴파일 에러가 발생합니다.

 public static void boot(Computer computer) {
    switch (computer) {
[에러]   case Computer computerTmp -> System.out.println("컴퓨터 부팅");
        case Laptop laptop when laptop.isOn() -> System.out.println("랩탑 부팅 성공");
        case DeskTop deskTop -> System.out.println("데스크탑 부팅 : " );
        case Laptop laptop -> System.out.println("랩탑 부팅 : ");
        case null -> ...
    }
}

LaptopDeskTop의 상위 클래스는 Computer입니다. case 문 첫 줄에 있는 경우 나머지 case 문은 전혀 도달 할 수 없습니다.

이런 경우 예외가 발생하게 됩니다.

대신 when을 추가하는 경우가능합니다. 왜냐하면 && 으로 조건이 추가 되기 때문입니다.

public static void boot(Computer computer) {
    switch (computer) {
        case Computer computerTmp when computerTmp.isIntel() -> System.out.println("컴퓨터 부팅");
        case Laptop laptop when laptop.isOn() -> System.out.println("랩탑 부팅 성공");
        case DeskTop deskTop -> System.out.println("데스크탑 부팅 : " );
        case Laptop laptop -> System.out.println("랩탑 부팅 : ");
        case null -> ...
    }
}
switch의 NPE 조건도 변경Permalink

이전 버전에서는 switch 선택자에 null이 들어오는 경우 바로 NPE가 발생했습니다.

21 버전부터는 선택자 is null And Case null not existing 일 경우에만 NPE가 발생하게 됩니다.

//switch pattern matching example by Computer
public static void boot(Computer computer) {
    switch (computer) {
        case DeskTop deskTop -> deskTop.boot();
        case Laptop laptop -> laptop.boot();
        case null, default -> System.out.println("컴퓨터가 없습니다.");
    }
}

맨 아랫줄에 case null, defualt -> 가 없을 경우에만 NPE 예외가 발생합니다.

그런데 이런 생각이 들었습니다. default가 나머지를 다 처리하니 null이 들어와도 실행되지 않을까?

예외가 발생합니다.

public static void main(String[] args) {
    boot(null);
}

//switch pattern matching example by Computer
public static void boot(Computer computer) {
    switch (computer) {
        case DeskTop deskTop -> deskTop.boot();
        case Laptop laptop -> laptop.boot();
        default -> System.out.println("컴퓨터가 없습니다.");
    }
}
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Objects.java:233)
sealed class 와 같이 활용Permalink

한 API에서 서로 다른 스펙을 반환하는 경우에 sealed classswitch pattern matching을 사용하면 조금 더 깔끔한 코딩이 가능하게 됩니다. 예를 들어 API 결과 response로 노트북이나 데스크탑 정보를 반환해야할 때 두 스펙이 다를 경우 사용할 수 있는 방법입니다.

public record DeskTop(
        String name,
        boolean isIntelCPU,
        int price
) implements Computer {}
public record Laptop(
        String name,
        double screenSize,
        int price
) implements Computer {} 

ComputerAsService

이후 수정

Math API clamp 함수Permalink

테스트하려는 value의 값과 minmax 사이에 있는지 확인합니다.

  1. min <= value <= max : value 반환
  2. value < min: min 반환
  3. max < value : max 반환
public static void main(String[] args) {
    //test clamp
    // case 1 : value < min // 1
    System.out.println(Math.clamp(0, 1, 5));

    // case 2 : value > max // 5
    System.out.println(Math.clamp(10, 1, 5));

    // case 3 : value == min // 1
    System.out.println(Math.clamp(1, 1, 5));

    // case 4 : value == max // 5
    System.out.println(Math.clamp(5, 1, 5));

    // case 5 : min < value < max // 3
    System.out.println(Math.clamp(3, 1, 5));
}

String 관련 함수Permalink

String indexOf 함수Permalink

특정 범위 안에 있는문자 또는 문자열의 위치를 찾을 수 있습니다.

public static void main(String[] args) {
    // test String.indexOf(ch,idx,end)
    String str = "안녕하세요. 자바 21 입니다.";
    // find 1st '자' from 0 to 7 (exclusive)
    System.out.println(str.indexOf('자', 0, 7)); // -1
    System.out.println(str.indexOf('자', 0, 10)); // 7

    // find 2nd '자'
    System.out.println(str.indexOf('자', 8, 10)); // -1
}

주의사항은 마지막 인덱스는 exclusive로 포함되지 않습니다.

String splitWithDelimiters 함수Permalink

delimiters까지 포함하여 배열로 반환합니다.

String str2 = "안녕하세요.|자바|21|입니다.";
String[] split = str2.splitWithDelimiters("\\|", -1);
System.out.println(Arrays.toString(split));
// [안녕하세요., |, 자바, |, 21, |, 입니다.]
StringBuffer/Builder repeat 함수Permalink
public static void main(String[] args) {
    //test StringBuilder repeat
    StringBuilder sb = new StringBuilder();
    sb.repeat("abc ", 3);
    System.out.println(sb);
}
// abc abc abc 
Character 클래스 - emoji 관련 함수들 추가Permalink
public static void main(String[] args) {
    int codePoint = Character.codePointAt("😀", 0); // 예시 이모지 코드 포인트 ()

    // isEmoji: 주어진 코드 포인트가 이모지인지 확인
    System.out.println(Character.isEmoji(codePoint));

    // isEmojiPresentation: 주어진 코드 포인트가 이모지 프레젠테이션을 가지고 있는지 확인
    System.out.println(Character.isEmojiPresentation(codePoint));

    // isEmojiModifier: 주어진 코드 포인트가 이모지 수정자인지 확인
    System.out.println(Character.isEmojiModifier(codePoint));

    // isEmojiModifierBase: 주어진 코드 포인트가 이모지 수정자 베이스인지 확인
    System.out.println(Character.isEmojiModifierBase(codePoint));

    // isEmojiComponent: 주어진 코드 포인트가 이모지 구성 요소인지 확인
    System.out.println(Character.isEmojiComponent(codePoint));

    // isEmojiZeroWidthJoiner: 주어진 코드 포인트가 확장된 그림문자인지 확인
    System.out.println(Character.isExtendedPictographic(codePoint));

    // codePoint가 이모지인지 확인하고, 맞다면 int로 반환하는 함수
    int emojiInt = getEmojiCodePoint(codePoint);
    if (emojiInt != -1) {
        System.out.println("이모지 코드 포인트: " + emojiInt);
    } else {
        System.out.println("이모지가 아닙니다.");
    }
}
public static int getEmojiCodePoint(int codePoint) {
    if (Character.isEmoji(codePoint) ||
            Character.isEmojiPresentation(codePoint) ||
            Character.isEmojiModifier(codePoint) ||
            Character.isEmojiModifierBase(codePoint) ||
            Character.isEmojiComponent(codePoint) ||
            Character.isExtendedPictographic(codePoint)) {
        return codePoint;
    }
    return -1; // 이모지가 아닌 경우 -1 반환
}
true
true
false
false
false
true
이모지 코드 포인트: 128512

Sequenced Collection API 추가Permalink

자바 21에 추가된 새로운 인터페이스로 순서가 있는 컬렉션의 요소에 대해 통일된 방법으로 접근하고 조작할 수 있도록 설계되었습니다.

이 개념은 List와 Deque 처럼 요소의 순서가 중요한 컬렉션에서 일관된 방법으로 요소를 접근,추가,제거할 수 있습니다.

반대로 뒤집는 reversed() 도 사용할 수 있습니다.

SequencedCollectionDiagram20220216

기존 비일관적인 APIPermalink
작업 List 예시 코드 Deque 예시 코드 Sequenced Collection 예시 코드
첫 번째 위치에 요소 추가 list.add(0, "첫 번째"); deque.addFirst("첫 번째"); sequencedCollection.addFirst("첫 번째");
마지막 위치에 요소 추가 list.add("마지막"); deque.addLast("마지막"); sequencedCollection.addLast("마지막");
첫 번째 요소 가져오기 list.get(0); deque.getFirst(); sequencedCollection.getFirst();
마지막 요소 가져오기 list.get(list.size() - 1); deque.getLast(); sequencedCollection.getLast();
첫 번째 요소 제거 list.remove(0); deque.removeFirst(); sequencedCollection.removeFirst();
마지막 요소 제거 list.remove(list.size() - 1); deque.removeLast(); sequencedCollection.removeLast();
reversed()Permalink

자바 view Collection을 만드는 List<String> result = Collections.unmodifiableList(list); 와 유사합니다.

차이점은 reversed만 원본의 데이터가 변경되면, reversed 결과도 변경되어 보이고, reversed의 데이터가 바뀌면, 원본도 바뀝니다.

원본 데이터 변경시 영향을 받는다.Permalink
public static void main(String[] args) {
    List<Integer> origins = new ArrayList<>(List.of(1, 2, 3, 4, 5));
    List<Integer> reversed = origins.reversed();
    List<Integer> unmodifiableList = Collections.unmodifiableList(origins);

    // size 비교 3가지 모두 동일
    System.out.println(origins.size()); // 5
    System.out.println(reversed.size()); // 5
    System.out.println(unmodifiableList.size()); // 5

    // 원본 데이터 추가
    origins.addLast(6);

    // size 비교 3가지 모두 동일
    System.out.println(origins.size()); // 6
    System.out.println(reversed.size()); // 6
    System.out.println(unmodifiableList.size()); // 6
}
reversed 수정시 원본 데이터도 영향을 받는다.Permalink
public static void main(String[] args) {
    List<Integer> origins = new ArrayList<>(List.of(1, 2, 3, 4, 5));
    List<Integer> reversed = origins.reversed();
    List<Integer> unmodifiableList = Collections.unmodifiableList(origins);

    // size 비교 3가지 모두 동일
    System.out.println(origins.size()); // 5
    System.out.println(reversed.size()); // 5
    System.out.println(unmodifiableList.size()); // 5

    // reversed 데이터 추가
    reversed.addLast(6);

    // size 비교 3가지 모두 동일
    System.out.println(origins.size()); // 6
    System.out.println(reversed.size()); // 6
    System.out.println(unmodifiableList.size()); // 6
}
Set은 다르게 동작한다.Permalink
  1. LinkedHashSet: 집합의 동일한 원소는 하나만 존재하므로 위치가 재조정됩니다.
  2. SortedSet: 맨처음과 맨 뒤에 데이터를 저장하면 예외(UnsupportedOperationException)이 발생합니다.
    요소를 추가할때 자동으로 정렬된 순서로 배치되기 때문에 addFirst,addLast는 예외가 발생합니다.
Map은 다르게 동작한다.Permalink

정렬된 상태로 관리하는 SortedMap,LinkedHashMap에서 추가된 메서드가 있습니다.

메서드 설명 반환 타입 불변(Immutable) 여부
firstEntry() 첫 번째 키-값 쌍을 반환합니다. Map.Entry<K, V> 불변
lastEntry() 마지막 키-값 쌍을 반환합니다. Map.Entry<K, V> 불변
pollFirstEntry() 첫 번째 키-값 쌍을 제거하고 반환합니다. Map.Entry<K, V> 불변
pollLastEntry() 마지막 키-값 쌍을 제거하고 반환합니다. Map.Entry<K, V> 불변
putFirst(K key, V value) 맵의 첫 번째 위치에 키-값 쌍을 추가합니다. V 아니오
putLast(K key, V value) 맵의 마지막 위치에 키-값 쌍을 추가합니다. V 아니오
reversed() 역순으로 정렬된 SequencedMap<K, V>를 반환합니다. SequencedMap<K, V> 아니요
sequencedEntrySet() 순서를 유지하면서 키-값 쌍을 반환하는 Set<Map.Entry<K, V>>를 반환합니다. Set<Map.Entry<K, V>> 불변
sequencedKeySet() 순서를 유지하면서 키들을 반환하는 NavigableSet<K>을 반환합니다. NavigableSet<K> 불변
sequencedValues() 순서를 유지하면서 값들을 반환하는 Collection<V>을 반환합니다. Collection<V> 불변

코드로 확인해보겠습니다.

  1. 불변 반환 확인
    LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
    linkedHashMap.put("key1", "value1");
    linkedHashMap.put("key2", "value2");
    linkedHashMap.put("key3", "value3");
       
    Map.Entry<String, String> firstEntry = linkedHashMap.pollFirstEntry();
    firstEntry.setValue("new value"); // 예외발생
    
  2. reversed() 수정확인

    @Test
    @DisplayName("reversed 데이터 추가 및 원본 데이터 수정가능하다.")
    void add() {
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("key1", "value1");
        linkedHashMap.put("key2", "value2");
        linkedHashMap.put("key3", "value3");
       
        SequencedMap<String, String> reversed = linkedHashMap.reversed();
        reversed.put("key4", "value4");
       
        Assertions.assertSame(4, linkedHashMap.size());
    }
    
Iterable로 조회한 경우 수정이 가능하다.Permalink

firstEntry()와 같은 메서드가 아닌 이터레이터로 데이터를 가져오는 경우 원본 데이터를 수정할 수 있습니다.

@Test
@DisplayName("iterator로 조회한 Entry의 value 수정시 원본 데이터 수정가능하다.")
void modify() {
    LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
    linkedHashMap.put("key1", "value1");
    linkedHashMap.put("key2", "value2");
    linkedHashMap.put("key3", "value3");

    Map.Entry<String, String> firstEntry = linkedHashMap.entrySet().iterator().next();
    firstEntry.setValue("new value");

    Assertions.assertSame("new value", linkedHashMap.get("key1"));
}

댓글남기기