본문 바로가기

Dev Book Review/Java8 in Action

[자바8인액션] Chap.3 람다 표현식

소스코드

https://github.com/mjung1798/Jyami-Java-Lab/tree/master/java8-in-action

 

mjung1798/Jyami-Java-Lab

💻 Jyami의 Spring boot 및 Java 실험소 💻. Contribute to mjung1798/Jyami-Java-Lab development by creating an account on GitHub.

github.com

동작 파라미터화 : 변화하는 요구사항에 효과적으로 대응할 수 있는 코드

  • 익명 클래스 : 다양한 동작 구현 가능 -> 코드가 깔끔하지 않다
  • 람다 표현식 : 더 깔끔한 코드로 동작을 구현할 수 있다.

 

1. 람다란 무엇인가

a. 람다의 특징

  • 익명 : 보통 메서드와 다르게 이름이 없다
  • 함수 : 메서드처럼 특정 클래스에 종속되지 않는다. 하지만 메서드처럼 파라미터 리스트 바디, 반환 형식, 가능한 예외 클래스 포함
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.

b. 람다의 구성

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight);
  • 파라미터 리스트 : Comparator의 compare 메서드의 파라미터 (두 개의 사과)
  • 화살표 : 람다의 파라미터 리스트와 바디를 구분
  • 람다의 바디 : 두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식

c. 람다 예제

사용 사례 람다 예제
불린 표현식 (List<String> list) -> list.isEmpty()
객체 생성 ()->new Apple(10)
객체에서 소비 (Apple a)->{System.out.println(a.getWeight)}
객체에서 선택/추출 (String s)-> s.length()
두 값을 조합 (int a, int b)-> a * b
두 객체 비교 (Apple a1, Apple a2) -> a1.getWeight.compareTo(a2.getWeight);

 

2. 람다를 사용하는 곳

함수형 인터페이스 문맥에서 사용

a. 함수형 인터페이스

  • 정확히 하나의 추상메서드를 지정하는 인터페이스
  • 많은 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스

ex ) Comprator, Runnable

람다는 함수형 인터페이스의 추상 메서드 구현을 직접 전달 할 수 있어 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.

@FunationalInterface : 함수형 인터페이스임을 가리키는 어노테이션, 이를 선언하고 실제로 함수형 인터페이스가 아니면 컴파일 에러 발생

b. 함수 디스크립터(function descriptor)

람다 표현식의 시그너처를 서술하는 메서드

() -> void : 파라미터 리스트가 없으며 void 를 반환하는 함수 : Runnable

(Apple, Apple) -> int : 두개의 Apple을 인수로 받아 int를 반환하는 함수

c. 사용법

  • 함수형 인터페이스를 인수로 받는 메서드로 전달
  • 변수에 할당

 

3. 람다 활용 : 실행 어라운드 패턴

실행 어라운드 패턴(execute around pattern) : 자원을 열고 -> 처리한 다음(변동) -> 자원을 닫는다

// 자원 열고 닫는 로직이 중복적으로 앞뒤에 있다.
public static String processFile2(BufferedReaderProcessor p) throws IOException {
  InputStream fileResourceAsStream = FileProcessor.class.getClassLoader().getResourceAsStream("data.txt");
  try(BufferedReader br = new BufferedReader(new InputStreamReader(fileResourceAsStream))){
    return p.process(br);
  }
}

//처리 로직에 대한 동작을 함수형 인터페이스를 이용해 파라미터화 하였다.
@FunctionalInterface
public interface BufferedReaderProcessor {
  String process(BufferedReader br) throws IOException;
}

// 처리 로직에 대한 동작을 선언하여, 실행 어라운드 패턴안에서 해당 동작을 수행한다.
@Test
@DisplayName("실행 어라운드 패턴 개선1")
void name2() throws IOException {
  String s = FileProcessor.processFile2(br -> br.readLine());
  assertThat(s).isEqualTo("line1");
}

 

4. 함수형 인터페이스 사용

함수 디스크립터(function descriptor) : 함수형 인터페이스의 추상 메서드 시그너처

공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합 : Comparable, Runnable, Callable 등 자바에서 포함하고 있다

a. Predicate

Predicate<String> lengthOnePredicate = (String s) -> s.length() == 1;
List<String> filter = PredicateProcess.filter(list, lengthOnePredicate);
  • Input : 제네릭 형식 T 객체
  • Output : Boolean 반환

b. Consumer

Consumer<Integer> printConsumer = i -> System.out.println(i);
ConsumerProcess.forEach(list, printConsumer);
  • Input : 제네릭 형식 T 객체
  • Output : void 반환

c. Function

Function<String, Integer> parseIntegerFunction = s -> Integer.parseInt(s);
List<Integer> map = FunctionProcess.map(list, parseIntegerFunction);
  • Input : 제네릭 형식 T 객체
  • Output : 제네릭 형식 R 객체

d. 기본형 특화

  • 박싱(boxing) : 기본형 -> 참조형 변환
  • 언박싱(unboxing) : 참조형 -> 기본형 변환
  • 오토박싱(autoboxing) : 박싱과 언박싱이 자동으로 이루어짐

기본형 특화 함수형 인터페이스 : 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있게 함

함수형 인터페이스 함수 디스크립터 기본형 특화
Predicate<T> T -> boolean IntPredicate, LongPredicate, DoublePredicate
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction
Supplier<T> () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator<T> T -> T IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator<T> (T, T) -> T IntBinaryOprator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L,R> (L, R) -> boolean  
BiConsumer<T,U> (T, U) -> void ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer
BiFunction<T,U,R> () ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

 

5. 형식 검사, 형식 추론, 제약

a. 형식검사

람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있다.

대상형식 (target type) : 어떤 컨텍스트에서 기대되는 람다 표현식의 형식

filter(inventory, (Apple a)->a.getWeight > 150);

1. 람다가 사용된 컨텍스트확인 : filter의 정의 확인
2. 두 번째 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대한다.
3. Predicate<Apple>의 추상메서드를 확인한다.
4. Apple을 인수로 받아 boolean을 반환하는 test 메서드다 (Apple->boolean)
5. 함수 디스크립터와, 람다의 시그너처가 일치한다! : 형식검사 완료

b. 같은 람다 다른 함수형 인터페이스

대상 형식이라는 특징덕분에 같은 람다표현식이더라도 호환되는 추상메서드를 가진 다른 함수형 인터페이스 사용이 가능하다.

Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;

하나의 람다표현식을 다양한 함수형 인터페이스에 사용할 수 있다.

c. 형식추론

자바 컴파일러

  • 람다 표현식이 사용된 컨텍스트(대상 형식) 를 이용해 람다 표현식과 관련된 함수형 인터페이스 추론
  • 대상 형식을 이용해서 함수 디스크립터를 알 수 있다.
  • 컴파일러는 람다의 시그너처도 추론할 수 있다.

상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있다.

// 형식 추론을 하지 않는다
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());


// 형식 추론을 한다
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

d. 지역변수 사용

  • 자유변수(free variable) : 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수 => 람다캡처링(capturing lambda)
  • 제약 : final로 선언되어있거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.

람다 표현식은 한번만 할당할 수 있는 지역변수를 캡쳐한다.

int portNumber = 1337;
Runnable r = ()->System.out.println(portNumber);
portNumber = 31337;

제약이 있는 이유

  • 인스턴스 변수는 힙에 저장, 지역변수는 스택에 저장된다.
  • 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서 해당 변수에 접근하려 할 수 있다.
  • 원래 변수에 접근을 허용하지 않고 자유 지역 변수의 복사본을 제공한다
  • 복사본의 값이 바뀌지 않아야한다 == 지역 변수에는 한 번만 값을 할당해야 한다.

e. 클로저

클로저 : 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스

설명! 클로저 람다
다른함수의 인수로 전달할 수 있다. O O
자신의 외부 영역의 변수에 접근할 수 있다. O O
람다가 정의된 메서드의 지역변수 값을 바꿀 수 있다. O X

 

6. 메서드 레퍼런스

특정 메서드만을 호출하는 람다의 축약형이다.

명시적으로 메서드명을 참조하여 가독성을 높일 수 있다! 메서드 명 앞에 구분자(::)를 붙이는 방식으로 활용한다.

Apple 클래스에 정의된 getWeight 메서드 레퍼런스

  • Apple::getWeight
  • (Apple a)-> a.getWeight()

a. 메서드 레퍼런스를 만드는 방법

1. 정적 메서드 레퍼런스

Function<String, Integer> stringIntegerFunctionLambda = (String str) -> Integer.parseInt(str);
Function<String, Integer> stringIntegerFunction = Integer::parseInt;

 

2. 다양한 형식의 인스턴스 메서드 레퍼런스

Function<String, Integer> stringIntegerFunctionLambda = (String arg0) -> arg0.length();
Function<String, Integer> stringIntegerFunction = String::length;

3. 기존 객체의 인스턴스 메서드 레퍼런스

람다 표현식에 현존하는 외부 객체의 메서드를 호출 할 때 사용

Apple apple = new Apple("red", 100);
Supplier<Integer> supplierLambda = () -> apple.getWeight();
Supplier<Integer> supplier = apple::getWeight;

메서드 레퍼런스는 컨텍스트의 형식과 일치하는지 확인한다.

b. 생성자 레퍼런스

ClassName::new 처럼 클래스와 new 키워드를 사용해 기존 생성자의 레퍼런스를 만들 수 있다.

@Getter
public class Apple {
    private String color;
    private Integer weight;
    public Apple() {
    }
    public Apple(String color) {
        this.color = color;
    }
    public Apple(Integer weight) {
        this.weight = weight;
    }
    public Apple(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }
}
// 기본 생성자 레퍼런스
Supplier<Apple> appleSupplier = Apple::new;

//인수 1개 생성자 레퍼런스 : 무게
Function<Integer, Apple> appleFunction = Apple::new;

// 인수 1개 생성자 레퍼런스 : 색깔
Function<String, Apple> appleFunction = Apple::new;

// 인수 2개 생성자 레퍼런스
BiFunction<String, Integer, Apple> appleBiFunction = Apple::new;

시그너처를 대응시켜서 생성자에 접근이 가능하다.

 

7. 람다, 메서드 레퍼런스 정리

  • 코드 전달 : 함수형 인터페이스를 구현하여 사용
  • 익명 클래스 사용 : 클래스를 구현하지 않고 바로 인스턴스 화 할 수 있으나 코드가 지저분하다.
  • 람다 표현식 사용 : 추상 메서드의 시그너처(함수 디스크립터)가 람다 표현식의 시그너처를 정의한다 = 형식추론에 이용
  • 메서드 레퍼런스 활용 : 람다 표현식의 인수를 더 깔끔하게 전달할 수 있다.

 

8. 람다 표현식을 조합할 수 있는 메서드

람다 표현식을 조합할 수 있는 유틸리티 메서드 : 디폴트메서드를 사용한다

@FunctionalInterface
public interface Comparator<T> {
  int compare(T o1, T o2); // 추상메서드
  default Comparator<T> reversed() {...}
  default Comparator<T> thenComparing(Comparator<? super T> other) {...}
}

실제로 위와 같이 선언이 되어있다.

단순한 람다 표현식을 조합해서 더 복잡한 람다 표현식을 만들 수 있다.

a. Comparator 조합

inventory.sort(
  Comparator.comparing(Apple::getWeight)
  .reversed() // 역정렬
  .thenComparing(Apple::getColor)); // 두번째 비교자를 만들 수 있다 (두 사과 비교 후 같을 때 정렬 법)

b. Predicate 조합

Predicate<Apple> redApple = a -> "red".equals(a.getWeight());
// 기존 Predicate를 반전
Predicate<Apple> notRedApple = redApple.negate();
// 기존 Predicate에 and를 이용해서 빨강이면서 무거운 사과로 조합
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
// 기존 Predicate에 or를 이용해서 빨강이면서 무거운 사과 또는 그냥 녹색사과로 조합
Predicate<Apple> redAndHeavyAppleOrGreen = redApple
  .and(a -> a.getWeight() > 150)
  .or(a -> "green".equals(a.getColor()));

c. Function 조합

andThen : 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환

compose : 인수로 주어진 함수를 먼저 실행 한 후에 그 결과를 외부 함수의 인수로 제공

@Test
@DisplayName("Function 연결")
void name3() {
  Function<Integer, Integer> f = x -> x + 1;
  Function<Integer, Integer> g = x -> x * 2;
  Function<Integer, Integer> h = f.andThen(g); // g(f(x))
  int result = h.apply(1);
  assertThat(result).isEqualTo(4);
}

@Test
@DisplayName("Function 연결")
void name4() {
  Function<Integer, Integer> f = x -> x + 1;
  Function<Integer, Integer> g = x -> x * 2;
  Function<Integer, Integer> h = f.compose(g); // f(g(x))
  int result = h.apply(1);
  assertThat(result).isEqualTo(3);
}