자바

Java - 람다 표현식 (lambda expression) 개요

알쓸개잡 2023. 9. 10.

Java SE 8 부터 람다 표현식 이라는 개념이 도입되었다. 람다 표현식은 Functional Interface 를 구현하는 구현체라고 보면 좋을 것 같다. 혹은 Functional Interface의 익명 클래스 인스턴스를 생성하는 간단한 방법이라고 봐도 좋을 것 같다.

이번 포스팅에서는 람다 표현식에 대한 소개를 하고자 한다.

 

람다 표현식 작성 세 단계

람다 표현식을 작성할 때는 아래의 세 단계에 따라 작성하면 도움이 될 것 같다.

  1. 작성하려는 람다 표현식의 유형 식별하기 (Functional Interface)
  2. 구현할 올바른 메소드 찾기
  3. 메소드 구현하기

람다 표현식을 작성할 때 고려할 위 3가지 단계에 따라서 차근 차근 알아보자.

 

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

람다 표현식의 유형에는 Functional Interface(함수형 인터페이스)여야 한다. 따라서 함수형 인터페이스를 구현하지 않는 익명 클래스는 람다 표현식으로 작성할 수 없다. 함수형 인터페이스는 추상 메서드가 하나만 있는 인터페이스를 말한다. 물론 함수형 인터페이스는 디폴트 메소드(java8 부터 지원됨)나 static 메소드도 가질 수 있다. 함수형 인터페이스에서 중요한 것은 구현해야 할 추상 메소드가 한 개라는 점이다. 참고로 함수형 인터페이스에 반드시 @FunctionalInterface 어노테이션을 반드시 붙일 필요는 없다.

 

우선 자바에서 제공하는 몇가지 함수형 인터페이스를 살펴보자.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

run() 추상 메소드를 하나만 가지고 있기 때문에 Runnable 인터페이스는 함수형 인터페이스 이다.

@FunctionalInterface
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 인터페이스 역시 구현해야 할 accept() 메소드를 하나만 가지고 있기 때문에 함수형 인터페이스 이다.

 

아래 표는 java.util.function 패키지에 포함된 몇 가지 대표적인 함수형 인터페이스다.

함수형 인터페이스 표현식 메소드
Predicate<T> T -> boolean boolean test(T t)
BiPredicate<T, U> (T, U) -> boolean boolean test(T t, U u)
Consumer<T> T -> void void accept(T t)
BiConsumer<T, U> (T, U) -> void void accept(T t, U u)
Supplier<T> () -> T T get()
Function<T, R> T -> R R apply(T t)
BiFunction<T, U, R> (T, U) -> R R apply(T t, U u)

위 표에 기재된 함수형 인터페이스 외에도 IntPredicate, IntConsumer, IntFunction, IntSupplier 과 같이 Integer 타입에 특화된 함수형 인터페이스도 제공된다.

 

구현할 올바른 메소드 찾기

람다 표현식은 함수형 인터페이스의 유일한 추상 메소드의 구현이다. 따라서 올바른 메소드를 찾는 것은 함수형 인터페이스의 메소드를 찾기만 하면 된다. 혹은 직접 함수형 인터페이스를 만들어 사용할 수도 있겠다.

 

몇가지 예로

Runnable 인터페이스는 

public abstract void run();

Predicate 인터페이스는

boolean test(T t);

Consumer 인터페이스는

void accept(T t);

와 같이 정의되어 있고 위와 같이 필요한 쓰임에 맞는 형식의 함수형 인터페이스를 찾아서 메소드 시그니처와 리턴 타입만 맞추면 된다. 메소드 시그니처란 메소드 파라미터의 순서와 타입과 개수를 의미한다.

 

메소드 구현하기 (람다 표현식 작성하기)

람다 표현식을 구현한다는 것은 함수형 인터페이스의 추상 메소드를 구현한다는 것이다.

람다 표현식은 세개의 요소로 이루어 진다.

  • 파라미터 블록
  • 화살표 (->)
  • 메소드의 본문인 코드 불록

Predicate<T> 함수형 인터페이스의 test() 를 구현하는 람다 표현식을 예로 작성해 보겠다. test() 메소드 정의는 아래와 같다.

boolean test(T t);

매개변수를 한개만 전달 받고 boolean 타입을 리턴하는 람다 표현식이어야 한다.

매개변수로 String 타입의 변수를 받고 String 타입의 변수의 시작 문자열이 "lambda" 인지 여부를 체크하는 람다 표현식이다.

Predicate<String> predicate =
			(String s) -> {
				return s.startsWith("lambda");
			};

(String s) 는 파라미터 블록이고 {} 안의 내용이 코드 블록이다.

위 구문은 아래와 같이 단순화 시킬 수 있다.

Predicate<String> predicate = s -> s.startsWith("lambda");

컴파일러는 매개변수 타입이 String 타입이라는 것을 타입 추론을 통해서 알 수 있으므로 매개변수 블록에 타입지정을 생략할 수 있다. 매개변수가 한개인 경우에는 괄호를 생략할 수도 있다. 매개변수가 2개 이상인 경우에는 괄호를 유지해야 한다.

(String s) --> (s) --> s

메소드 본문(코드 블록)에 코드가 한 줄만 있는 경우에는 중괄호와 return 키워드를 생략할 수 있다.

{ return s.startWith("lambda") } --> s.startWith("lambda")

 

다음은 간단한 Consumer<String> 람다 표현식을 작성해 보겠다. Consumer<T> 의 추상 메소드 정의는 아래와 같다.

void accept(T t)

한개의 매개변수를 받고 리턴 값이 없다.

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

간략한 형식으로 작성하였다. 위 람다 표현식은 String 타입의 변수에 대해서 시스템 출력을 하는 람다 표현식이다.

 

람다 표현식 호출하기

아래 코드는 이전 예제에서 작성한 Predicate 람다 표현식을 이용하여 호출 하는 코드를 예로 작성한 코드이다.

//아래의 predicate 변수는 Predicate 함수형 인터페이스를 구현한 인스턴스이다.
//s -> s.startWith("lambda"); 이 구문은 Predicate 함수형 인터페이스의 test() 메소드를
//오버라이드 하여 구현한 구현체이다.
Predicate<String> predicate = s -> s.startsWith("lambda");

List<String> stringList = Arrays.asList(
    "lamb-test",
    "lambda-test",
    "lambda2-test",
    "aaaaa");

for (String s : stringList) {
	//아래 test() 호출이 위에서 구현한 s -> s.startWith("lambda") 코드가 적용된다.
    if (predicate.test(s)) {
        System.out.println(s + " is start with lambda");
    }
}
lambda-test is start with lambda
lambda2-test is start with lambda

람다 표현식을 작성할 때마다 람다가 구현하는 인터페이스에 정의된 모든 메소드를 호출 할 수 있다. 추상 메소드를 호출하면 이 람다 표현식이 해당 메소드의 구현이므로 람다 자체의 코드가 호출된다. 만약 인터페이스에 default 메소드가 정의되어 있고 default 메소드를 호출하게 되면 인터페이스에 작성된 default 메소드의 코드가 호출된다. 람다가 default 메소드를 재정의 할 수는 없다.

 

로컬 변수 캡쳐

String mergedString = "";
//아래 lambda 표현식에서는 외부 변수인 mergedString 을 사용하고 있다.
//하지만 아래 코드는 다음과 같은 컴파일 오류가 발생한다.
//error: local variables referenced from a lambda 
//expression must be final or effectively final
Consumer<String> consumer = s -> mergedString += s;
List<String> stringList = Arrays.asList(
    "lamb-test",
    "lambda-test",
    "lambda2-test",
    "aaaaa");

for (String s : stringList) {
    consumer.accept(s);
}

System.out.println(mergedString);

위 코드에서 람다 표현식에서 외부 변수인 mergedString을 사용하고 있다. 하지만 아래와 같은 컴파일 오류가 발생한다.

error: local variables referenced from a lambda expression must be final or effectively final

이유는 람다는 코드 블록 외부에 정의된 변수를 수정할 수 없기 때문이다. 외부에 정의된 변수가 final 로 선언된 변경이 불가능한 변수라면 사용할 수 있다. 명시적으로 외부 변수가 final 로 선언되지 않더라도 값을 변경하지 않는다면 사용할 수 있는데, 이는 컴파일러가 final 을 암묵적으로 추가해주기 때문이다.

 

String mergedString = "merged string: ";
Consumer<String> consumer = s -> System.out.println(mergedString + s);
List<String> stringList = Arrays.asList(
    "lamb-test",
    "lambda-test",
    "lambda2-test",
    "aaaaa");

for (String s : stringList) {
    consumer.accept(s);
}

위 코드는 람다 표현식 코드 블럭에서 외부 변수인 mergedString을 변경하지 않기 때문에 오류 없이 실행된다.

댓글

💲 추천 글