본문 바로가기
개발

Java8 Lambda tutorial

by autocat 2024. 10. 23.

What i read

Identifying the type of lambda expression

Java언어에서 모든 타입은 컴파일 시점에 알게된다. 람다표현식의 타입도 변수인지 혹은 필드, 메서드의 파라미터 혹은 메서드의 리턴타입이 될지도 알 수 있다. 람다식에는 타입의 제한이 있는데, 그건 바로 Functional Interface 여야 한다는 것이다. Functional Interface로 구현되지 않은 익명클래스는 람다식으로 작성될 수 없다.
Functional Interface의 정확한 정의는 조금 복잡하지만 알아야 할 요점은 Functional Interface는 abstract method만을 갖는 인터페이스 라는것이다.
Java8에서 인터페이스 내에 구체적인 메서드를 포함시킴으로써, 기존의 추상 메서드만 허용되던 인터페이스의 역할이 확장되었다. default, static 메서드 덕분에 인터페이스는 이제 구현된 메서드를 가질수 있으며 이를 통해 코드의 유연성과 재사용성이 높아졌다.

Examples of Functional Interface

@FunctionalInterface // 생략하여 사용해도 된다.
public interface Runnalbe{
    public abstract void run();
}

Runnable인터페이스는 오직 추상메서드만 가지고 있기때문에 Functional interface이다.

public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after){
        // the body of this method has been removed
    }
}

Consumer<T> 인터페이스는 하나의 추상메서드와 하나의 default를 가지고 있기때문에 Functional interface이다.

public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        // the body of this method has been removed
    }

    default Predicate<T> negate() {
        // the body of this method has been removed
    }

    default Predicate<T> or(Predicate<? super T> other) {
        // the body of this method has been removed
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        // the body of this method has been removed
    }

    static <T> Predicate<T> not(Predicate<? super T> target) {
        // the body of this method has been removed
    }
}

Predicate<T> 인터페이스는 조금 복잡하지만 Functional interface이다.
하나의 추상메서드가 존재하고, 세개의 default 메서드, 2개의 static 메서드 가 있지만 default와 static메서드는 Funtional interface의 개수를 계산할때는 포함되지 않는다.

Functional Interface는 하나의 추상메서드만을 가져야 하고, 구체적인 메서드(default, static)는 functional interface의 정의에서 추상메서드로 간주하지 않는다.

Finding the tight method to implement

Lambda expression은 함수형 인터페이스의 유일한 추상형메서드의 구현체이다.
Runnable 인터페이스에서는

public abstract void run();

Predicate 인터페이스에서는

boolean test(T t);

Consumer<T> 인터페이스에서는

void accept(T t);

Implementing the right method with a Lambda expression

Predicate<String>를 구현하며 Lambda expression을 작성해보자

람다표현식에는 3가지 요소의 syntax가 존재한다.

  • 파라미터 블록
  • -> 화살표(=> 뚱뚱한 화살표가 아니다)
  • 메서드의 body부분을 담당하는 코드 블록
    이제 이걸 사용하는 간단한 예시를 통해 알아보자.

어떠한 String 문자열이 3개와 일치하다면 true를 리턴하는 Predicate인스턴스가 필요하다고 가정해본다.

  1. 람다 표현식의 타입은 Predicate
  2. 구현해야할 메서드는 boolean test(String s)
    Predicate<String> predicate = 
     (String s) -> { // 파라미터 블록과 화살표 표현
         return s.length() == 3; // 메서드의 body를 담당하는 코드블록
     };
    Simplifying the syntax많은 것들을 예측해주는 컴파일러덕분에 더욱 간단한 신택스로 작성할수 있다.
  3. 컴파일러는 Predicate인터페이스의 추상메서드를 구현한다는걸 알고있으며, 이 메서드가 String 타입을 메서드의 인자로 작성한것도 알것이다. 그러므로 (String s)(s) 로 함축적으로 사용할 수 있고 더 나아가 소괄호()까지도 제거한 s 까지 사용할 수 있다. 물론 이런 경우는 인자가 1개일뿐일때만 가능하다.
  4. 메서드의 body블럭이 1라인의 코드라면 return중괄호{}를 제거할 수 있다
    위의 내용을 적용하면 최종 형태는 아래와 같을것이다.
    Predicate<String> predicate = s -> s.length() == 3;
    이렇게 항상 람다를 짧게 한라인으로 사용함으로써 더 가독성있는 코드를 가질수 있게된다.

Implementing a Consumer<String>

개발자들이 Consumer나 Predicate 같은 함수형 인터페이스를 설명할 때, 간단하게 “Consumer는 입력만 받고 출력은 없다” 또는 “Predicate는 문자열이 세 글자면 true를 반환한다”라고 말하는 경우가 많습니다. 함수형 인터페이스, 추상 메서드, 람다 표현식은 실제로 밀접하게 연관되어 있으니 이런 단순한 표현 방식이 현실적으로 괜찮다는 거죠. 하지만 주의할 점은, 이런 설명이 너무 단순화되어서 오해를 불러일으키거나 정확한 개념을 혼동하게 만드는 경우는 피해야 한다는 것입니다.
이제 String 타입을 consume하고 System.out을 하는 코드를 함축적으로 작성해보면 아래와 같을것이다.

Consumer<String> print = s -> System.out.println(S);

Implementing a Runnable

Runnable인터페이스의 구현체는 void run() 이다. 파라미터블록은 비어있기 때문에 괄호 없이 사용할 수 있을것이다. 다시 명심해야할것은 괄호를 제거하는건 파라미터가 1개인 상황이다 Ruunable의 경우엔 빈 파라미터 즉 0개 라는것을 명심해야한다.

Runnalbe runnable = () -> system.out.println("I am running");

Calling a lambda expression

Predicate예시로 다시 돌아가서, 주어진 문자열의 character가 실제로 3개일지 어떻게 테스트를 해볼수 있을까?
람다표현식을 작성하였더라도, 람다표현식은 그저 Pridcate인터페이스의 인스턴스라는걸 잊지 말아야한다. Predicate인터페이스는 test() 라는 default 메서드를 정의하고있고 우리가 작성한 람다표현식을 test()를 통해 문자열이 boolean을 return 하는지 확인해줄것이다.

boolean isStringsOfLengh3(String strings){

    Predicate<String> predicate = s -> s.length == 3;

    return predicate.test(strings);
}

또한 함수형 인터페이스에 정의된 default 메서드는 재정의 할 수 없다. 람다는 추상메서드만 구현할 수 있을뿐, default메서드 자체는 그 자체로 유지된다.

Capturing local values

람다표현식은 Collections Framework, Stream API 등등 과도 잘 호환되지만 사용에 제약이 있다.
아래 코드는 컴파일시점에 에러를 뱉는다.

int calculateTotalPrice(List<Product> products) {

    int totalPrice = 0;
    Consumer<Product> consumer =
        product -> totalPrice += product.getPrice();
    for (Product product: products) {
        consumer.accept(product);
    }
}

코드 자체는 보기에 문제가 없어보이지만, IDE로 확인하거나 컴파일하면 totalPrice사용 시점에 에러를 뱉는다.

Variable used in lambda expression should be final or effectively final

람다표현식은 코드블럭 외부의 변수를 수정하지 못한다. immutable 한 final일때만 읽어올수있다.
여기서 중요한 개념이 effectively final(사실상 final) 이다. Java SE 8에서 도입된 이 개념은, 변수에 final을 명시하지 않았더라도, 그 변수를 람다에서 읽기만 하고 수정하지 않는다면, 컴파일러가 자동으로 그 변수를 사실상 final로 처리한다는 것이다. 즉, 우리가 코드에서 final 키워드를 붙이지 않더라도, 변수의 값이 변경되지 않는다면 컴파일된 코드에서 컴파일러가 알아서 final로 간주한다.
이 과정에서 effectively final 변수는 소스 코드에서 final로 선언되지는 않지만, 컴파일러가 “이 변수는 변경되지 않는다”고 판단하여 사실상 final 변수처럼 처리한다. 이 기능 덕분에 불필요하게 final을 명시하지 않아도 컴파일러가 안전하게 코드를 처리할 수 있으며, 이는 람다 표현식을 사용할 때 매우 유용하다.

Serializing lambdas

람다 표현식도 필드에 저장될 수 있는데, 이 필드가 생성자나 setter 메서드를 통해 접근 가능하게 된다면, 우리가 의식하지 못하는 사이에 객체의 상태에 람다 표현식이 포함될 수 있다. 예를 들어, 객체의 특정 동작을 람다 표현식으로 정의한 후, 그 객체를 직렬화해야 하는 상황이 있을 수 있다.
Java에서는 기존의 직렬화 가능한 클래스를 사용하는 많은 코드가 이미 존재한다. 이와의 호환성을 유지하기 위해 람다 표현식도 직렬화될 수 있도록 설계되었다. 다시 말해, 기존의 직렬화 가능한 객체들에서 발생할 수 있는 문제를 피하고, 호환성을 보장하기 위해 람다 표현식도 직렬화가 가능한 것이다.
결론적으로, 람다 표현식이 객체의 상태에 포함될 수 있고, 이러한 객체가 직렬화되는 상황을 대비해, 람다 표현식을 직렬화 가능하게 설계함으로써 기존 Java 직렬화 메커니즘과의 호환성을 유지할 수 있다는 것이다.