2369 단어
12 분
Java Lambda
2025-01-13

람다 표현식#

람다 표현식은 메서드의 파라미터로 넘길 수 있는 익명 함수를 간단하게 표현한 것이라고 설명할 수 있다.

람다의 특징#

람다는 다른 메서드와 다르게 이름이 없으므로 익명이라고 하고, 메서드처럼 특정 클래스에 종속되지 않으므로 함수 라고 한다. 또한 람다 식의 결과를 변수에 저장하거나 다른 메서드의 파라미터로 전달이 가능하다.


람다 표현식의 구조#

Comparator<Apple> name = (a1, a2) -> a1.getName().equals(a2.getName());

람다 표현식은 파라미터 리스트, 람다 바디로 구성된다.

  • 람다 파라미터

    • Comparatorcompare 메서드 파라미터(T o1, T o2)

      public interface Comparator<T> {
          int compare(T o1, T o2);
      }
  • 람다 바디

    • compare 메서드의 실제 구현 내용을 작성한다. 람다의 반환 값에 해당하는 표현식이다.

람다 표현식에서는 return을 명시적으로 작성하지 않아도 된다. 람다 표현식 내부에 return이 들어가 있기 때문이다.

사용 예시#

(String s) -> s.length(); 

파라미터로 String을 받고 s.length()의 결과인 int를 반환한다.


(int x, int y) -> {
    System.out.println("sum = ");
    System.out.println(x+y);
}

람다는 위의 예제처럼 2줄 이상의 행을 작성할 수 있다. 대신에 { } 안에 작성해야 한다.

() -> 42

파라미터가 없고 int 42를 반환한다.


람다 표현식 스타일#

(parameters) -> expression // 람다 표현식 스타일
(parameters) -> {statement;} // 블록 스타일

함수형 인터페이스#

정확히 하나의 추상 메서드를 가지고 있는 인터페이스이다. 자바 API에서 함수형 인터페이스로 Comparator, Runnable 등이 있다.

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

public interface Runnable {
    void run();
}

여기서 고려해야 할 점은 인터페이스 내에서 default 메서드가 여러개 정의되어 있다고 하더라도 추상 메서드가 오직 하나이면 함수형 인터페이스라고 한다.


함수 디스크립터#

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

Runnable 인터페이스의 추상 메서드 run은 파라미터가 없고 void를 반환하므로, 파라미터와 void를 반환하는 시그니처로 생각할 수 있다. 이걸 () -> void 처럼 표현할 수 있다.


지금까지의 정리#

  1. 람다 표현식은 변수에 할당하거나 함수형 인터페이스를 파라미터로 받는 메서드로 넘길 수 있다.

  2. 인터페이스의 추상 메서드 시그니처와 람다 표현식의 시그니처가 같다.


람다 활용#

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

위의 코드에서 기존의 초기화와 리소스 정리 코드(try-with-resources)는 그대로 수행하고 다른 동작을 시키게 하고 싶다. 그렇다면 processFile()에게 동작 파라미터를 넘겨서 수행하게 하면 된다. 그렇다면 BufferdReader -> String의 시그니처를 갖는 함수형 인터페이스를 설계 해보자.


@FunctionalInteface
public interface BufferdReaderProcessor {
    String process(BufferdReader b) throws IOException;
}
public String process(BufferdReaderProcessor b) throws IOException {
	...
}

함수형 인터페이스를 파라미터로 넘기는 곳에 람다 표현식을 사용할 수 있으므로 파라미터 리스트는 BufferdReader이고 반환값은 String, IOException의 시그니처를 가지는 함수형 인터페이스를 만든다.


public String processFile(BufferdReaderProcessor b) throws IOException {
	try (BufferedReader br =
			new BufferedReader(new FileReader("data.txt"))) {
		return b.process(br); // BufferdReaderProcessor에 있는 process() 호출
	}
}
String str = processFile(br -> br.readLine());

함수형 인터페이스의 시그니처가 BufferdReader -> String이므로 람다 표현식으로 br -> br.readLine() 또는 br -> br.readLine() + br.readLine()과 같이 사용할 수 있게 된다.


함수형 인터페이스#

Predicate#

T -> boolean

Predicate<T> 인터페이스는 boolean test(T t)라는 추상 메서드를 정의하고 제너릭 타입 T를 파라미터로 받아 boolean를 반환한다. 제너릭 T 타입의 객체를 받아서 boolean 값(식)이 필요한 경우에 사용할 수 있다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for(T t : list) {
        if(p.test(t)) {
            result.add(t);
        }
    }
    return result;
}
Predicate<String> p = (s) -> !s.isEmpty(); // 문자열이 비어 있지 않으면 true
List<String> list = filter(strList, p);


Consumer#

T -> ()

Consumer<T> 인터페이스는 T 객체를 받아서 void를 반환하는 accept(T t) 추상 메서드를 가지고 있다. T 형식의 객체를 파라미터로 받아서 어떤 특정한 동작을 정의하고 싶을 때 사용한다.

Integer 리스트를 받아서 각 Integer에 대해서 어떤 동작을 수행하는 메서드를 정의할 때 사용할 수 있다고 가정해보자.

@FunctionalList
public interface Consumer<T> {
    void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> consumer) {
    for(T t : list) {
        consumer.accept(t);
    }
}
forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));

위의 코드를 보면 (Integer i) -> System.out.println(i)에서 Consumer 인터페이스의 void accept(T t) 추상 메서드를 직접 구현하고 있다.



Function#

(T, R) -> R

Function<T, R> 인터페이스는 제너릭 T 타입의 객체를 받아 제너릭 R 타입으로 변환한 객체를 반환하는 R apply(T t) 추상 메서드를 가지고 있다. 입력을 출력으로 매핑하는 람다를 정의할 때 사용한다.

String을 받아 Integer 타입으로 바꾸는 메서드를 정의해보자.

@FunctionalInterface
public interface Function(T, R) {
    R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> function) {
    List<R> transferList = new ArrayList<>();
    for(T t : list) {
        transferList.add(function.apply(t));
    }
    
    return transferList;
}
List<Integer> newIntegerList = map(Arrays.asList("predicate", "consumer", "function"),
                                  	str -> str.length());

위의 코드에서 보면 map 메서드의 파라미터로 있는 Function 인터페이스의 applystr -> str.length() 람다식으로 표현하면서 구현하는 것을 볼 수 있다.



함수형 인터페이스와 람다 사용 예제#

방식람다 표현식함수형 인터페이스
boolean 반환(List<String> list) -> list.isEmpty()Predicate<List<String>>
객체 생성() -> new Apple(10)Supplier<Apple>
객체 소비Apple a -> System.out.println(a.getName())Consumer<Apple>
객체 추출(String s) -> s.length()Function<String, Integer>
조합(int a, int b) -> a*bIntBinaryOperator
객체 비교(Apple a, Apple b) -> a.getWeight().compareTo(b.getWeight)Comparator<Apple>


람다 내부 동작#

람다로 함수형 인터페이스의 인스턴스를 만들고 파라미터로 넘길 수 있다고 했다. 그런데 람다 표현식 자체에는 어떤 함수형 인터페이스를 구현하는지 전혀 모른다. 그렇기 때문에 람다를 좀 더 이해하기 위해서는 내부 동작을 알아야한다.


형식 검사#

람다가 사용되는 상황을 이해해서 람다의 형식을 추론할 수 있다. 람다 식이 전달되는 메서드의 파라미터나 할당 되는 변수 타입을 봐서, 나올 것이라고 예상할 수 있는 람다식의 형식을 대상 형식이라고 한다.

List<Apple> selectApple = filter(bag, apple -> apple.getWeight() > 150);

위의 코드를 형식 검사를 해보자.

  1. 람다가 사용된 상황은 무엇인가?

    • filter 메서드의 정의를 봐본다.
  2. 확인을 해보니까 filter(List<Apple> bag, Predicate<Apple> p)이다.

    • 그렇다면 대상 형식으로 Predicate<Apple>이 나올 것으로 예상한다.
  3. Predicate<Apple> 인터페이스를 확인해보니 boolean test(T t)의 추상 메서드를 가지고 있다.

    • 함수 디스크립터: Apple -> boolean
  4. 함수의 디스크립터는 Apple -> boolean이고 람다식(apple.getWeight() > 150)의 시그니처(Apple -> boolean)과 똑같다.

  5. 형식 검사 완료


void 호환 규칙#

람다 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터를 사용할 수 있다. 이때 파라미터 리스트 또한 동일해야 한다. Listadd 메서드는 Consumer<T>에 의해서 T -> void의 함수 디스크립터를 가지고 있지만, Predicate<T>의 boolean으로도 호환이 가능하다.

Consumer<String> con = str -> list.add(str);

// T -> ()은 T -> boolean 처럼 호환 가능, 이때 바디에 일반 표현식과 파라미터 리스트가 동일해야함
Predicate<String> pre = str -> list.add(str);


형식 추론#

람다 표현식이 사용된 대상 형식을 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론다. 그렇기 때문에 람다식의 시그니처도 추론할 수 있는 것이다.


int num = 1234;
Runnable r = () -> System.out.println(num);

람다 표현식에서는 람다 바디 말고 외부의 변수를 사용할 수 있는데, 이를 람다 캡처링이라고 한다.

람다는 인스턴스 변수와 정적 변수를 캡처할 수 있다. 하지만 이때 제약 조건이 있는데, final로 선언되어 있거나 final이 선언된 변수처럼 사용한 변수에 대해서 가능하다. 다시 말해 람다 표현식은 딱 한번만 할당할 수 있는 지역 변수만 쓸 수 있다는 것이다.

따라서 아래의 코드는 num을 2번 할당하기 때문에 사용할 수 없는 코드가 된다.

int num = 1234;
Runnable r = () -> System.out.println(num);
num = 2222;


메서드 참조#

Java Lambda
https://realits.me/posts/lambda/
저자
realitsyourman
게시일
2025-01-13
라이선스
CC BY-NC-SA 4.0