본문 바로가기

Java

람다 표현식

람다 표현식이란

 

람다 표현식(Lambda Expression)은 Java 8부터 도입된 함수형 프로그래밍 기능 중 하나로 이름이 없는(익명) 함수를 간결하게 표현하는 구문이다.
Java 8 이전에는 단일 메소드를 구현하려 해도 반드시 해당 메소드를 포함하는 클래스를 정의해야 했지만, 람다 표현식을 사용하면 불필요한 boilerplate 코드가 크게 줄어들고 함수 자체를 값처럼 전달하거나 변수에 바로 할당할 수 있게 되었다.

// 입력 받은 두 정수의 합산 결과를 리턴하는 메소드
int add(int x, int y) {
    return x + y;
}

// 람다 표현식을 이용해 아래와 같이 단축 가능 (메소드 반환 타입, 메소드 이름 생략 가능)
(int x, int y) -> {
	return x + y;
};

// 매개변수 타입도 생략이 가능
(x, y) -> {
	return x + y;
};

// 함수에 리턴문 한줄만 있을 경우 더 단축시킬 수 있다. (중괄호, return 생략)
(x, y) -> x + y;

 

* 타입을 생략해도 컴파일러가 에러를 띄우지 않는 이유는 타입 추론 기능이 작동하기 때문이다.

 


람다 표현식과 함수형 인터페이스

 

람다식의 형태를 보면 마치 자바의 메소드를 “값” 처럼 다루는 듯 보이지만, 자바 문법상 메소드는 독립적으로 선언할 수 없다. 타입에 대한 제약 사항이 강한 자바에서 이와 같은 방식이 가능한 이유는 함수형 인터페이스 덕분이다.


함수형 인터페이스란 java 8 부터 등장한 기술로 단 하나의 추상 메소드만을 갖는 인터페이스를 말하며 @FunctionalInterface 어노테이션을 붙여 컴파일 시점에 이를 보장할 수 있다.

이러한 함수형 인터페이스는 함수를 객체처럼 취급하는 함수형 프로그래밍을 지원하기 위해 등장했다.

함수형 프로그래밍과 함수형 인터페이스에 대한 보다 자세한 설명은 아래 링크를 참고하면 된다.

https://jaystorage.tistory.com/55

 

함수형 프로그래밍

프로그래밍 패러다임(Programming Paradigm) 프로그래밍 패러다임이란 코드를 어떻게 구조화 할지 정의하는 방식을 의미한다. 이러한 프로그래밍 패러다임은 그 종류가 매우 다양하나 큰 틀로 나눴을

jaystorage.tistory.com

https://jaystorage.tistory.com/56

 

함수형 인터페이스

함수형 인터페이스란 자바에서 함수형 인터페이스란 추상 메소드가 1개만 정의된 인터페이스를 의미한다. 이러한 함수형 인터페이스는 java 8 부터 등장했는데 람다 표현식을 이용해 함수형 프

jaystorage.tistory.com

 

@FunctionalInterface
public interface Converter<F, T> {
    T convert(F from);
}

// 람다 표현식으로 구현
Converter<String, Integer> converter = s -> Integer.valueOf(s);

 

람다식은 이처럼 인터페이스의 단일 추상 메소드를 구현하는 익명 객체를 생성하는 간단한 방법을 제공한다.

 

* 자바에서 람다식은 함수형 인터페이스에만 바인딩 될 수 있도록 설계되어 있다.

 


람다 표현식의 타입 추론

 

앞서 람다식은 함수형 인터페이스의 추상 메소드를 구현한다고 설명했다. 그런데 리턴 타입도 파라미터도 없는 람다식을 컴파일러가 대체 어떤 타입의 함수인 줄 알고 문법을 허용하는 것일까?

그 비밀은 바로 제네릭 타입 변수 추론에 있다.

 

  1. 제네릭 메소드 호출
    - filter 선언부에 <T>가 있으므로 호출부에서 넘긴 List<String>을 보고 타입 변수 T를 String 으로 추론한다.

  2. 타겟 타입(Target Type) 결정
    - filter의 두 번째 파라미터 타입은 Predicate<? super T>
    - 여기서 T가 String으로 결정되었으므로 Predicate<? super String> 이 되고, 결과적으로 Predicate<String> 처럼 사용된다.

  3. 람다식 바인딩 
    - 이제 컴파일러는 람다식 (name -> name.length() > 3) 을 boolean test(String name) 를 구현하는것으로 간주하여 name 파라미터를 String으로, 반환값을 boolean으로 설정한다.

이처럼 람다식은 “메소드 호출 문맥”에서 제네릭 메소드의 타입 변수 T를 먼저 추론하고, 그 T로 함수형 인터페이스(Predicate<? super T> 등)의 실제 타입을 결정한다. 이후 그 인터페이스의 단일 추상 메소드 시그니처를 보고 파라미터, 반환 타입을 채워 넣어 컴파일한다.

 


람다 표현식 사용법



기본 문법

람다 표현식은 크게 파라미터, 화살표, 본문으로 구성된다.

 

1. 파라미터

  - 0개 : 빈 괄호 사용

() -> System.out.println("No args");


  - 1개 : 괄호 생략 가능

x -> x * x

 

  - 2개 이상 : 반드시 괄호로 묶기

(a, b) -> a + b

 

  - 타입 명시(선택)

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

 

  - var 키워드(Java 11 이상)

// Java 10까지: 타입 생략 시 컴파일러가 문맥(타깃 타입)으로 추론
(x, y) -> x + y

// Java 11+: 매개변수에 var 사용 가능
// x와 y를 컴파일러가 추론한 타입으로 잡되, 문법적으로는 var 키워드를 쓴다는 선언
(var x, var y) -> x + y

// 매개변수에 @NonNull 같은 어노테이션을 붙이려면 타입을 명시해야 하는데 이전 방식에서는 불가능 함
(@NonNull var x, var y) -> x + y

 

 

2. 화살표

  - 파라미터와 본문을 구분하는 기호로 반드시 -> 형태여야 함

 

3. 본문

  1) 단일 표현식(Expression)
    - 중괄호, return 생략

    - 표현식 결과가 자동으로 반환

(a, b) -> a + b

 

   2) 블록(Block)

    - 여러 문장 작성 가능

    -  return 문 필요

(a, b) -> {
    System.out.println("Adding numbers");
    return a + b;
}

 


 

메소드 참조

람다 본문이 단순히 기존 메소드를 호출하는 한 줄 뿐이라면 ClassOrInstance::methodName 형태로 더 축약할 수 있다.

 

1. 정적 메소드 참조

Function<String,Integer> f = Integer::parseInt;

 

2. 특정 인스턴스의 인스턴스 메소드 참조

PrintStream out = System.out;
Consumer<String> c = out::println;

 

3. 임의 객체의 인스턴스 메소드 참조 (unbound instance method)

BiPredicate<String,String> p = String::equalsIgnoreCase;
// (a,b) -> a.equalsIgnoreCase(b) 와 동일

 

4. 생성자 참조

Supplier<List<String>> s = ArrayList::new;  
// () -> new ArrayList<>()

 

5. 배열 생성자 참조

Function<Integer,String[]> arr = String[]::new;
// length -> new String[length]

 


 

예외 처리

람다식에서 Checked Exception을 던지려면 함수형 인터페이스에 throws 선언이 필요하다.

@FunctionalInterface
interface IOConsumer<T> {
    void accept(T t) throws IOException;
}

IOConsumer<Path> reader = p -> Files.readString(p);

 


 

변수 캡쳐 규칙

람다식은 사실상 final(effectively final) 인 로컬 변수만 참조할 수 있다. (여기에서 말하는 사실상 final이란 변수를 final로 선언하지 않아도 한 번만 값이 정해지고 그 이후로 재할당되지 않는 경우를 의미함)

int base = 5;  // 한 번만 값이 할당되고 변경되지 않음 → effectively final

Function<Integer,Integer> f = x -> x + base;  
// 람다 안에서 base를 읽기만 함

 

아래처럼 변수에 재할당이 일어나면 컴파일 오류가 발생한다.

int count = 0;
Function<Integer,Integer> f2 = x -> x + count;  // 여기까지는 OK
count = 1;  // 오류: “local variables referenced from a lambda expression must be final or effectively final”

 

단 람다 안에서 list.add(...) 같은 객체 내부 상태 변경은 가능하다.

List<String> list = new ArrayList<>();

// 람다 안에서 list.add(...)로 리스트 내부 상태를 변경 가능
Consumer<String> addToList = s -> list.add(s);

addToList.accept("Hello");
addToList.accept("World");

System.out.println(list);  // 출력: [Hello, World]

 

클래스의 멤버 변수(인스턴스 필드와 static 필드)는 final 여부에 상관없이 람다식 안에서 자유롭게 읽고 변경할 수 있다.

public class FieldCaptureDemo {
    private int instanceCounter = 0;     // 인스턴스 필드
    private static int staticCounter = 0;// 정적(static) 필드

    public static void main(String[] args) {
        FieldCaptureDemo demo = new FieldCaptureDemo();

        // 인스턴스 필드 변경
        Runnable incInstance = () -> demo.instanceCounter++;
        // static 필드 변경
        Runnable incStatic   = () -> staticCounter++;

        incInstance.run();
        incStatic.run();
        incInstance.run();
        incStatic.run();

        System.out.println("instanceCounter = " + demo.instanceCounter); // 출력: 2
        System.out.println("staticCounter   = " + staticCounter);       // 출력: 2
    }
}

 

람다 표현식의 한계

 

람다식은 boilerplate 코드를 줄이고 가독성을 높이는데 크게 일조하지만 모든 상황에 적합한 것은 아니다.

 

  1. 문서화 불가 
    - 람다 자체가 이름이 없는 함수이기 때문에 메소드와 클래스와 다르게 문서화가 불가능

  2. 디버깅이 어려움
    - 람다 내부는 익명 함수로 처리되어 스택 트레이스에 명확한 클래스명, 메서드명이 나오지 않음

  3. Checked Exception 처리 불편
    - 표준 함수형 인터페이스(Function, Consumer…)의 apply나 accept 시그니처에 throws가 없어 람다 본문에서 Checked Exception을 직접 던질 수 없음

  4. stream api 에서 람다를 사용할 시 for문 보다 성능이 떨어짐

  5. 과도한 중첩 시 가독성 저하
    - 복잡한 로직을 람다 안에 모두 넣을 경우 가독성이 크게 저하됨

출처 : 
https://www.youtube.com/watch?v=4ZtKiSvZNu4&t=152s
https://www.youtube.com/watch?v=nsK4TP_uvaY&t=326s

https://inpa.tistory.com/entry/%E2%98%95-Lambda-Expression

 

'Java' 카테고리의 다른 글

함수형 인터페이스  (3) 2025.06.08
함수형 프로그래밍  (2) 2025.06.06
Java Collection Framework  (0) 2025.05.25
Enum  (0) 2025.05.11
Singleton 패턴  (0) 2024.07.16