Java의 새로운 기능

Java 16의 새로운 기능

늦깎이_개발자 2024. 6. 10. 22:53
반응형

1. Proxy 객체에서 default 메서드 호출

Java 16에서는 인터페이스의 default 메서드에 대한 개선된 기능으로 java.lang.reflect.InvocationHandler는 리플렉션을 사용하여 동적 프록시를 통해 인터페이스의 default 메서드를 호출한다.

아래에 간단한 예시를 살펴보자.

interface HelloWorld {
    default String hello() {
        return "world";
    }
}

 

이제 우리는 reflection을 사용하여 이 인터페이스의 프록시에 대한 default 메서드를 호출할 수 있다.

Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[] { HelloWorld.class },
    (prox, method, args) -> {
        if (method.isDefault()) {
            return InvocationHandler.invokeDefault(prox, method, args);
        }
        // ...
    }
);
Method method = proxy.getClass().getMethod("hello");
assertThat(method.invoke(proxy)).isEqualTo("world");

 

2. 새로운 시간 Symbol

DateTimeFormatter에 am/pm에 대한 대체 표현을 제공하는 새로운 symbol인 " B"가 추가되었다.

LocalTime date = LocalTime.parse("15:25:08.690791");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h B");
assertThat(date.format(formatter)).isEqualTo("3 in the afternoon");

 

우리는 "3 pm" 대신 "3 in the afternoon"을 출력값으로 얻을 수 있다. 또한 우리는 " B", " BBBB", " BBBBB" 등의 패턴을 통해 짧거나 전체로 이뤄진 스타일을 적용할 수 있다.

 

3. Stream.toList 메서드

Collectors.toList와 Collectors.toSet과 같은 Stream Collector로 이뤄진 보일러플레이트 코드들을 줄이기 위해 Stream.toList 메서드가 추가되었다. 아래 예시를 살펴보자.

List<String> integersAsString = Arrays.asList("1", "2", "3");
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();

 

예시에서 ints와 intsEquivalent는 동일한 결과 값을 가진다.

 

4. Vector API (인큐베이션 단계)

Java 16에서 Vector API는 초기 인큐베이션 단계에 있다. 이 API의 목적은 전통적인 스칼라 계산보다 최적화된 퍼포먼스를 내기 위해, 벡터 계산을 제공하는 것이다.

먼저 전통적인 방법으로 두 배열을 곱하는 예시를 살펴보자.

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};

var c = new int[a.length];

for (int i = 0; i < a.length; i++) {
    c[i] = a[i] * b[i];
}

 

이 예시에서는 배열의 길이에 해당하는 4번의 cycle이 실행될 것이다. 이제 벡터 기반의 계산을 사용한 예시를 보자.

int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};

var vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
var vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
var vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);

 

위 예시에서 우리가 처음으로 한 것은 fromArray 메서드를 통해 배열로부터 2개의 IntVector를 생성한 것이다. 메서드의 첫번째 파라미터는 벡터의 사이즈에 해당하고, 그 다음 파라미터는 배열, 그 다음은 offset 값이다. 여기서 가장 중요한 것은 벡터의 사이즈를 128 bit로 설정했다는 것이다. Java에서 각각의 int 값은 4 byte에 해당한다. 이 예시에서는 4개의 int를 가진 배열이 나오기 때문에, 이것을 저장하기 위해서는 128(4 X 8 X 4) bit가 필요하다. 여기서 단일 벡터는 전체 배열을 저장할 수 있다.

특정 아키텍처에서 컴파일러는 바이트 코드를 최적화하여 계산 과정을 4 -> 1 cycle로 줄일 수 있을 것이다. 이러한 최적화는 머신 러닝이나 암호화 프로세스에 유리하다.

Vector API는 인큐베이션 단계에 있는 것이기 때문에 Java의 새로운 릴리즈에서는 변경될 수 있다는 것을 기억해야 한다.

 

5. Record

Record는 Java 14에서 처음 소개되었다. Java 16은 여기에 점증적인 변화를 가져왔다.

Record는 제한된 class의 형태라는 점에서 enum과 비슷하다. Record를 정의하는 것은 불변 데이터 객체를 정의하는 간결한 방법이다.

 

5.1 Record가 없을 때

아래 예시에서 Book class를 정의해보자.

public final class Book {
    private final String title;
    private final String author;
    private final String isbn;

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }

    @Override
    public boolean equals(Object o) {
        // ...
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, author, isbn);
    }
}

 

Java에서 간단한 데이터 객체를 생성하는데는 많은 보일러플레이트 코드들이 필요하다. 이것은 다루기 번거롭고 때때로 개발자들이 equals와 hashCode와 같은 메서드들을 오버라이드하지 않아 버그를 발생시키기도 한다.

대부분의 요즘 IDE들은 getter, setter, 생성자 등과 같은 코드들에 대한 자동 생성 기능을 제공한다. 이것은 위에서 말했던 문제들을 경감시키고 개발자들의 부담을 덜어준다. 그러나 Record는 이러한 보일러플레이트 코드들 없이 똑같은 결과를 만들어 낸다.

 

5.2 Record가 있을 때

아래에 Record를 이용해서 다시 작성된 Book class를 보자.

public record Book(String title, String author, String isbn) {
}

 

record 키워드를 사용함으로써 Book class의 코드는 2줄로 줄어들었다. 이것은 훨씬 쉽고 에러가 발생시킬 확률이 적다.

 

5.3 Record에 대한 추가 사항

Java 16에서는 inner class의 멤버변수로 record를 사용하는 것이 가능하다.

class OuterClass {
    class InnerClass {
        Book book = new Book("Title", "author", "isbn");
    }
}

 

6. instanceof에 대한 패턴 매칭

instanceof에 대한 패턴 매칭은 Java 12에서 Preview 모드로 처음 소개되었다. Java 16에서 프로덕션 레벨로 올라온 것으로 보인다.

패턴 매칭이 없는 코드 예시는 아래와 같다.

Object obj = "TEST";

if (obj instanceof String) {
    String t = (String) obj;
    // do some logic...
}

 

패턴 매칭을 사용하여 아래와 같이 코드를 다시 작성할 수 있다.

Object obj = "TEST";

if (obj instanceof String t) {
    // do some logic
}

 

우리는 이제 instanceof check 과정의 일부로 변수(여기서는 t)를 선언할 수 있다.

 

7. Sealed class (Preview)

7.1 예제

인터페이스와 이를 구현하는 2개의 클래스를 예시로 살펴보자.

public sealed interface JungleAnimal permits Monkey, Snake  {
}

public final class Monkey implements JungleAnimal {
}

public non-sealed class Snake implements JungleAnimal {
}

 

sealed 키워드는 permits 키워드와 함께 사용되었는데, 이는 어떤 class들이 이 인터페이스를 구현하도록 허락되었는지를 결정해준다. 이 예제에서는 Monkey와 Snake이다.

sealed class의 모든 자식 class들은 반드시 아래의 것들 중 하나로 표시되어야 한다.

1) sealed : 어떤 class가 이 class를 상속하도록 허락되었는지를 permits 키워드를 사용하여 나타내야한다는 의미이다.

2) final : 추가적인 자식 class가 없다는 뜻이다.

3) non-sealed : 이 class는 어떤 클래스도 상속이 가능하다는 의미이다.

 

sealed class의 가장 큰 이점은 모든 경우의 수를 커버할 필요가 있는 패턴 매칭 체크를 완벽하게 해낼 수 있다는 것이다. 위에서 정의한 class로 예를 들면, 우리는 JungleAnimal의 자식 클래스에 대한 모든 가능한 경우를 커버할 수 있다.

JungleAnimal j = // some JungleAnimal instance

if (j instanceof Monkey m) {
    // do logic
} else if (j instanceof Snake s) {
    // do logic
}

 

여기서 sealed class는 오직 Monkey와 Snake만 자식 클래스로 허용하기 때문에, 추가적인 else 구문이 필요가 없다.

 

 

※ 참조

https://www.baeldung.com/java-16-new-features

반응형