모던 자바 인 액션 3 - 람다

UkJJang·2022년 3월 16일
0

람다 표현식이란 ?

  • 람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있다.

람다란 무엇인가?

  • 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
  • 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있다.
    • 익명 : 보통의 메서드와 달리 이름이 없기 때문에 "익명"이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다.
    • 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환형식, 예외 리스트를 포함한다.
    • 전달 : 람다 표현식을 메ㅓ드 인수로 전달하거나 변수로 저장할 수 있다.
    • 간결성 : 익명 클래스처럼 많은 자잘구레한 코드를 구현할 필요가 없다.

람다를 사용하면 동작 파라미터 형식의 코드를 더 쉽게 구현할 수 있다.

        Comparator<Apple> comparator1 = new Comparator<>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight() - o2.getWeight();
            }
        };

		// 람다를 사용하면 훨씬 더 간결해진다.
        Comparator<Apple> comparator2 = 
        			(Apple a1, Apple a2) -> a1.getWeight() - a2.getWeight();
        
  • 중요한 것은 비교를 하는데 필요한 코드를 전달할 수 있다는 점이다.
  • 람다 표현식을 사용하면 compare 메서드의 바디를 직접 전달하는 것 처럼 코드를 전달할 수 있다.

람다 표현식 사용법은 파라미터, 화살표, 바디로 이루어진다.
(Apple a1, Apple a2) : 람다 파라미터
-> : 화살표
a1.getWeight() - a2.getWeighte(); : 람다 바디

람다 예제

        // 불리언 표현식
        (List<String> list) -> list.isEmpty();
        
        // 객체 생성
        () -> new Apple(10);
        
        // 객체에서 소비
        (Apple a) -> {
            System.out.println(a.getWeight());
        }
        
        // 객체에서 선택 / 추출
        (String s) -> s.length();
        
        // 두 값을 조합
        (int a1, int a2) -> a1 * a2;
        
        // 두 객체 비교
        (Apple a1, Apple a2) -> a1.getWeight() > a2.getWeight();

어디에 어떻게 사용될까?

  • 함수형 인터페이스라는 문맥에서 람다를 사용할 수 있다.

함수형 인터페이스

  • Predicate 인터페이스로 필터 메서드를 파라미터 했던 예제를 떠올려 보자. 이 Predicate가 함수형 인터페이스다. 오직 하나의 추상 메서드만 지정하기 때문이다.
  • 간단히 말해 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다. 지금까지 살펴본 함수형 인터페이스에는 Comparator, Runnable 등이 있다.
public interface Predicate<T> {
	boolean test(T t);
}

함수형 인터페이스는 어디에 사용할 수 있을까?

  • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.

함수 디스크립터

  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다.
  • 예를들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로 void이다. Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있다.

@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader br) throws IOException;
    
}
    // 기존
    public String processFile1() throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return br.readLine();
        }
    }

    // 람다 사용
    public String processFile2(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return p.process(br);
        }
    }

    public void execute() throws IOException{

        processFile2((BufferedReader br) -> br.readLine());
        processFile2((BufferedReader br) -> br.readLine()+br.readLine());


    }

Predicate

  • Predicate 인터페이스는 test라는 추상메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환합니다.
  • 우리가 만들었던 인터페이스와 같은 형태인데 따로 정의할 필요 없이 바로 사용할 수 있다는 점이 특징입니다.
  • T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있습니다.
    public <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> results = new ArrayList<>();
        for (T t : list) {
            if (p.test(t)) {
                results.add(t);
            }
        }
        return results;
    }

    public static void main(String[] args) {

        Predicate<String> predicate = (String s) -> !s.isEmpty();
        List<String> list = new ArrayList<>();
        List<String> nonEmpty = filter(list, predicate);    
    
    }

Consumer

  • Consumer 인터페이스는 제네릭 형식 T 객체를 받아 void를 반환하는 accept라는 추상 메서드를 정의합니다.
  • T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있습니다.
  • 예를들어 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 사용할 수 있습니다.
    public static <T> void forEach(List<T> list, Consumer<T> consumer) {
        for (T t : list) {
            consumer.accept(t);
        }
    }

    public static void main(String[] args) {
        forEach(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));
    }

Function

  • Function 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의합니다.
  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있습니다.
  • 예를들어 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑 등 다양한 예제에 활용할 수 있습니다.
    static public <T, R> List<R> map(List<T> list, Function<T, R> f) {
        List<R> result = new ArrayList<>();
        for (T t : list) {
            result.add(f.apply(t));
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> result = map(Arrays.asList("lambda", "TTT", "RRRR"),
                (String s) -> s.length()
        );
        System.out.println(result);
    }

기본형 특화!

  • 위의 세 개의 제네릭 함수형 인터페이스를 살펴봤다. 하지만 특화된 형식의 함수형 인터페이스도 있다.
  • 자바의 모든 형식은 참조형(Integer, Byte, Object, List) 아니면 기본형(int, double)에 해당한다. 하지만 제네릭 파라미터에는 참조형만 사용할 수 있다. 제네릭의 내부 구현때문에 어쩔 수 없는 일이다.
  • 자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱(Boxing)이라고 한다.
  • 참조형을 기본형으로 바꾸는 동작을 언박싱(Unboxing)이라고 한다.
  • 또한 편리하게 코드를 구현할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱(autoboxing)이라는 기능도 제공한다. 예를들어 다음은 유효한 코드다
    List<Integer> list = new ArrayList<>();
    for(int i=300; i<400; i++) {
      list.add(i)
    }                	

    하지만 이런 변환 과정은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.

자바8 에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다. 예를들어 아래 예제에서 IntPredicate는 1000이라는 값을 박싱하지 않지만 Predicate는 1000이라는 값을 Integer 객체로 박싱한다.


  public interface IntPredicate {
  	boolean test(int t);
  }
  
  IntPredicate evenNumbers = (int i) -> i % 2 == 0;
  evenNumbers.test(1000); // 참 (박싱없음)
  
  Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
  oddNumbers.test(1000) // 거짓(박싱)
  
  • 일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer, LongBinaryOperator, IntFucntion처럼 형식명이 붙는다.
  • Function 인터페이스는 ToIntFunction, IntToDoubleFunction 등의 다양한 출력 형식 파라미터를 제공한다.

예외, 람다, 함수형 인터페이스의 관계

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

0개의 댓글