본문 바로가기
java-liveStudy

15주차. 람다식

by 에드박 2021. 3. 5.

목표

자바의 람다식에 대해 학습하세요.

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

동작 파라미터란 무엇인가?

 


람다란 무엇인가?

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

 

람다 표현식에는 이름은 없지만 아래에 나열된 것을 가질 수 있습니다.

  • 파라미터 리스트
  • 바디
  • 반환 형식
  • 발생할 수 있는 예외 리스트

 

람다의 특징

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

람다(lamda) 용어의 유래

  • 람다 미적분학 학계에서 개발한 시스템에서 유래됐습니다.

람다 표현식이 중요한 이유?

  • 코드를 전달하는 과정에서 지저분한 코드를 깔끔하게 바꿔줄 수 있음
  • 람다를 이용해서 간결한 방식으로 코드를 전달할 수 있습니다.

람다는 자바 8 이전의 자바로 할 수 없었던 일을 제공하는 것은 아닙니다.

 

람다식이 동작하는 일은 동작 파라미터 형식의 코드로 구현할 수 있었지만

람다식을 활용하면 코드가 간결하고 유연해집니다.

 

동작 파라미터 형식의 코드


Comparator<Apple> byWeight = new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
};

 

람다식을 적용한 코드

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a.getWeight().compareTo(a2.getWeight());
Apple 의 객체 a1, a2 두개를 인자로 받아서 비교해주는 Comparator<Apple> 객체를 생성해줍니다.

 

람다 파라미터

  • (Apple a1, Apple a2)

화살표

  • ->

람다 바디

  • A1.getWeight().compareTo(a2.getWeight());

 

자바8에서 지원하는 다섯 가지 람다 표현식 예제

  • (String s) -> s.length()
    • String 타입의 파라미터 하나를 가지며 int를 반환합니다. 람다 표현식에는 return이 함축되어 있으므로 return 문을 명시적으로 사용하지 않아도 됩니다.
  • (Apple a) -> a.getWeight() > 150
    • Boolean 표현식
    • Apple 타입의 파라미터 하나를 가지며 boolean(사과가 150그램 보다 무거운지 결정)을 반환합니다.
  • (Int x, int y) -> {
        System.out.println(“Result : ” + (x + y));
    }
    • int 타입의 파라미터 두 개를 가지며 리턴값이 없습니다(void 리턴). 이 예제에서 볼 수 있듯이 람다 표현식은 여러 행의 문장을 포함할 수 있습니다. ( 여러 문장을 포함시킬 때 중괄호 ‘{}’ 안에 문장들을 포함시켜야합니다. )
  • () -> new Apple(10)
    • 파라미터는 따로 없으며 애플 객체를 반환합니다.
  • (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
    • Apple 타입의 파라미터 2개를 가지며 int(두 사과의 무게 비교결과)를 반환합니다.

표현식 스타일(expression style)

람다의 기본 문법으로 아래와 같이 표현할 수 있습니다.

 

실행할 문장이 하나일 때는 중괄호’{}’ 를 생략할 수 있습니다.

  • (Parameter) -> expression

파라미터가 하나 일때는 소괄호’()’를 생략할 수 있습니다.

  • Parameter -> expression

여러개의 파라미터와 여러개의 문장을 사용한다면 아래와 같이 표현합니다.

  • (Parameter1, Parameter2) -> {
        Expression1;
        Expression2;
    }

 

어디에 어떻게 람다를 사용할 수 있는가?

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

 

함수형 인터페이스란?

-> 오직 하나의 추상 메서드만 가지는 인터페이스

 

이전에 애너테이션에서 배웠듯이 @FunctionalInterface 애너테이션은 아래의 경우를 제외하고 오류 메세지를 발생시킵니다.

  • 유형은 인터페이스 이며 @interface(애너테이션), enum, class 가 아닙니다.
  • 애너테이션이 달린 유형은 함수형 인터페이스의 요구사항을 충족합니다.
    • 디폴트 메서드를 제외하고 추상 메서드를 한개만 가지는 인터페이스
인터페이스는 디폴트 메서드를 포함할 수 있습니다. 하지만 여러개의 디폴트 메서드(인터페이스에서 바디를 가질 수 있는 메서드)를 가지고 있더라도 추상메서드가 오직 하나면 함수형 인터페이스 입니다.

 

꼭 @FunctionalInterface 애너테이션을 붙여야 함수형 인터페이스가 되는것이 아닙니다.

-> 단지 애너테이션을 붙이면 클라이언트에게 함수형 인터페이스인지 표현할 수 있으며 함수형 인터페이스 조건을 충족하는지 검사할 수 있습니다.

 

아래는 추상메서드를 하나도 가지지 않은 인터페이스 입니다. 함수형 인터페이스의 조건을 충족하지 못하므로 @FunctionalInterface 에서 컴파일 에러를 발생시킵니다.

추상메서드가 하나도 없기 때문에 @FunctionalInterface가 컴파일 오류를 발생시킵니다.

 

아래는 추상메서드를 한개만 가지는 인터페이스 입니다. 함수형 인터페이스의 조건을 충족했으므로 정상적으로 컴파일 됩니다.

아래는 디폴트 메서드 하나와 추상메서드 하나를 가지는 인터페이스 입니다. 이 또한 함수형 인터페이스의 조건을 충족하기때문에 정상적으로 컴파일됩니다.

 

함수형 인터페이스를 이용한 람다 표현식

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

 

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

  • -> 기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스로 취급.
  • 즉, 함수형 인터페이스를 구현한 익명 클래스의 인스턴스

이전에 Runnable 인터페이스를 구현해서 쓰레드를 구현할 수 있다는걸 배웠습니다.

  • 이 Runnable이 대표적인 함수형 인터페이스 중 하나입니다.

Runnable 인터페이스의 코드

 

다음은 Runnable 함수형 인터페이스를 사용하는 다양한 방법입니다.

  • Runnable r1 : 람다 표현식을 이용해서 함수형 인터페이스를 구현한 클래스의 인스턴스를 저장합니다.
  • Runnable r2 : r1과 같이 함수형 인터페이스를 구현한 클래스의 인스턴스를 저장하지만 람다 표현식을 사용하지 않고 구현했습니다.
  • Runnable r3 : 람다 표현식으로 구현한 동작 파라미터를 메서드의 파라미터로 사용하고 있습니다.
package lastweek;

public class RunnableEx {
    Runnable r1 = () -> System.out.println("Hello World 111");

    Runnable r2 = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World 222");
        }
    };

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

    public static void main(String[] args) {
        RunnableEx runnableEx = new RunnableEx();

        process(runnableEx.r1);
        process(runnableEx.r2);
        process(() -> System.out.println("Hello World 333"));
    }
}

실행결과

 


함수 디스크립터

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

  •  추상 메서드 시그니처 == 람다 표현식 시그니처

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

  • 예를들어 Runnable 의 유일한 메서드 run메서드는 반환값이 void 이므로 Runnable 인터페이스는 반환값이 없는 시그니처로 생각할 수 있습니다.
  • (Apple, Apple) -> int 라는 함수 디스크립터는 Apple 2개를 인수로받아 int를 반환하는 함수를 가리킵니다.

함수 디스크립터는 함수형 인터페이스의 유일한 추상메서드에 대한 파라미터 타입과 반환 타입을 의미합니다. 

 


실행 어라운드 패턴 (Execute Around Pattern) - 람다 활용하기

실행 어라운드 패턴은 실제 자원을 처리하는 코드를 설정(set up)과 정리(clean up) 두 과정을 둘러싸는 형태를 말합니다.

 

아래와 같이 표현할 수 있습니다. 작업 A와 작업 B 는 자원을 처리하는 코드를 의미합니다.

초기화 / 준비 코드 초기화 / 준비 코드
작업 A 작업 B
정리 / 마무리 코드 정리 / 마무리 코드

 

아래의 예제는 파일에서 한개의 행을 읽는 코드입니다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ExecuteAroundPatternEx {

    public String processFile() throws IOException {
        try (BufferedReader br =
                     new BufferedReader(new FileReader("data.txt"))) { // 초기화 & 정리 하는 코드
            return br.readLine(); // 자원을 처리하는 코드
        }
    }
}

 

현재는 한개의 행씩 읽는 기능에 만족할 수 있지만 만약 두개의 행씩 읽고싶거나 전체의 행을 읽어서 반환해주고 싶다면  지금과 같은 코드는 변경에 취약한 코드라고 할 수 있습니다.

 

이것을 동작 파라미터화 시킨다면 다음과 같이 변경할 수 있습니다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ExecuteAroundPatternEx {

    public String processFile(BufferedReaderProcessor bufferedReaderProcessor) throws IOException {
        try (BufferedReader br =
                     new BufferedReader(new FileReader("data.txt"))) { // 초기화 & 정리 하는 코드
            return bufferedReaderProcessor.process(br); // 자원을 처리하는 코드
        }
    }
}

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

 

BufferedReaderProcessor라는 함수형 인터페이스를 생성했습니다.

 

이제 를 사용할 때 우리는 다음과 같이 사용할 수 있습니다.

package lastweek;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ExecuteAroundPatternEx {

    public String processFile(BufferedReaderProcessor bufferedReaderProcessor) throws IOException {
        try (BufferedReader br =
                     new BufferedReader(new FileReader("data.txt"))) { // 초기화 & 정리 하는 코드
            return bufferedReaderProcessor.process(br); // 자원을 처리하는 코드
        }
    }

    public static void main(String[] args) throws IOException{
        ExecuteAroundPatternEx e = new ExecuteAroundPatternEx();
        
        String oneLine = e.processFile((BufferedReader br) -> br.readLine());

        String twoLines = e.processFile((BufferedReader br) -> br.readLine() + br.readLine()); // 두개의 행을 읽는다.

        String allDocument = e.processFile((BufferedReader br) -> { // 전체 행을 읽는다.
            StringBuilder sb = new StringBuilder();
            while (true) {
                String line = br.readLine();
                if (line == null) {
                    break;
                }
                sb.append(line);
            }
            return sb.toString();
        });
    }
}

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


java.util.function 패키지의 함수형 인터페이스

 

함수형 인터페이스는 오직 하나의 추상 메서드를 지정합니다. 다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요합니다.

 

자바 API는 Comparator, Runnable, Callable 등의 다양한 함수 인터페이스를 이미 가지고 있습니다.

 

우리가 만드는 대부분의 메서드를 패턴이 비슷합니다.

  • 매개변수가 없거나 한개 또는 두개
  • 반환값은 없거나 한개

이런 차이점들은 제네릭 메서드로 정의하면 매개변수나 반환타입의 변경에도 동적으로 대응할 수 있습니다.

자바 8의 라이브러리 설계자들은 java.util.function 패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 만들어놓았습니다.

 

따라서 매번 새로운 함수형 인터페이스를 만드는것보다 만들고자 하는 함수형 인터페이스가 java.util.function 패키지 안에 존재한다면 그것을 활용하는 것이 좋습니다.

-> 이미 있는것을 활용해야 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋습니다.

 

함수형 인터페이스 함수 디스크립터 추상 메서드 설명
Predicate<T> T -> boolean boolean test(T t) 조건식을 표현하는데 사용합니다.
매개변수는 하나, 반환타입은 boolean 입니다.
Supplier<T> () -> T T get() 매개변수는 없고, 반환값이 있습니다.
Consumer<T> T -> void void accept(T t) Supplier와 반대로 매개변수만 있고 반환값은 없습니다.
Function<T, R> T -> R apply(T t) 일반적인 함수, 하나의 매개변수를 받아서 결과를 반환
UnaryOperator<T> T -> T T apply(T t) 매개변수 하나와 반환하는 값을 있는데
매개변수와 반환 타입이 같다는 특성을 제외하고는 Function과 같은 기능을 합니다.

 

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

즉, T 타입의 객체를 받아서 받아온 객체를 조건식으로 검사한 뒤 결과값을 boolean타입으로 반환하는 용도로 사용할 수 있습니다.

2의 배수인지 판단하기 위한 Predicate 예시

package lastweek;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class PredicateEx {

    public <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> result = new ArrayList<>();
        for (T t: list) {
            if (p.test(t)) { // Predicate<T> 의 test 메서드 호출
                result.add(t);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        Predicate<Integer> isMultipleOf2 = (Integer i) -> (i % 2) == 0;
        List<Integer> filterNumbers = new PredicateEx().filter(numbers, isMultipleOf2);
    }
}

실행 결과

 

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

즉, T타입의 객체에 조작을 하고싶을 때 사용합니다.

아래는 Consumer<T>를 사용해서 리스트의 객체를 하나씩 출력하는 forEach 메서드의 예시입니다.

package lastweek;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class ConsumerEx {

    public <T> void forEach(List<T> list, Consumer<T> c) {
        for (T t : list) {
            c.accept(t); // Consumer<T> 의 accept 메서드 호출
        }
    }

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

 

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

입력받는 T타입의 객체를 R타입으로 반환해줄 때 활용할 수 있습니다.

  • 예를들어서 문자열(String) 객체를 가져와서 문자열의길이(Integer)를 반환할 때 사용할 수 있습니다.
package lastweek;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class FunctionEx {

    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)); // Function<T, R> 의 apply 메서드
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> list = new FunctionEx().map(
                Arrays.asList("longContent", "sc", "Medium"),
                (String s) -> s.length()
        );

        System.out.println(list.toString());
    }
}


매개변수가 두 개인 함수형 인터페이스

앞에서 봤던 Predicate, Consumer, Function, UnaryOperator 의 매개변수가 2개인 함수형 인터페이스가 있습니다.

각각의 이름에 접두사 'Bi' 가 붙습니다. 예외로 Operator만이 BinaryOperator 라는 이름으로 사용됩니다.

Bi 는 Binary 의 약어입니다. 
Supplier는 원래 매개변수가 없기 때문에 포함되지 않습니다.
함수형 인터페이스 메서드 설 명
BiFunction<T, U, R> R apply(T t, U, u) 두 개의 매개변수를 받아서 하나의 결과를 반환
BinaryOperator<T> R apply(T t1, T t2) 두 개의 매개변수를 받아서 하나의 결과를 반환
BiPredicate<T, U> boolean test(T t, U u) 조건식을 표현하는데 사용됨, 매개변수는 둘, 반환값은 boolean
BiConsumer<T, U> void accept(T t, U u) 두개의 매개변수만 있고, 반환값이 없음

컬렉션 프레임워크와 함수형 인터페이스

Java8 부터는 컬렉션 프레임워크의 인터페이스에 다수의 디폴트 메서드가 추가되었습니다. 그 중의 일부는 함수형 인터페이스를 사용합니다.

 

인터페이스 메서드 설명
Collection boolean removeIf(Predicate<E> filter) 조건에 맞는 요소를 삭제
List void replaceAll(UnaryOperator<E> operator) 모든 요소를 반환하여 대체
Iterable void forEach(Consumer<T> action) 모든 요소에 작업 action을 수행
Map V compute(K key, BiFunction<K,V,V> f) 지정된 키의 값에 작업 f를 수행
V computeIfAbsent(K key, Function<K, V> f) 키가 없으면, 작업 f  수행 후 추가
V computeIfPresent(K key, BiFunction<K, V, V> f) 지정된 키가 있을 때, 작업 f 수행
V merge(K key, V value, BiFunction<V,V,V> f) 모든 요소에 병합작업 f를 수행
void forEach(BiConsumer<K, V> action) 모든 요소에 작업 action을 수행
void replaceAll(BiFunction<K,V,V> f) 모든 요소에 치환작업 f를 수행

 


기본형을 사용하는 함수형 인터페이스

지금까지 소개한 함수형 인터페이스는 매개변수와 반환값의 타입이 모두 제네릭 타입이었습니다.

  • Function<T, R> / R apply(T t)

기본 자료형 (primitive type)의 값을 처리(매개변수로 받거나, 반환 타입 처리)할 때도 래퍼(wrapper) 를 사용해왔습니다.

wrapper 클래스는 Integer, Double, Boolean 등을 의미합니다.

래퍼 클래스를 사용하는 이유는 제네릭의 타입 매개변수에는 기본 자료형이 아닌 참조형(Reference Type)만 사용할 수 있기 때문입니다.

자바에서는 기본형을 참조형으로 변환하는 기능을 제공하는데. 이 기능을 박싱(boxing) 이라고 합니다.

반대로 참조형을 기본형으로 변환하는 반대 동작을 언박싱(unboxing)이라고 합니다.

박싱과 언박싱이 자동으로 이뤄지는 동작을 오토박싱(auto-boxing)이라고 합니다.

 

박싱 언박싱
int -> Integer Integer -> int

 

하지만 박싱, 언박싱, 오토박싱을 수행하는 동작(변환 과정)은 모두 비용이 소모됩니다. 

박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장됩니다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정을 거칩니다.

 

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

함수형 인터페이스 함수 디스크립터 기본형 특화 설명
Predicate<T> int -> boolean IntPredicate Predicate<T>의 기본형은 매개변수로 기본 자료형을 받고 boolean 타입으로 반환합니다.
long -> boolean LongPredicate
double -> boolean DoublePredicate
Consumer<T> int -> void IntConsumer Consumer<T>의 기본형은 매개변수로 기본 자료형을 받고 반환값이 없습니다.
long -> void LongConsumer
double -> void DoubleConsumer
Function<T, R> R -> int IntFunction<R> Function<T, R>의 기본 자료형은 AToBFunction 일 때
A 기본형을 매개변수로 받고
B 기본형을 반환합니다.
int -> double IntToDoubleFunction
int -> long IntToLongFunction
R -> long LongFunction<R>
long -> double LongToDoubleFunction
long -> int LongToIntFunction
R -> double DoubleFunction<R>
double -> int DoubleToIntFunction
double -> long DoubleToLongFunction
T -> int ToIntFunction<T>
T -> double ToDoubleFunction<T>
T -> long ToLongFunction<T>
Supplier<T> () -> boolean BooleanSupplier Supplier<T>의 기본 자료형은 
매개변수가 없고 기본 자료형을 반환합니다.
() -> int IntSupplier
() -> long LongSupplier
() -> double DoubleSupplier
UnaryOperator<T> int -> int IntUnaryOperator UnaryOperator<T>의 기본 자료형은 매개변수와 반환타입이 하나의 기본자료형입니다.
long -> long LongUnaryOperator
double -> double DoubleUnaryOperator
BinaryOperator<T> (int, int) -> int IntBinaryOperator BinaryOperator<T> 의 기본 자료형은 두개의 매개변수와 반환타입이 하나의 기본 자료형입니다.
(long, long) -> long LongBinaryOperator
(double, double) -> double DoubleBinaryOperator
BiConsumer<T, U> (T, int) -> void ObjIntConsumer<T> BiConsumer<T, U>의 기본 자료형은 두개의 매개변수를 받는데 하나는 제네릭 타입이고 하나는 기본 자료형을 받습니다.
반환타입은 없습니다.
(T, long) -> void ObjLongConsumer<T>
(T, double) -> void ObjDoubleConsumer<T>
BiFunction<T, U, R> (T, U) -> int ToIntBiFunction<T, U> BiFunction<T, U, R>의 기본 자료형은 두개의 제네릭 타입 매개변수를 받고 반환값으로 기본자료형을 반환합니다.
(T, U) -> long ToLongBiFunction<T, U>
(T, U) -> double ToDoubleBiFunction<T, U>

형식 검사

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

그렇다면 어떻게 람다 표현식으로 함수형 인터페이스의 인스턴스를 만들 수 있는걸까요?

 

람다가 사용되는 컨텍스트(context)를 이용해서 람다의 타입을 추론할 수 있습니다.

컨텍스트 : 람다에 전달될 메서드 파라미터나 람다가 할당되는 변수 등등

어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식(target type) 이라고 부릅니다.

 

아래는 람다 표현식을 사용할 때 형식 검사의 과정입니다.

public <T> List<T> filter(List<T> list, Predicate<T> p);

List<Apple> heavierThan150g = filter(appleBox, (Apple apple) -> apple.getWeight() > 150);

 

  1. filter 메서드의 호출을 확인합니다.
  2. filter 메서드의 선언 구조를 확인하러 갑니다.
  3. filter 메서드는 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대합니다.
  4. Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스 입니다.
  5. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터(함수형 인터페이스의 시그니처)를 묘사합니다.
  6. filter 메서드로 전달된 인수는 위의 요구사항을 만족해야합니다.

람다 표현식의 형식 검사 과정

 

대상 형식만 함수 디스크립터와 일치한다면 같은 람다 표현식이더라도 호환되는 여러 함수형 인터페이스로 사용될 수 있다.

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

ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

BiFunction<Apple, Apple> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

위의 3라인의 람다 표현식을 보면 모두 같은것을 확인할 수 있습니다.

 


형식 추론

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

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

 

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

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

 

여러 파라미터를 포함하는 람다 표현식에서는 코드 가독성이 더 향상됩니다.

 

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


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

 

하지만 꼭 람다의 파라미터 타입을 생략한다고해서 좋은 것은 아닙니다.

때로는 타입을 명시하는것이 가독성을 향상시키기도 합니다.

 

정해진 규칙은 없기 때문에 개발자 스스로 또는 팀과 의견을 나누어 어떤 코드가 가독성을 향상 시킬 수 있는지 결정해야합니다.


변수 캡처(Variable Capture)

Java 의 람다 표현식은 특정 상황에서 람다 표현식 외부에서 선언 된 변수에 접근 할 수 있습니다.

이 같이 외부에서 선언된 변수를 사용하는 동작을 람다 캡쳐링(capturing lambda)라고 합니다.

 

Java의 람다는 다음 유형의 변수를 캡쳐할 수 있습니다.

  • 지역 변수 (local variable)
  • 인스턴스 변수 (instance variable)
  • 정적 변수 (static variable)

지역 변수 캡쳐

package lastweek;

public class VariableCaptureEx {
    public static void main(String[] args) {
        String name = "자바 스터디 ";

        MyInterface myInterface = text -> System.out.println(name + text);

        myInterface.print("감사합니다.");
    }
}

interface MyInterface {
    void print(String text);
}

지역변수 캡쳐 실행결과

 

인스턴스 변수 캡쳐

package lastweek;

public class VariableCaptureEx {
    String name = "자바 스터디 인스턴 변수에서도 ";

    public static void main(String[] args) {

        MyInterface myInterface = text -> System.out.println(new VariableCaptureEx().name + text);

        myInterface.print("감사합니다.");
    }
}

interface MyInterface {
    void print(String text);
}

인스턴스 변수 캡쳐 실행결과

 

package lastweek;

public class VariableCaptureEx {
    static String name = "자바 스터디 static 변수에서도 ";

    public static void main(String[] args) {

        MyInterface myInterface = text -> System.out.println(VariableCaptureEx.name + text);

        myInterface.print("감사합니다.");
    }
}

interface MyInterface {
    void print(String text);
}

정적 변수 캡쳐 실행결과

 


변수 캡쳐의 제약사항

위의 변수 캡쳐에도 약간의 제약사항이 있습니다.

람다는 인스턴스 변수(instance variable)와 정적 변수(static variable)는 자유롭게 캡쳐할 수 있습니다.

 

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

 

 

위 코드에서는 아래와 같은 컴파일 에러가 발생합니다.

/Users/parksmac/IdeaProjects/live-study/src/main/java/lastweek/VariableCaptureEx.java:10: error: local variables referenced from a lambda expression must be final or effectively final MyInterface myInterface = text -> System.out.println(name + text);

번역 :람다 식에서 참조되는 지역 변수는 final이거나 사실상 final이어야합니다.

 

즉, 외부의 지역 변수를 람다 표현식 내부에서 사용하려면 지역변수를 final로 선언해서 상수로 만들거나 지역변수의 값을 재할당하지 않아야 한다는뜻입니다.

String name = "자바 스터디 "

name = "자바 스터디 3000만큼 "  <-- 변수의 값을 재할당했습니다.

 

사실상 final 이어야한다는 말은 변수에 재할당 즉, 최초의 초기화만 허용한다는 뜻입니다.

 

이런 제약이 있는 이유는 내부적으로 인스턴스 변수와 지역 변수의 태생이 다르기 때문입니다.

  • 인스턴스 변수는 힙에 저장됩니다.
  • 지역 변수는 스택에 위치합니다.

람다는 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있습니다.

 

스레드 1이 사라진 후 변수 a에 대한 할당 해제됐습니다.

 

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

이러한 구현에 의해 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 최초 한 번만 값을 할당해야 한다는 제약이 생긴것입니다.

 


메서드 참조 (method reference)

람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조' 라는 방법으로 람다식을 간략하게 표현할 수 있습니다.

 

다음과 같은 유형의 메서드를 참조 메서드로 사용할 수 있습니다.

  • static 메서드
  • 매개변수 객체의 인스턴스 메서드(함수형 인터페이스를 구현한 인스턴스)
  • 인스턴스 메서드
  • 생성자

메서드 참조가 중요한 이유?

메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있습니다.

  • 예를들어, 람다가 "이 메서드를 직접 호출해" 라고 명령한다면 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조하는 것이 편리합니다.

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

메서드가정의된클래스명::메서드명

 

문자열을 정수로 변환하는 람다식은 아래와 같이 작성할 수 있습니다.

Function<String, Integer> function = (String str) -> Integer.parseInt(str);

 

위의 람다식은 아래와 같이 메서드 참조를 이용해서 표현할 수 있습니다.

Function<String, Integer> function = Integer::parseInt;

 

람다식의 일부가 생략됐지만 컴파일러는 우변의 parseInt 메서드 선언부나 좌변의 Function 인터페이스에 지정된 제네릭 타입으로부터 형식 추론을 할 수 있습니다.

 

람다 표현식의 파라미터가 2개의 타입이 같은 경우에도 메서드 참조를 이용해서 표현할 수 있습니다.

BiFunction<String, String, Boolean> biFunction = (str1, str2) ->  str1.equals(str2);

BiFunction<String, String, Boolean> biFunction = String::equals

 

참조변수 biFunction의 타입을 보면 람다식이 두개의 String 타입의 매개변수를 받는가는 것을 알 수 있으므로, 람다식의 매개변수들은 없어도 괜찮은 것입니다.

 

 

생성자 참조 (Constructor Reference)

람다식에서 객체를 생성해야할 때 생성자 참조를 활용할 수 있습니다.

생성자 참조는 아래의 형식으로 사용할 수 있습니다.

ClassName::new

 

Supplier의 () -> new Apple() 과 같은 형식을 사용한다고 하면 아래와 같이 사용할 수 있습니다.

 

Supplier<Apple> appleSupplier = Apple::new;
Apple apple = appleSupplier.get();

 

생성자에 매개변수(생성자 시그니처)를 갖는 경우 생성자는 Function이나 BiFunction 함수형 인터페이스를 사용해볼 수 있습니다.

 

Apple(Integer weight)라는 시그니처를 갖는 생성자는 다음과 같이 구현할 수 있습니다.

 

Function<Integer, Apple> appleFunction = Apple::new;
Apple apple = appleFunction.apply(10);

참고자료

- 모던 자바 인 액션

- 자바의 정석 3판

- tutorials.jenkov.com/java/lambda-expressions.html#variable-capture

- www.youtube.com/watch?v=kBc8S40HdoM&list=PLL8woMHwr36HQhhPPdV_T8rigbuywMpD7&index=4

-

-

 

 

'java-liveStudy' 카테고리의 다른 글

14주차. 제네릭  (1) 2021.03.01
13주차. I/O  (0) 2021.02.15
12주차. 애너테이션  (0) 2021.01.31
11주차. Enum  (0) 2021.01.24
10주차 과제. 멀티쓰레드 프로그래밍  (0) 2021.01.17

댓글