티스토리 뷰

JAVA

[Java Study 15] 람다식

DevBee 2021. 5. 15. 17:48

람다식 (lambda expression)

람다식이란 메소드를 하나의 식으로 표현하는 것을 말합니다. Java 8부터 사용 가능합니다.

 

// 메소드
int min(int x, int y) {
    return x < y ? x : y;
}

// 람다 표현식
(x, y) -> x < y ? x : y;

 

위 예제처럼 메소드를 람다식으로 표현하면, 클래스를 작성하고 객체를 생성하지 않아도 메소드를 사용할 수 있습니다.

 

자바에서는 클래스 선언과 동시에 객체가 생성되는데, 단 하나의 객체만을 생성할 수 있는 클래스인 익명 클래스를 제공합니다. 따라서 자바의 람다식은 익명 클래스와 같다고 할 수 있습니다.

 

// 람다식
(x, y) -> x < y ? x : y;

// 익명 클래스
new Object {
    int min(int x, int y) {
        return x < y ? x : y;
    }
}

 

이러한 람다식은 메소드의 매개변수로 전달될 수 있으며, 메소드의 결과값으로 반환될 수도 있습니다. 람다 표현식을 사용하면 기존의 불필요한 코드를 줄이고 코드의 가독성을 높일 수 있습니다.

 

람다식 사용법

화살표 기호(->)를 사용하여 람다식을 작성할 수 있습니다.

 

(매개변수 리스트) -> { 함수몸체 }

 

람다 표현식을 작성할 때 유의할 점은 다음과 같습니다.

 

  • 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있습니다.
  • 매개변수가 하나인 경우에 괄호(())를 생략할 수 있습니다.
  • 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호({})를 생략할 수 있습니다. (이때, 세미콜론(;) 생략)
  • 함수의 몸체가 하나의 return 문으로 이루어진 경우 중괄호({})를 생략할 수 없습니다.
  • return 문 대신 표현식을 사용할 수 있으며 이때 반환 값은 표현식의 결과값이 됩니다. (이때, 세미콜론(;) 생략)

예제로 전통적인 방식의 쓰레드 생성과 람다를 사용한 방식을 비교해 보겠습니다.

 

// 전통적인 쓰레드 생성
new Thread(new Runnable(){
    public void run() {
        System.out.println("전통적인 방식의 일회용 쓰레드 생성");
    }
}).start();

// 람다식 쓰레드 생성
new Thread(() -> System.out.println("람다식 일회용 쓰레드 생성")).start();

 

함수형 인터페이스

람다식을 저장하기 위해서는 참조변수 타입을 결정해야 합니다.

 

참조변수타입 참조변수이름 = 람다식

 

위의 문법처럼 람다식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라고 합니다.

 

함수형 인터페이스는 추상 클래스와는 달리 하나의 추상 메소드만을 가집니다. 또한, @FunctionalInterface 라는 애노테이션을 사용해서 함수형 인터페이스임을 명시할 수 있습니다.

 

@FunctionalInterface 애노테이션을 인터페이스 선언 앞에 붙이면 컴파일러는 해당 인터페이스를 함수형 인터페이스로 인식합니다. 자바 컴파일러는 이렇게 명시된 함수형 인터페이스에 두 개 이상의 메소드가 선언되면 오류를 발생시킵니다.

 

예제를 살펴보겠습니다.

 

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

public class LambdaFunctionalInterface {
    public static void main(String[] args) {
        Calc minNum = (x, y) -> x < y ? x : y;  // 추상 메소드 구현
        System.out.println(minNum.min(3, 4));  // 함수형 인터페이스 사용
    }
}

 

자바는 java.util.function 패키지를 통해 여러 상황에서 사용할 수 있는 다양한 함수형 인터페이스를 미리 정의하여 제공합니다.

 

Variable Capture

람다의 바디에서는 파라미터 말고 바디 외부에 있는 변수를 참조할 수 있습니다.

 

public class LambdaCapturing {
    private int a = 12;
    
    public void test() {
        int b = 123;
        
        final Runnable ra = () -> System.out.println(a);
        final Runnable rb = () -> System.out.println(b);
    }
}

 

이렇게 람다 시그니처에 파라미터로 넘겨진 변수가 아닌 외부에 정의된 변수를 자유 변수(Free Variable)이라고 하고 람다 바디에서 자유 변수를 참조하는 행위를 람다 캡처링(Lambda Capturing)이라고 합니다.

 

지역 변수를 람다 캡처링하기 위해서는 다음과 같은 제약 조건이 있습니다.

 

  • 지역 변수는 final 로 선언되어 있어야 합니다.
  • final로 선언되어 있지 않은 지역 변수는 final 처럼 동작해야 합니다.
package study.lambda;

public class LambdaCapturing {
    private int a = 12;

    public void test() {
        final int b = 123;
        int c = 123;
        int d = 123;

        final Runnable r1 = () -> {
            a = 123;  // a는 인스턴스 변수이므로 final로 선언될 필요도 없고 값의 변경이 일어나면 안되는 이유도 없습니다.
            System.out.println(a);
        };

        // 지역 변수 b는 final로 선언되어 있으므로 OK
        final Runnable r2 = () -> System.out.println(b);

        // 지역 변수 c는 final로 선언되어 있지는 않지만 값의 재할당이 일어나지 않았으므로 OK
        final Runnable r3 = () -> System.out.println(c);

        // 지역 변수 d는 final로 선언되지도 않았고 값의 재할당이 일어났기 때문에 람다식 바디에서 사용하면 컴파일 에러!
        // d = 12;
        // final Runnable r4 = () -> System.out.println(d);
    }
}

 

지역 변수의 경우 JVM의 Stack 영역에 생성되며 각각의 쓰레드마다 별도로 생성되기 때문에 공유되지 않습니다. 반면, 인스턴스 변수는 JVM의 Heap 영역에 생성되며 이 영역은 모든 쓰레드에서 공유하여 사용합니다.

 

람다는 별도의 쓰레드에서 실행이 됩니다. 따라서 원래 지역 변수가 있는 쓰레드가 종료되어 해당 지역 변수가 사라졌는데도 람다가 실행 중인 쓰레드는 살아있을 수 있습니다.

 

이때 람다가 참조 중인 지역 변수가 사라졌기 때문에 에러가 날 것 같지만 에러가 발생하지 않습니다. 이유는 람다가 별도의 쓰레드에서 실행될 때 참조하는 지역 변수를 람다를 실행하는 쓰레드의 스택 영역에 복사하여 사용하기 때문입니다. 이를 변수 캡처(Variable Capture)라고 합니다.

 

하지만 이렇게 복사하여 사용하는 변수의 값이 변경되는 경우 어떤 값을 신뢰해야할지 알 수 없기 때문에 위와 같은 제약 조건이 생긴 것입니다.

 

인스턴스 변수의 경우 힙 영역에 생성되기 때문에 모든 쓰레드에서 공유하여 사용하므로 복사할 필요도 없고 힙 영역에 직접 접근하여 작업할 수 있기 때문에 위와 같은 제약조건이 필요하지 않습니다.

 

메소드, 생성자 레퍼런스

메소드 레퍼런스(Method Reference)는 람다식이 단 하나의 메소드만을 호출하는 경우에 해당 람다 표현식의 불필요한 매개변수를 제거하고 사용할 수 있도록 해줍니다.

 

메소드 레퍼런스를 사용하면 불필요한 매개변수를 제거하고 다음과 같이 '::' 기호를 사용하여 표시할 수 있습니다.

 

클래스명::메소드명
또는
참조변수명::메소드명

 

다음 예제는 두 개의 값을 전달받아 제곱 연산을 수행하는 Math 클래스의 클래스 메소드인 pow() 메소드를 호출하는 람다식입니다.

 

(base, exponent) -> Math.pow(base, exponent);

 

위 람다식을 메소드 레퍼런스를 사용하여 다음과 같이 표현할 수 있습니다.

 

Math::pow;

 

또한, 특정 인스턴스의 메소드를 참조할 때도 참조 변수명을 통해 메소드 참조를 사용할 수 있습니다.

 

MyClass obj = new MyClass();
Function<String, Boolean> func = (a) -> obj.equals(a); // 람다 표현식
Funciton<String, Boolean> func = obj::equals(a);       // 메소드 참조

 

생성자를 호출하는 람다 표현식도 위에서 살펴본 메소드 참조를 사용할 수 있습니다. 즉, 단순히 객체를 생성하고 반환하는 람다 표현식은 생성자 참조로 변환할 수 있습니다.

 

(a) -> { return new Object(a); }  // 람다 표현식
Object::new;                      // 생성자 참조

 

이때 생성자가 존재하지 않으면 컴파일 에러가 발생합니다.

 

또한, 배열을 생성할 때도 다음과 같이 생성자 참조를 사용할 수 있습니다.

 

Function<Integer, double[]> func1 = a -> new double[a];  // 람다 표현식
Function<Integer, double[]> func2 = double[]::new;       // 생성자 참조

 

참조

- http://tcpschool.com/java/java_lambda_concept

- https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/

- https://futurecreator.github.io/2018/08/02/java-lambda-variable-scope/

- http://tcpschool.com/java/java_lambda_reference

- https://codechacha.com/ko/java8-method-reference/

'JAVA' 카테고리의 다른 글

[Java Version] Java 8 vs 11vs 17  (0) 2022.02.11
[Java] stream 알아보기  (0) 2022.02.11
[Java Study 14] 제네릭  (1) 2021.05.15
[Java Study 13] I/O  (0) 2021.05.13
[Java Study 12] 애노테이션  (0) 2021.05.13
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함