Application/JAVA & Kotlin

[모던 자바 인 액션 정리] 3장. 람다 표현식

반응형

람다란?


메소드로 전달할 수 있는 익명 함수를 단순화한 것.

람다의 키 포인트

  • 익명

  • 함수
    • 람다는 메소드처럼 특정 클래스에 종속되지 않으므로 함수로가 부른다.
  • 전달
    • 람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성
    • 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
// 기존의 익명 클래스을 이용한 코드
Comparator<Apple> byWeight = new Comparator<Apple>() {
		public int compare(Apple a1, Apple a2) {
				return a1.getWeight().compareTo(a2.getWeight());
		}
}

// 람다를 이용한 코드
Comparator<Apple> byWeight = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

 

람다 표현식


(Apple a1, Apple a2) → a1.getWeight().compareTo(a2.getWeight());
  • 파라미터 리스트
  • 화살표
  • 람다 바디


람다
는 어디에 사용할 수 있을까?


  • 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.


함수형 인터페이스란?


  • 정확히 하나의 추상 메소드를 지정하는 인터페이스. (default method는 추상메소드가 아님)
@FunctionalInterface // 선택
public interface Predicate<T> {
	boolean test (T t);
}

 

@FunctionalInterface


  • 함수형 인터페이스임을 가르키는 어노테이션
  • 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.
  • 예를 들어 추상 메소드가 두 개 이상이면 에러가 발생.


함수 디스크립터


  • 람다 표현식의 시그니처를 서술하는 메소드
  • ex) (Apple, Apple) → int


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


  • 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸인 형태

실행 어라운드 패턴

public String processFile() throws IOException {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
			return br.readLine(); // 실제 작업 (위 그림에서 작업 A, 작업 B)
	}
}


1. 동작 파라미터화를 기억하라

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

2. 함수형 인터페이스를 이용해서 동작 전달

@FunctionalInterface
public interface BUfferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}


public String processFile(BuffereedReaderProcessor p) throws IOException {
			...
}

 

3. 동작 실행

public String processFile(BufferedProcessor p) throws IOException {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
			return p.process(br);		
	}
}

 

4. 람다 전달

String oneLine = processFile((BufferedReader br) -> br.readLine());

String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

함수형 인터페이스 사용


  • Predicate T - > boolean
(List<String> list) -> list.isEmpty()

 

  • Consumer T -> void
(Apple a) -> System.out.println(a.getWeight())

 

  • Function<T, R> T -> R
(String s) -> s.length

 

  • Supplier () -> T
() -> new Apple(10)

 

  • UnaryOperator T -> T
(Integer i) -> i + 1

 

이외에도..

  • BinaryOperator (T, T) -> T
  • BiPredicate<T, U> (T, U) -> boolean
  • BiConsumer<T, U> (T, U) -> void
  • BiFunction<T, U, R> (T, U) -> R

 

기본형 함수형 인터페이스


  • 제네릭 파라미터에는 내부 구현 때문에 참조 형만 사용할 수 있다. (Consumer에서 T)
  • 자바 기본형 -> 참조형으로 변환하는 기능 (박싱, 반대는 언박싱)
  • but..! 변환 과정은 비용이 소모된다. 박싱 한 값을 기본형을 감싸는 래퍼며 힙에 저장됨.
  • 따라서 박싱한 값은 메모리를 더 소비하며, 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요. 따라서 기본형 함수형 인터페이스인 IntPredicate 등을 자바에서 따로 제공.

 

형식 검사


람다가 사용되는 Context를 이용해서 람다의 형식을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다.

List<Apple> result=filter(inventory,(Apple apple)->apple.getWeight() > 150);


1. 람다가 사용된 콘텍스트는 무엇인가? 우선 filter의 정의를 확인하자

filter(List<Apple> inventory, Predicate<Apple> p)

 

2. 대상 형식은 Predicate이다. (T는 Apple로 대치됨.)


3. Predicate 인터페이스의 추상 메소드는 무엇인가?

boolean test(Apple apple)


4. Apple을 인수로 받아 boolean을 반환하는 test 메소드다.

Apple -> booelan

 

5. 함수 디스크립터는 Apple -> boolean 이므로 람다의 시그니처와 일치한다. 람다도 Apple을 인수로 받아 boolean을 반환하므로 코드 형식 검사가 성공적으로 완료된다.

- 이러한 대상 형식이라는 특징 때문에 같은 람다 표현식이라도 호환되는 추상 메소드를 가진 다른 함수형 인터페이스로 사용될 수 있다.


형식 추론


  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다. 즉 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다.
// 형식 추론을 하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

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

 

지역 변수 사용


  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수를 활용할 수 있다.
  • 여기서 자유 변수란 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수
  • 이와 같은 동작을 람다 캡처링이라고 부른다.
int portNumber = 1337; 

Runnable r1 = () -> System.out.println(portNumber); // 자율변수를 활용
  • 단 제약은 있다.
    • 지역 변수는 명시적으로 final로 선언되어 있거나, final로 선언된 변수와 똑같이 사용되어어야 한다. 즉 불변성을 보장해야 한다.
    • 왜??
      • 우선 내부적으로 인스턴스 변수와 지역변수는 태생부터 다름.
      • 인스턴스 변수 -> 힙에 저장, 지역 변수 -> 스택에 위치
      • 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음.
      • 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라, 자유 지역 변수의 복사본을 제공한다.
      • 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에서는 한 번만 값을 할당해야 한다는 제약이 생김


메소드 참조


  • 특정 람다 표현식을 축약한 것
// 기존의 람다 표현식
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

// 메소드 참조
inventory.sort(Comparator.comparing(Apple::getWeight));

 

  • 위를 비교해 보면 알 수 있듯이 메소드 참조를 사용해서 가독성을 높일 수 있다.

 

람다 표현식을 조합할 수 있는 유용한 메소드


  • 자바 8에서 Compartor, Function, Predicate, Consumer 등 함수형 인터페이스에서 다양한 유틸리티 메소드를 제공한다.
  • 근데 함수형 인터페이스는 추상 메소드가 하나만 있어야 하는데 이게 가능할까?라고 의문이 들 수 있는데
    • 그건 바로 디폴트 메소드를 통해 유틸성 메소드를 제공하기 때문에 이것이 가능해진다.

1. Compartor 역 정렬

inventory.sort(Comparator.comparing(Apple::getWeight).reversed());

 

2. thenComparing

  • 첫 번째 비 교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달할 수 있다.
inventory.sort(Comparator.comparing(Apple::getWeight)
		.reversed()
		.thenComparing(Apple::getCountry)); // 두 사과의 무게가 같으면 국가별로 정렬

 

3. Predicate의 or, and, netgate

Predicate<Apple> notRedApple = redApple.negate(); // 결과를 반전

Predicate<Apple> redAndHeavyApple = redApple
			.and(apple -> apple.getWeight() > 150);

Predicate<Apple> redAndHeavyApple = redApple
			.or(apple -> apple.getWeight() > 150);

 

람다 정리


  • 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
  • 람다 표현식으로 간결한 코드를 구현 가능
  • 함수형 인터페이스는 하나의 추상 메소드만을 정의하는 인터페이스 (디폴트 메소드 제외)
  • 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
  • 람다 표현식을 이용해서 함수형 인터페이스의 추상 메소드를 즉석으로 제공할 수 있으며, 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
  • 람다 표현식의 기대 형식을 대상 형식이라고 한다.
  • 메소드 참조를 이용하면 기존의 메소드 구현을 재사용하고 직접 전달할 수 있다.
  • Comparator, Predicate, Function 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양한 디폴트 메소드를 제공한다.
반응형