Modern Java In Action 정리

Modern Java In Action을 읽고 내용을 정리해본다.

1장 자바 8, 9, 10, 11

자바에 포함된 함수형 프로그래밍의 핵심적인 두 아이디어

  • 메서드와 람다를 일급값으로 사용한다.
  • 가변 공유 상태가 없는 병렬 실행을 이용하여 효율적이고 안전하게 함수나 메서드를 호출한다. 스트림은 위 두가지 아이디어를 모두 활용한다.

디폴트 메서드

  • 인터페이스 메서드를 추가하고자 한다면 이미 해당 인터페이스를 구현중인 모든 클래스를 같이 변경해줘야 하는 문제를 해결한다.
  • Java8은 인터페이스 규격명세에 default라는 새로운 키워드를 추가했다.
  • 인터페이스에 default 키워드로 선언한 디폴트메서드는 하위 계층의 클래스들이 구현하지 않아도 된다.

2장 동작 파라미터화 코드 전달하기

  • 어떤 작업을 수행할 것인지를(코드블록) 파라미터화 해서 전달한다.
  • 동작 파라미터화를 적용하면 변화하는 요구사항에 유연하게 대처할 수 있다. 요구사항이 변하면 파라미터(동작)만 달라지게 된다.
  • 하지만 메서드의 인자로 동작(객체)를 전달하기 위해 더미 코드가 늘어나 가독성이 떨어진다.
  • 이를 익명 클래스로 완화할 수 있지만, 람다 표현식을 사용하면 더 깔끔한 코드가 된다.

3장 람다 표현식(1/2)

람다(lambda) 표현식

  • 메서드로 전달할 수 있는 익명 클래스를 단순화한 것이다.
  • 기술적으로는 자바8에 할 수 없었던 일을 제공하는 것은 아니지만 판에박힌 코드를 중복으로 작성하지 않아도 된다.

함수형 인터페이스

  • 추상 메서드가 1개만 있는 인터페이스로 함수형 인터페이스 인스턴스를 익명클래스나 람다표현식으로 대체할 수 있다.
  • 람다표현식으로 함수형 인터페이스 인스턴스를 대체하려면 함수 디스크립터와 일치하는 람다표현식을 사용하여야 한다.
  • 함수 디스크립터(function descriptor) : 함수형 인터페이스가 1개 가지는 추상메서드의 시그니처
    Predicate<T> 인터페이스의 boolean test (T t); 추상메서드는 T타입을 인자로 받고 boolean을 리턴하는 시그니처를 가진다. 이와 동일한 시그니처의 람다 표현식이면 대체가 가능하다.
  • java.util.function 패키지에서 jdk가 제공하는 함수형 인터페이스를 확인할 수 있다.

함수형 인터페이스 종류

  • 원하는 동작에 따라 함수 디스크립터를 정의하고 매칭되는 함수형 인터페이스를 선택하여 사용한다. 아래의 함수형 인터페이스 외에도 제공되는 많이 제공된다.
  • Consumer<T> : T 타입을 소비한다.(리턴이 없다.)
  • Predicate<T> : T 타입에서 boolean을 리턴한다.
  • Function<T, R> : T 타입에서 R 타입을 리턴한다.
  • DoublePredicate, IntConsumer 등 자료형이 붙은 함수형 인터페이스는 참조형(Byte, Integer …)과 기본형 사이를 변환하는 오토박싱에 소비되는 메모리를 방지할 수 있도록 기본형을 사용한다.

람다 지역변수의 제약

인스턴스 변수와 지역 변수는 태생부터 다르다.
인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다.
람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

  • 위 인용구문의 이유로 지역변수(portNumber)를 수정하려 하면 컴파일 에러가 발생한다.
    Error: local variables referenced from a lambda expression must be final or effecstively final
    @Test
    public void Runnable_사용(){
        int portNumber = 1;
        run(() -> {
            System.out.println(portNumber);
            portNumber = 3; // 람다에 바인딩한 변수를 수정하면 컴파일 에러 발생
        });
    }
    
    public static void run(Runnable p){
        p.run();
    }
    

3장 람다 표현식(2/2)

메서드 참조

  • 함수형 인터페이스 인스턴스를 메서드 참조로 대체할 수 있다.
  • 즉, 기존의 메서드 정의를 재활용(메서드 참조)해서 람다처럼 전달하는 것이 메서드 참조이다.
  • 메서드명 앞에 구분자(::)를 붙이는 방식으로 활용하며, 메소드명이 사용되므로 가독성을 높일 수 있다.

메서드 참조 구현

  1. 정적 메서드 참조
    람다 : (args) → ClassName.staticMethod(args)
    메서드참조 : ClassName::staticMethod

  2. 다양한 형식의 인스턴스 메서드 참조
    람다 : (arg, rest) → arg.instanceMethod(rest)
    메서드참조 : ClassName::instanceMethod

  3. 기존 객체의 인스턴스 메서드 참조
    람다 : (arg) → expr.instanceMethod(arg)
    메서드참조 : expr::instanceMethod

@Test
public void 메서드참조_3가지(){
    // 1. 정적메서드 참조 - ToIntFunction<String> 대체
    func1("1", (i) -> Integer.parseInt(i)); // 람다
    func1("1", Integer::parseInt); // 메서드참조

    // 2. 다양한 형식의 인스턴스 메서드 참조 - Predicate<String> 대체
    func2("2", (str) -> str.isEmpty()); // 람다
    func2("2", String::isEmpty); // 메서드참조

    // 3. 기존 객체의 인스턴스 메서드 참조 - Predicate<String> 대체
    func3("123", (prefix) -> ref.refFunc(prefix)); // 람다
    func3("123", ref::refFunc); // 메서드참조
}

int func1(String str, ToIntFunction<String> f){
    return f.applyAsInt(str);
}

boolean func2(String str, Predicate<String> f){
    return f.test(str);
}

boolean func3(String str, Predicate<String> f){
    return f.test(str);
}

FuncUtil ref = new FuncUtil();
class FuncUtil{
    boolean refFunc(String str){
        return str.startsWith("1");
    }
}

생성자 참조

  • ClassName::new 처럼 기존 생성자에 대한 참조가 가능하다.
  • Functional Interface 인스턴스 ↔ 익명 클래스 인스턴스 ↔ 람다 표현식 ↔ 생성자 참조의 변환이 가능하다.
  • Supplier 구현 객체를 생성자 참조로 변환
      Supplier<Apple> c1 = Apple::new; // 생성자 참조
      Supplier<Apple> c2 = () -> new Apple(); // 람다 표현식
      Apple a1 = c1.get(); // 생성자가 실행되어 생성된 Apple 객체가 리턴된다.
    
  • 파라미터가 있는 생성자는 같은 시그니처의 Functional Interface로 대체가 가능하다. Functional Interface의 시그니처에 따라 참조하는 생성자가 추론된다. 아래 코드에서는 인자가 2개(Integer, String)인 Apple 클래스의 생성자가 추론된다.
      BiFunction<Integer, String, Apple> c1 = Apple::new;
      BiFunction<Integer, String, Apple> c2 = (size, name) -> new Apple(size, name);
      Apple a1 = c1.apply(1, "redApple");
    

예제로 람다, 메서드 참조 활용

Human 인스턴스 리스트를 자바8 List API의 sort 메서드를 활용하여 정렬하고자 한다. Human 클래스는 age만 프로퍼티로 가지며 단순하게 나이순으로 오름차순 정렬한다.

class Human {
    int age;

    public Human(int age){
        this.age = age;
    }

    public int getAge(){
        return this.age;
    }
}
// 정렬할 list
List<Human> list = Arrays.asList(new Human(3), new Human(2), new Human(1), new Human(4));
  • 먼저 sort 메서드에 건네줄 Comparator 구현체를 익명 클래스로 구현한다.
    list.sort(new Comparator<Human>() {
      @Override
      public int compare(Human h1, Human h2) {
          return Integer.compare(h1.getAge(), h2.getAge());
      }
    });
    
  • 익명 클래스는 람다로 치환이 가능하니 compare 메서드의 시그니처에 맞게 람다로 구현한다.
    list.sort((Human h1, Human h2) -> Integer.compare(h1.getAge(),h2.getAge()));
    
  • 람다표현식에서 파라미터는 구현하는 Functional Interface가 가지는 메서드에 따라 추론이 가능하므로 생략할 수 있다.
    list.sort((h1, h2) -> Integer.compare(h1.getAge(), h2.getAge()));
    
  • Comparator는 Comparable 키를 추출하여 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적메서드 comparing을 제공한다. 즉 제공하는 Comparator.comparing 메서드를 이용하여 Comparator를 생성할 수 있다.
    여기서 Human객체 비교를 위한 Comparable 키는 age의 래퍼클래스인 Integer가 된다.
Comparator<Human> c1 = Comparator.comparing((h1) -> h1.getAge());
list.sort(c1);

// 또는

lists.sort(Comparator.comparing((h1) -> h1.getAge()));
  • comparing 메서드의 인자인 Function 인터페이스 구현체는 메서드 참조로 대체할 수 있다.
    lists.sort(Comparator.comparing(Human::getAge));
    
  • 만일 메서드를 정적임포트 한다면 아래와 같이 가독성 좋은 코드로 구현이 가능하다.
    import static java.util.Comparator.comparing;
    lists.sort(comparing(Human::getAge));
    
  • 추가로 Comparator를 포함한 몇몇 Functional Interface는 람다 표현식을 조립하여 사용할 수 있도록 디폴트 메서드를 제공해준다.
    만일 age로 내림차순된 정렬을 하고 싶으면 제공되는 디폴트 메서드인 reversed()를 활용한다.
    이 밖에도 Comparator.thenComparing() 으로 두번째 비교자를 지정할 수도 있다.
    list.sort(comparing(Human::getAge)
      .reversed() // age 오름차순을 내림차순으로 변경
      .thenComparing(Human::getHeight)); // age가 같으면 height 오름차순으로 정렬