Java
Java8 이후로 달라진 기능들
흥부가귀막혀
2023. 5. 4. 18:19
개요
- LTS 버전중에 현재 가장 점유율이 높고 사랑받는(?) 버전은 Java 8 과 11 인듯 싶다.(관련 통계: https://www.jetbrains.com/ko-kr/lp/devecosystem-2022/java/#Java_which-versions-of-java-do-you-regularly-use 참고)
- 최근 몇년간 신규 Java 버전이 계속 릴리즈 되었고 이와 관련되서 새로운 기능들이 많아졌다.
- 통계적으로 가장 많이 사용하고 있는 Java 8 이후에 어떤 기능들이 추가되었는지 살펴보자.
Java 21(2023. 09 월 release 예정) 까지의 주요 업데이트
📌 ZGC Garbage Collector 추가
- JDK 11부터 공개되었고, “Stop-The-World”로 인한 성능저하를 개선하기 위한 목적을 가지고 개발되었다.
ZGC 동작 원리 (Wiki, GitHub)
- Pause Mark Start: Colored pointers 알고리즘을 기반해 ZGC Root에서 가리키는 객체의 상태값을 Mark(저장) 합니다.
- Pause Mark End: 새로 들어온 객체를 대상으로 Mark가 일어나고, ZPage(ZGC에서 다루는 영역)를 찾아 RelocationSet()에 배치합니다.
- Pause Relocate Start: Root 참조 객체에 대한 재배치를 하며, 이후 Load barriers 알고리즘을 통해 모든 객체를 안전하게 업데이트합니다.
특징
- 대기 시간이 짧은 Application에 적합한 Garbage Collection입니다.
- Thread 가 실행 중일 때 동시 작업을 수행하기에 모든 작업을 동시에 수행합니다. (병렬 처리)
- 처리 시간이 10ms를 초과하지 않아 짧은 지연시간을 보장합니다.
- 8MB부터 16TB까지의 Heap 크기를 지원합니다.
적용 방법
Java Application 실행 시 다음 옵션 실행 (기본 설정: G1GC)
java -XX:+UseZGC -jar Application.java
Reference
📌 Collection Factory Method 강화
- Set, List, Map 인터페이스에 Immutable 성격의 Collection 을 생성할 수 있는 메서드가 추가 되었다.
// AS-IS
Set<String> set = new HashSet<>();
set.add("제이든");
set.add("Jayden");
List<String> list = new ArrayList<>();
list.add("제이든");
list.add("버나드");
list.add("자이노");
list.add("메이슨");
list.add("엘빈");
Map<String, String> map = new HashMap<>();
map.put("J","Jayden");
map.put("B","Bernard");
map.put("Z","Zino");
map.put("M","Mason");
map.put("E","Elvin");
// TO-BE
Set<String> set = Set.of("제이든", "Jayden", "개발3팀");
List<String> list = List.of("제이든", "버나드", "자이노", "메이슨", "엘빈");
Map<String, String> map
= Map.of(
"J", "Jayden",
"B", "Bernard",
"Z", "Zino",
"M", "Mason",
"E", "Elvin");
📌 “var” 변수 타입 지원
- 로컬 변수 선언 시, “타입 추론”을 이용하여 명시적 타입 선언 없이도 변수 선언이 가능하도록 지원하는 신규 Keyword.
- JDK 10 버전부터 공식 지원하기 시작하였고 이후 LTS 버전인 JDK 11 부터는 람다 타입에서도 사용 가능하도록 지원.
- 컴파일 시 변수 타입을 추론하기 때문에 성능에 영향을 주지는 않지만 가독성 높은 코드 작성을 위해 무분별한 “var” Keyword 이용은 지양하는 것이 좋다.
제약
- 멤버 변수 또는 Method 파라미터로 선언은 불가능하며, 오로지 Method 내 로컬 변수로만 선언 가능.
// 가능한 사항
public void test() {
var strData = "Jayden";
var doubleData = 1.5;
var intData = 100;
var datas = List.of(strData, doubleData, intData);
for (var data : datas) {
...
}
}
// 불가능한 사항
class MyModel {
private var test; // X
}
📌 신규 String Method 추가
- 문자열 내 공백 확인/제거 등 JDK 11 버전부터 String에 여러 편리한 기능을 지원하는 새로운 Method 들이 추가
신규 method
- isBlank: 문자열이 비어있거나 공백이면 True 반환
- lines: 줄 단위로 나뉘어 있는 문자를 배열로 반환
- strip: 문자열 공백 제거 (기존 “trim()”이 ‘\u0020’ 이하 공백만을 제거 하였다면, “strip()”은 유니코드의 공백들을 전부 제거)
- stripLeading: 문자열 앞의 공백을 제거
- stripTrailing: 문자열 뒤의 공백을 제거
- repeat: 문자열을 파라미터로 주어진 수 만큼 반복
public static void isBlankMethod() {
"".isBlank(); // Output :: true
"Jayden".isBlank(); // Output :: false
}
public static void linesMethod() {
"Jayden\nBernard\nZino\nElvin\nMason".lines().toArray();
// Output :: [Jayden, Bernard, Zino, Elvin, Mason]
}
public static void stripMethod() {
" 여기어때컴퍼니 ".strip(); // Output :: (여기어때컴퍼니)
" 여기어때컴퍼니 ".stripLeading(); // Output :: (여기어때컴퍼니 )
" 여기어때컴퍼니 ".stripTrailing(); // Output :: ( 여기어때컴퍼니)
}
public static void repeatMethod() {
"Jayden ".repeat(3); // Output :: Jayden Jayden Jayden
}
📌 텍스트 블록
- JDK 15부터 정식 지원하는 새로운 기능.
- “”” {String 문자열 } “”” 형식을 이용해 Java 문자열을 보다 가독성있게 작성하도록 도와준다.
- TextBlock 내 데이터를 동적으로 다루게 될 경우 %s 문자와 format Method 를 이용하여 구현할 수 있고, 또한 블록 내 “+” 연산자를 이용하여 구현할 수도 있다.
String message = """
[
{
"type": "header",
"text": {
"type": "plain_text",
"text": "%s :mag:",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*API*: %s\\n*remoteIP*: %s\\n*serverIP*: %s"
}
},
{
"type": "divider"
}
]
""";
📌 Switch 표현식 기능 향상
Case문 람다식 지원
static void test(Day day){
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
}
Switch 문 값 직접 반환
public void test(Day day) {
System.out.println(
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
});
}
yield 예약어 이용한 값 리턴 방식 추가
public void test(Day day) {
int cnt = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
case WEDNESDAY -> {
int k = day.toString().length();
int result = k+5;
yield result;
//break result; <----- java 12 Switch Expression
}
default -> 0;
};
}
- yield 키워드는 항상 Switch 블록 내부에서만 사용된다.
public void test(Day day) {
int cnt = switch (day) {
case MONDAY -> 0;
case TUESDAY -> yield 1; // error! yield는 block 안에서만 유효다
case WEDNESDAY -> { yield 2;} // ok
default -> 0;
}
}
- 추가로 보통 java 예약어는 변수명으로 지정이 불가능하지만 yield 키워드는 변수명으로 사용이 가능하다.
int volatile = 3; // error!
int yield = 3; // ok
default 선언 여부에 따른 compile 에러
enum Test {
FIRST,
SECOND,
THIRD;
}
Test test = getTest();
// default 를 선언하지 않아 compile 에러가 발생
switch(test) {
case FIRST -> 1;
case SECOND -> 2;
}
// default 를 선언하여 case 에 모든 조건을 추가하지 않아도 compile 에러가 발생하지 않음.
switch(test) {
case FIRST -> 1;
default -> 0;
}
Pattern Matching for switch
- 아직 Preview 기능이라 정식 지원되는 기능은 아님 (https://openjdk.org/jeps/433)
- instanceof 분기처리
// if
static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
// switch
static String formatterPatternSwitch(Object obj) {
return switch (obj) {
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 -> obj.toString();
};
}
- null 분기처리
// if
static void testFooBar(String s) {
if (s == null) {
System.out.println("Oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
// switch
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
- when 예약어 사용
// AS-IS
static void testTriangle(Shape s) {
switch (s) {
case null:
break;
case Triangle t:
if (t.calculateArea() > 100) {
System.out.println("Large triangle");
break;
}
default:
System.out.println("A shape, possibly a small triangle");
}
}
// TO-BE
static void testTriangle(Shape s) {
switch (s) {
case null ->
{ break; }
case Triangle t
when t.calculateArea() > 100 ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
📌 instanceof 연산자에 대한 Pattern Matching
- Instanceof 내부에서 객체를 캐스팅 하는 과정이 필요하였으나, 캐스팅 과정을 내부에서 지원할 수 있도록 변경
- JDK 16 부터 지원
// 기존 Java 8 코드
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student student = (Student) obj;
if (this.studentNumber == student.studentNumber) {
return true;
} else {
return false;
}
}
return false;
}
// Java 16 instanceof 연산자에 대한 pattern matching
@Override
public boolean equals(Object obj) {
if (obj instanceof Student student) {
if (this.studentNumber == student.studentNumber) {
return true;
} else {
return false;
}
}
return false;
}
📌 Record Data Class
- JDK 14 버전부터 공개된 Immutable 객체를 생성하는 새로운 유형의 클래스.
- Record 선언을 하게 되면 기존 toString, equals, hashCode 메소드를 자동으로 구현해주며, 모든 인스턴스 필드를 초기화해주는 생성자가 생성이 됩니다.
- Immutable 객체이기 때문에 모든 값은 생성자를 통해 설정되어야 합니다.
- DTO 와 같은 Data Object 용도로 활용 시 보다 편리하고 간결하게 구분할 수 있습니다.
- setter 를 통해 값을 주입하는 라이브러리 혹은 기능을 구현한다면 Record Class 를 사용할 수 없다.
- Record Class는 상속이 불가능하다. (모든 필드는 “private final ,,”로 선언이 되기에,,)
- 단순 선언
public record RecordUserData(String name, int weight) {
}
...
RecordUserData userRecord = new RecordUserData("Jayden", 110);
// Output :: Jayden
System.out.println(userRecord.name());
// Output :: 110
System.out.println(userRecord.weight());
// Output :: false
System.out.println(userRecord.equals(new RecordUserData("Jayden", 90)));
// Output :: true
System.out.println(userRecord.equals(new RecordUserData("Jayden", 110)));
// Output :: RecordUserData[name=Jayden, weight=110]
System.out.println(userRecord);
- Static 변수와 Method 선언
public record Citizen(String name, String country) {
public static String UNKNOWN_COUNTRY = "Unknown";
}
public record Citizen(String name, String country) {
public static Citizen justCountry(String country) {
return new Citizen("", country);
}
}
String country = Citizen.UNKNOWN_COUNTRY;
Citizen citizen = Citizen.justCountry("korea");
📌 Interface Private Method 지원
- JDK9 부터 지원
// example
public interface Foo {
default void bar() {
System.out.print("Hello");
baz();
}
private void baz() {
System.out.println(" world!");
}
}
// static method 도 사용 가능
public interface Foo {
static void buzz() {
System.out.print("Hello");
staticBaz();
}
private static void staticBaz() {
System.out.println(" static world!");
}
}
📌 Sealed class
- 무분별한 상속을 막기 위한 목적으로 등장한 기능으로 지정한 클래스 외 상속을 허용하지 않으며, 지정한 클래스 외 상속 불가능.
- 즉, 클래스 또는 인터페이스는 이제 어떤 클래스가 이를 구현하거나 확장할 수 있는지 정의할 수 있다. 이를 통해 도메인 모델링 및 라이브러리 보안 강화에 유용해진다.
- JDk 17 부터 지원
- Sealed interface
public sealed interface Service permits Car, Truck {
int getMaxServiceIntervalInMonths();
default int getMaxDistanceBetweenServicesInKilometers() {
return 100000;
}
}
- Sealed abstract class
public abstract sealed class Vehicle permits Car, Truck {
protected final String registrationNumber;
public Vehicle(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public String getRegistrationNumber() {
return registrationNumber;
}
}
- 추가 확장을 방지하고자 한다면 final 을 선언한다.
public final class Truck extends Vehicle implements Service {
private final int loadCapacity;
public Truck(int loadCapacity, String registrationNumber) {
super(registrationNumber);
this.loadCapacity = loadCapacity;
}
public int getLoadCapacity() {
return loadCapacity;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 18;
}
}
- 추가 확장을 허용하려면
non-sealed
를 정의한다.
public non-sealed class Car extends Vehicle implements Service {
private final int numberOfSeats;
public Car(int numberOfSeats, String registrationNumber) {
super(registrationNumber);
this.numberOfSeats = numberOfSeats;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 12;
}
}
제약
- 허용된 모든 하위 클래스는 봉인된 클래스와 동일한 모듈에 속해야 한다.
- 허용된 모든 하위 클래스는 봉인된 클래스를 명시적으로 확장해야 한다.
- 허용된 모든 하위 클래스는 수정자를 정의해야 한다.(final, sealed 또는 non-sealed)
Record 와의 궁합
- Record class 는 내부적으로 final 이 명시된 class 이기 때문에 좀더 간결한 구조로 정의가 가능하다.
public sealed interface Vehicle permits Car, Truck {
String getRegistrationNumber();
}
public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {
@Override
public String getRegistrationNumber() {
return registrationNumber;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
}
public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {
@Override
public String getRegistrationNumber() {
return registrationNumber;
}
public int getLoadCapacity() {
return loadCapacity;
}
}
Reflection
java.lang.Class
에서 다음과 같은 2개의 API 를 추가로 지원한다.- isSealed: 주어진 클래스 또는 인터페이스가 봉인된 경우 true 를 반환.
- getPermittedSubclasses: 허용된 모든 하위 클래스를 나타내는 객체 배열을 반환.
Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().getPermittedSubclasses())
.contains(ClassDesc.of(truck.getClass().getCanonicalName()));
📌 Virtual Thread(aka. Project Loom)
- Java 21 에 정식 지원 예정 (관련 기사 : https://www.infoq.com/news/2023/04/virtual-threads-arrives-jdk21/)
- JDK 21 부터 Java 는 플랫폼 쓰레드와 가상 쓰레드라는 2종류의 쓰레드를 지원
- 플랫폼 쓰레드는 OS 쓰레드에 대한 1대1 매핑이고, 가상 쓰레드는 Java 가 제공하는 경량 쓰레드로 하나의 OS 쓰레드 내에서 가상 쓰레드 1만개를 동시 실행 가능
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
Reference
- http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html
- https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html