Java

[모던 자바] Java8 업데이트된 라이브러리와 언어

Beekei 2022. 4. 5. 10:10
반응형

Java8의 라이브러리와 언어의 업데이트된 부분을 알아보자.

 

어노테이션(annotation)

Java의 어노테이션은 부가 정보를 프로그램에 장식할 수 있는 기능이다.

즉, 어노테이션은 문법적 메타데이터(syntactic metadata)다.

Java8의 어노테이션은 두 가지가 개선되었다.

1. 어노테이션 반복

이전 자바에서는 선언에서 지정한 하나의 어노테이션만 허용했다.

@interface Author { Stirng name(); } 
@Auth(name="a") @Auth(name="b") @Auth(name="c") // 에러 발생 : 중복된 어노테이션

이제 반복 조건만 만족한다면 선언을 할 때 하나의 어노테이션 형식에 여러 어노테이션을 지정할 수 있다.

어노테이션 반복은 기본으로 제공되는 기능이 아니므로 반복할 수 있는 어노테이션임을 명시적으로 지정해야 한다.

반복할 수 있는 어노테이션 만들기

반복할 수 있는 어노테이션을 설계하면 바로 어노테이션을 활용할 수 있다.

하지만 사용자에게 어노테이션을 제공하는 상황이라면 어노테이션을 반복할 수 있음을 설정하는 과정이 필요하다.

  1. 어노테이션을 @Repeatable로 표시
  2. 컨테이너 어노테이션 제공
@Repeatable(Authors.class) // 1. 어노테이션을 @Repeatable로 표시
@interface Author { Stirng name(); } 

@interface Authors { Author[] value(); } // 2. 컨테이너 어노테이션 제공

@Authors({ @Auth(name="a"), @Auth(name="b"), @Auth(name="c") })

Book 클래스의 중첩 어노테이션 때문에 코드가 복잡해졌기 때문에 Java8에서는 반복 어노테이션과 관련한 제한을 해제했다. 

Class 클래스는 반복된 어노테이션을 사용하라 수 있는 getAnnotationsByType을 제공한다.

Author[] authors = Book.class.getAnnotationByType(Author.class);
Arrays.asList(authors).forEach(a -> { System.out.println(a.name()); });

여기서 반복할 수 있는 어노테이션과 컨테이너는 런타임 보유 정책을 반드시 가지고 있어야 한다.

2. 형식 어노테이션

Java8에서는 모든 형식에 어노테이션을 적용할 수 있다.

즉, new 연산자, instanceof, 형식 캐스트, 제네릭 형식 인수, implements, throws 등에 어노테이션을 사용할 수 있다.

@NonNull String name = person.getName();
List<@NonNull Car> cars = new ArrayList<>();

형식 어노테이션은 프로그램을 분석할 때 유용하다.

해당 코드를 보고 기능의 예측이 가능하므로 예상하지 못한 에러 발생 가능성을 줄일 수 있다.


일반화된 대상 형식 추론

java8은 제네릭 인수 추론 기능을 개선했다.

java8 이전에도 콘텍스트 정보를 이용한 형식 추론은 지원했다.

다음처럼 메서드를 호출할 때 명시적으로 형식을 사용해서 형식 파라미터의 형식을 지정할 수 있었다.

List<Car> cars = new ArrayList<Car>();

하지만 java8은 암시적으로 제네릭 인수를 추론할 수 있다.

List<Car> cars = new ArrayList<>();

컬렉션

Java API 설계자는 많은 새로운 메서드를 컬렉션 인터페이스와 클래스에 추가했다.

클래스/인터페이스 새로운 메서드
Map getOrDefault, forEach, compute, computeIfAbsent, computeIfPresent, merge, putIfAbsent, remove(key, value), replace, replaceAll
Iterable forEach, spliterator
Iterator forEachRemaining
Collection removeIf, stream, parallelStream
List replaceAll, sort
BitSet stream

맵(Map)

Map은 다양한 편의 메서드가 추가된면서 가장 많이 업데이트된 인터페이스다.

getOrDefault

key에 매핑되는 값이 있는지 여부를 확인하는 기존의 get 메서드를 getOrDefault로 대신할 수 있다.

getOrDefault는 key에 매핑되는 값이 없으면 기본 값을 반환한다.

Map<String, String> map = new HashMap<>();
String value = "default value";
if (map.containsKey("key")) {
    value = map.get("key");
}

위 코드를 아래처럼 사용할 수 있다.

Map<String, String> map = new HashMap<>();
String value = map.getOrDefault("key", "default value");

위 코드는 키에 매핑되는 값이 없을때만 작동한다. 

예를 들어 키가 명시적으로 null로 매핑되어 있으면 기본값이 아니라 null이 반환된다.

computeIfAbsent

해당 키에 저장된 값이 없다면 값을 저장한다.

Map<String, String> map = new HashMap<>();
String value = map.get("key");
if (value == null) {
    map.put("key", "value");
}

위 코드를 아래처럼 사용할 수 있다.

Map<String, String> map = new HashMap<>();
map.computeIfAbsent("key", "value");

컬렉션(Collection)

removeIf 메서드로 Predicate와 일치하는 모든 요소를 컬렉션에서 제거할 수 있다.

List<String> values = new ArrayList<>();
for (String value : values) {
    if (value.equals("value")) {
        values.remove(value);
    }
}

위 코드를 아래처럼 사용할 수 있다.

List<String> values = new ArrayList<>();
values.removeIf(value -> value.equals("value"));

removeIf 메서드는 Stream API의 filter와는 다르다는 사실을 주의해야 한다.

Stream API의 filter 메서드는 새로운 스트림을 생성하므로 현재 스트림이나 소스를 갱신하지 않는다.


리스트(List)

replaceAll 메서드는 리스트의 각 요소를 주어진 연산자를 리스트에 적용한 결과로 대체한다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.replaceAll(x -> x * 2);

replaceAll 메서드는 스트림의 map과 비슷하지만 새로운 요소를 생성하는 map과 달리 리스트의 요소를 갱신한다는 점이 다르다.


Collections 클래스

Collections는 오랫동안 컬렉션 관련 동작을 수행하고 반환하는 역할을 담당해온 클래스다.

Collections는 불변의(unmodifiable), 동기화된(synchronized), 검사된(checked), 빈(empty) NavigableMap과 NavigableSet을 반환할 수 있는 새로운 메서드를 포함한다.

또한 정적 형식 검사에 기반은 둔 Queue 뷰를 반환하는 checkedQueue라는 메서드도 제공한다.


Comparator

Comparator 인터페이스는 디폴트 메서드와 정적 메서드를 추가도 제공한다.

  • comparing : 정렬 할 기준을 추출한다.
  • reversed : 현재 Comparator를 역순으로 반전시킨 Comparator를 반환한다.
  • thenComaring : 두 객체가 같을 때 다른 Comparator를 사용하는 Comparator를 반환한다.
  • thenComparingInt, thenComparingDouble, thenComparingLong :
    thenComparing과 비슷한 동작을 수행하지만 기본형에 특화된 함수를 인수로 받는다.(ToIntFunction, ToDoubleFunction, ToLongFunction)
  • comparingInt, comparingDouble, comparingLong :
    comparing과 비슷한 동작을 수행하지만 기본형에 특화된 함수를 인수로 받는다.(ToIntFunction, ToDoubleFunction, ToLongFunction)
  • naturalOrder : Comparable 객체에 자연 순서를 적용한 Comparable 객체를 반환한다.
  • nullsFirst : null 객체를 null이 아닌 객체보다 작은 값으로 취급하는 Comparator을 반환한다.
  • nullsLast : null 객체를 null이 아닌 객체보다 큰 값으로 취급하는 Comparator을 반환한다.
  • reverseOrder : naturalOrder().reversed()와 같다.

동시성

Java8에는 동시성과 관련한 기능도 많이 업데이트되었다.


아토믹

java.util.concurrent.atomic 패키지는 AtomicInteger, AtomicLong 등 단일 변수에 아토믹 연산을 지원하는 숫자 클래스를 제공한다.

  • getAndUpdate : 제공된 함수의 결과를 현재값에 아토믹하게 적용하고 기존 값을 반환한다.
  • updateAndGet : 제공된 함수의 결과를 현재값에 아토믹하게 적용하고 업데이트된 값을 반환한다.
  • getAndAccumulate : 제공된 함수를 현재값과 인수값에 적용하고 기존 값을 반환한다.
  • accumulateAndGet : 제공된 함수를 현재값과 인수값에 적용하고 업데이트된 값을 반환한다.

Adder와 Accumulator

Java API는 여러 스레드에서 읽기 동작보다 갱신 동작을 많이 수행하는 상황(예를 들면 통계 작업)이라면 Atomic 클래스 대신 LongAdder, LongAccumulator, DoubleAdder, DoubleAccumulator를 사용하라고 권고한다.

이들 클래스는 동적으로 커질 수 있도록 설계되었으므로 스레드 간의 경쟁을 줄일 수 있다.

 

LongAdder, DoubleAdder는 덧셈 연산을 지원하며 LongAccumulator와 DoubleAccumulator는 제공된 함수로 값을 모은다.

예를 들어 다음처럼 LongAdder로 여러 값의 합계를 계산할 수 있다.

LongAdder adder = new LongAdder(); // default 생성자에서 초기 sum값을 0으로 설정
adder.add(10); // 여러 스레드에서 어떤 작업을 수행
...
long sum = adder.sum(); // 어떤 시점에서 합계를 구함

또는 다음처럼 LongAccumulator를 사용할 수 있다.

LongAccumulator acc = new LongAccumulator(Long::sum, 0);
acc.accumulate(10); // 여러 스레드에서 값을 누적
...
long result = acc.get(); // 특정 시점에서 결과를 얻음

ConcurrentHashMap

ConcurrentHashMap은 동시 실행 환경에서 친화적인 새로운 HashMap이다.

ConcurrentHashMap은 내부 자료구조의 일부만 잠근 상태로 동시 덧셈이나 갱신 작업을 수행할 수 있는 기능을 제공한다. 따라서 기존의 동기화된 HashTable에 비해 따른 속도로 읽기 쓰기 연산을 수행한다.

성능

성능을 개선하면서 ConcurrentHashMap의 내부 구조가 바뀌었다.

보통 맵의 개체는 키로 생선한 해시코드로 접근할 수 있는 버킷에 저장된다. 

하지만 키가 같은 해시코드를 반환하는 상황에서는 O(n) 성능의 리스트로 버킷을 구해야 하므로 성능이 나빠진다.

Java8에서 버킷이 너무 커지면 동적으로 리스트를 정렬 트리(O(long(n))의 성능으로) 교체한다.

키가 Comparable 일때만(예를 들면 String이나 Number 클래스) 이 기능을 사용할 수 있다.

스트림 같은 연산

ConcurrentHashMap은 마치 스트림을 연상시키는 세 종류의 연산을 지원한다.

  • forEach : 각 키/값 쌍에 주어진 동작을 실행한다.
  • reduce : 제공된 리듀싱 함수를 모든 키/값 쌍의 결과를 도출한다.
  • search : 함수가 null이 아닌 결과를 도출할 때까지 각 키/값 쌍에 함수를 적용한다.

각 연산은 키, 값, Map.Entry, 키/값 쌍을 인수로 받는 함수를 인수로 받으며 다음과 같은 네 가지 형식의 연산을 지원한다.

  • 키/값 쌍으로 연산(forEach, reduce, search)
  • 키로 연산(forEachKey, reduceKeys, searchKeys)
  • 값으로 연산(forEachValue, reduceValues, searchValues)
  • Map.entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)

이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 요소에 직접 연산을 수행한다.

연산에 적용된 함수는 순서에 의존하지 않아야 하며, 계산 과정에서 바뀔 수 있는 다른 객체나 값에 의존하지 않아야 한다.

 

또한 모든 연산에 병렬성 한계갑을 지정해야 한다.

현재 맵의 크기가 한계값보다 작다고 추정되면 순차적으로 연산을 수행한다.

즉, 1이라는 값은 공용 스레드 풀을 사용해서 병렬성을 최대화하며 Long.MAX_VALUE라는 값은 하나의 스레드로 연산을 수행한다는 의미다.

 

다음은 reduceValues를 이용해서 맵의 최댓값을 찾는 예제다.

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));

또한 (reduceValuesToInt, reduceKeysToLong 등) int, long, double 기본형에 특화된 reduce 연산이 있다.

카운팅

ConcurrentHashMap 클래스는 맵의 매핑 개수를 long으로 반환하는 mappingCount라는 새로운 메서드를 제공한다.

맵의 매핑 개수가 정수 범위를 초과할 수 있으므로 앞으로는 int를 반환하는 size 대신 mappingCount를 사용하는 것이 좋다.

집합 뷰

ConcurrentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 새로운 메서드 keySet을 제공한다.

즉, 맵을 바꾸면 집합에도 그 결과가 반영되어 마찬가지로 집합을 바꾸어도 맵에 영향을 미친다.

또한 newKeySet을 이용하면 ConcurrentHashMap의 원소를 포함하는 새로운 집합을 만들 수 있다.


Arrays

Arrays 클래스느 배열을 조작하는 데 사용하는 다양한 정적 메서드를 제공한다.


parallelSort

parallelSort 메서드는 자연 순서(natural order)나 객체 배열의 추가 Comparator를 사용해서 특정 배열을 병렬로 정렬하는 기능을 수행한다.


setAll, parallelSetAll

setAll은 지정된 배열의 모든 요소를 순차적으로 설정하고, parallelSetAll은 모든 요소를 병렬로 설정한다.

두 메서드 모두 제공된 함수로 각 요소를 계산하는데, 이 함수는 요소 인덱스를 받아 해당 값을 반환한다.

parallelSetAll은 병렬로 실행되므로, parallelSetAll에 전달하는 함수는 부작용이 없어야 한다.

int[] evenNumbers = new int[10];
Arrays.setAll(evenNumbers, i -> i * 2); // 0, 2, 4, 6 ...

parallelPrefix

parallelPrefix 메서드는 제공된 이항 연산자를 이용해서 배열의 각 요소를 병렬로 누적하는 동작을 수행한다.

int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b); // 1, 2, 3, 4, 5 ...

Number와 Math

Java8 API에서는 Number와 Math 클래스에 새로운 메서드가 추가되었다.


Number

number 클래스는 다음 메서드가 추가되었다.

  • Short, Integer, Long, Float, Double 클래스에는 정적 메서드 sum, min, max가 추가되었다.
  • Integer와 Long 클래스에는 부호가 없는 값을 처리하는 compareUnsined, divideUnsigned, remainderUnsigned, toUnsignedString 등의 메서드가 추가되었다.
  • Integer와 Long 클래스에는 문자열을 부호가 없는 int나 long으로 파싱 하는 정적 메서드 parseUnsignedInt와 parseUnsignedLong이 추가되었다.
  • Byte와 Short 클래스는 인수를 비부호 int나 long으로 변환하는 toUnsignedInt와 toUnsignedLong 메서드를 제공한다. 마찬가지로 Integer 클래스에도 toUnsignedLong 정적 메서드가 추가되었다.
  • Double과 Float 클래스에는 인수가 유한 소수점인지 검사하는 정적 메서드 isFinite가 추가되었다.
  • Boolean 클래스에는 두 불리언 값에 and, or, xor 연산을 적용하는 정적 메서드 logicalAnd, logicalOr, logicalXor이 추가되었다.
  • BigInteger 클래스에는 BigInteger를 다양한 기본형으로 바꿀 수 있는 byteValueExact, shortValueExact, intValueExact, longVAlueExact 메서드가 추가되었다. 변환 과정에서 정보 손실이 발생하면 산술 연산 예외가 발생한다.

Math

Math 클래스에는 연산 결과에 오버플로가 발생했을 때 산술 예외를 발생시키는 addExact, subtractExact, multiplyExact, incrementExact, decrementExact, negateExact 등의 메서드가 추가되었으며, int와 long을 인수로 받는다.

또한 long을 int로 변경하는 정적 메서드 toIntExact와 floorMod, floorDiv, nextDown 등의 정적 메서드도 추가되었다.


Files

Files 클래스에는 파일에서 스트림을 만들 수 있는 기능이 추가되었다.

  • Files.lines : 파일을 스트림으로 게으르게 읽을 수 있는 기능을 제공한다.
  • Files.list :
    주어진 디렉터리의 개체를 포함하는 Stream<Path>를 생성한다. 이 과정은 재귀가 아니고, 스트림은 게으르게 소비되므로 특히 큰 디렉터리를 처리할 때 유용한 메서드다.
  • Files.walk : 
    Files.list와 마찬가지로 주어진 디렉터리의 개체를 포함하는 Stream<Path>를 생성한다. 이 과정은 재귀적으로 실행되며 깊이 수준을 설정할 수 있다. 깊이 우선(depth-first) 방식으로 탐색을 수행한다.
  • Files.find : 디렉터리를 재귀적으로 탐색하면서 주어진 Predicate와 일치하는 개체를 찾아서 Stream<Path>를 생성한다.

리플렉션

바뀐 어노테이션 기법을 지원할 수 있도록 리플렉션 API도 업데이트 되었다.

그밖에도 리플렉션 API에서 이름, 변경자 등의 메서드 파라미터 정보를 사용할 수 있게 되었다.

java.lang.reflact.Parameter라는 클래스가 새로 추가되었는데 Method와 Constructor의 공통 기능을 제공하는 슈퍼클래스 역할을 하는 java.lang.reflect.Executable에서 java.lang.reflect.Parameter를 참조한다.


String

String 클래스에는 구분 기호(delimiter)로 문자열을 연결할 수 있는 join이라는 새로운 정적 메서드가 추가되었다.

String introduce = String.join(" ", "My", "name", "is", "beekei");
System.out.println("introduce : " + introduce); // introduce : My name is beekei
반응형