[Live Study] 15주차 과제: 람다식
포스트
취소
Preview Image

[Live Study] 15주차 과제: 람다식

이 스터디는 백기선님께서 Github유튜브로 진행하시는 스터디 입니다.

참여하시고 싶으신 분은 아래 링크를 참고해 주세요 :)

대부분의 내용은 [O’REILLY] Java in a Nutshell, 7th Edition 에서 참고 하였습니다.

(최대한 직접 해석하면서 읽고 있으며 모르는 단어는 번역기로 찾았습니다.)

학습 목표

자바의 람다식에 대해 학습하세요.

람다식 사용법

람다식은 자바8에서 도입된 기능으로 자바 플랫폼에 큰 변화를 주었습니다.

  • 더 표현력있는 프로그래밍
  • 더 좋은 라이브러리
  • 간결한 코드
  • 향상된 프로그래밍 안전성
  • 잠재적으로 증가된 데이터 병렬처리

또한 람다에는 기능의 필수 특성을 정의하는데 도움이 되는 주요 특징이 있습니다.

  • 리터럴로 코드를 작성할 수 있음
  • 타입 추론을 사용하여 자바 코드의 엄격한 네이밍 규칙을 완화함
  • 자바 프로그래밍의 더 기능적인 스타일을 용의하게 하기 위함

자바8 이전에는 익명 클래스를 사용하던 것을 람다를 사용하여 더 간결한 코드를 작성할 수 있습니다.

람다가 익명 클래스의 문법 설탕은 아닙니다. 람다는 메소드 핸들과 invokedynamic이라는 특별한 바이트 코드를 사용하여 구현됩니다.

1
2
3
4
5
6
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("run Runnable...");
    }
};

이런 익명클래스를 람다식으로 바꾸면 아래와 같습니다.

1
Runnable runnable = () -> System.out.println("run Runnable...");

함수형 인터페이스

람다식은 특정한 타입에서만 사용이 가능한데 이것을 함수형 인터페이스(Functional Interface) 라고 합니다.

  • 인터페이스(interface)이어야 함
  • default 메소드가 아닌 메소드가 하나만 있어야 함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 함수형 인터페이스
@FunctionalInterface
public interface MyListener {
    void listen(String data);
}

// 함수형 인터페이스를 파라미터로 받는 메소드
public void onAction(MyListener listener) {
    listener.listen("data");
}

// 함수형 인터페이스 람다로 구현
MyListener listener = data -> System.out.println("listening data : " + data);

// 함수 실행
onAction(listener);
1
listening data : data

만약 MyListener가 재사용되지 않는다면 이렇게 작성도 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 함수형 인터페이스
@FunctionalInterface
public interface MyListener {
    void listen(String data);
}

// 함수형 인터페이스를 파라미터로 받는 메소드
public void onAction(MyListener listener) {
    listener.listen("data");
}

// 메소드 파라미터에 람다로 바로 구현
onAction(data -> {
    System.out.println("data : " + data);
    System.out.println("data length : " + data.length());
});
1
2
data : data
data length : 4

Variable Capture

람다식에서 내부의 파라미터를 제외한 외부의 변수를 참조하는 것을 Variable Capture 라고 합니다.

이 때 참조할 수 있는 변수의 제약 조건이 있습니다.

  • final 변수이어야 함
  • final 변수가 아니라면 값이 재할당 되지 않아야 함 (Effectively final)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LambdaApp {
    
    public void run() {
        final String str = "this is local variable"; // final 변수
        int num = 10; // final은 아니지만 정의 이후 재할당 되지 않는 변수

        onAction(data -> {
            System.out.println("str : " + str);
            System.out.println("num : " + num);
        });
    }

    public void onAction(MyListener listener) {
        listener.listen("data");
    }

    public static void main(String[] args) {
        LambdaApp app = new LambdaApp();
        app.run();
    }
}
1
2
str : this is local variable
num : 10

만약 num 변수의 값이 재할당 된다면 컴파일 에러가 발생 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LambdaApp {
    
    public void run() {
        final String str = "this is local variable"; // final 변수
        int num = 10; // final은 아니지만 정의 이후 재할당 되지 않는 변수

        num = 20; // num 재할당

        onAction(data -> {
            System.out.println("str : " + str);
            System.out.println("num : " + num); // 컴파일 에러 발생!
        });
    }

    ...
}

만약 재할당된 값을 람다식에서 사용하고 싶다면 해당 값을 다른 변수로 할당하여 처리할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LambdaApp {
    
    public void run() {
        final String str = "this is local variable"; // final 변수
        int num = 10; // final은 아니지만 정의 이후 재할당 되지 않는 변수

        num += IntStream.range(0, 5).sum();

        int finalNum = num; // 새 변수에 할당

        onAction(data -> {
            System.out.println("str : " + str);
            System.out.println("num : " + finalNum); // 새 변수 사용
        });
    }

    ...
}
1
2
str : this is local variable
num : 20

다만 인스턴스 변수라면 상황이 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LambdaApp {
    private int instanceInt = 100; // 인스턴스 변수 선언

    public void run() {
        final String str = "this is local variable";
        int num = 10;

        onAction(data -> {
            System.out.println("str : " + str);
            System.out.println("num : " + num);
            System.out.println("instanceInt : " + instanceInt); // 람다에서 인스턴스 변수 사용
        });
    }

    public void onAction(MyListener listener) {
        listener.listen("data");
    }

    public static void main(String[] args) {
        LambdaApp app = new LambdaApp();
        app.run();
    }
}
1
2
3
str : this is local variable
num : 10
instanceInt : 100

이렇게 람다에서 인스턴스 변수인 instanceInt를 사용할 수 있고 중간에 instanceInt 값이 재할당된다고 하더라도 람다에서 사용이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LambdaApp {
    private int instanceInt = 100;

    public void run() {
        final String str = "this is local variable";
        int num = 10;

        instanceInt += 200; // 인스턴스 변수 재할당

        onAction(data -> {
            System.out.println("str : " + str);
            System.out.println("num : " + num);
            System.out.println("instanceInt : " + instanceInt); // 컴파일 에러가 나지 않고 정상 실행
        });
    }

    ...
}
1
2
3
str : this is local variable
num : 10
instanceInt : 300

이게 가능한 이유는 JVM의 메모리 구조와 관련이 있습니다.
지역 변수는 스택 영역에 저장이 되며 스택 영역은 스레드마다 별도로 생성이 됩니다.
인스턴스 변수는 힙 영역에 생성이 되며 스레드끼리 공유가 가능합니다.
람다식에서는 스택에 저장되는 지역 변수를 바로 참조하는 것이 아니라 복사된 값을 사용하게 되는데 멀티 스레드 환경에서 문제가 되기 때문에 재할당된 변수를 람다에서 사용할 수 없게 처리하였습니다.

메소드, 생성자 레퍼런스

람다에서 조금 더 간결한 문법을 사용할 수 있는 방법으로써 기존 메소드를 람다식으로 사용하는 방법 입니다.

람다식의 파라미터의 타입을 추론하여 해당 타입의 메소드를 사용하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 함수형 인터페이스
@FunctionalInterface
public interface ButtonClickListener {
    void onClick(String data);
}

public class Button {
    // 함수형 인터페이스를 받는 메소드
    public void click(ButtonClickListener listener) {
        listener.onClick(data);
    }
}

Button button = new Button();

이러한 함수형 인터페이스와 그 것을 받는 메소드가 있다면 다음과 같이 람다식으로 사용할 수 있을 겁니다.

1
2
3
4
5
Button button = new Button();

button.click(data -> data.length()); // data의 메소드 사용
button.click(data -> System.out.println(data)); // data를 파라미터로 사용
button.click(data -> Integer.parseInt(data)); // 스태틱 메소드에서 data를 파라미터로 사용

이 것들을 모두 메소드 레퍼런스로 변경하면 다음과 같습니다.

1
2
3
4
5
6
7
8
Button button = new Button();

// Unbound Method Reference
button.click(String::length); 
// Bound Method Reference
button.click(System.out::println);
// Static Bound Method Reference
button.click(Integer::parseInt); 

생성자 레퍼런스를 사용할 수도 있습니다.

1
2
3
4
5
// 기존 람다식
button.click(data -> new StringBuilder(data));

// 생성자 레퍼런스
button.click(StringBuilder::new);
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[Live Study] 14주차 과제: 제네릭

[fp-ts] 타입스크립트로 함수형 프로그래밍 시작하기: Eq