SummaryBook/[모던 자바 인 액션]

[Chap 3]람다 표현식

seung_soos 2023. 7. 3. 23:32

3.1 람다란 무엇인가?

람다 표현식은 메서드로 전달 할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생 할 수 있는 예외 리스트는 가질 수 있다.

다음은 람다의 특징이다.

1. 익명

 - 보통의 메서드와 달리 이림이 없으므로 익명이라 표현한다.

2. 함수

 - 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식 ,가능한 예외

    리스트를 포함한다.

3. 전달

 - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장 할 수 있다.

4. 간결성

 - 익명 클래스처럼 많은 코드를 구현 할 필요가 없다.

 

람다표현식을 사용시 간결한 방식으로 코드를 전달 할 수있다. 람다가 기술적으로 Java 8 이전의 Java로 할 수 없었던 일을 제공하지는 않는다. 

    	// 기존코드
        Comparator<Apple> byWeight1 = new Comparator<Apple>() {
            public int compare(Apple o1, Apple o2) {
                return Integer.compare(o1.getWeight(), o2.getWeight());
            }
        };
        
        // 람다를 이용한 코드
        Comparator<Apple> byWeight2 =
                (Apple o1, Apple o2) -> Integer.compare(o1.getWeight(), o2.getWeight());

람다 표현식을 이용하면 compare 메서드의 바디를 직접 전달하는 것처럼 코드를 전달 할 수 있다.

3.2 어디에, 어떻게 람다를 사용할까?

3.2.1 함수형 인터페이스

오직 하나의 추상 메서드가 있는(디폴트 메서드 제외)  Predicate, Comparator, Runnable 등 이있다.

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

(기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스) 할 수 있다.

 

예전에는 함수형 인터페이스의 입, 출력을 다 기억했는데, 간만에 보니 새롭다.🥲

  public static void main(String[] args) {

        Runnable r1 = () -> System.out.println("run() Method 실행-1");

        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("run() Method 실행-2");
            }
        };

        process(r1);
        process(r2);
        process(()-> System.out.println("run() Method 실행-3"));

    }

    public static void process(Runnable r){
        r.run();
    }

3.2.2 함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

Ex) Runnable의 run메서드는 인수와 반환값이 없으므로(void 반환) 인수와 반환값이 없는 시그니처이다.

 

람다와 함수형 인터페이스를 가리키는 특별한 표기법을 사용한다.

() -> void 표기는 파라미터가 없고, void를 반환하는 함수를 의미한다. 즉, 앞에서 설명한 Runnable이 이에 해당한다.

(Apple,  Apple) -> int 는 두개의 Apple을 인수로 받아 int를 반환하는 함수를 가리킨다.

	 // 정상적인 람다 표현식
        process(()-> System.out.println("run() Method 실행"));
        // 중괄호로 감싼 표현
        process(()->{
            System.out.println("run() Method 실행");
        });
        //한개의 void 메소드 호출은 중괄호로 감쌀 필요가 없다.

✅ @FunctionalInterface 란?

함수형 인터페이스를 가르키는 어노테이션이다. @FunctionalInterface로 인터페이스를 선언했지만, 실제로 함수형 인터페이스가 아니면

컴파일러가 에러를 발생시킨다.

Ex) 추상 메서드가 한개 이상이라면 에러발생

3.3 람다 활용 : 실행 어라운드 패턴

자원처리(예를들면 데이터베이스의 파일처리)에 사용하는 순환패턴은 자원을 열고, 처리한 다음, 자원을 닫는 순서로 이루어진다.

설정과 정리 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두과정이 둘러싸는 형태를 갖는다.

실행 어라운드 패턴

3.3.1 1단계 : 동작 파라미터화를 기억하라

현재 코드는 파일에서 한번에 한 줄만 읽을 수 있다. 한번에 두중을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야할까?

processFile의 동작을 파라미터화하자.

processFile 메서드가 한번에 두 행을 읽게 하려면 아래 코드로 수정이 가능하다.

    // 파일 두행을 읽도록 수정
    String result = processFile((BufferedReader br) -> 
                        { return br.readLine() + br.readLine()});

3.3.2 2단계 : 함수형 인터페이스를 이용해서 동작 전달

함수형 인터페이스 자리에 람다를 사용 할 수 있다. BufferedReader와 일치하는 인터페이스를 정의하자.

@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader br) throws IOException;
}

3.3.3 3단계 : 동작실행

이제 위 코드에서 정의한 BufferedReaderProcessor 인터페이스의 process 메서드의 시그니처 (BufferedReader -> String)와

일치하는 람다를 전달 할 수 있다. 람다 표현식으로 함수형 인터페이스이 추상 메서드 구현을 직접 전달 할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리된다.

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

3.3.4 4단계 : 람다 전달

    //람다 표현
    String oneLine = processFile((BufferedReader br) -> br.readLine());

    String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.4 함수형 인터페이스 사용

3.4.1 Predicate

Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 Boolean을 반환한다.

    List<String> listOfStrings = new ArrayList<>();
    
    public <T> List<T> filter(List<T> list, Predicate<T> predicate) {
        List<T> results = new ArrayList<>();
        for (T t : list) {
            if (predicate.test(t)) {
                results.add(t);
            }
        }
        return results;
    }

    Predicate<String> nonEmpryStringPredicate = (String s) -> !s.isEmpty();
  
    List<String> nonEmpty = filter(listOfStrings, nonEmpryStringPredicate);

3.4.2 Consumer

Consumer<T> 인터페이스는 제네릭 형식 T객체를 받아서 void를 반환하는 accept라는 추상메서드를 정의한다.

    public void test(){

        forEach(Arrays.asList(1,2,3,4,5),
                (Integer i) -> System.out.println(i));
    }

    public <T> void forEach(List<T> list, Consumer<T> c){

        for (T t : list) {
            c.accept(t);
        }
    }

3.4.3 Function

Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.

    public void test(){

        List<Integer> l = map(
                Arrays.asList("lambdas", "in", "action"),
                (String s) -> s.length());
    }

    public <T, R> List<R> map(List<T> list, Function<T, R> function) {
        List<R> result = new ArrayList<>();

        for (T t : list) {
            result.add(function.apply(t));
        }
        return result;
    }

기본형 특화

자바의 모든 형식은 참조형(Byte, Integer, Object, List 등) 아니면 기본형(int, double, byte, char 등)에 해당한다.

하지만 제네릭 파라미터에는 참조형만 사용 할 수 있다. 제네릭의 내부 구현 때문에 어쩔수 없는 일이다.

자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱이라고한다.

반대로 참조형을 기본형으로 변환하는 동작을 언박싱이라고 한다. 또한,개발자가 편리하게 코드를 구현 할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱이라는 기능도 제공한다.

하지만 이런 변환과정은 메모리를 더 소비하며 기본형을 가져올때도 메모리를 탐색하는 과정이 필요하다.

자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.

   IntPredicate evenNumbers = (int i) -> i % 2 == 0;

    Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
    public void test() {
        evenNumbers.test(1000); // true(박싱 없음)

        oddNumbers.test(1000); // false(박싱)
    }

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

3.5.1 형식 검사

람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론 할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상형식이라고 한다. 

   List<Apple> result = filter(inventory, (Apple apple) 
   					-> apple.getWeight > 150);

1. filter 메서드의 선언을 확인한다.

2. filter 메서드는 두번째 파라미터로 Predicate<Apple> 형식을 기대한다.

3. Predicate<Apple>은 test라는 한개의 추상 메서드를 정의하는 함수형 인터페이스이다.

4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.

5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

3.5.2 같은 람다, 다른 함수형 인터페이스

대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용 될 수 있다.

    Comparator<Apple> c1 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };

    ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };
    BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> {
        Integer a3 = a1.getWeight();
        Integer a4 = a2.getWeight();

        return a3.compareTo(a4);
    };

3.5.3 형식 추론

자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.

즉 컴파일러는 람다 표현식의 파라미터 형식에 접근 할 수 있으므로 람다 문법에서 이를 생략 할 수 있다.

    // 형식을 추론하지않았음.
    Comparator<Apple> c1 = (Apple a1, Apple a2) -> {
        Integer overrideA1 = a1.getWeight();
        Integer overrideA2 = a2.getWeight();

       return overrideA1.compareTo(overrideA2);
    };

    //형식을 추론
    Comparator<Apple> c2 = (a1, a2) -> {
        Integer overrideA1 = a1.getWeight();
        Integer overrideA2 = a2.getWeight();

        return overrideA1.compareTo(overrideA2);
    };

상황에 따라 명시적으로 형식을 알맞게 포함, 생략하는 것이 중요한듯하다.

3.5.4 지역 변수 사용

지금까지 모든 람다 표현식은 인수를 자신의 바디 안에서 사용했지만, 자유변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 

활용 할 수 있다. 이와 같은 동작을 람다 캡처링이라한다.

   int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);

하지만 자유변수에도 제약이 있다. 

지역변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.

    public static void main(String[] args) {

        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber); // 에러발생
        portNumber = 1111;
        r.run();
    }

3.6 메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달 할 수 있다.

        // 기존 코드
        List<Apple> inventory = new ArrayList<>();

        inventory.sort((Apple a1, Apple a2) ->{
            Integer overrideA1 = a1.getWeight();
            Integer overrideA2 = a2.getWeight();

            return overrideA1.compareTo(overrideA2);
        });

        // 메서드 참조
        inventory.sort(comparing(Apple::getWeight));

3.6.1 요약

메서드 참조를 이용하면, 기존 메서드 구현으로 람다 표현식을 만들 수 있다. 이때 명시적으로 메서드명을 참조함으로서 가독성 을 높일 수 있다. 

메서드 명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용 할 수 있다. 

Ex) Apple::getWeight는 Apple 클래스에 정의된 getWeight의 메서드 참조이다.

메서드 참조를 만드는 방법

1. 정적 메서드 참조

 - Ex) Integer의 paseInt 메서드는 Integer::parseInt로 표현

2. 다양한 형식의 인스턴스 메서드 참조

 - Ex) String의 length 메서드는 String::length로 표현

3. 기존 객체의 인스턴스 메서드 참조

 - Ex) Transction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면,  

            이를, expensiveTransaction::getValue라고 표현

컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인한다.

즉, 메서드 참조는 콘텍스트의 형식과 일치해야한다.

3.6.2 생성자 참조

ClassName::new 처럼 클래스 명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.

Supplier, Function 예제

        /**
         * 3.6.2 생성자 참조
         */
        Supplier<Apple> c1 = Apple::new;
        Apple a1 = c1.get();

        Supplier<Apple> c2 = () -> new Apple();
        Apple a2 = c2.get();

        Function<Integer, Apple> c3 = Apple::new;
        Apple a3 = c3.apply(100);

        Function<Integer, Apple> c4 = (Integer) -> new Apple(Integer);
        Apple a4 = c4.apply(120);

Integer를 포함하는 List 예제

    List<Integer> weights = Arrays.asList(7, 3, 4, 10);
    List<Apple> apples = map(weights, Apple::new);

    public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
        List<Apple> result = new ArrayList<>();
        for (Integer i : list) {
            result.add(f.apply(i)); // Function을 이용 Integer -> Apple 변형 후 add
        }
        return result;
    }

BiFunction 인터페이스 예제

    BiFunction<Color, Integer, Apple> c3 = Apple::new;
    Apple a3 = c3.apply(Color.GREEN, 110);

    BiFunction<Color, Integer, Apple> c4 = (color, weigth) -> new Apple(color, weigth);
    Apple a4 = c4.apply(Color.GREEN, 100);

인스턴스화하지 않고도 새 ㅇ성자에 접근 할 수 있는 기능을 다양한 상황에 응용 할 수 있다. 

예를들어 Map으로 생성자와 문자열값을 관련 시킬 수 있다. 그리고 String과 Integer가 주어졌을때 다양한 무게를 갖는 여러 종류의 과일을 만드는 giveMeFruit라는 메서드를 만들수 있다.

    static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
    static {
        map.put("apple", Apple::new);
        map.put("orange", Orange::new);
    }
    public static Fruit giveMeFruit(String fruit, Integer weight) {
        return map.get(fruit.toLowerCase()).apply(weight);
    }

3.7 람다, 메서드 참조 활용하기

3.7.1 1단계 : 코드 전달

/**
 * 람다, 메서드 참조 활용하기
 */
public class AppleComparator implements Comparator<Apple>{
    /**
     * 3.7.1 1단계 : 코드 전달
     */
    @Override
    public int compare(Apple o1, Apple o2) {
        return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
    }

    public static void main(String[] args) {
        List<Apple> inventory = new ArrayList<>();
        inventory.sort(new AppleComparator());
    }
    
}

3.7.2 2단계 : 익명 클래스 사용

한번만 사용할 Comparator를 위코드 처럼 구현하는 것보다는 익명 클래스를 이용하는것이 좋다.

        /**
         * 3.7.2 2단계 : 익명 클래스 사용
         */
		inventory.sort(new Comparator<Apple>() {
                @Override
                public int compare(Apple o1, Apple o2) {
                    return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
            }
        });

3.7.3 3단계 : 람다 표현식 사용

        /**
         * 3.7.3 3단계 : 함다 표현식 사용
         */
        inventory.sort((Apple o1, Apple o2) -> {
            return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
        });
        /**
         * 파라미터 추론방식
         */
        inventory.sort((o1, o2) -> {
            return ((Integer) o1.getWeight()).compareTo((Integer) o2.getWeight());
        });

 

3.7.4 4단계 : 메서드 참조 사용

        /**
         * 3.7.4 4단계 : 메서드 참조 사용
         */
        inventory.sort(comparing(Apple::getWeight));

Java 8 이전에 비해 코드만 짧아진 것이 아니라 코드의 의미도 명확해졌다.

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

3.8.1 Comparator 조합

정적 메서드 Comparator.comparing을 이용해서 비교에 사용할 키를 추출하는 Function 기반의 Comparator를 반환 할 수 있다.

        /**
         * 3.8.1 Comparator 조합
         */
        Comparator<Apple> c = comparing(Apple::getWeight);

역정렬

사과의 무게를 내림차순으로 정렬하고 싶다면 어떻게 해야 할까?

인터페이스 자 체에서 주어진 비교자의순서를 뒤바꾸는 reverse라는 디폴트 메서드를 사용하면 된다!

        /**
         * 역정렬
         */
        inventory.sort(comparing(Apple::getWeight).reversed());

Comparator 연결

여기서 무게가 같은 사과가 있다면 어떻게 해야할까?

이럴때는 비교결과를 더 다듬는 두번째 Comparator를 만들 수 있다.

        /**
         * Comparator 연결
         */
        inventory.sort(comparing(Apple::getWeight)
                .reversed()
                .thenComparing(Apple::getColor)); // 색깔별로 정렬

3.8.2 Predicate 조합

Predicate 인터페이스는 negate, and, or 세가지 메서드를 제공한다.

        /**
         * 3.8.2 Predicate 조합
         */
        Predicate<Apple> redApples = (Apple apple) -> Color.RED.equals(apple.getColor());
        // redApples의 결과를 반전시킨다.
        Predicate<Apple> notRedApple = redApples.negate();
        // 두 Predicate를 연결하여 새로운 Predicate로 만든다.
        Predicate<Apple> redAndHeavyApples = redApples.and((apple -> apple.getWeight() > 150));
        // Predicate 를 연결하여 복잡한 Predicate를 만든다.
        Predicate<Apple> redAndHeavyAppleOrGreen = redApples.and(apple -> apple.getWeight() > 150)
                                                .or(apple -> Color.GREEN.equals(apple.getColor()));

3.8.3 Funciton 조합

Function 인터페이스는 Function 인터페이스를 반환하는 andThen, compose 두가지 메서드를 제공한다.

andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 ㅇ비력으로 전달하는 함수를 반환한다.

compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.

        // andThen
        Function<Integer, Integer> f1 = x -> x + 1;
        Function<Integer, Integer> g1 = x -> x * 2;
        Function<Integer, Integer> h1 = f1.andThen(g1);
        int result1 = h1.apply(1); // 4을 반환

        // compose
        Function<Integer, Integer> f2 = x -> x + 1;
        Function<Integer, Integer> g2 = x -> x * 2;
        Function<Integer, Integer> h2 = f2.compose(g2);
        int result2 = h2.apply(1); // 3을 반환

예제 코드 : https://github.com/seungsoos/ModernJavaInAction