티스토리 뷰

728x90

람다란 무엇인가?

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

람다 표현식에는 이름은없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수있는 예외 리스트는 가질 수 있다.

커스텀 Comparator 코드와 람다를 이용한 새로운 코드 비교

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

람다를 이용한 새로운 코드

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

아래 그림이 보여주는 것처럼 람다는 세 부분으로 이루어진다.

  • 파라미터 리스트
    • Comparator의 compare 메서드의 파라미터(두 개의 사과)
  • 화살표
    • 화살표(→)는 람다의 파라미터 리스트와 바디를 구분한다.
  • 람다의 바디
    • 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.

다음은 자바 8에서 지원하는 다섯 가지 람다 표현식 예제다.

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

이전 예제에서는 Comparator 형식의 변수에 람다를 할당했다.

2장에서 구현했던 필터 메서드에도 람다를 활용할 수 있었다.

List<Apple> result = filter(inventory, (Apple apple) -> "green".equals(apple.getColor()));

람다는 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.

위 예제에서는 함수형 인터페이스 Predicate를 기대하는 filter메서드의 두 번째 인수로 람다 표현식을 전달했다.

함수형 인터페이스

바로 Predicate가 함수형 인터페이스다.

Predicate는 오직 하나의 추상 메서드만 지정하기 때문이다.

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

간단히 말해, 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스이다.

💡 인터페이스는 디폴트 메서드(인터페이스의 메서드를 구현하지 않은 클래스를 고려해서 기본 구현을 제공하는 바디를 포함하는 메서드)를 포함할 수 있다. 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스다.

 

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급 (기술적으로 따지면 함수형 인터페이스를 concrete 구현한 클래스의 인스턴스)할 수 있다.

함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.

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

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

public static void process(Runnable r){
    r.run();
}
process(r1);  //Hello World 1 출력
process(r2);  //Hello World 2 출력
process(() -> System.out.println("Hello world3")); //직접 전달된 람다 표현식으로 Hello World3 출력

함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부른다.

public static void process(Runnable r){
    r.run();
}
process(() -> System.out.println("This is awesome!!"));

위 코드를 실행하면 "This is awesome!!" 이 출력된다.

() -> System.out.println("This is awesome!!")) 은 인수가 없으며 void를 반환하는 람다 표현식이다.

이는 Runnable 인터페이스의 run 메서드 시그니처와 같다.

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

람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제

자원 처리 (ex 데이터베이스의 파일 처리) 에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다.

설정과 정리 과정은 대부분 비슷하다.

즉 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.

이를 실행 어라운드 패턴 이라고 부른다.

public static String processFile() throws IOException{
    try ( BufferedReader br = new BufferedReader(new FileReader("data.xtx"))){
        return br.readLine();  //실제 필요한 작업을 하는 행이다.
    }
}

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

현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다. 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할 까?

기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다. processFile의 동작을 파라미터화하는 것이다.

processFile 메서드가 BufferedReader를 이용해서 다른 동작을 수행할 수 있도록 processFile 메서드로 동작을 전달해야 한다.

람다를 이용해서 동작을 전달 할 수 있다.

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

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

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

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

3단계: 동작 실행!

이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader → String) 와 일치하는 람다를 전달할 수 있다.

람다의 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다.

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

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

4단계: 람다 전달

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

//한 행을 처리하는 코드
String oneLine = processFile((BufferedReader b) -> b.readLine());
System.out.println(oneLine);

//두 행을 처리하는 코드
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());
System.out.println(twoLines);

함수형 인터페이스 사용

함수형 인터페이스는 오직 하나의 추상 메서드를 지정한다.

다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.

  • 이미 자바 API는 Comparable, Runnable, Callable 등의 다양한 함수형 인터페이스를 포함하고 있다.
  • java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공

Predicate

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

T 형식의 객체를 사용하는 불린 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있다.

다음 예제처럼 String 객체를 인수로 받는 람다를 정의할 수 있다.

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

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (p.test(e)) {
            result.add(e);
        }
    }
    return result;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.IsEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer

java.util.function.Consumer 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.

T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.

ex) Interger 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용

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

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

forEach(
    Arrays.asList(1,2,3,4,5),
    (Integer i) -> System.out.println(i)  //Consumer의 accept 메서드를 구현하는 람다
);

Function

java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의한다.

입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다. (예를 들면 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑)

ex) String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 이라는 메서드를 정의

@FunctionalInterface
public interface Function<T, R>{
    R apply(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for (T s : list) {
        result.add(f.apply(s));
    }
    return result;
}

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

기본형 특화

자바의 모든 형식은 참조형 (예를 들면 Byte, Integer, Object, List) 아니면 기본형 (int, dobule, byte, char)에 해당한다.

하지만 제네릭 파라미터 (Consumer 의 T)에는 참조형만 사용할 수 있다.

자바에서는 기본형을 참조형으로 변환할 수 있는 기능을 제공한다.

이 기능을 박싱 이라고 한다. 참조형을 기본형으로 변환하는 반대 동작을 언박싱 이라고 한다.

또한 프로그래머가 편리하게 코드를 구현할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱 이라는 기능도 제공한다.

//유효한 코드
List<Integer> list = new ArrayList<>();
for (int i = 300; i < 400; i++) {
    list.add(i);
}

하지만 이런 변환 과정은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다.

따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.

자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.

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

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

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

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

람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있다.

즉 형식 추론이 가능하므로 코드를 좀 더 단순화 할 수 있다.

List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

//형식 추론
//람다에서 파라미터가 하나이면 중괄호를 생략할 수 있다.
List<Apple> heavierThan150g = filter(inventory, apple -> apple.getWeight() > 150);

상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 작성하지 않는 것이 가독성을 향상 시킬때도 있다. 정해진 규칙은 없으므로 개발자 스스로 무엇이 가독성이 더 좋을지 결정해야 한다.

지역 변수 사용

람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.

자유 변수에는 제약이 있는데 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.

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

메서드 레퍼런스

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

//메서드 참조 java.util.Comparator.comparing을 활용
inventory.sort(comparing(Apple::getWeight);

메서드 참조는 특정 메서드만을 호출하는 람다의 축약형

메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있다.

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

  1. 정적 메서드 참조 - Integer의 parseInt 메서드는 Integer::parseInt로 표현 가능
  2. 인스턴스 메서드 참조 - String의 length 메서드는 String::length로 표현 가능
  3. 기존 객체의 메서드 참조 - Apple::getWeight

또 하나 생성자 참조 방식도 있는데 ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이것은 정적 메서드의 참조를 만드는 방법과 비슷하다.

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함