Java

Java8 이후로 달라진 기능들

흥부가귀막혀 2023. 5. 4. 18:19

개요

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

// 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

📚 Reference