자바 기본 15. 람다식

장난·2021년 6월 26일
0

자바 기본

목록 보기
15/15
post-thumbnail

15주차 과제: 람다식


📌 목표

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


📌 학습할 것

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

📜 시작에 앞서

  • 백기선 님의 라이브 스터디(2020년 11월부터 2021년 3월까지) 커리큘럼을 따라 진행한 학습입니다
  • 뒤늦게 알게 되어 스터디 참여는 못했지만 남아있는 스터디 깃허브 주소유튜브 영상을 참고했습니다

📑 람다식 사용법


람다식 (Lambda Expression)

  • 람다식: 메서드로 전달할 수 있는 익명 함수를 단순화 한 것
    • 함수형 인터페이스 파트에서 살펴보겠지만, 사실 기술적으로 따지면 함수형 인터페이스를 구현한 익명 클래스의 인스턴스라 할 수 있다
  • 특징
    • 익명: 보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드에 대한 걱정거리가 줄어든다
    • 함수: 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다
    • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
    • 간결성: 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
    • 출저: 모던 자바 인 액션

람다식 작성

//표현식 스타일
(parameters) -> expression
//블록 스타일
(parameters) -> {statements;}
  • 용어
    • 파라미터리스트
    • 화살표
    • 람다 바디

메서드를 람다식으로 바꾸는 과정

출저: 자바의 정석 기본편


  1. 메서드에서 이름과 반환 타입을 제거하고 매개변수 선언부와 몸통{} 사이에 ->를 추가
int max(int a, int b) {
    return a > b ? a : b;
}

(int a, int b) -> {
    return a > b ? a : b;
}

  1. 반환값이 있는 경우, return문 대신 '식(expresstion)'으로 대신할 수 있다. 식의 연산결과가 자동으로 반환값이 된다. 이때는 '문장(statements)'이 아닌 '식'이므로 끝에 ;을 붙이지 않는다
(int a, int b) -> a > b ? a : b

  1. 람다식에 선언된 매개변수 타입은 추론 가능할 경우 생략 가능. 람다식에 반환타입이 없는 이유도 항상 추론 가능하기 때문이다
(a, b) -> a > b ? a : b

참고

  • 람다의 모든 매개변수 타입은 생략하자
    • 타입을 명시해야 코드가 더 정확하거나, 컴파일러가 타입을 추론할 수 없다고 할 대 제외
  • 매개변수가 하나일 때 괄호()생략 가능
    • 매개변수의 타입까지 작성해줘야 할 때는 생략 불가
  • 람다 바디의 문장이 하나일 때 괄호{} 생략 가능
    • return문 포함할 경우 괄호{} 생략 불가
  • 람다의 직렬화는 피하자
    • 람다도 익명 클래스처럼 직렬화 형태가 구현별로(가상 머신별로) 다를 수 있다

📑 함수형 인터페이스

Q. 어디에서 람다를 사용할 수 있는 건가?

A. 함수형 인터페이스


함수형 인터페이스

  • 람다식을 다루기 위한 인터페이스
  • 조건: 하나의 추상 메서드만 정의되어 있어야 한다
    • 자바 8에 추가된 디폴트 메서드의 경우 구현부가 있으므로 상관 없다. 오직 추상 메서드가 하나라면 함수형 인터페이스다
    • java.lang.Object의 메서드를 오버라이딩하는 추상 메서드도 추상 메서드로 카운트하지 않는다

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

//java.lang.Runnable
public interface Runnable {
    void run();
}

@FunctionalInterface

  • 함수형 인터페이스임을 표시하는 애노테이션
    • @FunctionalInterface로 인터페이스를 선언했지만 함수형 인터페이스의 조건을 만족하지 않을 경우 컴파일 에러 발생
      • 참고로 컴파일러는 함수형 인터페이스의 조건을 만족하는 모든 인터페이스를 함수형 인터페이스로 취급하는데, 이는 하위버전 호환을 위함이다
      • 그럼에도 @FunctionalInterface를 사용하면 함수형 인터페이스를 명시적으로 표현할 수 있고 코드 작성시 컴파일러의 도움을 받을 수 있으므로 꼭 사용

함수형 인터페이스와 익명 클래스

이처럼 람다의 시대가 열리면서 익명 클래스는 설 자리가 크게 좁아진 게 사실이다. 하지만 람다로 대체할 수 없는 곳이 있다. 람다는 함수형 인터페이스에서만 쓰인다. 예컨대 추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없으니, 익명 클래스를 써야 한다. 비슷하게 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때도 익명 클래스를 쓸 수 있다. 마지막으로, 람다는 자신을 참조할 수 없다. 람다에서의 this 키워드는 바깥 인스턴스를 가리킨다. 반면 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다. 그래서 함수 객체가 자신을 참조해야 한다면 반드시 익명 클래스를 써야 한다.

출저: 이펙티브 자바 3/E


📑 Variable Capture

  • Variable Capture 이전에 관련 내용부터 쭉 이어서

람다 캡쳐링

  • 람다식은 익명 함수처럼 자유 변수(free variable)를 활용할 수 있다. 이 같은 동작을 람다 캡처링(capturing lambda)라고 한다

    • 자유 변수(free vaiable): 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수
  • 접근 가능 대상

    • 인스턴스 변수
    • 스태틱 변수
    • 지역변수
      • 람다에서 참고하는 지역 변수는 final로 선언되거나 final이 붙은 것처럼 이후 변경이 없어야 한다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; //컴파일 에러

출저: 모던 자바 인 액션


지역 변수의 제약

왜 지역 변수에 이런 제약이 필요한지 이애할 수 없는 독자도 있을 것이다. 우선 내부적으로 인스턴스 변수와 지역 변수는 태생부터 다르다. 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 혀용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

출저: 모던 자바 인 액션

  • 지역 변수에 대해 직접 접근하는 게 아닌 값 복사(Variable Capture)를 통해 각기 다른 메모리 공간에서의 변수 스코프에 대한 참조 문제를 해결하는 내용

📑 메서드, 생성자 레퍼런스


메서드 참조

  • 특정 메서드만 호출하는 람다의 축약형
  • 명시적으로 메서드명을 참조하여 상황에 따라 가독성을 높일 수 있다

종류람다메서드 참조
정적 메서드 참조(args) -> ClassName.staticMethod(args)ClassName::staticMethod
다양한 형식의 인스턴스 메서드 참조(arg0, rest) -> arg0.instanceMethod(rest)ClassName::instanceMethod
기존 객체의 인스턴스 메서드 참조(args) -> expr.instanceMethod(args)expr::instanceMethod

생성자 참조

  • 클래스명과 new 키워드를 통해 기존 생성자의 참조를 만들 수 있다

Supplier<MyClass> s1 = () -> new MyClass(); //람다식
Supplier<MyClass> s2 = MyClass::new; // 생성자 참조

MyClass m1 = s1.get();
MyClass m2 = s2.get();
  • Supplier를 통해 파라미터가 없는 생성자를 생성자 참조를 통해 람다 축약 + 사용 코드
  • 생성자에 파라미터가 있다면 Function, BiFunction를 통해 비슷하게 구현 가능
    • Function<Integer num> c2 = MyClass::new;
    • 생성자의 파라미터가 3개 이상이라면 직접 함수형 인터페이스를 만들어야 생성자 참조 통한 구현 가능 하지만 정말 그럴 필요가 있을지 고민
  • 참고로 배열은 Function<Integer, T[] = T[]::new 같은 식으로 하면 된다

람다, 메서드 참조 활용

public class Sorting {

  public static void main(String... args) {
    
    List<Apple> inventory = new ArrayList<>();
    inventory.addAll(Arrays.asList(
        new Apple(80, Color.GREEN),
        new Apple(155, Color.GREEN),
        new Apple(120, Color.RED)
    ));

    // 1단계: 코드 전달
    // [Apple{color=GREEN, weight=80}, Apple{color=RED, weight=120}, Apple{color=GREEN, weight=155}]
    inventory.sort(new AppleComparator());
    System.out.println(inventory);

    // reshuffling things a little
    inventory.set(1, new Apple(30, Color.GREEN));

    // 2단계: 익명 클래스 사용
    // [Apple{color=GREEN, weight=30}, Apple{color=GREEN, weight=80}, Apple{color=GREEN, weight=155}]
    inventory.sort(new Comparator<Apple>() {

      @Override
      public int compare(Apple a1, Apple a2) {
        return a1.getWeight() - a2.getWeight();
      }
    });
    System.out.println(inventory);

    // reshuffling things a little
    inventory.set(1, new Apple(20, Color.RED));

    // 3단계: 람다 표현식 사용
    // [Apple{color=RED, weight=20}, Apple{color=GREEN, weight=30}, Apple{color=GREEN, weight=155}]
    inventory.sort((a1, a2) -> a1.getWeight() - a2.getWeight());
    System.out.println(inventory);

    // reshuffling things a little
    inventory.set(1, new Apple(10, Color.RED));

    // 4단계: 메서드 참조 사용
    // [Apple{color=RED, weight=10}, Apple{color=RED, weight=20}, Apple{color=GREEN, weight=155}]
    inventory.sort(comparing(Apple::getWeight));
    System.out.println(inventory);
  }

  static class AppleComparator implements Comparator<Apple> {

    @Override
    public int compare(Apple a1, Apple a2) {
      return a1.getWeight() - a2.getWeight();
    }
      
  }

}

출저: 모던 자바 인 액션


✏️ 자바8의 함수형 인터페이스


java.util.function

  • 제네릭을 사용하면 함수형 인터페이스의 다양한 함수 디스크립터를 단순화하여 일반적인 형식의 함수형 인터페이스를 만들 수 있다
    • 함수 디스크립터: 함수형 인터페이스의 추상 메서드 시그니처
  • java.util.function에 이러한 인터페이스가 정의돼 있다

기본형

함수형 인터페이스메서드함수 디스크립터
Supplier<T>T get()( ) -> T
Consumer<T>void accept(T t)T -> void
Function<T, R>R apply(T t)T -> R
Predicate<T>boolean test(T t)T -> boolean
BiConsumer<T, U>void accept(T t, U u)(T, U) -> void
BiFunction<T, U, R>R apply(T t, U u)(T, U) -> R
BiPredicate<T, U>boolean test(T t, U u)(T, U) -> boolean
UnaryOperator<T>T apply(T t)T -> T
BinaryOperator<T>T apply(T t, T t)(T, T) -> T

특화형

자바에서 제네릭 파라미터에는 참조형만 가능하다. 따라서 제네릭을 통해 정의된 함수형 인터페이스에서는 기본형에 따른 래퍼 클래스를 사용해야 하며, 이때 박싱과 언박싱에 따른 비용이 발생한다. 자바8에서는 기본형을 입출력으로 사용할때 오토박싱을 피할 수 있도록 각 기본형에 특화된 함수형 인터페이스를 제공한다.

  • IntPredicate, LongPredicate, DoublePredicate
  • IntConsumer, LongConsumer, DoubleConsumer
  • ... 이런식이고 기본형을 알면 이름이 직관적이라 알기 쉽다

Predicate 조합, Function 조합

  • 단순한 람다식을 조합하여 복잡한 람다식을 만들 수 있다
  • 각각 이를 위한 스태틱 메서드가 정의되 있다
  • Predicate
    • negate, and, or
  • Function
    • andThen, compose

📑📌📜✏️

0개의 댓글