본문 바로가기
그룹 스터디 공부(IT 서적)/모던 자바 인 액션

chapter 3. 람다 표현식

by hanyugyeong 2023. 7. 21.
반응형
SMALL

람다 표현식

더 깔끔한 코드로 동작 구현 및 전달하는 자바 8의 새로운 기능
람다 표현식은 익명 클래스처럼 이름이 없는 함수이면서 매서드를 인수로 전달할 수 있다.

3.1 람다란 무엇인가?

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

  • 익명 : 보통의 메서드와 달리 이름이 없다.
  • 함수 : 특정 클래스에 종속되지 않는다.
  • 전달 : 메서드 인수로 전달, 변수로 저장 가능
  • 간결성 : 익명 클래스처럼 코드 구현 필요 X

1) 기존 코드

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

2) 람다를 이용한 코드

Comparator<Apple> byRamdaWeight = (Apple a3, Apple a4)->a3.getWeight().compareTo(a4.getWeight());
  • 파라미터 리스트

Comparator의 compare 메서드 파라미터 (사과 두개)

  • 화살표

화살표(->)는 람다의 파라미터 리스트와 바디를 구분한다.

  • 람다 바디

두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.

예제 3-1 자바 8의 유효한 람다 표현식

// String 형식 파라미터, int 반환 (return 함축)
(String s) -> s.length()
// Apple 형식 파라미터, boolean 반환
(Apple a) -> a.getWeight() > 150
// int 파라미터 2개 리턴 (void 리턴) 람다 표현식은 여러 행의 문장을 포함할 수 있다.
(int x, int y) -> {
    System.out.println("Result:");
        System.out.println(x + y);
}
// int 42 반환
() -> 42
// Apple 형식 파라미터 2개, int 반환
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

3.2 어디에, 어떻게 람다를 사용할까?

예 : 필터메서드

List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));

3.2.1 함수형 인터페이스

2장에서 필터 메서드를 파라미터화하기 위해 사용했던 Predicate가 바로 함수형 인터페이스 이다. 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스 로, Comparator, Runnable 등이 있다.

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

  public interface Runnable{
    void run();
  }

  public interface ActionListener extends EventListener{
    void actionPerformed(ActionEvent e);
  }

  public interface Callable<V>{
    V call() throws Exception;
  }

  public interface PrivilegedAction<T>{
    T run();
  }

인터페이스는 디폴트 매서드를 포함할 수 있다. 많은 디폴트 매서드가 있더라도 추상 매서드가 오직 하나면 함수형 인터페이스이다.

다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이다.

Runnable r1 = () -> System.out.println("Hello World 1"); //람다 사용

 Runnable r2 = new Runnable() {  //익명 클래스 사용
      @Override
      public void run() {
        System.out.println("Hello World 2");
     }
};

process(r1);
process(r2);
process(()->System.out.println("Hello World 3"));// 직접 전달된 람다 표현식으로 'Hello World 3'출력

3.2.2 함수 디스크립터

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 한다. () -> void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다.

람다와 메소드 호출

//정상적인 람다 표현식
process(()->System.out.println("This is awesome"));

위 코드에서는 중괄호를 사용하지 않았고 System.out.println은 void를 반환하므로 완벽한 표현식이 아닌 것처럼 보인다.

process(()->{System.out.println("This is awesome");});

결론적으로 이미 살펴본 것처럼 중괄호는 필요 없다. 한 개의 void 메소드 호출은 중괄호로 감쌀 필요가 없다.

퀴즈 3-3 어디에 람다를 사용할 수 있는가?

1) 람다 표현식 ()->{}의 시그니처는 ()->void며 Runnable의 추상 메서드 run의 시그니처와 일치하므로 유효한 람다 표현식이다.

execute(()->{});
  public static void execute(Runnable r){
    r.run();
  }

2) fetch 매서드의 반환 형식은 Callable이다.
T를 String으로 대치했을 때 Callable 매서드의 시그니처는 ()->String이 된다.

public static Callable<String> fetch(){
    return () -> "Tricky example ;-)";
  }

3) 람다 표현식 (Apple a) -> a.getWeight()의 시그니처는 (Apple) -> Integer이므로 Predicate:(Apple) -> boolean의 test 메서드의 시그니처와 일치하지 않는다.

Predicate<Apple> p = (Apple a) -> a.getWeight();

3.3 람다 활용 : 실행 어라운드 패턴

실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴 이라고 부른다.

public String processFile() throws IOException{
    try(BufferedReader br =
            new BufferedReader(new FileReader("./data.txt"))){
      return br.readLine();
    }
  }

1단계 : 동작 파라미터화를 기억하라

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

2단계 : 함수형 인터페이스를 이용해서 동작 전달

함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.

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

정의한 인터페이스를 processFile 매서드의 인수로 전달할 수 있다.

 public String processFile(BufferedReaderProcessor p) throws IOException{
        ...
}

3단계 : 동작 실행

processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.

public String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

4단계 : 람다 전달

  • 이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.

    • 한 행 처리 코드

       String oneLine = processFile((BufferedReader br) -> br.readLine());
  • 두 행 처리 코드3.4 함수형 인터페이스 사용

  • String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

  • 함수형 인터페이스의 추상메서드를 함수 디스크립터 (function descriptor)라고 한다.

  • 3.4.1 Predicate

  • java.util.function.Predicate 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.

@FunctionalInterface
  public interface Predicate<T>{
    boolean test(T t);
  }

public static <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;
  }
   List<String> listOfStrings = new ArrayList<>(Arrays.asList("java", "", "banna","","test")); 

  Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

  List<String> nonEmpty = filter(listOfStrings,nonEmptyStringPredicate);

3.4.2 Consumer

  • java.util.function.Consumer 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의
@FunctionalInterface
  public interface Consumer<T>{
    void accept(T t);
  }

   public static <T> void forEach(List<T> list, Consumer<T> c){
    for(T t:list){
      c.accept(t);
    }
  }

  forEach(
      Arrays.asList(1,2,3,4,5),
            (Integer i)->System.out.println(i)
    );

3.4.3 Function

  • java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply 정의
    @FunctionalInterface
    public interface Function<T, R> {
    R apply(T t);
    }
public <T, R> List map(List list, Function<T, R> f) {  
List result = new ArrayList<>();  
for (T t : list) {  
result.add(f.apply(t));  
}  
return result;  
}

List l = map(  
Arrays.asList("lambdas", "in", "action"),  
(String s) -> s.length(); // <- Function의 apply 메서드를 구현하는 람다  
);

기본형 특화

  • 특화된 형식의 함수형 인터페이스

  • 기본형을 참조형으로 변환하는 기능인 박싱 (Boxing), 참조형을 기본형으로 변환하는 언박싱 (Unboxing), 박싱과 언박싱이 자동으로 이루어지는 오토박싱(autoboxing)이라는 기능도 제공한다.
    ex) int가 Integer로 박싱됨

    List<Integer> list = new ArrayList<>();
      for(int i=300;i<400;i++){
        list.add(i);
      }
  • 자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할수 있도록 함수형 인터페이스 제공
    다음 예제에서는 IntPredicate는 1000이라는 값을 박싱하지는 않지만 Predicate는 1000이라는 값을 Integer 객체로 박싱한다.

public interface IntPredicate {
    boolean test(int t);
}

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

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000); // 거짓 (박싱)

3.5 형식 검사, 형식 추론, 제약

3.5.1 형식 검사

  • 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상형식 이라고 한다.

    List<Apple> heavierThan150gram = 
    filter(inventory, (Apple apple) -> apple.getWeitht() > 150);

다음과 같은 순서로 형식 확인 과정이 진행된다.

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

3.5.2 같은 람다, 다른 함수형 인터페이스

  • 대상 형식이라는 특징으로 같은 람다 표현식도 호환되는 추상 매서드가 여러개가 되면 다른 함수형 인터페이스로 사용될 수 있다.
Callable<Integer> c = () -> 42;
PrevillegedAction<Integer> p = () -> 42;
Comparator c1 =  
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());  
ToIntBiFunction c2 =  
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());  
BiFunction c3 =  
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

3.5.3 형식 추론

  • 컴파일러는 람다 표현식의 파라미터의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.
List<Apple> greenApples =
        filter(inventory, apple -> GREEN.equals(apple.getColor())); // 파라미터 a에는 형식을 지정하지 않았다.
  • 여러 파라미터를 포함하는 표현식에서는 가독성 향상이 더 두드러진다.

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

3.5.4 지역 변수 사용

  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수를 활용할 수 있다. 이를 람다 캡처링 이라고 한다.

    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);
  • 자유 변수에는 제약이 있는데, final로 선언 또는 final로 선언된 변수와 똑같이 사용되어야 한다.
  • 따라서, 다음은 컴파일 오류가 나는 코드이다.
    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);
    portNumber = 31337;

3.6 매서드 참조

  • 기존 코드
inventory.sort((Apple a1,Apple a2)->a1.getWeight().compareTo(a2.getWeight()));
  • 매서드 참조와 java.util.Comparator.comparing을 활용한 코드
inventory.sort(comparing(Apple::getWeight));

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

람다 매서드 참조 단축 표현
(Apple apple)->apple.getWeight() Apple::getWeight
()->Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str,i)->str.substring(i) String::substring
(String s)->System.out.println(s)(String s) System out::println
->this.isValidName(s) this::isValidName

매서드 참조를 만드는 방법

-> 매서드 참조는 세 가지 유형으로 구분할 수 있다.

  • 정적 메서드 참조
    -> Integer의 parseInt매서드는 Integer::parseInt로 표현할 수 있다.
  • 다양한 형식의 인스턴스 매서드 참조
    ->String의 length 메서드는 String::length로 표현할 수 있다.
  • 기존 객체의 인스턴스 메서드 참조
    -> 예를 들어 Transcation 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue메서드가 있다면, 이를 expensiveTransaction::getValue 라고 표현할 수 있습니다.
    -> 비공개 메서드를 정의한 상황에서 유용하게 활용이 가능 하다
List<String> str = Array.asList("a", "b", "A", "B");
str.sort((s1, s2) -> s1.comparToIgnoreCase(s2));

//메서드 참조 사용  
List str = Array.asList("a", "b", "A", "B");  
str.sort(String::compareToIgnoreCase);

3.6.2 생성자 참조

ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.

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

Color(int,int,int) 처럼 인수가 세 개인 생성자를 사용하려면 직접 함수형 인터페이스를 생성해야 한다.

public interface TriFunction<T, U, V, R> {
  R apply (T t, U u, V v);
}

TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;

3.7.1 1단계:코드 전달

  static class AppleComparator implements Comparator<Apple> {
    @Override
    public int compare(Apple a1, Apple a2) {
      return a1.getWeight() - a2.getWeight();
    }
  }
  inventory.sort(new AppleComparator());

3.7.2 2단계:익명 클래스 사용

한 번만 사용할 Comparator를 위 코드처럼 구현하는 것보다는 익명 클래스를 이용하는 것이 좋다.

 inventory.sort(new Comparator<Apple>() {
      @Override
      public int compare(Apple a1, Apple a2) {
        return a1.getWeight() - a2.getWeight();
      }
    });

3.7.3 3단계:람다 표현식 사용

inventory.sort((Apple a1, Apple a2)->a1.getWeight() - a2.getWeight());

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론한다.

 inventory.sort((a1, a2)->a1.getWeight() - a2.getWeight());

코드를 다음처럼 간소화할 수 있다.

import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));

3.7.4 4단계:매서드 참조 사용

inventory.sort(comparing(Apple::getWeight));

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

예를 들어 Comparator,Function,Predicate같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 매서드를 제공한다.

3.8.1 Comparator 조합

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

역정렬

inventory.sort(java.util.Comparator.comparing(Apple::getWeight).reversed()); //무게를 내림차순으로 정렬

Comperator 연결

  • 두 사과 비교 후 컬러가 같다면 컬러별로 사과 정렬

     inventory.sort(comparing(Apple::getWeight)
                   .reversed()
                   .thenComparing(Apple::getColor));

3.8.2 Predicate 조합

  • negate,and,or 세가지 메서드 제공
  • negate를 이용한 빨간색이 아닌 사과
Predicate<Apple> notRedApple = redApple.negate(); //기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
  • and를 이용한 빨간색이면서 무거운 사과
Predicate<Apple> redAndHeavyApple = 
redApple.and(apple -> apple.getWeight() > 150);
  • or을 이용한 150 이상 또는 녹색 사과

    Predicate<Apple> redAndHeavyAppleOrGreen = 
    redApple.and(apple -> apple.getWeight() > 150)
    .or(apple -> GREEN.equals(apple.getColor()));

3.8.3 Function 조합

  • andThen, compose 두 가지 디폴트 메서드 제공

  • andThen 매서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
 Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4 반환
  • compose 매서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
    f.andThen(g)에서 andThen 대신에 compose를 사용하면 g(f(x))가 아니라 f(g(x))라는 수식이 된다.
Function<Integer, Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1); //3 반환
public static class Letter{
    public static String addHeader(String text){
      return "From Raoul, Mario and Alan: "+text;
    }
    public static String addFooter(String text){
      return text + " Kind regards";
    }
    public static String checkSpelling(String text){
      return text.replaceAll("labda","test");
    }
  }


   Function<String,String> addHeader = Letter::addHeader;
    Function<String,String> transformationPipeline =
        addHeader.andThen(Letter::checkSpelling)
        .andThen(Letter::addFooter);

3.10 마치며

  • 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
  • 람다 표현식으로 간결한 코드를 구현할 수 있다.
  • 함수형 인터페이스는 하나의 추상 매서드만을 정의하는 이인터페이스다.
  • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
  • 람다 표현식을 이용해서 함수형 인터페이스의 추상 매서드를 즉석으로 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • java.util.function 패키지는
    Predicate<T>, Function<T,R>, Supplier<T>, BinaryOperator<T> 
    등을 포함해서 자주 사용하는 다양한 함수형 인터페이스를 제공한다.
반응형
LIST