[자바] JDK 17 까지의 변천사 by TestCode
References
2023년 10월 20일 기준 Java는 현재 4개의 LTS(Long Term Support) 버전을 제공하고 있다.
- JDK 8
 - JDK 11
 - JDK 17: Spring Boot 3.0 부터 JDK 17 이상을 사용해야 함
    
- 프리코스에서 JDK 17을 도입한 이유가 본 과정의 웹 프로그래밍 단계에서 Spring Boot 3.0 이상을 사용하기 때문인거 같음
 
 - JDK 21
 
첫 LTS 버전 JDK 8 이후인 9 부터 11, 11 부터 17 사이의 주요 변경사항을 테스트를 통해 학습하고 정리해 본다.
JDK 8 내용을 자세히 확인하고 싶으면 아래 Notion 페이지를 참고해 주세요
📌 JDK 9 ~ JDK 11 주요 업데이트
Collection Factory Method 기능 강화
- List, Set, Map 인터페이스에 immutable(불변) 생성할 수 있는 새로운 기능 추가
    
- 불변 객체: 한번 할당하면 내부 데이터를 변경할 수 없다.
        
- 하지만 재할당이 가능함
 
 
 - 불변 객체: 한번 할당하면 내부 데이터를 변경할 수 없다.
        
 - 불변 컬렉션: 데이터 추가, 삭제, 수정을시도하면 UnsupportedOperationException 발생
 - 각 인터페이스에 불변 컬렉션을 반환해주는 기능들이 추가
    
- of()
 - emptyList(), emptySet(), emptyMap()등 다양한 불변 컬렉션을 생성해줌
 
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class CollectionFactoryMethodInJdk11Test {
	/**
	 * List.of(): 불변 리스트(Immutable List) 생성
	 * - 값 추가, 삭제, 변경 불가능
	 * - 하지만 재할당 가능
	 */
	@Test
	@DisplayName("List Factory Method")
	void listFactory_test() {
		// given
		List<Integer> list = List.of(1, 2, 3);
		List<Integer> before = List.of(1, 2, 3, 4);
		// when & then
		assertThatThrownBy(() -> list.add(4))
			.isInstanceOf(UnsupportedOperationException.class);
		assertThatThrownBy(() -> list.remove(0))
			.isInstanceOf(UnsupportedOperationException.class);
		assertThatThrownBy(() -> list.set(0, 0))
			.isInstanceOf(UnsupportedOperationException.class);
		before = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
		assertThat(before).containsExactly(1, 2, 3, 4, 5);
	}
	@Test
	@DisplayName("Collections가 제공하는 empty collection")
	void test() {
		// given
		List<Integer> emptyList = Collections.emptyList();
		// when & then
		assertThatThrownBy(() -> emptyList.add(1))
			.isInstanceOf(UnsupportedOperationException.class);
		assertThat(emptyList.isEmpty()).isTrue();
	}
}
Interface private method
- 인터페이스 내 private method를 사용할 수 있다
 - 인터페이스를 구현하는 클래스에서 구현에 대한 세부 정보를 숨길 수 있다(은닉화 & 캡슐화)
 - private method를 사용하면 default method 간 코드를 공유하고 중복을 줄일 수 있다
    
- 코드 가독성과 유지보수성 향상은 덤
 
 
public interface MyInterface {
	default void publicMethod() {
		// 다양한 로직 수행
		privateMethod();
	}
	default void anotherPublicMethod() {
		// 다른 로직 수행
		privateMethod();
	}
	private void privateMethod() {
		// 중복되는 로직
	}
}
관련된 내용 Notion 정리 첨부
Optional 클래스의 ifPresentOrElse() 추가
- 기존 Optional의 ifPresent()는 Optional 객체가 값을 담고 있는 경우만 처리
 - ifPresentOrElse()는 해당 객체가 값이 없을 경우 처리할 내용까지 정의가 가능
    
- ifPresent() + 없을 때 대처법
 
 
관련된 내용 Notion 정리 첨부
신규 문자열 method
- isBlank(): empty() + 공백의 문자열이 들어오면 true를 반환
 - lines(): 개행문자(\n, \r, \r\n)를 기준으로 여러 줄의 문자열을 Stream
으로 반환  - strip(): 문자열 앞 뒤의 공백을 제거
    
- trim()과의 차이점: trim()은 ‘\u0020’ 이하의 공백만들 제거하지만, strip()은 유니코드의 공백들을 모두 제거
 - 유니코드에는 스페이스(’\u0020’), 탭(’\u0009’) 외에도 더 많은 종류의 공백 문자들이 존재하는데, strip()을 사용하면 더 많은 종류의 공백을 제거할 수 있음
 
 - stripLeading(): 문자열 앞의 공백을 제거
 - stripTrailing(): 문자열 뒤의 공백을 제거
 - repeat(): 인수로 받은 count 수 만큼 문자열을 반복해 반환
 
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class StringMethodInJdk11Test {
	/**
	 * isBlank(): empty() + 공백의 문자열이 들어오면 true를 반환
	 */
	@Test
	@DisplayName("신규 문자열 Method 기능: isBlank()")
	void isBlank_test() {
		String empty = "";
		String whitespaces = "    ";
		assertThat(empty.isBlank()).isTrue();
		assertThat(whitespaces.isBlank()).isTrue();
	}
	/**
	 * lines(): 개행문자(\\n, \\r, \\r\\n)를 기준으로 여러 줄의 문자열을 Stream<String>으로 반환
	 */
	@Test
	@DisplayName("신규 문자열 Method 기능: lines()")
	void lines_test() {
		// given
		String multiLine = "Hello\\nWorld\\rJDK11\\r\\nCOOL";
		// when
		List<String> result = multiLine.lines()
									   .map(String::trim)
									   .toList();
		// then
		assertThat(result).containsExactly("Hello", "World", "JDK11", "COOL");
		System.out.println("lines() result to list = " + result);
	}
	/**
	 * strip(): 문자열 앞 뒤의 공백들을 제거
	 * - 스페이스('\\u0020') 이하 공백만 제거하는 trim() 보다 더 많은 공백 문자들을 제거할 수 있음
	 * stripLeading(): 문자열 앞의 공백만 제거
	 * stripTrailing(): 문자열 뒤의 공백만 제거
	 */
	@Test
	@DisplayName("신규 문자열 Method 기능: strip()")
	void strip_test() {
		// given
		String input = "  Hello\\u2003"; // \\u2003: Em Space 공백
		// when
		String strip = input.strip();
		String stripLeading = input.stripLeading();
		String stripTrailing = input.stripTrailing();
		// then
		assertThat(strip).isEqualTo("Hello");
		assertThat(stripLeading).isEqualTo("Hello\\u2003");
		assertThat(stripTrailing).isEqualTo("  Hello");
	}
	/**
	 * repeat(): 인수로 받은 count 수 만큼 문자열을 반복해 반환
	 */
	@Test
	@DisplayName("신규 문자열 Method 기능: repeat()")
	void repeat_test() {
		// given
		String abc = "abc";
		// when
		String result = abc.repeat(2);
		// then
		assertThat(result).isEqualTo("abcabc");
	}
}

📌 JDK 12 ~ JDK 17 주요 업데이트
Record Data Class
public 레코드명(헤더) {
	바디
}
- 불변(immutable) 객체를 생성하는 새로운 유형의 클래스
    
- 불변 객체이기 때문에 모든 값은 생성자를 통해 주입해야 한다
 - abstract로 선언할 수 없음
 
 - 기존 클래스에서 구현해야 했던 toString(), equals(), hashcode() 메서드를 자동으로 구현해주며, 모든 인스턴스 필드를 초기화해주는 생성자가 생성된다
 - 모든 필드가 private final 로 선언되기 때문에 다른 클래스를 상속받을 수 없음, 다만 인터페이스 구현은 가능
    
- No extends, Yes implements
 
 - 바디 부분에 제공되는 메서드를 오버라이딩하거나 새로운 메서드를 추가로 작성할 수 있음
 
이제부터 name, age를 필드로 갖는 Member를 레코드를 사용하는 방법으로 변환해본다.
public class Member {
	private final String name;
	private final int age;
	public Member(String name, int age) {
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public int getAge() {
		return age;
	}
	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		Member member = (Member)o;
		return age == member.age && Objects.equals(name, member.name);
	}
	@Override
	public int hashCode() {
		return Objects.hash(name, age);
	}
	@Override
	public String toString() {
		return "Member{" +
			"name='" + name + '\\'' +
			", age=" + age +
			'}';
	}
}
레코드로 변환하면…
public record Member(String name, int age) {
}
- 마법처럼 단 2줄로 끝난다
 - toString()처럼 제공되는 메서드를 기존 클래스처럼 원하는 대로 재정의 가능
 
public record Member(String name, int age) {
	@Override
	public String toString() {
		return "이름 = " + name + ", 나이 = " + age;
	}
}
Record Data Class 테스트해보기
- 주의!! 레코드에서 getter를 사용할 때는 필드명만 사용하면 된다(like 빌더 패턴)
    
- getName()이 아니라 name()
 
 
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class MemberTest {
	@Test
	void record_test() {
		// given
		Member member1 = new Member("이름", 10);
		Member member2 = new Member("이름", 10);
		Member member3 = new Member("이름", 20);
		// when
		String memberName = member1.name();
		int memberAge = member1.age();
		// then
		// getter 테스트
		assertThat(memberName).isEqualTo("이름");
		assertThat(memberAge).isEqualTo(10);
		// equals() & hashcode() 테스트
		assertThat(member1).isEqualTo(member2);
		assertThat(member1).isNotEqualTo(member3);
		System.out.println(member1);
	}
}

댓글남기기