본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java8 In Action] #3 람다 표현식

by 돼지왕 왕돼지 2018. 12. 23.
반응형

[Java8 In Action] #3 람다 표현식





Java8 In Action 내용을 보며 정리한 내용입니다.

정리자는 기존에 Java8 을 한차례 rough 하게 공부한 적이 있고, Kotlin 역시 공부한 적이 있습니다.

위의 prerequisite 가 있는 상태에서 추가적인 내용만 정리한 내용이므로, 제대로 공부를 하고 싶다면 책을 구매해서 보길 권장합니다!


@ FunctionalInterface, and, andthen, arrow, bifunction, boolean, capturing lambda, checked exception lambda, comparator 연결, comparator 조합, comparing, compose, constructor reference, Consumer, consumer accept, doublepredicate, execute around pattern, Final, final variable lambda, free variable, function, function apply, function descriptor, function 조합, how to make method reference, instance method reference, intconsume, intfunction, inttodoublefunction, java8 in action, lambda body, Lambda expression, lambda syntax, longbinaryoperator, method reference, negate, new, or, parameter list, Predicate, predicate test, predicate 조합, Signature, single argument parenthesis, static method reference, target type, test method, thenComparing, tointfunction, type inference, 기본형 특화, 동작 파라미터화, 디폴트 메서드 함수형 인터페이스, 람다 캡처링, 람다 표현식, 람다 표현식 조합, 람다 화살표, 람다란 무엇인가, 메서드 레퍼런스, 생성자 레퍼런스, 스택, 실행 어라운드 패턴, 익명 함수, 지역 변수 사용, 지역 변수 제약, 컴파일 에러, 함수 디스크립터, 함수형 인터페이스, 함수형 인터페이스 사용, 형식 검사, 형식 추론


3.1. 람다란 무엇인가?


-

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


익명

    이름이 없으므로 익명이라 한다.


함수

    메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.

    하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.


전달

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


간결성

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



-

람다는 세 부분으로 이루어진다.

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

파라미터 리스트

    Comparator 의 compare 메서드의 파라미터 ( 두 개의 사과 )


화살표

    람다의 파라미터 리스트와 바디를 구분


람다 바디

    무게 비교



-

람다의 기본 문법(syntax)는 아래와 같다.

(parameters) -> expression // return 명시가 필요 없다.

(parameters) -> { statements; } // statements 에는 return 을 명시해야 한다.





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


3.2.1. 함수형 인터페이스


-

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 “인터페이스” 이다.



-

인터페이스는 디폴트 메서드를 포함할 수 있다.

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




3.2.2. 함수 디스크립터


-

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

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



-

왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있는 걸까?

언어 설계자들은 자바에 함수 형식(람다 표현식을 표현하는 데 사용한 시그너처와 같은 특별한 표기법)을 추가하는 방법도 대안으로 고려했었다.

하지만 언어 설계자들은 언어를 더 복잡하게 만들지 않는 현재의 방법을 선택했다.

또한 대부분의 자바 프로그래머가 하나의 추상 메서드를 갖는 인터페이스에 이미 익숙하다는 점도 고려했다.



-

@FunctionalInterface 라는 어노테이션은 함수형 인터페이스임을 가르키는 어노테이션이다.

선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.





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


-

실행 어라운드 패턴(execute around pattern)은 설정(setup)과 정리(cleanup) 과정 사이의 중간 과정만 로직을 전달받는 방식을 말한다.

try-with-resource 가 그 예 중 하나이다.

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




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


-

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



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



-

@FunctionalInterface
public interface BufferedReaderProcessor{
    String process(BufferedReader b) throws IOException;
}

public static String processFile(BufferedReaderProcessor p) throws IOException{
    ...
}




3.3.3. 3단계 : 동작 실행!


-

public static String processFile(BufferedReaderProcessor p) throws IOException{
     try(BufferedReader br = new BufferedReader(new FileReader(“data.txt”))){
        return p.process(br);
    }
}



3.3.4. 4단계 : 람다 전달






3.4. 함수형 인터페이스 사용


-

함수형 인터페이스의 추상 메서드 시그너처를 함수 디스크립터(function descriptor)라고 한다.

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


자바 8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.




3.4.1. Predicate


-

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




3.4.2. Consumer


-

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




3.4.3. Function


-

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

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



기본형 특화


-

Boxing, Unboxing 을 피하기 위해서 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에는 형식명이 붙는다.

DoublePredicate, IntConsume, LongBinaryOperator, IntFunction 등등.


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



-

함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다.

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





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


3.5.1. 형식 검사


-

어떤 콘텍스트(예를 들면 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식(target type)이라 부른다.




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




3.5.3. 형식 추론


-

형식 추론을 사용하는 경우 single argument 의 경우는 괄호 ( ) 를 생략할 수 있다.


List<Apple> greenApples = filter(inventory, a -> “green”.equals(a.getColor()));

Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());




3.5.4. 지역 변수 사용


-

람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(free variable, 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.

이와 같은 동작을 람다 캡처링(capturing lambda)라고 부른다.



-

자유변수에도 약간의 제약이 있다.

람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있다.

하지만 그러려면 지역 변수를 명시적으로 final 로 선언하여야 하거나, 실질적으로 final 로 선언된 변수와 똑같이 사용되어야 한다. ( final 이 아니어도 modify 가 일어나지 않는다. )



지역 변수 제약


-

인스턴스 변수는 힙에 저장되는 반면, 지역 변수는 스택에 위치한다.

스택에 있는 녀석은 스레드 실행조건에서는 해지된다.

따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다.

따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.





3.6. 메서드 레퍼런스


3.6.1. 요약


-

메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.

메서드 레퍼런스는 기존 메서드 구현에 대해서도 호출할 수 있고, 상황에 따라 가독성을 높일 수 있다.

메서드 레퍼런스는 메서드 호출이 아니므로 괄호가 필요 없다.



-

(Apple a) -> a.getWeight()
    Apple::getWeight

() -> Thread.currentThread().dumpStack()
    Thread.currentThread()::dumpStack

(str, i) -> str.substring(i)
    String::substring

(String s) -> System.out.println(s)
    System.out::println


메서드 레퍼런스를 만드는 방법


-

1. 정적 메서드 레퍼런스

    Integer 의 parseInt 메서드는 Integer::parseInt 로 표현할 수 있다.


2. 다양한 형식의 인스턴스 메서드 레퍼런스

    String 의 length 메서드는 String::length 로 표현할 수 있다.


3. 기존 객체의 인스턴스 메서드 레퍼런스

    Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, 이 객체에 getValue 메서드가 있다면, expensiveTransaction::getValue 라고 표현할 수 있다.



-

1. 정적 메서드 레퍼런스

(args) -> ClassName.staticMethod(args)

    ClassName::staticMethod


2. 다양한 형식의 인스턴스 메서드 레퍼런스

(arg0, rest) -> arg0.instanceMethod(rest)

    ClassName::instanceMethod // arg0 의 class type 이 ClassName


3. 기존 객체의 인스턴스 메서드 레퍼런스

(args) -> expr.instanceMethod(args)

    expr::instanceMethod



-

생성자, 배열 생성자, super 호출 등에 사용할 수 있는 특별한 메서드 레퍼런스도 있다.




3.6.2. 생성자 레퍼런스


-

ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 레퍼런스를 만들 수 있다.

이건 정적 레퍼런스를 만드는 방법과 비슷하다.

Supplier 의 () -> Apply 와 같은 시그너처를 갖는 생성자가 있다고 가정하면 된다.

Supplier<Apple> c1 = Apple::new
Apple a1 = c1.get();



-

Apple(Integer weight)라는 시그너처를 갖는 생성자는 Function 인터페이스의 시그너처와 같다.

Function<Integer, Apple> c2 = Apple::new
Apple a2 = c2.apply(100);



-

Apple(String color, Integer weight) 처럼 두 인수를 갖는 생성자는 BiFunction 인터페이스와 같은 시그너처를 가지므로 다음처럼 할 수 있다.

BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply(“green”, 110);




3.7. 람다, 메서드 레퍼런스 활용하기!


3.7.1. 1단계 : 코드 전달




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




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


-

Comparator 는 Comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 comparing 을 포함한다.

inventory.sort(comparing((a) -> a.getWeight());




3.7.4. 4단계 : 메서드 레퍼런스 사용






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


-

조합 메서드들은 default method (디폴트 메소드) 를 사용한다.



3.8.1. Comparator 조합


역정렬


-

inventory.sort( comparing( Apple::getWeight ).reversed() );


Comparator 연결


-

thenComparing 은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달한다.

inventory.sort( comparing(Apple::getWeight) )
        .reversed()
        .thenComparing( Apple::getCountry ) );




3.8.2. Predicate 조합


-

predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공한다.

Predicate<Apple> notRedApple = redApple.negate();

Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and( a -> a.getWeight() > 150).or(a -> “green”.equals(a.getColor()));
// 빨간색이면서 무거운 사과 또는 그냥 녹색 사과



-

a.or(b).and(c) 는 ( a || b ) && c 와 같다.




3.8.3. Function 조합


-

Function 인터페이스에서는 andThen, compose 두 가지 디폴트 메서드를 제공한다.


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

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x-> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4



-

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

즉 f.andThen(g) 에서 andThen 대신에 compose 를 사용하면 g(f(x)) 가 아니라 f(g(x)) 라는 수식이 된다.

Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 3





3.9. 비슷한 수학적 개념


3.9.1. 적분




3.9.2. 자바8 람다로 연결





3.10. 요약


-

람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.



-

람다 표현식으로 간결한 코드를 구현할 수 있다.



-

함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스다.



-

함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.



-

람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 제공할 수 있으며, 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.



-

java.util.function 패키지는 Predicate<T>, Function<T,R>, Supplier<T>, Consumer<T>, BinaryOperator<T> 등을 포함해서 자주 사용하는 다양한 함수형 인터페이스를 제공한다.



-

자바 8은 Predicate<T> 와 Function<T,R> 같은 제네릭 함수형 인터페이스와 관련한 박싱 동작을 피할 수 있도록 IntPredicate, IntToLongFunction 등과 같은 기본형 특화 인터페이스도 제공한다.



-

실행 어라운드 패턴(예를 들면 자원 할당, 자원 정리 등 코드 중간에 실행해야 하는 메서드에 꼭 필요한 코드)을 람다와 활용하면 유연성과 재사용성을 추가로 얻을 수 있다.



-

람다 표현식의 기대 형식을 대상 형식이라고 한다.



-

메서드 레퍼런스를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.



-

Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메서드를 제공한다.




반응형

댓글