개발 기록

> [Java] 람다 (Lambda) 개념과 사용 본문

JAVA

> [Java] 람다 (Lambda) 개념과 사용

1z 2022. 11. 11. 09:45

 

 

1. 개념  

람다식은 익명 함수(Anonymous Function)의 한 형태로, 함수형 프로그래밍 스타일을 지원하기 위한 기능이다. 쉽게 말해 메서드를 하나의 식으로 나타낸 것으로 메서드를 간결하고 간편하게 표현할 수 있도록 도와주는 개념이다.

 

▶사용 용도

 함수형 인터페이스 구현체로 사용 

 메서드 레퍼런스로 사용 (ex.System.out::println은 () -> System.out.println())

 스트림 API에서의 활용

스레드와 비동기 작업 (ex. Runnable, Callalbe 인터페이스 구현체)

 GUI 이벤트 핸들러

 컬렉션의 forEach 메서드 (ex.list.forEach(name -> System.out.println(name));)


2. 람다식 기본 문법  

매개변수 -> 실행 코드 형태로 작성되며, 화살표 기호(->)를 사용하여 매개변수 목록과 실행 코드를 구분한다.

(매개변수 목록) -> { 실행 코드 }
  • 매개변수 목록: 람다식이 전달받는 매개변수를 나열. 매개변수가 없는 경우에는 빈 괄호 ()를 사용한다.
  • →: 화살표연산자는 매개변수 목록과 실행 코드를 구분한다.
  • 실행 코드: 중괄호({}) 내에 실행할 코드 블록을 작성한다.

(1) 람다 표현식 규칙 

// 기본 람다식
(int a)->{System.out.println(a); }

// 1. 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않는다.
(a) -> {System.out.println(a); }

// 2. 매개 변수가 하나만 있다면 괄호 '()' 생략 가능
// 3. 하나의 실행문만 있다면 중괄호 '{}' 생략 가능(세미콜론(;)은 붙이지 않음)
a -> System.out.println(a)

// 4. 만약 매개 변수가 없다면 빈 괄호() 를 반드시 사용
() -> System.out.println("a")

// 5. 결과 값을 리턴해야 한다면 return 문 사용
(a, b) -> { return a + b; };

// 6. 중괄호에 return문만 있을 경우 생략 가능
(a, b) -> a + b

 


3. 람다식 사용   

 

람다식의 형태는 매개 변수를 가진 코드 블록이기 때문에 마치 자바의 메소드를 선언하는 것처럼 보여진다. 자바는 메서드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언하는 것을 생각하면, 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해 낸다는 것을 추측할 수 있다. 그리고 실제로 런타임시에 익명 구현 객체를 생성한다.

// 람다식은 인터페이스의 익명 구현 객체를 생성하고 객체화한다.
인터페이스 변수 = 람다식;

 

 

(1) 타겟타입과 함수형 인터페이스  

▶ 타겟 타입 : 람다식이 구현할 함수형 인터페이스 형식

함수적 인터페이스 : 모든 인터페이스를 람다식의 타켓 타입으로 사용할 수는 없다. 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타켓 타입이 될수 있다. 이런 인터페이스를 함수적 인터페이스 라고한다. 

 

※ 참고

@FunctionallInterface: 이 어노테이션을 붙일 경우 두 개이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해준다.

@FunctionallInterface 어노테이션은 선택사항으로, 이 어노테이션이 없더라도 하나의 추상 메소드가 있다면 함수적 인터페이스다.

// 함수형 인터페이스
@FunctionalInterface
interface MyFunctionalInterface {
    int min(int x, int y);
}

public class LambdaExam1 {

	public static void main(String[] args){
        
		// 람다식
        MyFunctionalInterface minNum = (x, y) -> x + y; 

		// 함수형 인터페이스의 사용
        System.out.println(minNum.min(3, 4));  

    }
}

 

 

(2) 메서드의 인자로 전달 

메서드의 인자로 람다식을 전달하여 해당 메서드에서 실행할 동작을 지정할 수 있다. 이때 인터페이스의 구현체로서 사용되는 경우도 있지만, 특정한 인터페이스와 결합되지 않고도 사용할 수 있다.

// 람다식을 메서드의 인자로 전달
void processNumbers(int x, int y, IntBinaryOperator operator) {
    int result = operator.applyAsInt(x, y);
    System.out.println("Result: " + result);
}

// 메서드를 호출할 때 람다식 전달
processNumbers(10, 5, (a, b) -> a * b); // "Result: 50" 출력

 

위의 예제에서 processNumbers 메서드는 두 개의 정수와 IntBinaryOperator 인터페이스의 구현체를 받아서 실행하는 메서드이다. 메서드를 호출할 때 람다식 (a, b) -> a * b를 직접 전달하여 곱셈 연산을 수행하고 결과를 출력한다.

 

 

(3) 변수로 할당

람다식은 함수의 일급 객체로 취급되기 때문에, 변수에 할당하여 나중에 실행하는 식으로 활용할 수 있다. 이때 변수의 타입은 해당 인터페이스의 타입을 명시해야 한다. 

// Runnable은 함수형 인터페이스 (추상 메서드 run() 하나만 가지고 있음)
Runnable runnable = () -> {
    System.out.println("Hello, Lambda!");
};

// 변수에 할당된 람다식 실행
runnable.run();

 

runnable 변수 람다식( () -> { System.out.println("Hello, Lambda!"); } )을 할당하고, 이후에 run() 메서드를 통해 람다식을 실행하여 결과를 얻는다. 

 

 

(4) 스레드 생성

람다식을 이용하여 쓰레드를 생성하거나 다른 비동기적인 동작을 정의할 수 있다.

public class Main {
    public static void main(String[] args) {
        // 람다식을 사용하여 새로운 스레드 생성
        Thread thread = new Thread(() -> {
            // 스레드가 실행할 작업 정의
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread is running: " + i);
                try {
                    Thread.sleep(1000); // 1초간 일시 정지
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 스레드 시작
        thread.start();
}

 

 

(5) 람다식을 리턴값으로 사용

아래 예제에서 incrementByOne 메서드는 IntUnaryOperator 인터페이스의 구현체를 리턴한다. 이 리턴된 람다식을 increment 변수에 할당하고, 이후에 applyAsInt 메서드를 통해 람다식을 실행한다.

// 람다식을 리턴값으로 사용
IntUnaryOperator incrementByOne() {
    return (x) -> x + 1;
}

// 메서드를 호출하여 람다식 실행
IntUnaryOperator increment = incrementByOne();
int result = increment.applyAsInt(5); // result는 6

 

 

(6) 익명 서브 클래스와 람다식

익명 서브 클래스는 클래스를 정의하고 생성자를 호출하여 인스턴스를 초기화할 수 있지만, 람다식은 익명으로 정의된 함수형 인터페이스의 구현체로서 단순히 인터페이스의 메서드를 구현하여 동작을 정의할 뿐이다.

// Runnable 인터페이스를 익명 서브 클래스로 구현하여 객체 생성 및 초기화
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Executing Runnable task");
    }
};

// Runnable 인터페이스의 구현체를 람다식으로 생성
Runnable runnable = () -> {
    System.out.println("Executing Runnable task using lambda expression");
};

 

 


4. 클래스 멤버와 로컬 변수 사용  

 

람다식에서는 클래스 멤버와 인스턴스 멤버를 자유롭게 사용할 수 있지만, 로컬 변수의 경우 final 또는 effectively final로 선언되어야 하며, 값의 변경이 불가능해야 한다. 즉 매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만,  람다식은 캡처한 변수의 복사본을 사용하므로, 해당 변수의 변경은 람다식에 반영되지 않는다.

 

* Java8 이상에서는 명시적으로 final 키워드를 사용하지 않더라도, 로컬 변수가 한 번만 초기화되고 다시 다른 값으로 변경되지 않으면 해당 변수는 "effectively final"로 간주된다.

public class LambdaExample {
    public void executeLambda(int arg) { 
        int count = 0; // 로컬 변수

		// arg = 31; // final 특성 때문에 수정 불가
        // 람다식에서 로컬 변수(count)를 사용
        Runnable runnable = () -> {
            System.out.println("Count: " + count);
            // count++; // 컴파일 에러: 변수 count는 final 또는 effectively final 특성으로 수정 불가
        };

        runnable.run(); // 람다식 실행
    }

    public static void main(String[] args) {
        LambdaExample example = new LambdaExample();
        example.executeLambda(20);
    }
}