본문 바로가기
Backend/Java

[Java] Lambda Expression(람다 표현식)

by 제이동 개발자 2023. 7. 22.
728x90

Lambda Expression(람다 표현식)

 Lambda Expression(람다 표현식)은 JDK 1.8부터 추가된 것으로 메서드를 하나의 식(expression)으로 표현하는 것을 말합니다. 람다식을 사용함으로써 코드가 간결해지고 또한 가독성이 증가되는 여러 이점 등이 있으며 메서드를 람다식으로 표현하면 메서드의 이름과 반환 값이 없어지므로 람다식을 익명함수(Anonymous Function)라고도 합니다.

 

 

1. Lambda Expression 장점

1-1. 불필요한 코드를 줄일 수 있다.

 일회성 메서드가 필요할 경우 람다 표현식을 사용하게 되면 익명 클래스와 같이 불필요한 코드를 사용하지 않아도 됩니다. 불필요한 코드를 사용하지 않으므로서 코드가 간결해지고 가독성이 증가되는 장점이 있습니다.

//람다식을 적용하기 전 코드
Comparator<String> s = new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
}

//람다식을 적용
Comparator<String> s = (String s1, String s2) -> s1.compareTo(s2));

 

1-2. 가독성을 높인다.

 람다 표현식에 익숙해지게 되면 한눈에 코드가 들어오기 때문에 가독성이 높아지게 됩니다. 위 1-1에서의 예제 코드를 보면 단 한 줄만 보고 어떤 함수인지 람다 표현식을 익히면 알 수 있게 됩니다. 실제로 실무에서 Stream과 람다 표현식을 사용하게 되면 보다 직관적이고 간결한 코드를 보며 어떤 처리가 이루어지는지 한눈에 파악할 수 있는 장점이 있습니다.

double avgAvg = list.stream()                               // 오리지날 스트림
                .filter(m -> m.getGender() == Member.MALE)  // 중간 처리 스트림
                .mapToInt(Member::getAge)                   // 중간 처리 스트림
                .average()                                  // 최종 처리 스트림 => 집계처리
                .getAsDouble();

 

1-3. 람다 표현식을 변수처럼 사용이 가능하다.

 람다 표현식은 메서드의 인수로 전달하거나 반환할 수 있으며 또한 변수로 저장할 수 있습니다. 위 1-2 예제 코드를 보면 'filter()', 'mapToInt()' 메서드의 인수로 람다 표현식을 전달하는 것을 볼 수 있습니다.

 

1-4. Lazy Initialization(지연 연산)이 가능하다.

 Lazy Initialization은 프로그램에서 특정 동작을 필요한 시점까지 연기하는 것을 의미합니다. 이를 통해 필요한 계산이나 처리를 최대한 늦추고, 필요한 시점에서만 실행함으로써 자원 효율성을 높이고 성능을 개선할 수 있습니다. 특히 대량의 데이터나 복잡한 계산 작업을 다룰 때 유용합니다.

IntStream.rangeClosed(1, 10)
         .filter(n -> {
             System.out.println("Filtering: " + n);
             return n % 2 == 0;
         })
         .limit(3)
         .forEach(System.out::println);
         
// 실행 결과
Filtering: 1
Filtering: 2
2
Filtering: 3
Filtering: 4
4
Filtering: 5
Filtering: 6
6

 

 위 코드를 보면 'limit(3)' 메서드를 사용하면 'filter()'의 결과를 3개로 제한함을 의미합니다. 때문에 필터링 작업의 결과가 3개가 나온 6까지 실행이 되고 7 이후는 실행되지 않는 것을 볼 수 있습니다.

 

1-5. 병렬 처리가 가능하다.

 람다 표현식을 사용하게 되면 'parallel()' 메서드를 통해 쉽게 병렬 처리가 가능합니다.

double average = list.stream()
        .parallel() // 병렬 처리
        .mapToInt(s -> s.getAge())
        .average()
        .getAsDouble();

 

 

2. 람다 표현식 사용법

// 메서드
반환타입 메서드이름(매개변수 선언){
	문장들
}

int max(int a, int b){
	return a> b ? a: b;
}

--------------------------------

// 람다식
(매개변수 선언) -> {
	문장들
}

(int a, int b) -> { return a > b ? a: b; }

 

2-1. return 문 대신 식(Expression)을 사용할 수 있다.

 반환 값이 있는 메서드의 경우, return문 대신 식(Expression)을 사용할 수 있습니다. 식의 연산 결과가 자동적으로 반환 값이 되며 '문장(Statement)'이 아닌 '식(Expression)'이므로 끝에 세미콜론(;)을 붙이지 않습니다. 단, 여러 개의 문장일 경우에는 return문 대신 식을 사용할 수 없습니다.

(int a, int b) -> { return a > b ? a : b; } // return문
(int a, int b) -> a > b ? a : b             // 식(expression)

 

2-2. 매개변수의 타입은 생략이 가능하다.

 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우에 생략이 가능합니다. 대부분의 경우 생략이 가능하며 람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문입니다. 단, 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않습니다.

(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b   // Ok. 매개변수 생략

(int a, b) -> a > b ? a : b // Error. 매개변수의 타입 생략 시 모두 생략해야 합니다.

 

2-3. 매개변수가 하나일 경우 괄호(())를 생략할 수 있다.

 선언된 매개변수가 하나뿐인 경우에는 괄호(())를 생략할 수 있습니다. 단, 매개변수의 타입이 있을 경우 괄호는 생략하면 안 됩니다.

a -> a * a       // Ok.
int a -> a * a   // error.
(int a) -> a * a // Ok.

 

2-4. 문장이 하나일 경우 괄호({})를 생략할 수 있다.

 괄호({}) 안의 문장이 하나일 때는 역시 괄호({})를 생략할 수 있습니다. 이때 문장의 끝에 세미콜론(;)을 붙이지 않아야 합니다.

(String name, int i) -> {
    System.out.println(name + " = " + i);
}

(String name, int i) -> System.out.println(name + " = " + i)

 

2-5. 괄호({}) 안의 문장이 return 문 일 경우 괄호({})를 생략할 수 없다.

(int a, int b) -> { return a > b ? a : b; } // Ok.
(int a, int b) ->   return a > b ? a : b    // Error. return문에 괄호 생략
(int a, int b) ->   a > b ? a : b           // Ok.

 

 

3. 람다 표현식과 함수형 인터페이스

 함수형 인터페이스란 오직 하나의 추상 메서드만 가지는 인터페이스를 말합니다. 람다식을 처음 접하면 메서드와 동등한 것으로 생각하게 되지만 사실 람다식은 익명 클래스의 객체와 동등합니다.

 

Function Interface Method Name Parameter Return
java.lang.Runnable void run() - void
Supplier<T> T get() - T
Consumer<T> void accept(T t) T void
Function<T, R> R apply(T t) T R
Predicate<T> boolean test(T t) T boolean

 

 

3-1. 함수형 인터페이스

@FunctionalInterface
interface MyFunction{
	public abstract int max(int a, int b);
}
@FunctionalInterface
함수형 인터페이스가 올바르게 정의되었는지 컴파일 단계에서 알려주는 어노테이션입니다.

 

3-2. 익명 클래스 객체

// 익명 클래스 객체
MyFunction f = new MyFunction(){
	public int max(int a, int b){
		return a > b ? a: b;
	}
}

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

 

3-3. 익명 클래스를 람다 표현식으로 표현

// 람다 표현식을 이용한 익명 클래스 객체 생성
MyFunction f = (int a, int b) -> a > b ? a : b;
int big = f.max(5, 3);


// 다양한 익명 객체의 메서드 사용 방법
MyFunction add = (int a, int b) -> {return a + b; };
MyFunction add1 = (int a, int b) -> a + b;
MyFunction add2 = Integer::sum;

 

 이처럼 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 가능한 이유는 람다식도 실제로는 익명 객체이고 MyFunction 인터페이스를 구현한 익명 객체의 메서드 'max(a, b)'가 람다식의 매개변수의 타입, 개수 그리고 반환 값이 일치하기 때문에 가능합니다. 하나의 메서드가 선언된 인터페이스를 정의하여 람다식을 사용하면 기존의 자바 규칙을 어기지 않으면서 자연스럽게 다룰 수 있습니다. 이와 같이 인터페이스를 통해 람다식을 다룰 수 있으며 람다식을 다루기 위한 인터페이스를 함수형 인터페이스(Functional Interface)라고 부릅니다.

 

 

4. 람다 표현식의 메서드 참조(Method Reference)

 람다식은 하나의 메서드만 호출해서 사용하는 경우에 메서드 참조(Method Reference)를 이용해 간략하게 표현할 수 있습니다.

 

4-1. Static Method Reference(정적 메서드 참조)

 정적 메서드를 참조하여 정적 메서드의 기능을 가진 람다식을 생성합니다.

클래스명::정적메서드명
(매개변수) -> Class.staticMethod(매개변수)

str -> String.valueOf(str)
String::valueOf

 

4-2. Instance Method Reference(인스턴스 메서드 참조)

 인스턴스 메서드를 참조하여 인스턴스 메서드의 기능을 가진 람다식을 생성합니다. 

객체명::인스턴스메서드이름
(obj, 매개변수) -> obj.instanceMethod(매개변수)

(value) -> value.length();
String::length

 

4-3. Constructor Method Reference(생성자 메서드 참조)

 생성자 메서드를 참조하여 생성자 메서드의 기능을 가진 람다식을 생성합니다. 

클래스명::new

// 매개변수가 없는 생성자
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;

// 매개변수가 있는 생성자
Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f2 = MyClass::new;

 

5. 람다 표현식에서 활용하는 함수형 인터페이스

 람다 표현식을 사용할 때 자주 사용되는 인터페이스는 Comparator, Predicate, Function 등이 있습니다. 각 인터페이스는 다양한 기능들을 제공하므로 해당 인터페이스를 살펴보는 것을 추천합니다.

 

5-1. Comparator

 Comparator는 두 개의 객체를 비교하는 데 사용되는 함수형 인터페이스로 주로 정렬이나 정렬 기준을 지정할 때 많이 사용됩니다. 

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    ...
}

 

예시

List<Apple> inventory = ...;

inventory.sort(Comparator.comparing(Apple::getWeight)
    .reversed()  // 무게를 내림차순으로 정렬
    .thenComparing(Apple::getCountry);   // 두 사과의 무게가 같으면 국가별로 정렬

 

5-2. Predicate

 Predicate는 주어진 조건을 만족하는지 건사하는 함수형 인터페이스로 매개변수를 받아 논리적인 조건을 검사하여 'true' 또는 'false'를 반환합니다. 주로 컬렉션의 요소를 검사하거나 조건에 맞는 요소를 필터링할 때 사용됩니다.

public interface Predicate<T> {

    boolean test(T t);

    ...
}

 

예시

// 빨간 사과를 만족하면 true를 반환하는 Predicate
Predicate<Apple> redApple = apple -> Color.RED.equals(apple.getColor());

// Predicate에서 제공하는 논리 부정
// (t) -> !test(t)
Predicate<Apple> notRedApple = redApple.negate();

// Predicate에서 제공하는 논리 And
// 빨간 사과이고, 무게가 150 이상이면 true를 반환하는 Predicate
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);

// 사용 예시
Predicate<Apple> redAndHeavyAppleOrGreen = 
        redApple.and(apple -> apple.getWeight() > 150)
                .or(apple -> Color.GREEN.equals(apple.getColor()));

 

5-3. Function

 Function은 하나의 값을 받아서 다른 값으로 매핑하는 함수형 인터페이스입니다. 입력과 출력이 다를 수 있으며 주로 데이터를 변환할 때 사용됩니다.

public interface Function<T, R> {

    R apply(T t);

    ...
}

 

예시

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);  // g(f(x)) or (g ο f)(x)
int result = h.apply(1);  // result = 4

Function<Integer, Integer> f2 = x -> x + 1;
Function<Integer, Integer> g2 = x -> x * 2;
Function<Integer, Integer> h2 = f2.compose(g2);  // f(g(x)) or (f ο g)(x)
int result2 = h2.apply(1);  // result = 3

 

 

6. 람다 캡처링(Captureing Lambda)

 람다의 바디에서는 바디 외부에 있는 변수를 참조할 수 있습니다. 람다 바디 외부에서 정의된 변수자유변수(Free Variable)이라 하며 람다 바디에서 자유 변수를 참조하는 행위람다 캡처링(Copturing Lambda)이라고 부릅니다.

public class CapturingLambda {
    private int a = 10; // 람다에서 참조하는 자유 변수인 인스턴스 변수

    public void freeVariableCallByLambda() {
        int b = 100;    // 람다에서 참조하는 자유 변수인 지역 변수

        final Runnable r = () -> System.out.println(a);  // 람다 캡처링(인스턴스 변수 a를 참조)
        final Runnable r2 = () -> System.out.println(b); // 람다 캡처링(지역 변수 b를 참조)
    }
}

 

6-1. 람다 캡처링의 제약 조건

 람다 캡처링, 즉 자유 변수를 참조할 때는 제약 조건이 있으며 이를 어기게 되면 다음과 같은 에러가 발생합니다

 

java: local variables referenced from a lambda expression must be final or effectively final

 

  해석을 하면 람다식에서 참조하는 로컬 변수들은 반드시 final(final 변수) 이거나 effectively final(초기화된 후 변경되지 않는 값)이어야 한다는 뜻입니다. 이와 같은 제약 조건이 생기게 된 이유가 무엇일까요?

 

 JVM에서는 인스턴스 변수는 Heap에 생성하고, 지역 변수는 각 쓰레드마다 별도의 Stack에서 생성됩니다. 때문에 인스턴스 변수는 모든 쓰레드들이 Heap에 접근하여 참조할 수 있지만 지역 변수는 쓰레드마다 별도의 Stack에 생성되기 때문에 쓰레드들이 공유할 수 없는 상황이 발생할 수 있습니다.(예 - 쓰레드 실행 종료)

 

 람다는 별도의 쓰레드 실행이 가능합니다. 그렇기 때문에 람다가 실행 중에 다른 쓰레드의 지역 변수를 참조(람다 캡처링)하게 된다면 참조 중인 지역 변수가 있는 쓰레드가 실행 종료가 되는 상황이 발생할 수 있습니다. 이때 람다에서 종료된 쓰레드의 지역 변수를 참조하고 있으면 어떻게 될까요? 일반적으로 오류가 날 것이라고 예상할 수 있겠지만 오류가 발생하지 않습니다. 그 이유는 람다에서 참조하고자 하는 다른 쓰레드의 지역 변수에 직접적으로 접근하는 것이 아니라 자신의 쓰레드 Stack에 복사하여 사용하기 때문에 람다는 참조 중인 지역 변수가 있는 쓰레드가 종료되어도 자신의 쓰레드 Stack에 있는 복사된 변수를 참조하여 오류 없이 사용할 수 있게 됩니다. 이와 같은 이유 때문에 복사 값이 변경되지 않아야 한다는 제약 조건이 생기게 되었습니다.

 

예시

public class CapturingLambda {
    private int a = 10;

    public void test() {
        final int b = 100;
        int c = 20;
        int d = 200;

        final Runnable r = () -> {
            a = 123;
            System.out.println(a); // (1) Ok. 제약조건 성립
        };

        final Runnable r2 = () -> System.out.println(b); // (2) Ok. 제약조건 성립 
        final Runnable r3 = () -> System.out.println(c); // (3) Ok. 제약조건 성립
        final Runnable r4 = () -> System.out.println(d); // (4) Error. 오류
        d = 300;
    }
}

- (1) : 인스턴스 변수 'a'는 Heap에 저장이 되어 참조가 가능하기 때문에 제약조건에 성립

- (2) : 지역 변수 'b'는 final로 선언된 변수로 제약 조건 성립

- (3) : 지역 변수 'c'는 final로 선언된 변수가 아니지만 값의 재할당이 일어나지 않았으므로 제약 조건 성립

- (4) : 지역 변수 'd'는 값의 재할당('d=300;')이 이루어졌기 때문에 Eroor 발생

 

728x90

'Backend > Java' 카테고리의 다른 글

[Java] 자바 8 - (2) Stream : 데이터 처리 연산  (0) 2023.07.23
[Java] 자바 8 - (1) Stream : 람다 표현식의 등장 배경  (0) 2023.07.23
[Java] ThreadLocal  (1) 2023.07.16
[Java] Garbage Collector  (0) 2023.07.16
[Java] JVM 이란?  (0) 2023.07.16