본문 바로가기

IT Book Summary/ModernJavaInAction

Chapter3 람다 표현식 _ 01

이 챕더에서는 람다 표현식을 어떻게 만드는지 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 설명할 것이다.

또한 자바 8에 추가된 중요한 인터페이스와 형식추론 등의 기능, 위력을 발휘하는 메서드 참조를 설명한다.

 

내용 자체도 중요하지만 람다표현식은 광범위하게 사용되므로 이 장의 내용을 완벽히 이해해야 한다.

 


람다란 무엇인가?

 

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

- 이름은 없지만 파라미터 리스트, 바디, 반환형식, 발생가능한 예외 리스트는 가질 수 있다.

 

특징

  • 익명 - 보통의 메서드와 달리 이름이 없음
  • 함수 - 특정 클래스에 종속되지 않음 
  • 전달 - 람다표현식을 메서드 인수로 전달하거나 변수로 저장
  • 간결성 - 코드 구현의 간결함.

자바8 이전의 할 수 없었던 일을 하는것은 아니다. 다만 동작 파라미터를 이용할 때 더 쉽게 구현 가능한 것.

 

람다 파라미터 + 화살표 + 람다 바디 로 구성된다.

(parameters) -> expression

또는

(parameters) -> { statements; }

로 표현할 수 있다.

 

사용사례 람다 예제
불리언 표현식 (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())

 


어디에 사용할까?

함수현 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.


함수형 인터페이스

 

: 하나의 추상메서드를 지정하는 인터페이스다.

ex) Comparator, Runnable 등

 

많은 디폴트 메서드가 있더라도 추상메서드가 오직 하나면 함수형 인터페이스

 

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

전체 표현식을 함수형 인터페이스의 인스턴스(함수형 인터페이스를 구현한 클래스의 인스턴스)로 취급함.

 

Runnable r1 = ()->System.out.println("Hello World 1"); // 람다사용

Runnable r2 = new Runnable() {
    public void run() {
        System.out.println("Hello World 2");
    }
};// 익명클래스 사용

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

process(r1);
process(r2);
//직접 전달된 람다 표현식
process(() -> System.out.println("Hello World 3"));

함수 디스크립터

 

함수형 인터페이스의 추상메서드 시그니처람다 표현식의 시그니처를 가리킨다.

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다.

 

Runnable 인터페이스의 유일한 추상 메서드는 void반환이므로 인수와 반환값이 없는 시그니처이다.

한개의 void 메소드 호출은 중괄호로 감쌀 필요가 없다.


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

 

실제 자원을 처리하는 코드 (ex 데이터베이스 파일처리)를 설정과 정리 두과정이 둘러싸는 형태 : 실행 어라운드 패턴

public String processFile() throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
        return br.readLine();
    }
}

 

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

processFile의 동작을 파라미터화

processFile 메서드가 한번에 두행을 읽게 하려면, BufferedReader를 인수로 받아 String 반환

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

 

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

시그니처와 일치하는 함수형 인터페이스를 만들자

 

@FunctionalInterface
public interface BufferdReaderProcessor {
    String process(BufferdReader b) throws IOException;
}

//정의한 인터페이스를 processFIle메서스의 인수로 전달
public String processFIle(BufferdReaderProcessor p) throws IOException {
    ...
}

 

3단계 - 동작 실행

이제 BufferdReaderProcessor에 정의된 process 메서드의 시그니처와 일치하는 람다를 전달할 수 있다.

람다 표현식으로 추상 메서드 구현을 직접 전달할 수 있음.

 

public String processFile(BufferdReaderProcessor p) throws IOException {
    try(BufferdReader br = new BufferedReader(new FileReader("data.txt"))){
        return p.process(br); // BufferdReader 객체처리
    }
}

 

4단계 - 람다 전달

 

람다를 이용해 다양한 동작을 processFile 메서드로 전달

 

//한행처리
String oneLine = processFile((BufferedReader br)->br.readLine());
//두행처리
String twoLine = processFile((BufferedReader br)->br.readLine()+br.readLine());

processFile 메서드를 더 유연하게 만듬.


함수형 인터페이스 사용

 

함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터 하고 한다.

다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술한 함수현 인터페이스 집합이 필요하다.

 

Pradicate

test 추상메서드 정의 , 제네릭 형태의 T 객체를 인수로 받아 불리언 반환

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
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;
}
Pradicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

 

Consumer

제네릭 T 객체를 받아서 void 반환하는 accept라는 추상메서드를 정의.

T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스 사용

 

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

public <T> void forEach(List<T> list, Consumer<T> c) {
    for(T t: list) {
        c.accept(t);
    }
}
//Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach메서드를 정의할 때 사용.
forEach( 
    Arrays.asList(1,2,3,4,5),
    (Integer i)->System.out.println(i)
);

 

Function

제네릭 형식 T를 인수로 받아서 제네릭형식 R 객체를 반환하는 추상메서드 apply를 정의

입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용.

ex) 사과의 무게정보 추출, 문자열을 길이와 매핑 ..

 

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

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;
}
// String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer리스트로 변환하는 map메서드 정의.
// [7, 2, 6]
List<Integer> l = map(
        Arrays.asList("lamdas", "in", "action"),
        (String s) -> s.length()
);

기본형 특화

 

제네릭 파라미터에는 참조형만 사용할 수 있다. 

자바는 기본형을 참조형으로 변환하는 기능을 제공한다. : 박싱 boxing

참조형을 기본형으로 변환하는 반대동작 : 언박싱 unboxing

박싱과 언박싱이 자동으로 이루어지는 : 오토박싱 autoboxing

 

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

특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 DoublePredicate, IntConsumer, IntFunction 처럼 형식명이 붙음.

Function 인터페이스는 ToIntFunction<T>, IntToDoubleFunction등의 다양한 출력형식 파라미터 제공

사용사례 람다예제 대응하는 함수형 인터페이스
불리언 표현 (List<String> list)->list.isEmpty() Predicate<List<String>>
객체 생성 ()->new Apple(10) Supplier<Apple>
객체에서 소비 (Apple a) -> System.out.println(a.getWeight()) Consumer<Apple>
객체에서 선택/추출 (String s)->s.length()

Function<String, Integer> 또는

ToIntFunction<String>

두 값 조합 (int a, int b)-> a*b IntBinaryOperator
두 객체 비교 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

Comparator<Apple. Apple, Integer> 또는

BiFunction<Apple, Apple, Integer> 또는

ToIntBiFunction<Apple, Apple>