CodeGym /행동 /JAVA 25 SELF /람다 표현식 소개

람다 표현식 소개

JAVA 25 SELF
레벨 21, 레슨 0
사용 가능

1. 소개

간단히 말해: 람다 표현식은 별도의 클래스나 익명 클래스를 선언하지 않고도 “즉석에서” 함수형 인터페이스의 구현을 빠르게 만드는 방법입니다. 이름이 없는 미니 메서드처럼 인수로 전달하거나 변수에 저장할 수 있습니다.

오랫동안 Java는 “전통적인” OOP 언어였습니다. 버튼을 눌렀을 때 무엇을 할지 같은 동작을 전달하려면 익명 클래스를 작성해야 했습니다:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick() {
        System.out.println("버튼이 눌렸습니다!");
    }
});

Java 8부터 람다의 마법이 등장했습니다:

button.setOnClickListener(() -> System.out.println("버튼이 눌렸습니다!"));

여기서 () -> System.out.println("버튼이 눌렸습니다!")가 바로 람다 표현식입니다.

형식적으로 말하면: 람다 표현식은 함수형 인터페이스의 단 하나의 추상 메서드 구현을 간결하게 표현한 형태입니다.

왜 중요할까요?

  • 동작을 값처럼 전달할 수 있습니다(예: 리스트의 각 요소를 어떻게 처리할지).
  • 간결하고 읽기 쉬운 코드를 작성할 수 있습니다.
  • 현대적인 Java API를 사용할 수 있습니다: Stream API, 이벤트 처리, 비동기 작업 등.

2. 람다 표현식 문법

일반 형태

(매개변수) -> 식
// 또는
(매개변수) -> { 코드 블록 }

여러 인터페이스에 대한 예

1. 매개변수 없음 (예: Runnable):

Runnable r = () -> System.out.println("안녕, 람다!");
r.run();

2. 매개변수 하나 (예: Consumer):

Consumer<String> printer = s -> System.out.println("전달된 값: " + s);
printer.accept("Java");

매개변수가 하나이고 그 타입을 추론할 수 있다면 괄호를 생략할 수 있습니다.
String은 우리가 전달하는 변수(s)의 타입입니다.

3. 여러 매개변수 (예: Comparator):

Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5

Integer는 우리가 전달하는 변수(a, b)의 타입입니다.

4. 여러 줄 본문(명시적 반환이 있으면 중괄호와 return이 필요):

Function<Integer, Integer> square = x -> {
    int result = x * x;
    return result;
};
System.out.println(square.apply(6)); // 36

첫 번째 Integer는 함수의 반환 타입이고, 두 번째 Integer는 매개변수 타입입니다.

축약과 간결함

  • 본문이 단일 표현식이라면 중괄호와 return을 생략할 수 있습니다.
  • 매개변수가 없으면 빈 괄호를 씁니다: () -> ...
  • 매개변수가 하나면 괄호를 생략할 수 있습니다: x -> ...
  • 둘 이상이면 괄호가 필요합니다: (a, b) -> ...

요약 표

같은 아이디어를 람다와 익명 클래스로 각각 어떻게 표현하는지 살펴봅시다:

인터페이스 람다 표현식(예) 익명 클래스와 동등한 코드
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

사용자 정의 인터페이스 예

다음과 같은 함수형 인터페이스가 있다고 합시다:

@FunctionalInterface
interface Operation {
    int apply(int a, int b);
}

예전에는 이렇게 구현했습니다:

Operation sum = new Operation() {
    @Override
    public void apply(int a, int b) {
        return a + b;
    }
};

이제는 더 모던하게:

Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8

표준 인터페이스 예

Runnable:

Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();

Comparator:

List<String> list = Arrays.asList("사과", "바나나", "키위");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);

Function:

Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123

3. 스코프와 변수 캡처

외부 컨텍스트의 변수 (effectively final)

람다 표현식은 둘러싼 메서드의 변수를 사용할 수 있습니다. 하지만 규칙이 있습니다: 그런 변수는 effectively final이어야 합니다 — 즉 final로 명시적으로 선언했거나, 초기화 이후 변경되지 않아야 합니다.

예:

String prefix = "결과: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// 여기서 prefix는 “고정”됩니다 — 이후에는 변경할 수 없습니다.
System.out.println(f.apply(5)); // 결과: 10

람다에서 사용된 후 prefix를 변경하려고 하면 컴파일 오류가 발생합니다.

왜 그럴까요?
람다 표현식은 변수가 선언된 메서드가 종료된 뒤에 호출될 수도 있습니다. “마법 같은” 버그를 피하기 위해 Java는 변경되지 않는 변수만 사용하도록 제한합니다.

익명 클래스와의 차이

익명 클래스에서도 동일한 규칙이 적용됩니다: 외부 메서드의 변수는 final/effectively final이어야 합니다. 하지만 스코프에는 차이가 있습니다. 익명 클래스 내부에서 this는 익명 클래스 자신을 가리키고, 람다에서는 외부 클래스를 가리킵니다.

public class Demo {
    public void test() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(this); // 출력: Demo$1 (익명 클래스)
            }
        };

        Runnable r2 = () -> System.out.println(this); // 출력: Demo (외부 클래스)

        r1.run();
        r2.run();
    }
}

람다와 클래스 필드

람다 표현식은 외부 클래스의 필드에 제한 없이 접근할 수 있습니다:

public class Counter {
    private int base = 10;

    public void printSum(int x) {
        Function<Integer, Integer> sum = y -> base + y + x;
        System.out.println(sum.apply(5));
    }
}

여기서 base는 클래스 필드이므로 변경할 수 있습니다.
x는 effectively final이어야 합니다.

4. 실습: 몇 가지 람다 표현식을 작성해 봅시다

예: 숫자 리스트 필터링

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);

nums.stream()
    .filter(n -> n % 2 == 0)
    .forEach(n -> System.out.println("짝수: " + n));

Stream API에 대해서는 30단계에서 더 자세히 배우게 됩니다 :P

예: 문자열 변환 함수

Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA

예: 사용자 정의 함수형 인터페이스

@FunctionalInterface
interface StringTransformer {
    String transform(String s);
}

StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("안녕")); // 안녕!

예: 외부 컨텍스트의 변수 사용

int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor는 이 이후에 변경할 수 없습니다!

5. 람다 표현식 사용 시 흔한 실수

오류 1: 람다가 캡처한 변수를 변경하려는 경우.
이 코드는 컴파일되지 않습니다 — 변수는 effectively final이어야 합니다:

int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // 오류: sum은 final이 아닙니다!

값을 누적해야 한다면 배열이나 래퍼 객체를 사용하세요.

오류 2: this의 스코프 혼동.
람다 표현식 내부에서 this는 람다가 아니라 외부 클래스를 가리킵니다(익명 클래스와 대조적).

오류 3: 여러 줄 람다에서 중괄호와 return을 빠뜨림.
람다 본문이 단일 표현식이 아니라면 중괄호와 return이 필요합니다:

Function<Integer, Integer> square = x -> {
    int y = x * x;
    return y;
};

오류 4: 람다 표현식의 타입을 잘못 지정.
람다 표현식은 항상 함수형 인터페이스를 구현합니다. 다음처럼 쓸 수는 없습니다:

var f = x -> x + 1; // 오류! 어떤 인터페이스 타입인지 알 수 없습니다.

타입을 명시해야 합니다:

Function<Integer, Integer> f = x -> x + 1;
1
과제
JAVA 25 SELF, 레벨 21, 레슨 0
잠금
시스템 음성 안내 📣
시스템 음성 안내 📣
1
과제
JAVA 25 SELF, 레벨 21, 레슨 0
잠금
사용자 입력 표준화 📝
사용자 입력 표준화 📝
1
과제
JAVA 25 SELF, 레벨 21, 레슨 0
잠금
선수들의 "타격력" 순위 🥊
선수들의 "타격력" 순위 🥊
1
과제
JAVA 25 SELF, 레벨 21, 레슨 0
잠금
동적 보고서 생성 📊
동적 보고서 생성 📊
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION