람다식(Lambda expression)

이성민·2025년 8월 9일

JAVA

목록 보기
3/4
post-thumbnail

서론

이 글은 자바의 정석 3판을 완독한 후 작성한 내용입니다.
자바는 대표적인 객체지향 언어이지만, 람다식의 도입으로 함수형 언어의 특징도 함께 갖추게 되었습니다. 특히 최근에는 실무에서 람다식이 활발히 사용되고 있어, 이에 대한 호기심이 생겨 추가로 학습하고 내용을 정리했습니다.
이 글이 훗날 제가 내용을 잊었을 때 참고 자료가 되거나, 람다식(Lambda Expression)을 처음 접하는 분들에게 도움이 되길 바랍니다.


람다식의 등장

자바가 1996년에 처음 등장한 이후로 두 번의 큰 변화가 있었는데, 그 중 한 번이 JDK1.8부터 추가된 람다식(Lambda expression)의 등장이다.
특히, 람다식의 도입으로 인해, 이제 자바는 객체지향언어인 동시에 함수형 언어가 되었다.


람다식이란?

간단히 말해서 메서드를 하나의 식(expression)으로 표현한 것이다.

메서드를 람다식으로 표현하면 메서드의 이름이 없고, 반환 타입 표기를 생략할 수 있으므로, 람다식을 ‘익명 함수(anonymous function)’이라고도 한다.

람다식을 더 자세히 알아가기 전에!!
메서드와 함수의 차이가 뭔지 확실하게 짚고 넘어가겠다.

메서드와 함수의 차이..?

객체지향 개념에서는 함수(Function) 대신, 객체의 행위나 동작을 나타내는 메서드(Method)라는 용어를 사용한다.

메서드

본질적으로 함수와 같지만, 반드시 특정 클래스(또는 객체)에 속해야 하는 특징이 있다.

함수

클래스에 속하지 않고 독립적으로 존재하는 경우를 일반적으로 ‘함수’라고 부른다.

BUT!

자바에서는 모든 기능이 클래스 안에 정의되므로 ‘함수’ 대신 메서드라는 용어만 사용한다.

따라서,

람다식은 이러한 메서드를 간단한 문법으로 표현하여, 마치 독립적인 함수처럼 보이게 한 것이다.

람다식 예시

  • 람다식 사용 전
public class LambdaEx {
    public static void main(String[] args) {
        // Runnable 인터페이스 구현 - 익명 클래스 사용
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello Lambda!");
            }
        };

        task.run();
    }
}
  • 람다식 사용 후
public class LambdaEx {
    public static void main(String[] args) {
        // Runnable 인터페이스 구현 - 람다식 사용
        Runnable task = () -> System.out.println("Hello Lambda!");

        task.run();
    }
}

위 예시를 보면 람다식의 장점이 분명하게 드러난다.
바로, 코드 길이가 줄어들고 가독성이 향상된다는 점이다.

이제 그래서 람다식은 어떻게 사용해?라는 부분에 대해 설명하겠다.


람다식 작성

람다식 작성은 바로바로 이해하기 쉽게 바로 예시를 들면서 하나하나 보이겠다.

1번

텍스트메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->'를 추가한다.

  • 1번 수정 전
int max(int a, int b) {
	return a > b ? a : b;
}
  • 1번 수정 후
(int a, int b) -> {
	return a > b ? a : b;
}

2번

반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신한다.

이때는 '문장(statement)'이 아닌 '식(expression)'이므로 끝에 ';'를 붙이지 않는다.

  • 2번 수정 전
(int a, int b) -> { return a > b ? a : b; }
  • 2번 수정 후
(int a, int b) -> a > b ? a : b

3번

선언된 매개변수의 타입이 추론 가능한 경우 생략할 수 있다.

대부분의 경우에 생략이 가능하다. -> 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.

  • 3번 수정 전
(int a, int b) -> a > b ? a : b
  • 3번 수정 후
(a, b) -> a > b ? a : b

주의! - (int a, b) -> a > b ? a : b와 같이 두 매개변수 중 하나의 타입만 생략하는 것은 허용 안됨.

4번

선언된 매개변수가 하나뿐인 경우에는 괄호() 생략할 수 있다.

  • 4번 수정 전
(a) -> a * a
(int a) -> a * a
  • 4번 수정 후
a -> a * a // OK
int a -> a * a // 에러

주의! - 매개변수의 타입이 있으면 괄호()를 생략할 수 없다.

5번

괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.

이때, 문장의 끝에 ';'를 붙이지 않아야 한다.

  • 5번 수정 전
(String name, int i) -> {
	System.out.println(name + "=" + i);
}
  • 5번 수정 후
(String name, int i) -> System.out.println(name + "=" + i)

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

하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다.
그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(function interface)'라고 부르기로 했다.

- 중요!
단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다.

  • 함수형 인터페이스 예시
@FunctionalInterface
interface MyFunction {
	public abstract int max(int a, int b); // 한 개의 추상 메서드
}

참고 - @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해 줌. 꼭 붙이는 것을 추천.

함수형 인터페이스 타입의 매개변수와 반환타입

함수형 인터페이스 MyFunction이 아래와 같이 정의되어 있다고 하자.

@FunctionalInterface
interface MyFunction {
	void myMethod(); // 추상 메서드
}

1. 매개변수 -> 함수형 인터페이스

메서드의 매개변수가 함수형 인터페이스 타입인 예시

void aMethod(MyFunction) { 
	f.myMethod();
}

람다식을 참조 변수에 담아 전달할 수 있다.

MyFunction f  = () -> System.out.println("myMethod()");
aMethod(f);

또는, 참조 변수를 만들지 않고 직접 람다식을 매개변수로 전달할 수도 있다.

aMethod (() -> System.out.println("myMethod()")); 

2. 반환타입 -> 함수형 인터페이스

메서드의 반환타입이 함수형 인터페이스타입인 예시

@FunctionalInterface
interface MyFunction {
    MyFunction myMethod(); // 반환타입이 MyFunction
}

위와 같이 메서드의 반환타입이 함수형 인터페이스라면,
이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반한 할 수 있다.

수정 전

MyFunction myMethod = () -> {
    MyFunction f = () -> System.out.println("Hello");
    return f;  
}

수정 후

MyFunction myMethod = () -> {
	return () -> System.out.println("Hello"); 
}

메서드 참조(Method Reference) 3가지 유형

1. 클래스이름::정적메서드이름

  • 정적 메서드를 호출하는 람다식을 간단히 표현
  • 형태: (args) -> ClassName.staticMethod(args) → ClassName::staticMethod
  • 예시:
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

Function<String, Integer> f2 = Integer::parseInt; // 메서드 참조

2. 클래스이름::인스턴스메서드이름

  • 첫 번째 매개변수가 해당 클래스의 인스턴스가 되는 경우
  • 형태: (obj, args) -> obj.instanceMethod(args) → ClassName::instanceMethod
  • 예시:
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);

BiFunction<String, String, Boolean> f2 = String::equals; // 메서드 참조

여기서 s1이 String 타입이므로, String 클래스 이름으로 참조 가능

3. 참조변수::인스턴스메서드이름

  • 이미 생성된 객체의 메서드를 참조하는 경우
  • 형태: (args) -> ref.instanceMethod(args) → ref::instanceMethod
  • 예시:
MyClass obj = new MyClass();

// 람다식
Function<String, Boolean> f = (x) -> obj.equals(x);

// 메서드 참조
Function<String, Boolean> f2 = obj::equals;

메서드 참조 결론

하나의 메서드만 호출하는 람다식은 다음 3가지 방식으로 단순화할 수 있다.

  • 클래스 이름::정적 메서드 이름 → 정적 메서드 참조
  • 클래스 이름::인스턴스 메서드 이름 → 첫 번째 매개변수가 해당 클래스의 인스턴스일 때
  • 참조 변수::인스턴스 메서드 이름 → 이미 생성된 객체의 인스턴스를 사용할 때

생성자의 메서드 참조

생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass::new; // 메서드 참조

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다. 필요하다면 함수형 인터페이스를 새로 정의해야 한다.

Funtion<Integer, MyClass> f = (i) -> new MyClass(i); // 람다식
Funtion<Integer, MyClass> f2 = MyClass::new // 메서드 참조

Funtion<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); // 람다식
Funtion<Integer, String, MyClass> bf2 = MyClass::new; // 메서드 참조
profile
BE 개발자

0개의 댓글