programming language/Java

[Java] 람다 표현식(Lambda Expreesion) - 2탄

공대키메라 2025. 5. 1. 18:26

지난 람다 표현식 1탄에 이어서

 

이번에는 뒤의 글을 더 읽어보도록 하겠다.

 


2. 람다 표현식의 이상적인 사용 케이스(Ideal Use Case For Lambda Expressions) 8부터~

접근 8: 더 확장해서 Generic을 사용하기

이전 코드에서 보았던 processCarsWithFunction 을 다시 보자.

 

다음은 어떤 데이터 타입의 요소라도 포함하는 컬렉션을 매개변수로 받는 제네릭 버전이다.

 

public static <X, Y> void processCarsWithFunction(
        Iterable<X> source,
        Predicate<X> tester,
        Function<X, Y> mapper,
        Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

 

이를 선언했으니 필자는 다음과 같이 코드를 작성했다.

 

CarTest.processCarsWithFunction(
    cars,
        c -> c.getSpeed() > 50,
        c -> c.getSpeed(),
        speed -> System.out.println("speed = " + speed)
);

 

조건값을 tester에 넘겨주고, 이를 만족하면 실행할 작업으로 speed를 받아오고, 이를 어떻게 소비할건지 정의했다.

 

좀 더 자세히 정리하면 코드들은 다음과 같은 과정을 수행한다.

 

1. source로부터 객체를 얻는다.

2. Predicate의 tester를 만족하는 객체를 걸러낸다.

3. 각 필터링된 객체를 mapper라는 Function 객체에 의해 지정된 값으로 매핑한다.

4. block이라는 Consumer 객체로 각각 매핑된 객체의 행위를 수행한다.

 

이 작업들을 따로 할 수 있지만 엄청 깔끔하게 코드가 작성된 것을 확인할 수 있다.

 

접근 9: 파라미터로 람다 표현식을 받는 집계 작업 사용하기

뒤의 예시는 speed 를 출력하는 예시이다.

 

// Approach 9: Use Bulk Data Operations That Accept Lambda Expressions
// as Parameters

cars.stream()
        .filter(c -> c.getSpeed() > 50)
        .map(p -> p.getSpeed())
        .forEach(speed -> System.out.println(speed));

 

파라미터로  람다 표현식을 stream에 넘겼다.

 

 

processCarsWithFunction 의 작업 집계 작업
객체의 소스를 가져옴 Stream<E> stream()
Precate 객체를 만족하는 객체들을 걸른다. Stream<T> filter(Predicate<? super T> predicate)
Function 객체에 정의된 다른 값들을 객체로 매핑한다. <R> Stream<R> map(Function<? super T,? extends R> mapper)
Consumer 객체로 정의된 행위를 수행한다. void forEach(Consumer<? super T> action)

 

차암 쉽죠잉?

 

집계 작업(Aggregate Operations)에 대해 관심이 있으면 공식문서에서는 다음을 보라고 

 

안내하고 있다. (또 볼게 있네잉...?)

 

스트림(Stream)은 데이터 요소들의 연속된 흐름이나 시퀀스를 나타낸다.

 

스트림은 데이터 소스에서 요소들을 하나씩 꺼내어 처리하는 파이프라인 같은 개념인데,

 

이에 대해 궁금하면 필자가 읽은 공식문서를 통해 같이 보면 좋다.

 

 

3. 람다 표현식의 문법(Syntax of Lambda Expressions)

람다 표현식은 다음으로 구성된다.

 

  • 괄호 안에 쉼표로 구분된 형식 매개변수들의 목록
  • 화살 토큰, ->
  • 바디(body), 하나의 표현 혹은 상태 블록으로 구성되어 있다. 

 

우리가 위에서 봤듯이, 람다 표현식은 쉼표로 구분된 일련의 목록들이 있음을 확인했다.

 

그리고 화살을 분기점으로 왼쪽은 parameter, 오른쪽은 시행할 작업을 정의한다.

 

Body는 하나의 표현인 경우 블록 ({}) 이 없으며 더 많으 작업을 할 경우에는 블록을 사용할 수 있다.

 

다음과 같이 말이다.

 

// 예시
p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}

 

return statement(반환문)은 표현식이 아니다. 람다 표현식에서, 중괄호({}) 로 꼭 감싸야 한다. 하지만 void 메소드 호출은 중괄호로 감싸지 않아도 된다. 다음과 같이 말이다.

email -> System.out.println(email)

 

 

4. 둘러싸는 범위의 로컬 변수에 접근하기( Accessing Local Variables of the Enclosing Scope )

익명함수처럼, 람다 표현식은 variables를 capture 할 수 있다.

 

여기서 전에 capture variables라는 표현이 나왔는데,

 

When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter.

=> 로컬 클래스가 둘러싸는 블록의 로컬 변수나 매개변수에 접근할 때, 그 변수나 매개변수를 캡처한다.

 

로컬 클래스는 메소드나 블록 내부에 정의된 클래스로 로컬 클래스, 또는 람다 표현식이 자신을 둘러싸고 있는 메소드나 브롥의 지역 변수가 매개변수를 사용하는 것을 capture variables 라고 표현한다. 뭔가 캐치해서 사용하는 느낌... 

 

그리고 이를 위해서는 외부 변수는 final 혹은 effectively final해야 한다고 했다.

 

 

또한, 람다 표현식은 shadowing 문제가 없다고 한다.

 

람다 표현식의 선언은 오직 닫힌 환경에 있는 것과 똑같은 방식으로만 해석된다.

 

즉, 람다 표현식 내에서 사용되는 변수나 메소드는 람다가 정의된 그 위치의 스코프 규칙을 그대로 따르며, 람다만의 독립된 스코프를 만들지 않는다는 것이다.

 

 

익명 함수에서는 동일한 이름의 변수를 정의할 있지만, 여기서는 필자가 z를 선언해서 하려고 하니 이미 선언이 되어있다고 한다.

 

즉, shadowing이 일어나지 않는 것이다! (와 나 지금 x나 카리스마 있었어 like 임창정~)

 

5. Target Typing

메소드가 기대하는 데이터 타입을 *대상 타입(target type)*이라고 한다.

 

메소드가 기대하는 타입?

 

다음 예시를 보자

 

CarTest.showCar(cars, c -> c.getSpeed() > 100);

 

필자가 이전에 사용했던 예시인데, 

 

자세히 들여다 보면 다음과 같다.

 


자바런타임이 showCar 메소드를 호출할 때, CheckCar 데이터 타입을 이렇게 보듯이 기대를 하고 있다.

 

 

그러나 이 Java 런타임이 showCarWithPredicate 메소드를 호출할 때는 Predicate<Car> 를 기대하므로 람다 표현식은 이 타입이 된다.

 

이렇게 메소드가 기대하는 데이터 타입을 대상 타입(Target Typing)이라고 하는데, 

 

람다 표현식의 타입을 결정하기 위해 Java 컴파일러는 람다표현식이 사용된 컨텍스트나 상황의 대상 타입을 사용한다. 

 

6. 대상 타입과 메소드 인수

Java 컴파일러는 두 가지 언어 기능을 활용해 람다 표현식의 대상 타입을 결정한다.

 

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

 

Runnable은 reutrn 값이 없고, Callable은 return 값이 있다.

 


 

이렇게... Lambda expression 공식문서 세션을 다 읽어보았다.

 

사실 필자가 읽은 부분은 이론적인 부분이 치중되어 있고,

 

실제로 어떻게 사용하는지는 dev.java 사이트를 확인하면 된다.

 

실제 사용법들을 어떻게 설명하는지 보고 싶다면, 해당 사이트를 찾아보길 바란다.

 

 

이 곳에도 공식적으로 어떻게 사용하는지 자세~히 설명하니 사실 다른 책을 읽을 필요가 없다.

 

그동안 나는 뭐를 가지고 공부해온거지...?

 

 

정말 미스터리하다...

 

다음에는 이에 이어서 Stream 지원 API 에 대해서 간단 정리할 예정이다.

 

출처:

https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

https://dev.java/learn/lambdas/