본문 바로가기

Programming/Java

[Java] 람다(lambda)

람다(lambda)란?

람다 표현식은 메서드로 전달할 수 있는 익명 함수(anonymous function)을 생성하기 위한 식이다.

매개 변수를 가진 코드 블럭이지만 런타임시에는 익명 구현 객체를 생성한다

 

// 인터페이스의 익명 구현 객체
Runnable runnable = new Runnable() {
    public void run() { ... }
}

// 람다식
Runnable runnable = () -> { ... };
인터페이스 변수명 = 람다식;

자바는 메소드만 단독으로 생성이 불가능하고 항상 클래스의 구성 멤버로 선언해야 한다. 따라서 람다는 메소드를 선언하는 것이 아닌 메소드를 가진 익명 구현 클래스를 생성하는 것이다.

 

람다식은 인터페이스 변수에 대입하고 대입될 인터페이스에 따라 작성법이 달라진다. 람다식이 대입되는 인터페이스를 람다식의 타겟 타입(target type)이라고 한다

 

특징

  1. 익명성 : 일반적인 메소드와는 달리 이름이 없기 때문에 익명이라고 표현한다.
  2. 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않기 때문에 함수라고 부른다. 그렇지만 메서드와 동일하게 parameter, body, return, 반환 예외 리스트를 포함할 수 있다
  3. 전달 : 메서드를 인수로 전달하거나 변수로 저장이 가능하다
  4. 간결성 : 익명 클래스는 생성할 때 많은 코드를 작성해야 하지만 람다는 불필요한 코드를 작성하지 않아도 된다.

 

 

문법

([타입] 매개변수, ... ) -> { 실행문(바디); }

람다의 표현식은 크게 파라미터와 화살표 바디로 구성되어 있다

 

하나의 매개변수만 존재한다면 앞의 괄호를 생략할 수 있지만, 없다면 반드시 빈괄호를 사용해야 한다.

하나의 실행문만 존재한다면 중괄호도 생략이 가능하고, return문만 존재한다면 return을 생략하고 중괄호를 생략할 수 있다.

 

형식 추론

자바 컴파일러는 람다 표현식이 사용된 컨텍스트를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론하고, 대상형식을 이용해 함수의 디스크립터를 알 수 있기 때문에 시그니처도 알수 있다. 결과적으로 컴파일러는 파라미터의 형식에 접근할 수 있기 때문에 생략이 가능하다.

(int x, int y) -> x + y;
(x, y) -> x + y;

 

 

함수적 인터페이스(@FunctionalInterface)

모든 인터페이스를 람다의 타겟 타입으로 사용할 수는 없다. 람다는 하나의 메소드만을 재정의하기 때문에 함수형 인터페이스에서만 사용이 가능하다.

 

함수형 인터페이스는 단 하나의 추상 메소드만을 가진 인터페이스이다. (디폴트 메소드는 제외)

 

@FunctionalInterface를 붙이지 않아도 람다식으로 사용이 가능하지만 애노테이션을 붙이면 인터페이스에 두 개 이상의 추상메소드가 선언될 때 컴파일러가 에러를 발생시킨다.

 

함수 디스크립터(function descriptor)

함수형 인터페이스의 추상 메서드 시그니처

메서드 시그니처를 람다 표현식으로 작성한 것이다

  • 메서드 시그니쳐: 메서드의 특성과 이름, 전달인자, 반환값의 데이터 타입을 표현한 형태
T -> boolean // boolean method(T t);
(int, int) -> void // void method(int n1, int n2)

 

 

표준 API의 함수적 인터페이스

Predicate

java.util.function.Predicate<T>

매개⭕, 리턴⭕

 

test()라는 추상 메서드를 정의한다. 제네릭형태의 객체를 받아 boolean을 리턴한다

 

Consumer

java.util.function.Consumer<T>

매개⭕, 리턴❌

 

제네릭 형태의 객체를 받아 void를 반환하는 accept() 추상 메서드를 정의한다. 단순히 매개값을 소비하고 리턴이 없다.

 

Supplier

java.util.function.Supplier<T>

매개❌, 리턴⭕

 

매개값이 없고 리턴값이 존재하는 get() 추상 메소드를 정의한다. 실행한 후 호출한 곳으로 데이터를 리턴(공급)한다.

 

Function

java.util.function.Function<T, R>

매개⭕, 리턴⭕

 

제네릭 형태의 T를 인수로 받아 제네릭 형태의 R 객체를 반환하는 추상 메서드 apply()를 정의한다. 매개값을 리턴값으로 매핑하는 람다를 정의할 때 사용할 수 있다.

 

기본형 특화

위의 인터페이스는 제네릭 타입이기 때문에 기본형을 사용하면 오토박싱이 일어나게 된다. 하지만 이는 비용이 발생하기 때문에 특정 형식으로 입력을 받는 인터페이스를 추가적으로 제공한다.

 

DoublePredicate, IntConsumer 처럼 형식명이 붙고, Function 인터페이스는 ToIntFunction<T>. IntToDubleFunction 등의 출력 형식도 제공한다.

 

IntPredicate isEven = (int i) -> i % 2 == 0; // 박싱 없음
Predicate<Integer> isOdd = (Integer i) -> i % 2 != 0; // 박싱

 

예외

함수형 인터페이스는 확인된 예외를 던지는 것을 허용하지 않는다. 만약 람다식에서 예외를 던지고 싶으면 함수형 인테페이스에 예외를 선언하거나 람다를 try-catch로 감싸 사용해야 한다

@FunctionalInterface
public interface MyInterface {
    String process(String str) throws Exception();
}
Function<String, String> func = (String str) -> {
	try {
        ...
		return ret;
    } catch(Exception e){
        throw new RuntimeException(e);
    }
}

 

 

제약

람다 표현식에서는 익명 함수처럼 외부에 정의된 변수인 자유 변수(free variable)를 사용할 수 있다. 이를 람다 캡처링(capturing lambda)라고 부른다.

 

하지만 자유 변수에도 제약이 존재한다. 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있지만, 이 변수는 final이거나 final 처럼 사용되어야 한다. 즉 한 번만 할당할 수 있는 변수를 사용할 수 있는 것이다.

int num1 = 100;
Runnable r1 = () -> System.out.println(num1); // 가능

int num2 = 100;
Runnable r2 = () -> System.out.println(num2); // 불가능
num2 = 150;

 

 

왜 그럴까❓

public class MyClass {
    int instanceVar; // 인스턴스 변수
    
    void method() {
        int localVar; // 지역 변수
    }
} 

내부적으로 인스턴스 변수는 힙에 저장이 되고, 지역 변수는 스택에 저장 된다.

람다가 지역 변수에 바로 접근할 수 있다면 람다가 스레드에서 실행되면 변수를 할당한 스레드가 사라져 변수 할당이 종료되었을 때에도 람다를 실행하는 스레드는 해당 변수에 접근하려고 할 수 있다.

 

따라서 자바는 원래 변수에 접근을 허용하는 것이 아닌 자유 지역 변수의 복사본을 제공하게 된다. 복사본은 변경되지 않아야 하기 때문에 변수에 한 번의 할당만 허용하게 된다.

 

 

메소드 참조(Method Reference)

메소드 참조를 통해 메소드의 정의를 재활용해 매개변수의 정보와 리턴 타입을 알아내 불필요한 매개변수를 제거할 수 있다.

(left, right) -> Math.max(left, right);
Math::max

 

메소드를 참조하면 기존 메서드 구현으로 람다 표현식을 만들 수 있게 되고 명시적으로 메소드명을 참조해 가독성을 높일 수 있게 된다.

 

형태

정적 메소드 참조

정적(static) 메소드를 참조할 경우, 클래스명과 메소드 이름을 적어 사용할 수 있다

(args) -> ClassName.staticMethod(args)
ClassName::staticMethod

 

인스턴스 메소드 참조

객체를 먼저 생성하고 참조 변수 뒤에 메소드 이름을 적어 사용한다

(args) -> myClass.instanceMethod(args)
myClass::instanceMethod  

 

매개 변수의 메소드 참조

람다식에서 제공하는 매개변수 a를 참조해 b 매개변수를 매개값으로 사용이 가능하다

(a, b) -> a.instanceMethod(b)
AClass::instanceMethod    

람다식 외부의 클래스 멤버일 수도 있고, 람다식에서 제공되는 매개변수의 멤버일 수도 있다.

 

생성자 참조

클래스명과 new 키워드를 이용해 생성자 참조를 생성할 수 있다.

메소드 호출로 구성된 람다식을 메소드 참조로 바꿀 수 있듯이 객체를 생성하고 리턴하도록 구성된 식은 생성자로 대체가 가능하다.

(a, b) -> new 클래스(a, b)
클래스::new    
BiFunction<String, String> fn = Member::new; // new Member(String, String)
Member newMember = fn.get(); //새로운 Member 생성해 반환

 

만약 3개의 인자를 전달하는 생성자를 생성자 참조로 사용하고 싶다면❓

위와 동일하게 클래스::new로 사용할 수 있지만, 이를 제공하는 함수형 인테페이스가 기본적으로 제공되지 않는다. 따라서 직접 함수형 인터페이스를 만들면 사용이 가능하다

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

 

 


📚 Reference

신용권, 『이것이 자바다』, 한빛미디어(2015)
라울-게이브리얼 우르마, 『모던 자바 인 액션』, 우정은, 한빛미디어(2018)

 

 

 

'Programming > Java' 카테고리의 다른 글

[Java] 스트림(Stream) 활용  (0) 2021.05.13
[Java] 스트림(Stream)  (0) 2021.05.11
[Java] Boxing, Unboxing  (0) 2021.05.05
[Java] Arrays.sort(), Collections.Sort() 속도 비교  (0) 2021.03.04
[Java] 어노테이션(Annotation)  (0) 2021.03.01