Java 8 to 10 - (1) 기초

Java 8 ~ 10에서의 큰 변화

Stream API

  • 컬렉션을 다루면서 발생하는 모호함과 반복적인 코드를 추상화, 패턴화하는 방식을 제공한다.
  • 작업을 고수준으로 추상화하여 일련의 흐름으로 만들고 이를 통해 스레드와 같은 복잡한 방식을 사용하지 않고도 병렬성을 얻을 수 있다.

메소드 참조, 람다

  • 코드를 전달하는 보다 간결한 기법으로 익명 클래스와 같은 방법을 대체할 수 있다.
  • 자바 8이전에는 first class value인 객체 인스턴스와 달리 second class value인 메소드, 클래스는 값으로서 전달될 수 없었다.
  • 하지만 새로운 메소드 참조, 람다식을 통해서 이를 전달하는게 가능해지며 보다 쉽게 동작 파라미터화를 할 수 있게 돕는다.
  • 달리 말하면 자바 8 부터는 메소드(함수)도 first class value 이다.

인터페이스의 디폴트 메서드

  • 기존에 널리 사용되던 인터페이스에 변경 (메소드 추가)이 발생할 경우 해당 인터페이스를 상속받는 모든 클래스를 수정하지 않고도 변경 사항을 적용할 수 있는 방법이다.

모듈

  • 대규모 코드를 다루는 새로운 방법을 제공한다.

동작 파라미터화

  • 동작 파라미터화는 아직 어떻게 실행될지 결정되지 않은 코드 블록을 받아서 메서드의 일부 동작을 미루는 방식이다.
  • 코드 블럭은 인터페이스를 상속받은 클래스, 익명 클래스로 표현 가능하다. 문제는 이렇게 표현하는게 너무 코드를 장황 verbosity 하게 만든다는 점이다.
  • 람다 표현식을 사용하면 좀 더 간결하고 명확하게 표현하는 것이 가능하다.

람다 표현식

람다 표현식 특징

  • 익명: 보통의 메소드와 달리 이름이 없다.
  • 함수: 메소드처럼 특정 클래스에 종속되지 않는다. 다만 메소드처럼 파라미터 목록, 함수 바디, 빈황 형식, 가능한 예외 목록을 포함한다.
  • 전달: 함다 표현식은 first class value처럼 메소드 인자로 전달하거나 변수로 저장할 수 있다.
  • 간결성: 클래스 상속이나 익명 클래스에 비해 간결하다.

함수형 인터페이스

  • 람다식은 함수형 인터페이스를 구현한 클래스의 인스턴스로 취급된다.
  • 여기서 함수형 인터페이스는 추상 메소드가 오직 하나인 인터페이스를 의미한다.
  • 달리 표현하자면 함수형 인터페이스는 여러개의 메소드를 가질 수 있지만 이 경우 추상 메소드는 반드시 하나여야 하며 나머지는 디폴트 메소드로 구성되어야 한다.
  • 함수 디스크립터는 함다 표현식의 시그니처를 서술하는 추상 메소드 시그니처를 말한다.
  • @FunctionalInterface 는 함수형 인터페이스를 나타내는 어노테이션으로 컴파일러는 해당 인터페이스가 함수형 인터페이스의 규칙을 지키는지 확인하여 만족하지 않는 경우 에러를 발생시킨다.
  • 자바에는 이미 Comparable, Runnable, Callable과 같은 함수형 인터페이스가 있으며 java.util.function 패키지를 통해 추가로 몇가지 유용한 함수형 인터페이스를 제공하고 있다.
  • 유틸로 제공되는 함수형 인터페이스는 대부분 제네릭으로 구현되므로 기본형 타입 primitive type 을 사용할 경우 박싱/언박싱에 대한 비용이 발생할 수 있다. 이를 피하기 위해서 기본형 특화 함수형 인터페이스를 제공한다.
  • 또한 유틸로 제공되는 함수형 인터페이스는 예외를 던지는 동작을 허용하지 않으므로 예외를 던지는 함수형 인터페이스를 새로 선언하거나 예외를 잡아서 RuntimeException으로 변환해주어야 한다.

형식 추론

  • 람다가 사용되는 context 내에서 람다식에 대한 형식 추론이 이루어진다.
  • 형식 추론형식 검사, 파라미터 추론의 2가지로 이루어진다.
  • 형식 검사의 경우 람다식이 사용되는 context에서 기대되는 형식인 대상 형식 (target type)과 일치하는지 검사하는 것을 말한다.
    • 람다식이 사용되는 위치의 파라미터 또는 변수의 타입을 바탕으로 대상 형식 (함수 디스크립터)을 추론한다.
    • 이렇게 추론된 대상 형식이 람다의 시그니처와 일치하는지 확인한다.
    • 동일한 람다식이더라도 서로 다른 대상 형식을 요구하는 context에 사용될 수 있다.
      • 특히 반환 형식의 경우 추론된 대상 형식이 void 인 경우 무시될 수 있다.
      @FunctionalInterface interface Action { boolean act(String s); }; // 두 경우 모두 context에서 추론된 (String) -> boolean 대상 형식과 일치한다. Predicate<String> p = s -> list.add(s); Action a = s -> list.add(s); // 아래의 경우 반환 형식 (boolean)은 무시되고 (String) -> void로 추론된다. Consumer<String> c = s -> list.add(s);
  • 파라미터 추론은 람다식에서 파라미터 타입이 생략되었을 경우 context에서 추론된 대상 형식으로 부터 람다의 시그니처를 추론하는 것이다.
    • Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); // 람다 시그니처는 (Apple, Apple) -> int 로 추론된다. Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

람다 캡쳐링

  • 익명 함수와 마찬가지로 외부에 선언된 자유 변수 (free variable)를 활용할 수 있다.
  • 단, 이렇게 활용되는 변수는 final로 선언되었거나 실질적으로 final 처럼 사용되는 변수(선언 이후 대입 X) 여야 한다.
  • 인스턴스 변수의 경우 this를 capture 하여 접근한 것이므로 인스턴스 변수 자체가 final일 필요는 없다.
  • 지역 변수는 스택에 저장되므로 지역 변수를 다른 스레드에서 캡쳐하려면 복사를 통해서만 가능하다. 이를 위해서는 해당 지역 변수가 final로 되어있어야 한다.

메서드 참조

  • 메서드 참조는 람다식을 축약하여 표현하는 방법을 제공한다.
  • 메서드 참조는 다음 4가지 방식을 제공한다.
// (1) 클래스의 정적 메서드 참조 ClassName::staticMethod // (1)의 동일한 람다식 (args) -> ClassName.staticMethod(args) // (2) 클래스의 인스턴스 메서드 참조 ClassName::instanceMethod // (2)의 동일한 람다식 (obj, args) -> obj.instanceMethod(args) // (3) 특정 객체의 메서드 참조 expr::instanceMethod // (3)의 동일한 람다식 (args) -> expr.instanceMethod(args) // (4) 클래스 생성자 참조 ClassName::new // (4)의 동일한 람다식 (args) -> new ClassName(args)
  • Generic type인 경우에 타입 파라미터는 생략해도 된다.
// 메서드 참조 List::contains // 람다식 BiPredicate<List<String>, String> p = (list, item) -> list.contains(item);
  • 메서드 참조의 활용 예 (getter에서 주는 값을 기준으로 정렬하기)
// 1. comparator를 직접 구현한다 public class AppleComparator implements Comparator<Apple> { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight); } }; appleList.sort(new AppleComparator()); // 2. 익명 클래스를 활용한다 appleList.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight); } }); // 3-a. 람다식 사용 appleList.sort( (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight)); // 3-b. 람다식 사용 (파라미터 생략) appleList.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight)); // 4. 유틸리티 메서드 사용 import static java.util.Comparator.comparing; appleList.sort(comparing((a) -> a.getWeight())); // 5. 메서드 참조 사용 appleList.sort(comparing(Apple::getWeight));

유틸리티 메서드

  • 람다 표현식을 조합하여 더 복잡한 람다 표현식을 만들 수 있는 유틸리티 메서드를 제공한다.
  • Comparator 조합
    • Comparator.comparing(expr): 비교값을 가져오는 함수 (key extractor)를 비교 함수 (comparator)로 변환한다. Primitive type (int, double, long)을 위한 특수화 함수 (comparingInt, comparingDouble, comparingLong)가 존재한다.
      • 언박싱 비용에 차이가 있긴 한데 별로 크지는 않다. 성능 측정 후 문제가 있으면 사용한다.
    • comparator.reversed(): 정렬 순서를 역순으로 변경한다 (chain 전체를 반전한다).
    • comparator.thenComparing(expr): 계단식 정렬을 수행한다. 람다 표현식에는 비교값을 가져오는 함수 (key extractor) 또는 비교 함수 (comparator)가 올 수 있다.
    • // width, height 오름차순 정렬 comparing(Rectangle::getWidth).thenComparing(Rectangle::getHeight) // width 내림차순, height 오름차순 정렬 comparing(Rectangle::getWidth).reversed() .thenComparing(Rectangle::getHeight) // width 오름차순, height 내림차순 정렬 comparing(Rectangle::getWidth) .thenComparing(comparing(Rectangle::getHeight).reversed()) // width, height 내림차순 정렬 comparing(Rectangle::getWidth) .thenComparing(Rectangle::getHeight).reversed()
  • Predicate 조합
    • predicate.negate(): 결과값을 반전시킨다.
    • predicate.and(expr), predicate.or(expr): 두 표현식의 and, or 조건 함수를 생성한다.
  • Function 조합
    • function.andThen(expr): function을 실행한 결과를 다음 표현식 expr에 입력 파라미터로 적용하여 나온 결과를 돌려주는 함수를 생성한다.
    • function.compose(expr): 다음 표현식 expr을 실행한 결과를 함수 function에 입력 파라미터로 적용하여 나온 결과를 돌려주는 함수를 생성한다.
    • // g(f(x)) f.andThen(g) // f(g(x)) f.compose(g)