📗 개발자 책 읽기/모던 자바 인 액션

[모던자바인액션] chapter 3. 람다 표현식이란 - 람다 문법, 구조, 함수형 인터페이스, 형식 추론

민돌v 2022. 8. 18. 09:56
728x90

 

 

" Modern Java In Action - 모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드 "
독서 스터디 후, 책 내용을 정리한 글입니다.


1) Chapter 1. 자바 8, 9, 10, 11 : 무슨일이 일어나고 있는가?
2) Chapter 2. 동작 파라미터화 코드 전달하기
3) Chapter 3. 람다 표현식
4) Chapter 4. Stream
5) Chapter 5. Stream 활용
6) Chapter 6. Stream으로 데이터 수집하기 
7) Chapter 7. Stream 병렬 데이터처리와 성능

 

 

 

chapter3 에서는 람다표현식에 대해 이야기합니다.

람다 문법, 람다의 구성, 람다 사용방법, 함수형 인터페이스, 형식 추론, 메소드 참조 등등,,

 

👏🏻람다의 구성과 형식추론이 가장 인상깊었던 장이였습니다.

 


1. 람다란

📌 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것 입니다.

  • 람다 표현식에는 이름은 없지만 다음과 같은 목록들을 가질 수 있습니다.
    1. 파라미터 리스트
    2. 바디
    3. 반환 형식
    4. 발생할 수 있는 예외리스트

1) 람다 특징

익명

  • 보틍의 메서드와 달리 이름이 없으므로 람다를 익명이라 표현합니다.
  • 익명이므로 구현해야 할 코드가 줄어듭니다.

함수

  • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.. (와우)
  • 하지만 메서드가 가지는 걸 대부분 가질 수 있다.

전달

  • 람다 표현식을 메서드 인수로 전달하거나 (동작 파라미터화)
  • 변수로 저장할 수 있다.

간결성

  • 람다는 익명클래스보다 매우 간결하게 작성할 수 있는 특징을 가진다.

 


2) 람다 구조

📌 람다 표현식의 구성은 파라미터, 화살표, 바디 3부분으로 이루어집니다.

ex) 람다 예시

/**
 * 람다를 사용하지 않았을 떄
 */
Comparator<Apple> byWeight = new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
};

/**
 * 람다를 사용하면 아래처럼
 * 람다는 3부분으로 이루어짐 : 람다 파라미터 ->(화살표) 람다 바디
 */
Comparator<Apple> byWeight_Lambda = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

 

  1. 파리미터 리스트 : Comparator 의 compare 매서드 파라미터 (사과 2개)
  2. 화살표 : (->) 람다의 파라미터 리스트와 바디를 구분하는 역할을 한다.
  3. 람다 바디 : 람다의 반환값에 해당하는 표현식 이다.

 


 

3) 람다 문법

자바 설계자는 람다를 다른 언어의 문법과 유사하게 만들고 싶어했기 때문에

  1. 람다의 기본 문법은, 표현식 스타일(expression style) 람다라고도 합니다. 
  2. 혹은 블록 스타일 람다로도 사용할 수 있습니다.

 

  • 표현식 스타일(expression style) 람다(기본 문법)
(parameters) -> expression 	//표현
  • 블록 스타일(block style) 람다
(parameters) -> { statements; } // 구문

 

ex) 람다 사용 방법

/**
 * 람다 예시
 */

//람다는 return이 함축되어 있음
 int slen = (String s) -> s.length();
 
 //람다 바디 중괄호 안에 여러 행의 문장을 포함할 수 있음
(int x, int y) -> {
    System.out.println("순서대로");
    System.out.println(x + y);
    System.out.println("실행해요");
}

//람다의 파라미터가 없을 수도 있다. 이때는 42를 그대로 반환
() -> 42

 

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

 


 

2. 함수형 인터페이스

📌함수형 인터페이스란, 정확히 1개의 추상메서드를 지정하는 인터페이스 입니다.

 

인터페이스는 디폴트 메소드를 가질 수 있다는 특징을 가집니다.

하지만 아무리 많은 디폴트 메소드가 있더라도, 1개의 추상메서드만 가진다면 "함수형 인터페이스" 입니다.

반대로 추상메소드없으면 함수형 인터페이스가 아닙니다.

 


 

1) 함수형 인터페이스를 사용하는 이유

👏🏻 함수형 인터페이스는 1개의 추상 메서드를 가집니다.

 

그렇기때문에, 람다 표현식으로 이 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있습니다.

즉, 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있습니다.

 

💡 메서드를 인스턴스화 시켜서 사용할 수 있음을 의미합니다!

 

함수형 인터페이스 사용 예시

  • 함수형 인터페이스 선언
public interface Runnable {
    void run();
}
  • 람다 이용
Runnable r1 = () -> System.out.println("람다를 이용해서 메소드를 바로 직접 구현");
  • 익명클래스 이용
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("익명 클래스를 이용해서 직접 구현한 케이스");
    }
};
  • 함수형 인터페이스는 파라미터로 전달 가능
public void process(Runnable r3) {
    r3.run();
}

process(() -> System.out.println("람다로 파라미터 전달"));

 


 

2) 함수 디스크립터

📌 함수 디스크립터란, 함수형 인터페이스의 추상메서드 시그니처를 말합니다.

  • 시그니처란 메서드 이름 +  매개변수 리스트를 나타내는 것을 말합니다.
  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가르킵니다.
  • 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 합니다

 

위 예시에 나온  Runnalbe 인터페이스는 void형이기 때문에 인수와 반환값이 없는 시그니처로 생각할 수 있습니다.

 


 

3) @Functionalinterface란

명시적으로 함수형 인터페이스를 나타내는 어노테이션 입니다.

 

@Functionalinterface 를 선언하면, 함수형 인터페이스가 아닐 때 컴파일 에러가 터집니다.

 


 

4) 👏🏻 함수형 인터페이스 우아하게 사용하기 

제가 생각했을 때, 함수형 인터페이스의 중요한 점은 파라미터 리스트와 반환값, 즉 함수 디스크립터가 무엇이냐라고 생각합니다.

 

함수 디스크립터가 갔다면, 그 구현이 어찌됬건 얼마든지 재활용해서 사용할 수 있습니다.

그렇기 때문에 Java.utill.function 패키지에 몆가지 구현된 함수형 인터페이스가 이미 있습니다.

 

이를 그저 가져다 사용한다면 코드의 양과 중복을 줄일 수 있을 것 같습니다.

 

1. boolean을 반환하는 Predicate

/**
 * Represents a predicate (boolean-valued function) of one argument.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #test(Object)}.
 *
 * @param <T> the type of the input to the predicate
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

간단 predicate 예제

Predicate<Integer> predicate = (num) -> num > 10;
boolean result = predicate.test(100);

System.out.println(result); //true

predicate 는 디폴트 매소드로, or, and, isEqual 등등이 구현되어있으니 한번 보고 필요에 맞게 사용하자!

 

2. 단일 입력값에 대해 void 를 반환하는 Consumer

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

간단 Consumer 예제

Consumer<String> consumer = s -> System.out.println(s.toUpperCase());

consumer.accept("hello world");		//HELLO WORLD 반환

 

3. 제네릭 형식 T 를 받아 제네릭 형식 R 객체를 반환하는 Function

/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}

간단 function 예제

Function<Integer, Integer> func1 = x -> x * x;
Integer result = func1.apply(10);

System.out.println(result);		//return 100

 


 

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

람다로 함수형 인터페이스의 인스턴스를 만들 수 있습니다.

람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않기 때문에

람다의 실제 형식을 파악해야 합니다.

 

 

1) 형식 검사

람다가 사용하는 컨텍스트(문맥) 를 이용해서 람다의 형식(type) 을 추론할 수 있습니다.

어떤 콘텍스트 (ex - 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등) 에서 기대되는 람다 표현식의 형식대상 형식(Target Type) 이라고 합니다.

 

ex) 형식 확인 과정

List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
    1. filter 메서드의 선언을 확인한다.
    2. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대한다.
    3. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다.
    4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
    5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.

 

👏🏻 컴파일러는, 콘텍스트를 통해 람다 표현식과 관련된 함수형 인터페이스를 추론합니다.

컴파일러가 형식을 추론하는 동작과정

 

 

2) 형식 추론

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

즉, 대상형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있습니다.

 

📗 결과적으로 컴파일러는 람다 표현식 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있음을 의미합니다.

 

 

 

3) 람다 캡쳐링

지금까지 살펴본 람다는, 바디에 있는 인수만을 사용했습니다.

💡 람다 캡쳐링이란, 람다는 바디 내부 뿐만 아니라 외부에 선언된 변수(자유 변수) 또한 사용할 수 있는데 이를 람다 캡려링이라 합니다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);		//portnumber 사용 = 람다 캡쳐링

 

 

📌 람다 캡쳐링 제약 조건

  • 람다는 인스턴스 변수와 자유 변수를 자유롭게 캡처할수 있지만
  • 그러기위해선, 지역 변수는 명시적으로 final 이 선언되어 있거나 실질적으로 final 과 똑같이 사용되어야만 합니다.
  • 즉, 자유 변수를 람다에서 캡쳐링하기 위해서는, 값이 한번 할당된후 변경되지 않는 불변한 값임을 보장해주어야합니다.

 

 

📌 람다캡쳐링 변수 제약이 있는 이유

  • 자바에서 인스턴스 변수는 힙에 저장되고, 지역 변수는 스택에 저장됩니다.
  • 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면
  • 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있습니다.

 

🔥 따라서 자바에서는 원래 변수에 접근을 허용하지 않고 복사본을 전달하는데, 이 복사본의 값이 바뀌지 않아야 하므로

람다에서 지역 변수를 사용할 때 한 번만 값을 할당해야 한다는 제약이 생긴 것 입니다.

 

또한 지역 변수의 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그래밍 패턴(병렬화를 방해하는 요소)에 제동을 걸 수 있습니다.

 


 

4. 메소드 참조

  • 메소드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있습니다.
  • 명시적으로 메소드 명만을 참조해서 파라미터를 전달해 호출 하는 것이기 때문에 가독성을 높일 수 있습니다.

 

메서드 참조 예시

 

메서드 참조 만드는 방법

  1. 정적 메서드 참조 : ex) s -> Integer.parseInt(s)  Integer::parseInt
  2. 다양한 형식의 인스턴스 메서드 참조 : ex) s -> s.length()  String::length
  3. 기존 객체의 인스턴스 메서드 참조 : ex) 람다 외부 변수 apple에 대해 () -> apple.getWeight()  apple::getWeight

  • 생성자, 배열 생성자, super 호출 등에 사용할 수 있는 특별한 형식의 메서드 참조도 있습니다
  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인합니다.
  • 즉, 메서드 참조는 컨텍스트의 형식과 일치해야 합니다

 


 

 

5. 생성자 참조

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

 

  • 인수가 없는 생성자, 즉 Supplier의 () -> Apple과 같은 시그니처를 갖는 생성자가 있다고 가정하자.
Supplier<Apple> c1 = Apple::new; // Apple()인 디폴트 생성자 참조
Apple a1 = c1.get(); // Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
Supplier<Apple> c1 = () -> new Apple();
  • Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다.
Function<Integer, Apple> c2 = Apple::new; // Apple(Integer weight)의 생성자 참조
Apple a2 = c2.apply(110); // Function의 apply 메서드에 무게를 인수로 호출해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
Function<Apple> c2 = (weight) -> new Apple(weight);
  • 이제 다양한 색과 무게를 갖는 사과를 다음과 같은 방식으로 만들어 보려고 한다.
BiFunction<String, Integer, Apple> c3 = Apple::new; // Apple(String color, Integer weight)의 생성자 참조
Apple a3 = c3.apply(GREEN, 110); // BiFunction의 apply 메서드에 색과 무게를 인수로 제공해서 새로운 Apple 객체를 만들 수 있다.

// 위 코드는 다음과 같다.
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
  • 인스턴스화하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응용할 수 있다.
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()) // map에서 Function<Integer, Fruit>를 얻었다.
                        .apply(weight); // Function의 apply 메서드에 정수 무게 파라미터를 제공해서 Fruit를 만들 수 있다.
}

 

 


 

 

6.  람다 표현식 조합

자바 8 이후부터는 람다 표현식을 합칠수 있는 유용한 메서드를 제공합니다.

 

작은 프리디케이트 람다를 합쳐서 커다란 프리디케이트를 만들 수 있는 것이죠

 

프리디케이트 조합 예시

Predicate<Apple> notRedApple = redApple.negate(); // 기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150); // 두 프레디케이트를 연결해서 새로운 프레디케이트 객체를 만든다.
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
                                .or(apple -> GREEN.equals(a.getColor())); // 프레디케이트 메서드를 연결해서 더 복잡한 프레디케이트 객체를 만든다.

 

람다 표현식의 조합이 가능한 이유는 디폴트 메서드 때문입니다.

🔥 위에서 보았던 Predicate, Comparator, Function 등 미리 구현되어있는 함수형 인터페이스에 정의된 디폴트 메서드 를 이용해서 람다를 조합할 수 있도록 해줍니다.

 

 

 

 

 


*참고

반응형