[모던 자바 인 액션] - 람다 표현식

김성혁·2022년 2월 28일
0

모던 자바 인 액션

목록 보기
3/7
post-thumbnail

람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 알아보자.

👨🏻‍💻 람다란?

  • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것

람다의 특징

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

람다가 자바 8 이전의 자바로 할 수 없었던 일을 제공하는 것은 아니다. 다만 동작 파라미터를 이용할 때 익명 클래스 등 판에 박힌 코드를 구현할 필요가 없다. 즉 코드가 간결하고 유연해진다.


(Apple a1, Apple a2) → a1.getWeight().compareTo(a2.getWeight());
  • 파라미터 리스트
  • 화살표
  • 람다 바디
    • 람다 표현식에는 return이 함축되어 있으므로 return 문을 명시적으로 사용하지 않아도 된다.

람다의 기본 문법

  • 표현식 스타일 람다: (parameters) → expression
  • 블록 스타일 람다: (parameters) → { statements; }

👨🏻‍💻 함수형 인터페이스

  • 정확히 하나의 추상 메서드를 지정하는 인터페이스
  • 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스다.
  • Predicate, Runnable, Comparator 등

함수형 인터페이스로 뭘 할 수 있을까?

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

함수 디스크립터

  • 람다 표현식의 시그니처를 서술하는 메서드
  • 함수형 인터페이스의 추상 메서드 시그니처
  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
  • ex. (Apple, Apple) → int


    람다 표현식은 변수에 할당하거나 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며, 함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖는다는 사실을 기억하자.

    람다 표현식의 형식을 어떻게 검사하는지에 대한 궁금증은 나중에 알아보자!

함수형 인터페이스의 사용

Predicate

  • T 형식의 객체를 사용하여 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있습니다.
@FunctionalInterface
public interface Predicate<T> {
	boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
	for(T t: list) {
		if(p.test(t)) {
			result.add(t);
		}
	}
	return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer

  • T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있습니다.
@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
}

public <T> void forEach(List<T> list, Comsumer<T> c) P
	for(T t: list) {
		c.accept(t);
	}
}
forEach(
				Arrays.asList(1,2,3,4,5),
				(Integer i) -> System.out.println(i)
);

Function

  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있습니다.
@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}

public <T, R> List<R> map(List<T> list, Function<T, R> f) {
	List<R> result = new ArrayList<>();
	for(T t: list) {
		result.add(f.apply(t));
	}
	return result;
}

List<Integer> l = map(
			Arrays.asList("lamdas", "in", "action"),
			(String s) -> s.length()
);

함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야 한다.

👨🏻‍💻 람다 활용 : 실행 어라운드 패턴

자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다. 설정과 정리 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는데 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 부른다.

다음 예제는 한 행을 읽는 코드다.

public String processFile() thorws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return br.readLine();
	}
}

1단계: 동작 파라미터화를 기억하라

한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?

기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다.

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

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

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
}

3단계: 동작 실행

public String processFile(BufferedReaderProcessor 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());

👨🏻‍💻 형식 검사, 형식 추론, 제약

대상 형식: 어떤 콘텍스트에서 기대되는 람다 표현식의 형식

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

✅ 특별한 void 호환 규칙 람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다.

람다 표현식은 대상 형식의 형식을 알 수 있기 때문에 람다 파리미터의 형식을 추론할 수 있습니다. 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있습니다. 어떤 방법이 좋은지 정해진 규칙은 없습니다.

람다 캡처링

  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있습니다.
  • 여기에는 약간의 제약이 있는데 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조)할 수 있고, 지역 변수는 final로 선언되어 있어야 캡처가 가능합니다. 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

    왜 지역 변수에 이런 제약이 필요할까? 우선 내부적으로 인스턴스 변수와 지역 변수는 태생부터 다르다. 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 할당해야 한다는 제약이 생기는 것이다.


👨🏻‍💻 메서드 참조

  • 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있습니다.
  • 메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있습니다.
  • 메서드명 앞에 구분자(::)을 붙입니다.
  • 메서드 참조를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편히라게 표현할 수 있는 문법으로 간주할 수 있습니다.

메서드 참조의 세 가지 유형

  • 정적 메서드 참조
    • Integer::parseInt
  • 다양한 형식의 인스턴스 메서드 참조
    • String::length
  • 기존 객체의 인스턴스 메서드 참조
    • expensiveTransaction::getValue

생성자 참조

  • ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있습니다.

👨🏻‍💻 람다, 메서드 참조를 활용하여 깔끔한 코드 만들기

1단계: 코드 전달

public class AppleComparator implements Comparator<Apple> {
	public int compare(Apple a1, Apple a1) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
}
inventory.sort(new AppleComparator());

2단계: 익명 클래스 사용

inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
});

3단계: 람다 표현식 사용

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

//형식 추론에 의해 다음과 같이 더 줄일 수 있습니다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

//Comparator의 정적 메서드 사용
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

inventory.sort(comparing(apple -> apple.getWeight()));

4단계: 메서드 참조 사용

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

0개의 댓글