본문 바로가기

프로그래밍언어/JAVA

[JAVA] 람다식 (lambda expression)

반응형

자바의 람다식을 사용하던 중 "Local variable number defined in an enclosing scope must be final or effectively final" 라는 에러 발생했다. 이 에러는 람다식에서 지역변수에 접근할때 발생하였는데, 찾아보니 메모리에서 람다식의 변수를 관리하는 방식과 관련된 문제였. 해당 에러 알아보면서 이번 기회에 람다식에 대해서도 한번 정리해보았다.

1. 람다식 (lambda expression)

람다식은 자바 1.8 버전부터 제공된 기능으로 함수를 하나의 식으로 표현한 것이다. 람다식은 메서드의 이름과 반환값이 필요없는 익명함수로 괄호와 화살표를 사용하여 식을 표현한다.

 

(매개변수, ...) -> {  실행문  }

 

람다식을 사용하게 되면 보다 간결하고 편하게 함수를 만들 있다. 자바에서는 모든 함수는 클래스에 포함된 메서드이다. 그렇기 때문에 함수를 만들기 위해서는 클래스도 새로 만들어야 하고 객체도 생성해야 하는데, 람다식을 사용하면 그런 과정이 필요없어진다.

 

또한 람다식은 익명함수인데, 익명함수는 함수를 변수나 매개변수로 사용할 있는 1급객체이다. 그렇기 때문에 람다식 또한 변수처럼 사용할 있다.

 

※ 1급객체
1급객체란 다른 객체들에 일반적으로 적용가능한 연산들을 모두 지원하는 객체를 가리키는 말로 변수에 대입하거나 객체 또는 함수의 매개변수 인자로 전달하기, 함수의 반환값으로 사용하기, 객체의 값 수정 등등의 연산을 지원한다.

 

void prints(int a, int b) {
    System.out.println("a: " + a + ", b: " + b);
}

(a, b) -> {
      System.out.println("a: " + a + ", b: " + b);
};

 

위의 예제는 두개의 매개변수를 받아서 화면에 출력하는 함수를 일반 함수와 람다식으로 각각 표현한 것이다. 람다식에서는 반환타입과 매개변수의 타입이 생략되어 있는데, 이들은 컴파일러에서 추론이 가능하기 때문에 생략된 것이다.

 

람다식의 특징으로는 람다식 내에서 사용되는 지역변수가 final 붙지 않아도 상수로 간주된다는 것이다. 이때문에 람다식 내에서 참조되는 변수들의 값이 다른 곳에서 변경되는 것은 허용되지 않는다. 또한 람다식으로 선언된 변수명은 다른 변수명과 중복될 없다. ( 내용은 처음에 나왔던 에러와 관련된 내용이다. 3 항목에서 관련하여 설명한다.)

 

람다식은 함수를 간결하게 만들 있고 가독성이 뛰어나지만 람다로 만든 익명함수의 재사용이 불가능하고 버깅이 어려운 단점이 있다.

2. 함수형 인터페이스 (Functional Interface)

바에서 모든 메서드는 클래스에 포함되어야 한다. 하지만 람다식은 특정 클래스의 메서드로 선언되지 않는 것처럼 보이는데, 사실 람다식은 메서드가 아닌 익명 클래스의 객체이다. 정확히 말하면 람다식으로 표현된 하나의 함수를 가지는 객체가 된다. 이때 해당 객체의 타입을 정의하기 위해서 만든 것이 자바의 함수형 인터페이스이다.

 

함수형 인터페이스는 객체지향 언어인 자바에 함수형 프로그래밍을 적용하기 위해 만든 인터페이스로 인터페이스에 @FunctionalInterface 어노테이션을 붙여서 정의할 있다. 해당 함수가 붙은 인터페이스를 통해 함수를 1 객체롤 다룰 있게 되는데, 말은 함수를 변수로 사용할 있다는 것이다. 인터페이스가 적용된 인터페이스는 하나의 추상 메서드만을 가지도록 제한한다. 이를 통해서 람다식과 인터페이스의 메서드가 1:1 연결될 있도록 한다. 반면에 static 메서드와 default 메서드의 개수에는 제약이 없다.

 

@FunctionalInterface
interface MyFunction<A, B> {
  void apply(A a, B b);
}


MyFunction<Integer, Integer> func = (a, b) -> {
  System.out.println("a: " + a + ", b: " + b);
};
func.apply(num1, num2);

 

위의 코드는 @FunctionalInterface 함수형 인터페이스인 MyFunction 이라는 인터페이스를 정의하고 이전에 만들었던 람다식을 MyFunction 타입의 변수 func 할당한 것이다. 이렇게 func 라는 변수에 람다식을 할당하면 MyFunction 인터페이스에 선언된 추상 메서드 apply() 사용하여 람다식을 사용할 있게된다.

 

이렇게 람다식을 객체로 사용할 중요한 점은 람다식으로 생성된 함수는 함수형 인터페이스로만 선언이 가능하다는 것이다. 다른 자바의 객체들과 달리 Object 타입으로 선언하거나 형변환 없다. 만약 Object 타입으로 형변환 하려면 함수형 인터페이스로 변환한 다음에 Object 타입으로 변환해야 한다.

 

자바에서는 다양한 함수형 인터페이스를 기본으로 제공하고 있는데, 이와 관련해서는 추후에 다른 글로 다루겠다.

3. 람다의 지역변수 사용

다시 글의 처음으로 돌아가서 람다식에서 지역변수에 접근할 에러가 발생하는 경우가 있다. 에러 로그를 그래도 번역하면 "지역변수 'number' final 이거나 effectively final 이어야 한다." 라는 의미이다. 한마디로 람다식이 외부의 지역변수에 접근할 때 상수처럼 취급되는 지역변수에만 접근할 있다는 것이다. 이는 앞서 람다의 특징을 설명할 때에도 있는데, 그럼 람다는 일반 상수처럼 취급되는 경우에만 지역변수를 사용할 있을까?

 

※ effectively final
errectively final 은 final 이 아닌 지역변수 중에서 초기화 이후에 한번도 값이 변하지 않는 변수를 의미한다. 값이 변경되지 않으므로 컴파일러에서는 사실상 final, 상수로 처리한다.

- 람다식에서의 외부 지역변수

람다식에서 외부 지역변수를 사용할 때 지역변수를 그대로 사용하지 못하고 값을 복사해서 사용한다. 그 이유는 변수가 메모리에 저장되는 구조에서 파악할 수 있다.

 

지역변수는 메모리의 스택 영역에 저장된다. 그렇기 때문에 지역변수가 선언된 블럭에서 탈출하면 해당 지역변수 또한 스택에서 제거된다. 만약 람다식이 해당 블럭밖에서도 사용된다면 이미 메모리에서 제거된 변수에 접근할 수 없게된다.

 

또한 지역변수가 존재하는 쓰레드와 람다식이 실행되는 쓰레드가 다른 경우도 존재할 수 있다. 이 경우에도 마찬가지로 서로 다른 스택은 공유되지 않기 때문에 람다식에서 지역변수에 접근할 수 없게된다.

 

이러한 이유들 때문에 람다식에서는 외부 지역변수를 직접 참조하지 않고 값을 복사해서 사용한다.

- final, effectively final 사용 이유

그렇다면 왜 람다에서 참조하는 외부 지역변수는 final 또는 effectively final 이어야 할까? 만약 변수의 값이 변할 수 있다면 어떤 문제가 발생할 수 있을까?

 

우선 지역변수가 위치하는 쓰레드와 람다식이 실행되는 쓰레드가 다른 경우에 지역변수의 값이 최신의 값인지 확신할 수 없다. 지역변수의 값이 람다식의 쓰레드로 복사될 때 최신의 값으로 전달이 됐는지, 또는 전달된 이후에 지역변수의 값이 변경되면서 최신의 값이 아니게 되는지 쓰레드의 동작 순서에 따라서 값이 보장되지 않는다. 이 때문에 변경이 가능한 외부 지역변수를 참조하는 람다식은 그 결과를 우리가 의도한대로 실행될지 예측할 수 없게 된다.

 

마찬가지로 람다식에서 외부 지역변수의 값을 변경해주는 것도 막아야 한다. 람다식은 컴파일 된 후에 static 메서드 형태로 저장된다. 이때 복사된 값이 매개변수로 전달되어 스택 영역에 위치하게 된다. 이 때문에 람다식에서 외부 지역변수를 변경해주어도 해당 쓰레드로 동기화해줄 수 없기 때문에 람다식 내부에서도 외부 지역변수의 값이 상수로 여겨져야 한다.

[Reference]

- https://mangkyu.tistory.com/113

 

[Java] 람다식(Lambda Expression)과 함수형 인터페이스(Functional Interface) - (2/5)

1. 람다식(Lambda Expression) 이란? Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어있다. 그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다. 그렇기 때문

mangkyu.tistory.com

- https://dev-jwblog.tistory.com/153

 

[Java] Lambda(람다) 사용 시 지역변수 사용하기(With. effectively final)

Java 에서 Lambda 를 사용하면서 개발하던 도중 아래 에러 메시지를 보여주면서 빨간 줄이 그어져 있었다. Variable is accessed from within inner class needs to be final or effectively final 대충 직역해보면 "내부클

dev-jwblog.tistory.com

- https://vagabond95.me/posts/lambda-with-final/

 

[Java] lambda 와 effectively final - 기록은 기억을 지배한다

자바 8에서 추가된 람다식에는 다음과 같은 규칙이 존재한다. 람다식은 외부 block 에 있는 변수에 접근할 수 있다. 외부에 있는 변수가 지역 변수 일 경우 final 혹은 effectively final 인 경우에만 접

vagabond95.me

 

반응형

'프로그래밍언어 > JAVA' 카테고리의 다른 글

[Java] Record  (1) 2024.06.15
[JAVA] Virtual Thread  (0) 2024.05.03
[JAVA] 자바 Thread-safe  (0) 2022.08.21
[JAVA] 메서드 참조 (::)  (0) 2022.04.14
[JAVA] 쓰레드의 동기화  (0) 2022.03.15