람다 표현식

김종준·2023년 1월 16일
0

모던자바

목록 보기
2/15

람다 표현식

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

즉, 새로운 개념은 아니다.

앞선 장의 동작을 파라미터화하여 코드로 전달하는 하나의 방법 중 하나이다.

예시로 이를 확인하면 아래와 같다.

// 앞선 장의 익명함수 예
List redApples = filterApples(inventory, new ApplePredicate() {
 	public boolean test(Apple apple) {
 	return RED.equals(apple.getColor);
 }
})

// 람다 예시
List redApples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor));

확실히 익명함수보다 간결해진 것을 느낄 수 있다.

이러한 람다는 다음 3가지로 부분으로 이루어진다.

  • 파라미터 리스트 : (Apple apple) 람다의 파라미터
  • 화살표 : 람다의 파라미터 리스트와 바디를 구분 (->)
  • 람다 바디 : RED.equals(apple.getColor) 람다의 반환값에 해당

이러한 람다는 2가지 스타일로 표현되는데 이는 아래와 같다.

  • (parameters) -> expression
  • (parameters) -> {statements;}

함수형 인터페이스

함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있는데 함수형 인터페이스란 정확히 하나의 추상 메서드를 지정하는 인터페이스이다.

앞선 장에서 익명 클래스의 틀로 인터페이스를 사용했다.

앞선 장의 ApplePredicate 역시 생각해 보면 추상 메서드가 하나만 존재하는 인터페이스였다.

위에서 람다는 익명 함수를 단순화한 것이라 하였는데 그렇다면 람다 역시 틀이 필요할 것이고 이 틀이 함수형 인터페이스이다.

꼭 함수형 인터페이스여야 하는 이유는 메서드가 많이 존재한다면 익명함수, 람다가 어떤 것을 틀으로 사용하는지 알 수 없기에 그렇다고 생각한다.

이 함수형 인터페이스 기본 목록은 아래와 같다.

함수형인터페이스목록

또 책에서 소개하는 함수형 인터페이스 예제는 다음과 같다.

  • 블리언 표현 : Predicate
  • 객체 생성 : Supplier
  • 객체에서 소비 : Consumer
  • 객체에서 선택, 추출 : Function
  • 두 값 조합 : Operator
  • 두 객체 비교 : Function

함수 디스크립터

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.

람다 활용 예제

람다 활용 예제를 통해 동작파라미터화까지 리마인드 해보자.

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

위의 코드는 파일에서 한 번에 한 줄만 읽는 코드이다.

이를 한 번에 두 줄을 읽거나 다른 조건을 주려면 위의 코드에서 한 번에 한 줄만 읽는다라는 코드를 파라미터화하여 조건에 맞는 다른 코드를 전달해주면 된다.

그렇다면 우선 동작을 파악하여야 한다.

위의 코드에서 동작은 br.readLine() 이다.

이를 조금 분석해보면 반환 값은 String, 동작을 수행하는 인스턴스는 br 즉 BufferedReader의 인스턴스이다.

이를 기반으로 우선 인터페이스를 작성해보면 다음과 같을 것이다.

public interface BufferedReaderProcessor {
  String process(BufferdReader b) throws IOException;
}

그리고 이에따라 processFile은 다음과 같이 수정될 것이다.

public String processFile(BufferedReaderProcessor bp) throws IOException {
  try (BufferedReader br = 
      		new BufferedReader(new FileReader("data.tx"))) {
    return bp.process(br);
  }
}

바로 람다 표현식으로 가는 것이 아닌 순서대로 가보자.

우선 BufferedReaderProcessor를 구현하고 사용해보자.

public class ReadTwoBufferedReader implements BufferedReaderProcessor {
  String process(BufferedReader b) throws IOException {
    return b.readLine() + b.readLine();
  } 
}


String read = processFile(new ReadTwoBufferedReader());

이제는 이를 익명 함수로 리펙토링 해보자.

String read = processFile(new BufferedReaderProcessor() {
  String process(BufferedReader b) throws IOException {
    return b.readLine() + b.readLine();
  } 
});

마지막으로 이를 람다 표현식으로 리펙토링 해보자.

String read = processFile((BufferdReader b) -> b.readLine() + b.readLine());

람다 형식 검사

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

이때 기대되는 람다 표현식의 형식을 대상 형식이라 한다.

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

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

람다 형식 추론

컴파일러는 대상 형식을 이용해서 함수 디스크립터를 알 수 있다.

따라서 컴파일러는 람다의 시그니처도 추론할 수 있고 이는 람다 문법을 생략할 수 있는 편리함으로 이어진다.

// 형식 추론하지 않은 경우
String read = processFile((BufferdReader b) -> b.readLine() + b.readLine());

// 형식 추론한 경우
String read = processFile((b) -> b.readLine() + b.readLine());

람다 지역 변수

람다표현식에서는 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수라고 하며 이를 사용하는 동작을 람다 캡처링이라 한다.

하지만 이 자유변수에는 약간의 제약이 있다.

  • 명시적으로 final 선언이 된 변수
  • 실질적으로 final 선언된 변수

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

이 이유는 책에 명시되어 있는데 아직은 이해하지 못하였다. (p.113)

메서드 참조

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

이는 크게 세 가지 유형으로 구분할 수 있다.

  1. 정적 메서드 참조
  2. (파라미터로 넘어온) 인스턴스 메서드 참조
  3. 기존의 인스턴스 메서드 참조

정적 메서드 참조

(args) -> ClassName.staticMethod(args)
  
 ClassName::staticMethod

(파라미터로 넘어온) 인스턴스 메서드 참조

(arg0, rest) -> arg0.instanceMethod(rest)
  
 arg'sClassName::instanceMethod

기존의 인스턴스 메서드 참조

(args) -> expr.instanceMethod(args)
  
 expr::instanceMethod

0개의 댓글