본문 바로가기

함수형 자바

[JAVA] 자바 람다 (Lambda) 와 캡처 (Capture) - Chapter 02

함수형 자바 프로그래밍 챕터 2를 공부하였다.

2장에서는 람다 표현식에 대해 다뤘다.

그 중에서 람다에서 외부 변수를 사용 할 때의 특징을 좀 더 알아보았다.

 

람다 (Lambda)

람다 표현식은 0개 이상의 매개변수를 갖고 값을 반환 할 수 있다.

 

먼저, 람다의 문법부터 살펴보면,

(<parameters>) -> {<body>} 형태로 이루어져 있다.

 

1)

여기서 매개 변수 (parameter) 들은 쉼표로 구분하며,

메서드 인수와 다르게 타입을 추론 할 수 있는 경우에는 타입을 생략 할 수 있다.

다만, 묵시적으로 타입 지정된 매개변수와 명시적으로 타입이 지정된 매개변수를 혼용하여 기재 할 수는 없다.

 

2)

매개변수와 람다의 바디는 화살표 (->) 로 이어져 있다.

 

3)

한 줄로 표현된 경우에는 중괄호 생략이 가능하며, return없이 암시적으로 반환이 가능하다.

그 외에는 중괄호로 감싸 표현하며, 값을 반환할 경우 return을 명시적으로 사용해야 한다.

 

아래는 같은 의미의 여러 람다 표현 방법들이다.

Predicate<String> isNull = (String input) -> {
            return input == null;
};
        
isNull = input -> {
	return input == null;
};
        
isNull = (String input) -> input == null;
        
isNull = input -> input == null;

 

 

함수형 인터페이스

람다는 함수형 인터페이스의 하위 타입으로 표현되어야 한다.

 

함수형 인터페이스는 SAM (Single Abstract Method)를 만족 해야 한다.

이는 한 개의 추상 메서드를 가진 인터페이스를 뜻한다. (기본 메서드나 정적 메서드는 여러개 존재 해도 괜찮다.)

 

자바에서는 @FunctionalInterface 마커 어노테이션이 있어, SAM의 원칙을 따라야 할 것을 알려준다.

즉, 해당 어노테이션이 있다면 람다로 표현 가능하다. (물론 해당 어노테이션이 없는 인터페이스라도 SAM을 만족한다면 람다로 표현이 가능하다)

 

람다와 캡처

람다는 캡처 (Capture)를 통해 람다 외부에서 정의된 변수를 람다식 내부에서 저장하고 사용 할 수 있다.

 

여기서 다양한 변수 (지역 변수, 스태틱 변수, 인스턴스 변수)를 람다식 내부에서 활용하는 코드를 구현 해 보았고,

변수의 특징에 따라 달라지는 제약 조건을 봐 보았다.

//파라미터가 아닌 람다 외부에서 정의된 변수를 람다식 내부에 저장하고 사용
public class Capture {

    static int staticAnswer = 42;
    private int instanceAnswer = 42;
    static void capture() {
        int theAnswer = 42;
        Runnable printAnswer = () -> System.out.println("the answer is " + theAnswer);
        printAnswer.run();
    }

    //캡처되는 지역 변수 (스태틱 영역)는 final이거나 effective final이어야 한다. (compile error)
    //지역 변수가 할당 해제 된 후에도 람다에서 사용 가능
//    public static void capture2() {
//        int theAnswer = 42;
//        Runnable changedAnswer = () -> {
//            theAnswer = 43;
//            System.out.println(theAnswer);
//        };
//        changeAnswer.run();
//    }

    //static 필드나 인스턴스 필드는 final 제약 x (힙 혹은 메서드 영역)
    static void capture3() {
        Runnable changedAnswer = () -> {
            staticAnswer = 43;
            System.out.println("the changed answer is " + staticAnswer);
        };
        changedAnswer.run();
    }

    void capture4() {
        Runnable changedAnswer = () -> {
            this.instanceAnswer = 43;
            System.out.println("the changed answer is " + this.instanceAnswer);
        };
        changedAnswer.run();
    }

    public static void main(String[] args) {
        capture();
        capture3();
    }
}

 

1)

capture method에서 람다 표현식 printAnswer는 지역 변수 theAnswer를 참조하였다.

여기서의 theAnswer는 final이거나, effective final (초기화 된 이후 값이 한번도 변경되지 않은 상태) 여야 한다.

 

caputer2 method에서 초기화 된 지역 변수의 값을 변경 하여  더이상 theAnswer가 effective final이 아니게 되자,

컴파일 에러가 발생 하였다.

 

왜 람다에서 참조하는 지역 변수가 불변성을 가져야 되는 지를 생각해보면,

지역 변수는 Stack영역에 올라가기 때문에, 람다에서 외부에 존재하는 지역 변수가 스레드에서의 할당이 해제되었을 때 문제가 있을 수 있다.

그래서 자바에서는 람다에서 그 변수를 참조 할 때에는 그 변수 자체가 아닌 복제본에 접근하도록 하기 때문에,

복제본의 값이 변경되는 것을 막아야 할 것이다.

 

2)

capture3 method에서의 람다 표현식인 changedAnswer는 스태틱 변수 staticAnswer를 참조하였고,

caputer4 method에서는 인스턴스 변수인 instanceAnswer를 참조하였다.

(이 때 외부의 인스턴스 변수를 참조 할 수 있는 것은 람다와 익명 내부 클래스간의 차이점이라 볼 수 있다.

내부 클래스는 자체 스코프 (scope)를 새로 생성하지만, 람다는 자신이 속한 스코프 범위 내에 존재 한다.

따라서 람다 내의 this는 람다가 생성된 인스턴스를 참조 하지만, 내부 익명 클래스의 this는 내부 클래스의 인스턴스 자체를 참조한다.)

 

스태틱 변수나 인스턴스 변수들은 Stack영역이 아닌 Heap영역에 올라가는 변수들이기 때문에, 람다에서 복제본에 접근하는 것이 아니라 직접 접근이 가능하기 때문에, 값 변경에 제한을 두지 않아도 될 것이다.

위의 코드에서도 람다 내에서 변수들의 값을 변경하여 사용하여도 컴파일 에러가 발생하지 않았다.

 

다만, 이 때에는 외부 상태에 영향을 주지 않는 순수한 함수의 개념에 반하고, side effect를 일으킬 수 있기 때문에

사용 시에 주의가 필요 할 듯 하다.

 

람다의 메서드 참조

람다의 메소드 참조 방식으로는 크게 4종류가 있다.

 

1) 정적 메서드  참조 ClassName::staticMethodName

public class Integer extends Number {
	public static String toHexString(int i) {
    	//...
    }
}

//lambda
Function<Integer, String> lambda = i -> Integer.toHexString(i);

//method reference
//ClassName::staticMethodName
Function<Integer, String> reference = Integer::toHexString;

 

스태틱 메서드가 선언된 클래스 이름과, 스태틱 메서드 이름으로 참조한다.

 

2) 인스턴스 메서드  참조 (바운드 비정적 메서드 참조) objectName::instanceMethodName

LocalDate now = LocalDate.now();

//lambda
Predicate<LocalDate> lambda = (localDate) -> now.isAfter(localDate);

//method reference
//obj::method
Predicate<LocalDate> reference = now::isAfter;

 

선언한 인스턴스 변수와, 인스턴스 내의 메서드 이름으로 참조를 한다.

 

3) 매개변수의 메서드 참조 (언바운드 비정적 메서드 참조) ClassName::instanceMethodName

//lambda
Function<String, String> lambda = (String str) -> str.toLowerCase();

//method reference
//ClassName::instanceMethodName
Function<String, String> reference = String::toLowerCase;

 

람다에 매개변수로 들어가는 객체의 클래스 이름과 메서드 이름으로 참조를 한다.

 

4) 생성자 참조 ClassName::new

//lambda
Function<String, Locale> lambda = language -> new Locale(language);

//method reference
//ClassName::new
Function<String, Locale> reference = Locale::new

 

생성자도 일종의 메서드이지만, 고유 메서드 명이 없어 new 라는 키워드를 통해 참조한다.

 

 

<Reference>

https://bugoverdose.github.io/development/lambda-capturing-and-free-variable/

 

[Java] 람다: 람다 캡처링

람다 캡처링이란 파라미터로 넘겨받은 데이터가 아닌 람다식 외부에서 선언된 변수를 참조하는 변수를 람다식 내부에 저장하고 사용하는 동작을 의미한다.

bugoverdose.github.io

https://catsbi.oopy.io/9b757e48-a756-4469-973e-a06d0f34e7a4

 

람다 캡처링(Capturing Lambda)

개요

catsbi.oopy.io

https://inpa.tistory.com/entry/JAVA8-%E2%98%95-%EB%9E%8C%EB%8B%A4%EC%8B%9D%EC%9D%84-%EB%8D%94-%EC%A7%A7%EA%B2%8C-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%B0%B8%EC%A1%B0Method-Reference

 

☕ 람다식을 더 짧게 - 메소드::참조 문법

람다식 메소드::참조 자바의 람다표현식을 통해 코드 정의를 혁신적으로 줄여주었지만 이보다 더 간략하게 줄이는 문법이 있다. 메소드 참조(Method Reference)는 말 그대로 실행하려는 메소드를 참

inpa.tistory.com