[Java 기초] 람다식 - Lambda Expression

Updated:

람다식의 도입으로 인해 자바는 객체지향 언어이면서 함수형 언어가 되었다.

이번 포스트에서는 자바의 람다식에 대해서 알아보도록 하겠다. 👨‍🏫

📋 목차

  • 람다식 사용법
  • 함수형 인터페이스
  • 메서드, 생성자 래퍼런스
  • Variable Capture

람다식 사용법

메서드 : 특정 클래스에 반드시 속해야 한다. 객체의 행위나 동작을 의미.

함수 : 클래스에 종속되지 않고 독립적인 기능을 수행하는 메서드.

람다식 (Lambda expression) : 메서드의 이름과 반환값을 생략하여 하나의 식으로 표현한 것.

  • 익명 함수라고 불린다.
  • 람다식 자체로 메서드의 역할을 수행. $($클래스를 만들고 객체 생성하여 메서드 호출하는 과정 필요없음.)
  • 메서드의 매개변수로 전달되어지는 것이 가능.
  • 메서드의 결과값으로 반환된는 것이 가능.
  • 익명객체이기 때문에 람다식을 다루기 위해서는 참조변수가 필요. $($참조변수의 타입은 함수형 인터페이스로 해야한다.)

람다식 작성방법

Lambda-Expression

반환값은 으로 대신할 수 있으며 끝에 ; 를 붙이지 않는다.

연산결과가 자동적으로 반환값이 된다.

메서드 람다식
int roll() { return (int) (Math.random() * 6); } () -> {return (int) (Math.random() * 6);}

() -> (int) (Math.random() * 6)

그렇다면 람다식을 다룰 참조변수의 타입은 어떻게 해야할까? 🧐

함수형 인터페이스

함수형 인터페이스 : 단 하나의 추상 메서드만 선언된 인터페이스. 람다식을 다루기 위해 사용.

@FunctionalInterface // 함수형 인터페이스를 올바르게 정의하였는지 검사
interface MyFunction {
    public abstract int max(int a, int b); // 추상 메서드
}
// 익명 클래스
MyFunction f = new MyFunction() { // 클래스의 선언, 객체 생성 동시 진행
    public int max(int a, int b) {
        return a > b ? a : b;
    }
}

int value = f.max(3, 5);

함수형 인터페이스 타입의 참조변수 $($MyFunction f)로 람다식을 참조할 수 있다.

단, 함수형 인터페이스의 메서드와 람다식의 매개변수 개수와 반환 타입이 일치해야 한다.

MyFunction f = (a, b) -> a > b ? a : b
int value = f.max(3, 5); // 실제로는 람다식이 호출된다.

즉, 함수형 인터페이스를 선언하고, 함수형 인터페이스 타입의 참조변수를 사용해서 람다식을 다루게 된다.

class Example {
    public static void main(String[] args) {
//        Object obj = (a, b) -> a > b ? a : b; // 람다식. 익명객체
//        MyFunction f = new MyFunction() {
//            public int max(int a, int b) { // 오버라이딩 - 접근제어자는 좁게 못바꾼다.
//                return a > b ? a : b;
//            }
//        };

        // 람다식(익명 객체)을 다루기 위한 참조변수의 타입은 함수형 인터페이스로 한다.
        MyFunction f = (a, b) -> a > b ? a : b;

        int value = f.max(3, 5);
        System.out.println("value = " + value);
    }
}

@FunctionalInterface // 함수형 인터페이스는 단 하나의 추상 메서드만 가져야 함.
interface MyFunction {
    // public abstract int max(int a, int b);
    int max(int a, int b); // 람다식은 이름을 생략하였기 때문에 호출시 부를 이름이 존재하지 않는다. 때문에 함수형 인터페이스에서 이름을 정의해 준다.
}

메서드를 변수처럼 주고받는것 또한 가능하다.

함수형 인터페이스를 이용해서 메서드의 매개변수로 람다식을 받을수 있다

//  void aMethod(MyFunction f) {
//          f.myMethod(); // 매개변수를 받아서 MyFuction에 정의된 메서드를 호출
//  }
//
//  MyFuction f = () -> System.out.println("myMethod()");
//  aMethod(f);

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

함수형 인터페이스 타입의 반환타입 지정이 가능하다

// MyFuction myMethod() {
//     MyFuction f = () -> { };
//     return f;
// }
        
MyFuction myMethod() { return () -> { }; }

예제

@FunctionalInterface
interface MyFunction {
    void run(); // public abstract void run();
}

class Example {
    static void execute(MyFunction f) { // 매개변수의 타입이 MyFunction인 메서드
        f.run();
    }

    static MyFunction getMyFunction() { // 반환타입이 함수형 인터페이스 MyFunction인 메서드
//        MyFunction f = () -> System.out.println("f3.run()");
//        return f;
        return () -> System.out.println("f3.run()");
    }

    public static void main(String[] args) {
        // 람다식으로 MyFunction의 run()을 구현
        MyFunction f1 = () -> System.out.println("f1.run()");

        // 함수형 인터페이스를 직접 구현한 경우
        MyFunction f2 = new MyFunction() { // 익명클래스로 run()을 구현
            public void run() { //  오버라이딩 - 범위는 더 좁게 될 수 없다.
                System.out.println("f2.run()");
            }
        };

        MyFunction f3 = getMyFunction();

        f1.run();
        f2.run();
        f3.run();

        execute(f1);
        execute( () -> System.out.println("run()"));
    }
}

java.util.fuction

java.util.fuctionPackage

출처 - 자바의 정석-기초편 유튜브 강의

Supplier<T> - 공급자

Supplier<Integer> f = () -> (int) (Math.random() * 100) + 1;

Supplier


Consumer<T> - 소비자

Consumer<Integer> f = i -> System.out.print(i + ", ");

Consumer


Function<T, R> - 함수

Function<Integer, Integer> f = i -> i / 10 * 10;

Function


Predicate<T> - 조건식

Predicate<Integer> f = i -> i % 2 == 0;

Predicate


메서드 참조 $($method reference)

하나의 메서드만 호출하는 람다식은 메서드 참조로 간단히 할 수 있다.

클래스이름::메서드이름

종류 람다 메서드 참조
static 메서드 참조 (x) -> ClassName.method(x) ClassName::method
인스턴스메서드 참조 (obj.x) -> obj.method(x) ClassName::method

static 메서드 참조 예제

Lambda-Expression 출처 - 자바의 정석-기초편 유튜브 강의

다음과 같이 람다식을 메서드 참조로 만들수 있다.

Function<입력, 출력>에서 입력이 String 타입인 것을 알 수 있기 때문에

f = (String s) -> Integer.parseInt(s)

에서 String s를 생략 할 수 있다.

import java.util.function.Function;

class Example {
    public static void main(String[] args) {
//        Function<String, Integer> f = (String s) -> Integer.parseInt(s);
//        Function<String, Integer> f = 클래스이름::메서드이름;
        Function<String, Integer> f = Integer::parseInt; // 메서드 참조
        System.out.println(f.apply("100") + 200);
    }
}

생성자 참조

클래스이름::new

// Function<Integer, MyClass> s = (i) -> new MyClass(i);
Function<Integer, MyClass> s = MyClass::new;

배열과 메서드 참조

// Integer 길이에 해당하는 int배열을 생성하는 함수식

Function<Integer, int[]> f = x -> new int[x]; // 람다식

Function<Integer, int[]> f = int[x]::new; // 메서드 참조

예제

import java.util.function.Function;
import java.util.function.Supplier;

class Example {
    public static void main(String[] args) {
//         Supplier는 입력X,
////        Supplier<MyClass> s = () -> new MyClass();
//        Supplier<MyClass> s = MyClass::new;

//        Function<Integer, MyClass> f = (i) -> new MyClass(i);
        Function<Integer, MyClass> f = MyClass::new;

        // System.out.println(s.get());

        MyClass mc = f.apply(100);
        System.out.println(mc.iv);
        System.out.println(f.apply(200).iv);

//        Function<Integer, int[]> f2 = (i) -> new int[i];
        Function<Integer, int[]> f2 = int[]::new;
        int[] arr = f2.apply(100);

        System.out.println("arr.length=" + arr.length);
    }
}

class MyClass {
    int iv;

    MyClass(int iv) {
        this.iv = iv;
    }
}

출력

100
200
arr.length=100

변수캡쳐 - Variable Capture

자유 변수(Free Variable) : 람다식의 매개변수가 아닌 외부에 정의된 변수.

변수 캡처링 (Variable Capture) : 람다 바디에서 자유변수를 참조하는 행위.

변수 캡쳐링의 제약조건

  • 람다식과 같은 Scope의 지역변수는 final로 선언되야 한다.

  • 만약 지역변수가 같은 Scope에서 final로 선언되지 않았다면 final처럼 동작해야한다. (Effective final)

  • 람다는 람다를 감싸고 있는 메서드와 같은 Scope이다.

    • 람다식의 몸통에 있는 변수와 람다식이 사용되고 있는 클래스내의 변수들은 같은 Scope이다.

Lambda-scope

출처 - https://www.notion.so/758e363f9fb04872a604999f8af6a1ae

쉐도윙 : 가려지는것, 덮어지는것

위의 이미지에서 익명클래스, 로컬클래스는 run 메서드내에 새로운 Scope을 만들어서 쉐도윙한다.

반면 람다식의 경우 run 메서드와 같은 Scope에 속하며 쉐도윙하지 않는다.

public class VariableCapturing {
    private int a = 12;

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

        final Runnable r = () -> {
            // 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
            a = 123;
            System.out.println(a);
        };

        // 지역변수 b는 final로 선언돼있기 때문에 OK
        final Runnable r2 = () -> System.out.println(b);

        // 지역변수 c는 final로 선언돼있지 않지만 final을 선언한 것과 같이 변수에 값을 재할당하지 않았으므로 OK
        final Runnable r3 = () -> System.out.println(c);

        // 지역변수 d는 final로 선언돼있지도 않고, 값의 재할당이 일어났으므로 final처럼 동작하지 않기 때문에 X
        d = 12; // 지역변수 d의 값을 재할당.
        final Runnable r4 = () -> System.out.println(d); // 에러!!
    }
}

변수캡쳐 제약조건 등장 배경

지역변수에는 변수캡쳐 제약조건이 붙는 반면 인스턴스 변수에는 조건이 붙지 않는다. 그 이유는 무엇일까?

  • 쓰레드는 각각 고유의 스택을 갖는다.

JVM에서 지역변수는 스택에 저장되고 스택은 쓰레드끼리 공유가 불가능하다.

반면 인스턴스 변수는 힙 영역에 저장되고 쓰레드는 힙 영역을 공유한다.

  • 람다는 별도의 쓰레드에서 실행이 가능하다.

람다식이 참조하던 지역변수가 있는 쓰레드가 사라졌는데도 불구하고 람다가 실행 중인 쓰레드는 살아있을 수 있다.

이러한 경우 사라진 지역변수를 참조하기 때문에 에러가 발생할 것이라 생각하지만 그렇지 않다. 또한 람다가 실행되는 쓰레드는 고유의 스택영역을 가질테고 다른 쓰레드의 스택에 존재하는 지역변수를 어떻게 참조할 수 있을까?

  • 다른 쓰레드의 스택 영역에 있는 지역변수를 람다가 수행되는 쓰레드의 스택에 복사하여 저장

이러한 이유 때문에 다른 쓰레드의 스택영역에 있는 지역변수를 람다가 수행되는 쓰레드에서 참조하는것이 가능하고 지역변수가 선언되어있는 쓰레드가 사라져도 계속 수행할 수 있는것이다.

이렇게 변수를 복사해서 사용하기 때문에 변수의 값이 변경된다면 복사본의 신뢰성이 깨지게 된다.

따라서 지역 변수는 final 혹은 Effective final로 선언되어야 한다는 제약 조건이 붙게된 것이다.

참조
Java의 정석 $($남궁 성)
whiteship 자바 라이브 스터디
오늘도 끄적끄적 - 람다 캡처링과 final 제약조건
SSON Dev Study Blog

Categories:

Updated:

Leave a comment