Java

Lambda Expression

snail voyager 2020. 9. 14. 22:56
728x90
반응형

람다식

  • 함수형 프로그래밍을 구현하는 방식
  • 클래스를 생성하지 않고 함수의 호출만으로 기능 수행
  • 변수처럼 사용 (자료형 기반, 매개변수로 전달, 메서드의 반환값)
  • 이름이 없는 함수 (Anonymous Function)
() -> {}	//파라미터가 없으며 void를 반환
() -> "Text"	//파라미터가 없으며 문자열을 반환
() -> {return "Text";}	//파라미터가 없으며 문자열을 반환
(Integer i) -> {return "Text" + i;}	//파라미터가 있으며 문자열을 반환

(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())	//두 객체 비교
  • 함수형 인터페이스를 선언
  • 매개변수의 타입이 유추 가능할 경우 타입 생략 가능
  • 매개변수가 하나일 경우 괄호 생략 가능
  • 바로 리턴하는 경우 중괄호, return 생략 가능

함수형 인터페이스

  • 정확히 하나의 추상 메서드를 지정하는 인터페이스
  • 람다의 전체 표현식을 함수형 인터페이스의 인스턴스로 취급
  • Default method와 static method는 이미 구현이 되어있으므로 있어도 괜찮음
  • 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식 사용 가능
  • @FunctionalInterface는 함수형 인터페이스임을 가리키는 어노테이션
  • 추상 메서드는 람다 표현식의 시그니처(function descriptor)를 묘사
public interface Predicate<T> {
    boolean test(T t);	//function descriptor
}

public interface Comparator<T> {
	int compare(T o1, T o2);
}

Runnable r1 = () -> System.out.println("Hello World 1");	//람다 표현식

Predicate

test()는 제너릭 형식 T의 객체를 인수로 받아 boolean을 반환

    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;
    }

    Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
    List<String> nonEmpty = filter(Arrays.asList("aaa", "bbb", ""), nonEmptyStringPredicate);

Consumer

제네릭 형식 T 객체를 받아서 void를 반환하는 accept 정의

입력값을 받아서 그 값을 소비하거나 처리하는 역할

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

Function

  • 제네릭 형식 T 객체를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply 정의
  • 입력값을 변환하거나 매핑하는 작업에 유용
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Function.identity()

  • 입력 값을 그대로 반환하는 항등 함수(identity function)를 생성하는 데 사용
static <T> Function<T, T> identity() {
    return t -> t;
}

public class IdentityFunctionExample {
    public static void main(String[] args) {
        List<String> strings = List.of("apple", "banana", "cherry", "date");

        // 문자열 리스트에서 길이가 5 이상인 문자열만 선택
        List<String> result = strings.stream()
            .filter(s -> s.length() >= 5)
            .map(Function.identity()) // Function.identity()를 사용하여 요소를 그대로 반환
            .collect(Collectors.toList());

        System.out.println(result);
    }
}

UnaryOperator

  • 하나의 입력값을 받아서 하나의 출력값을 반환하는 함수를 나타내며, 입력값과 출력값의 타입이 동일
  • 주어진 입력값을 수정하거나 변환하여 동일한 타입의 출력값을 생성하는 역할
@FunctionalInterface
public interface UnaryOperator<T> {
    T apply(T t);
}

UnarOperator andThen()

  • 두 개의 UnaryOperator를 연결하여 순차적으로 적용할 때 사용
  • 현재의 UnaryOperator를 먼저 적용하고, 
    그 다음에 after 파라미터로 전달된 UnaryOperator를 적용하는 새로운 UnaryOperator를 반환
default UnaryOperator<T> andThen(UnaryOperator<T> after)
public class UnaryOperatorExample {
    public static void main(String[] args) {
        // 문자열을 대문자로 변환하는 UnaryOperator
        UnaryOperator<String> toUpperCase = s -> s.toUpperCase();

        // 문자열을 "!"로 끝나게 하는 UnaryOperator
        UnaryOperator<String> addExclamation = s -> s + "!";

        // 첫 번째 UnaryOperator를 적용한 후 두 번째 UnaryOperator를 적용
        UnaryOperator<String> combined = toUpperCase.andThen(addExclamation);

        String input = "hello";
        String result = combined.apply(input);
        System.out.println(result); // "HELLO!"
    }
}

기본형 특화

  • 제네릭 파라미터에는 참조형만 사용 가능
  • 오토박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장
  • 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정 필요
  • DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 ==0;	//박싱
oddNumbers.test(1000);	//false

IntPredicate evenNumbers = (int i) -> i % 2 == 0;	//박싱 없음
evenNumbers.test(1000);	//true

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

자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태

    public String processFile() throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return br.readLine();   //한 행을 읽는 코드
        }
    }

    @FunctionalInterface
    public interface BufferedReaderProcessor {      //함수형 인터페이스 생성
        String process(BufferedReader b) throws IOException;
    }

    public String processFileNew(BufferedReaderProcessor p) throws IOException {  //인터페이스를 메서드 인수로 전달
        try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
            return p.process(br);   //BufferedReader 객체 처리
        }
    }

    public void main() throws Exception {
        String oneLine = processFileNew((BufferedReader br) -> br.readLine());  //람다 표현식으로 추상 메서드 구현을 직접 전달
        String twoLine = processFileNew((BufferedReader br) -> br.readLine() + br.readLine());
    }

예외, 람다, 함수형 인터페이스의 관계

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

형식 검사

  • 람다가 사용되는 context를 이용해서 람다의 형식을 추론
  • context(메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식(target type)
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

filter(List<Apple> inventory, Predicate<Apple> p);

1.filter 메서드의 선언을 확인
2.두번째 파라미터로 Predicate<Apple> 형식을 기대
3.Predicate<Apple>은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
4.test는 Apple을 입력받아 boolean을 반환하는 함수 디스크립터
5.filter 메서드로 전달된 인수는 람다의 시그니처와 일치하여 형식 검사 성공
  • 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있음
Callable<Integer> c = () -> 42;
PriviledAction<Integer> p = () -> 42;

형식 추론

  • context(대상 형식)을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론
  • 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론
List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor());

지역변수 사용 제약

  • 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용 가능
  • 람다 캡처링 capturing lambda
  • 지역 변수는 명시적으로 final로 선언되어 있거나
  • 실질적으로 final로 선언된 변수와 똑같이 사용되어야함
  • 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있음
  • 인스턴스 변수는 힙에 저장, 지역 변수는 스택에 위치
  • 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면
  • 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 
  • 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음
  • 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공
  • 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

portNumber = 31337;	//compile error

메서드 참조

  • 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조
  • 가독성을 높임
  • 람다표현식의 인수를 더 깔끔하게 전달

정적 메서드 참조

ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);
ToIntFunction<String> stringToInt = Integer::parseInt;

인스턴스 메서드 참조

BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
BiPredicate<List<String>, String> contains = List::contains;

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

Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);
Predicate<String> startsWithNumber = (String string) -> this::startsWithNumber;

생성자 참조

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

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

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

static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
	map.put("apple", Apple::new);
    map.put("oragne", Orange::new);
}

public static Fruit giveMeFruit(String fruit, Integer weight) {
	return map.get(fruit.toLowerCase())	//map에서 Function<Integer, Fruit>
 	          .apply(weight);	//Fruit 생성
}

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

  • 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공 (default method)
  • 간단한 여러 개의 람다 표현식을 조합해서 복잡한 람다 표현식을 만들 수 있음
//Comparator 역정렬
inventory.sort(comparing(Apple::getWeight).reversed());

//Comparator 연결. 첫번째 비교자로 조건이 같으면 두번째 비교자 전달
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

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

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

Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
                                            	.or(apple -> GREEN.equals(apple.getColor());
                                                    
//Function 조합
Function<Integer, Integer> f = x -> x+1;
Function<Integer, Integer> g = x -> x*2;

Function<Integer, Integer> h = f.andThen(g);	//f를 수행한 후 그 결과를 g 수행

Function<Integer, Integer> h = f.compose(g);	//g를 수행한 후 그 결과를 f 수행

함수형 프로그래밍

  • 순수함수를 구현하고 호출
  • 매개 변수만을 사용하여 외부에 side effect가 발생하지 않도록함, 안정성
  • 병렬처리 가능, 확장성
728x90
반응형