Java

모던 자바 인 액션 - chapter 2, 3 동작 파라미터화, 람다 표현식

뽀루피 2024. 11. 19. 23:55

동작 파라미터화

동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다. 이는 자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.

 

동작 파라미터화 방법

기존엔 클래스를 통한 동작을 메서드의 인수로 전달하는 방법을 통해 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 람다(익명 함수)를 제공한다.

  • 선택 조건을 결정하는 인터페이스 선언(Predicate)
public interface ApplePredicate {
    boolean test(Apple apple);
}
  • 클래스를 통한 동작 파라미터화
public class AppleGreenColorPredicate implements ApplePerdicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}

List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());
  • 익명 클래스를 통한 동작 파라미터화
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate() {
        public boolean test(Apple apple) {
            return GREEN.equals(apple.getColor());
        }
});
  • 람다를 통한 동작 파라미터화
List<Apple> greenApples
     = filterApples(inventory, (Apple apple) -> GREEN.equals(apple.getColor()));

람다를 이용하면 동작을 메서드 인수로 전달하는 코드가 매우 간결해진다.

 

 

 

람다 표현식

람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 알아보자. 또한 자바 8에 추가된 중요한 인터페이스와 형식 추론 등의 기능을 확인한다.

 

 

람다의 특징

  • 익명: 보통의 메서드와 달리 이름이 없다
  • 함수: 보통의 메서드처럼 특정 클래스에 종속되지 않으므로 함수라 부른다.
  • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성: 익명 클래스와 같이 많은 코드를 구현할 필요가 없다.

 

람다의 구성

파라미터 / 화살표 / 바디

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 파라미터: 람다 바디에서 사용할 메서드 파라미터를 명시한다.
  • 화살표: 람다의 파라미터와 바디를 구분한다.
  • 바디: 람다의 반환값에 해당하는 표현식이다.

 

람다의 사용

함수형 인터페이스

함수형 인터페이스는 하나의 추상메서드를 가진 인터페이스다(여기서 디폴트 메서드는 제외한다). 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 인터페이스의 인스턴스로 취급할 수 있다.

함수 디스크립터

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 한다.
예를 들어 함수형 인터페이스 Comparator의 compare 메서드의 함수 디스크립터는 (T, T) -> int이다.

 

 

함수형 인터페이스

Predicate

(T) -> boolean

@FuncationalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Consumer

(T) -> void

@FuncationalInterface
public interface Consumer<T> {
    void accept(T t);
}

Supplier

() -> T

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Function

(T) -> R

@FuncionalInterface
public interface Function<T, R> {
    R apply(T t);
}

기본형 함수형 인터페이스

제네릭은 참조형 타입만 지정할 수 있다. Integer와 같이 기본형에 대해 박싱된 타입을 통해 제네릭을 이용할 수 있지만 오토박싱으로 인해 변환 비용이 소모된다. 자바 8에서는 오토박싱을 피할 수 있도록 기본형에 특화된 버전의 함수형 인터페이스를 제공한다.
앞서 보았던 함수형 인터페이스의 이름 앞에 기본형 타입의 이름을 합친 이름으로 제공된다. 예로 파라미터로 int를 받는 Predicate, IntPredicate를 제공한다.

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}
  • Unary: 파라미터 타입과 반환 타입이 같은 경우
  • @FunctionalInterface public interface UnaryOperator<T> extends Function<T, T> { }
  • Bi: 파라미터 인자가 두개인 경우
  • @FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); }

 

함수형 인터페이스의 예외

함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만드려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try-catch 블록으로 감싸야 한다.

 

형식 검사, 형식 추론, 제약

람다 표현식이 어떻게 동작하게 되는지 알아보자

형식 검사

람다가 사용되는 context를 이용해서 람다의 형식을 추론할 수 있다. 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다. 형식검사는 다음과 같은 과정으로 진행된다.

  1. 람다가 사용된 메서드의 선언을 확인한다.
  2. 람다가 사용된 메서드의 파라미터로 대상 형식을 기대한다.
  3. 기대하는 파라미터의 함수형 인터페이스를 파악한다.
  4. 함수형 인터페이스의 함수 디스크립터를 묘사한다.
  5. 전달받은 인수의 람다가 그 요구사항을 만족해야 한다.

형식 추론

제네릭을 사용할 때 선언부에 타입 매개변수를 명시하면 생성자에서는 빈 다이아몬드 연산자로 남겨둬도 자바 컴파일러는 생성 객체의 타입을 추론할 수 있는 것처럼 람다 표현식도 마찬가지이다. 람다 표현식이 사용된 컨텍스트를 이용해서 관련된 함수형 인터페이스를 추론한다.

// 형식 추론을 하지 않음
Comparator<Apple> c =
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 형식을 추론함
Comparator<Apple> c = 
    (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

제약

람다 표현식에서는 final로 선언되어 있거나 final로 선언된 것처럼 불변한 지역변수(외부에서 정의된 변수)를 활용할 수 있다. 이를 람다 캡처링이라 부른다.
인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 지역변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바에서는 원본 변수에 접근을 허용하는 것이 아니라 지역 변수의 복사본을 제공한다. 즉, 복사본의 값이 변하지 않아야 하므로 지역변수에는 불변한 값을 할당해야 한다는 제약이 발생한 것이다.

 

 

메서드 참조

명시적으로 메서드 명을 참조함으로써 가독성을 높일 수 있다. 메서드 참조는 메서드명 앞에 구분자(::)를 붙이는 방식으로 사용할 수 있다. Class::method 형식을 취한다. 메서드 참조는 세 가지 유형으로 구분할 수 있다.

  1. 정적 메서드 참조 - Integer::parseInt
  2. 다양한 형식의 인스턴스 메서드 참조 - String::length
    • 아직 객체가 생성되지 않은 상태에서 메서드 참조를 생성하고, 나중에 객체를 전달받아 실행한다.
      Function<String, Integer> stringLengthGetter = String::length;
      System.out.println(stringLengthGetter.apply("hello")); // 출력: 5
  3. 기존 객체의 인스턴스 메서드 참조
    • 이미 생성된 객체에 연결된 인스턴스 메서드를 참조한다.
      Apple apple = new Apple(200);
      Supplier<Integer> weightGetter = apple::getWeight;
      System.out.println(weightGetter.get()); // 출력: 200
  4. 생성자의 참조 - ClassName::new

 

람다 표현식을 조합할 수 있는 유용한 메서드

Comparator

  • comparing - 비교에 사용할 Function 기반의 키 지정
  • reversed - 역정렬
  • thenComparing - 동일한 조건에 대하여 추가적인 비교

Predicate

  • and
  • or
  • negate - not 연산

Function

  • andThen - 이후에 처리할 Function 추가
  • compose - 이전에 처리되어야할 Function 추가