ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 람다 표현식 설명부터 사용법까지 완벽 정리 #JAVA #람다
    JAVA 2023. 1. 3. 00:20

    자바 람다 표현식 설명부터 사용법까지 완벽 정리  #JAVA #람다


    안녕하세요? 장장스입니다.
    오늘은 자바 8에 추가된 람다에 대해 정리하겠습니다.

    람다란 무엇인가?


    람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있습니다. 람다 표현식에는 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트를 가질 수 있습니다.

    람다의 특징을 하나씩 살펴보면,

     

    ▶ 익명
    보통의 메서드와 달리 이름이 없으므로 익명이라 표현한다. 구현해야 할 코드가 줄어든다.

    ▶ 함수
    람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.

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

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

    익명 클래스란?

    말 그대로 이름이 없는 클래스로, 일시적으로 만들어서 사용되고 버려지는 클래스를 말한다.
    Comparator 클래스를 직접 구현하여 사용 하는 것을 예로 들 수 있다. 
    Comparator<Tiger> comparator = new Comparator<Tiger>() {
      @Override
      public int compare(Tiger t1, Tiger t2) {
        return Integer.compare(t1.getWeight(), t2.getWeight());
      }
    };


    위의 익명 클래스를 이용한 코드를 람다를 사용하면 아래처럼 바꿀 수 있다.

    Comparator<Tiger> byWeight = 
        (Tiger t1, Tiger t2) -> t1.getWeight() - t2.getWeight();

     

    람다 표현식은 람다 파라미터, 화살표, 람다 바디로 구성된다.



    람다는 어디에 어떻게 쓰일까?


    그래서 람다 표현식은 어디에 어떻게 쓰는지에 대한 궁금증이 생긴다. 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.

    함수형 인터페이스(Functional Interface)?

    함수형 인터페이스(Functional Interface)는 정확히 하나의 추상 메서드를 지정하는 인터페이스다. 위에서 사용했던 Comparator도 자바 API의 함수형 인터페이스이다.

    ※ 함수형 인터페이스@FunctionalInterface 어노테이션을 사용해서 표기한다.
    필수적으로 구현해야 하는 추상 메서드가 1개 있으며, @FunctionalInterface 로 선언했지만 실제로 함수형 인터페이스가 아닌경우 컴파일 에러를 발생할 수 있다.
     
    위에서 예시로 사용했던 함수형 인터페이스 Comparator는 추상 메서드 compare를 직접 구현하여야 한다.


    람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접하여 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. (자바 문법으로는 함수형 인터페이스를 구현한 클래스의 인스턴스이다.)

    함수 디스크립터(function descriptor)

    람다 표현식의 시그니처를 서술하는 추상 메서드를 함수 디스크립터(function descriptor)라고 부른다. 람다 표현식의 시그니처는 함수형 인터페이스의 추상 메서드 시그니처와 동일하다.

    예를 들어, Comparator 인터페이스의 메서드 compare가 두개의 인수를 받아 int를 반환하므로 Comparator 인터페이스는 두개의 인수를 받아 int를 반환하는 시그니처로 볼 수 있다.

    다른 예로, Runnable 인터페이스의 추상 메서드 run은 파라미터를 받지 않고 void를 반환하므로 Runnable 인터페이스의 시그니처는 파라미터를 받지 않고 void를 반환 하는 것으로 생각하면 된다.

     

    확인된 예외(Exception)을 던지지 않는 함수형 인터페이스

    메서드를 만들때 우리는 확인된 예외(Exception)를 던지는 일을 한다. 아래 예시를 보면, BufferedReader 클래스의 readLine 함수를 사용했을 때, IOException이 예상이 된다.

    public void printInputText() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        String s = bufferedReader.readLine();
        System.out.printf("inputText : %s", s);
    }

    그러나 람다는 확인된 예외를 던지기 위해서는 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나, 람다를 try~catch문을 사용하여 감싸야한다.

    1. 확인된 예외를 선언하는 함수형 인터페이스 정의
    BufferedReader를 사용할 때 발생할 수 있는 IOException을 선언하는 함수형 인터페이스를 직접 정의하여 사용 할 수 있다.

    @FunctionalInterface
    public interface BufferedReaderProcessor{
        String process(BufferedReader bufferedReader) throws IOException;
    }
    BufferedReaderProcessor bufferedReaderProcessor =
            (BufferedReader bufferedReader) -> bufferedReader.readLine();
    
    String s = bufferedReaderProcessor.process(new BufferedReader(new InputStreamReader(System.in)));


    2. try~catch문으로 사용하여 감싸기
    BufferedReader를 사용할 때 발생할 수 있는 IOException을 try~catch 문으로 감싼다.

    public void printInputText(){
        Function<BufferedReader, String> function = (BufferedReader br) -> {
            try {
                return br.readLine();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        };
    
        String s = function.apply(new BufferedReader(new InputStreamReader(System.in)));
        System.out.printf("inputText : %s", s);
    }



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


    다양한 람다 표현식을 사용하려면 다양한 함수 디스크립터를 기술하는 함수형 인터페이스들이 필요하다. 자바 API는 다양한 함수형 인터페이스들을 제공한다.

    함수형 인터페이스 함수 디스크립터 기본형 특화
    Predicate<T> T -› boolean IntPredicate
    LongPredicate
    DoublePredicate
    Consumer<T> T -> void IntConsumer
    LongConsumer
    DoubleConsumer
    Function<T> T -> R IntFunction<R>
    IntToDoubleFunction
    IntToLongFunction

    LongFunction<R>
    LongToDoubleFunction
    LongToIntFunction

    DoubleFunction<R>
    DoubleToIntFunction
    DoubleToLongFunction

    ToIntFunction<T>
    ToLongFunction<T>
    ToDoubleFunction<T>
    Supplier<T> () -> T BooleanSupplier
    IntSupplier
    LongSupplier
    DoubleSupplier
    UnaryOperator<T> T -> T IntUnaryOperator
    LongUnaryOperator
    DoubleUnary0perator
    BinaryOperator (T, T) -> T IntBinaryOperator
    LongBinaryOperator
    DoubleBinary0perator
    BiPredicate<L, R> (L, R) -> boolean  
    BIConsumer<T, U> (T, U) -> void ObjIntConsumer<T>
    ObjLongConsumer<T>
    ObjDoubleConsumer<T>
    BIFunction<T, U, R> (T, U) -> R ToIntBiFunction<T, U>
    ToLongBiFunction<T, U>
    ToDoubleBiFunction<T, U>



    람다로 함수형 인터페이스의 인스턴스는 어떻게 만드는 걸까?


    람다 표현식은 함수형 인터페이스의 인스턴스를 만들 수 있다. 그러나 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다. 따라서 람다 표현식을 이해하려면 실제 함수형 인터페이스의 형식을 파악해야 한다.

    public void filterHeavyTiger(){
        List<Tiger> heavierThen290kg = filter(tigers, (Tiger tiger) -> tiger.getWeight() > 290);
    }
    
    public static List<Tiger> filter(List<Tiger> tigers, Predicate<Tiger> predicate) {
        return tigers.stream()
                 .filter(predicate)
                 .collect(Collectors.toList());
    }

    위 코드는 호랑이 몸무게가 290kg을 넘는 호랑이를 필터링 하기 위한 filterHeavyTiger 메서드이다.

    (Tiger tiger) -> tiger.getWeight() > 290

    여기에 사용된 람다는 어떻게 Predicate 함수형 인터페이스인 것을 확인 할 수 있었을까?

    람다의 형식 검사

    람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식을 대상 형식(target type)이라고 부른다.

    (Tiger tiger) -> tiger.getWeight() > 290

    위 코드의 형식검사의 과정을 살펴보면 아래와 같다.

    1. 람다가 사용된 콘텍스트는 무엇인가?
    - filter 메서드를 확인한다.
    2. 대상 형식을 확인한다.
    - 두 번째 파라미터가 Predicate<Tiger> 대상 형식을 기대한다.
    3. 대상 형식(Predicate<Tiger>)의 추상 메서드는 무엇인가?
    - Predicate<Tiger>는 test 추상 메서드를 정의하는 함수형 인터페이스이다.
    - 이해가 안가면 Predicate 인터페이스를 확인해 보자
    4. Tiger를 인수로 받아 boolean을 반환하는 test 메서드다.
    - test 메서드는 Tiger를 받아 boolean을 반환하는 함수 디스크립터를 묘사한다.
    5. 함수 디스크립터와 람다의 시그니처와 일치를 확인한다.
    - 형식 검사가 완료 된다.

    대상 형식(Target Type)이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있다.

    Callable<Integer> c         = () -> 7;
    PrivilegedAction<Integer> p = () -> 7;

    Callable과 PrivilegedAction는 파라미터를 받지 않고, 제네릭 형식 T를 반환하는 함수를 정의한다. 따라서 위 코드는 모두 유효한 코드이다.

    형식 추론

    자바 컴파일러는 대상 형식(콘텍스트)을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다. 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다. 즉, 자바 컴파일러는 다음 처럼 람다 파라미터 형식을 추론할 수 있다.

    Comparator<Tiger> c1 = (Tiger t1, Tiger t2) -> t1.getWeight().compareTo(t2.getWeight());
    Comparator<Tiger> c2 = (t1, t2) -> t1.getWeight().compareTo(t2.getWeight());

    어떤 코드가 더 좋다라고는 할 수 없다. 상황에 따라 형식을 배제하는 것이 가독성을 향상 시킬 수도 있고, 떨어지게 할 수도 있다. 이는 개발자 스스로 어떤 코드가 가독성을 향상시킬 수 있을지 결정해야 한다.

    람다의 제약

    지금까지 람다 표현식 설명을 위해 사용한 코드들을 보면 람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 하지만 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수를 활용할 수 있다. 이와 같은 동작을 람다 캡처링이라고 부른다.

    int waitNumber = 102;
    Runnable r = () -> System.out.println(waitNumber);

     

    위의 코드는 유효한 코드로 정상 동작한다. 그렇다면 다음 코드는 어떻게 될까?

    int waitNumber = 102;
    Runnable r = () -> System.out.println(waitNumber);
    waitNumber = 500;

    이 코드는 컴파일 에러가 발생한다.

    람다 표현식에서 자유 변수에 제약이 있다. 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있다. 하지만 지역 변수는 명시적으로 final로 선언 되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다. 즉, 람다 표현식은 한번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

    람다에는 왜 이런 제약이 걸리는걸까?

    자바에서 인스턴스 변수는 힙 메모리에 저장된다. 반면 지역 변수는 스택 메모리에 저장된다.

    람다가 스레드에서 실행되면 변수를 할당한 스레드가 사라져서 지역 변수의 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 지역 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하지 않고 자유 지역 변수의 복사본을 제공한다.
    따라서 복사본의 값이 바뀌지 않아야 하므로 자유 지역 변수에는 한번만 값을 할당해야 한다는 제약이 생긴 것이다.



    메서드 참조


    자바에는 메서드 참조 기능이 있다. 메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다. 예를 들어 람다가 '이 메서드를 직접 호출해' 라고 명령한다면 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조하는 것이 편리하다.

    이때 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있다.

    tigers.sort(Comparator.comparing(Tiger::getWeight));

    메서드명 앞에 구분자 (::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다. 예를 들어 Tiger::getWeight는 Tiger 클래스에 정의된 메서드 참조다.

     

     

    메서드 참조를 만드는 방법

    메서드 참조는 세 가지 유형으로 구분할 수 있다.
    1. 정적 메서드 참조
    예를 들어, Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있다.

    2. 다양한 형식의 인스턴스 메서즈 참조

    예를 들어, String의 length 메서드는 String::length로 표현할 수 있다.

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

    예를 들어,  Tiger 객체를 할당 받은 tiger 인스턴스트에 getWeight 메서드가 있다면, tiger::getWeight 메서드로 사용할 수 있다.

     

    생성자 참조

    생성자(new) 또한 메서드 참조를 이용해서 만들 수 있다.

    인수가 없는 생성자, Supplier의 () -> Tiger 와 같은 시그니처를 갖는 생성자가 있다고 가정하면, 아래와 같이 사용 할 수 있다.

     

    Post


    References


    • 모던 자바 인 액션

     


    잘못된 코드나 내용이 있다면 댓글을 남겨주세요. 즉시 수정하도록 하겠습니다! :)

     

    댓글