람다와 함수형 인터페이스

  • 자바에 람다 표현식이 추가됨으로써 함수형 인터페이스, 메서드 참조가 등장
  • 이를 바탕으로 스트림 API 탄생 및 컬렉션 기반의 기술을 편리하고 빠르게 처리할 수 있게 됨
  • 람다 표현식은 자바 혁신의 핵심
  • 다음과 같은 요소를 정확하게 이해할 필요가 있음
    • 람다 표현식
    • 람다 표현식 주요 문법
    • 함수형 인터페이스
    • 메서드 참조

람다 표현식이 필요한 이유

  • 자바 기반의 프로그램이 비대해지면서 인터페이스 기반으로 개발을 많이하는데 결과적으로 많은 클래스 파일이 생성됨
  • 이에 대응하기 위해 간단한 코드라면 중첩 클래스, 익명 클래스를 사용
  • 하지만 많은 익명 클래스는 중복코드를 발생 -> 코드가 비대해짐 -> 가독성 저하
  • 람다 표현식은 익명 클래스를 태체하는데 유용
public class BaseballPalyer implements Comparable<BaseballPlayer> {
  private String playerName;
  //...
  @Override
  public int compareTo(BaseballPalyer baseballPlayer) {
    return playerName.compareTo(baseballPlayer.getPlayerName());
  }
  //...
}
  • 위 코드는 Comparable을 구현하여 비교하는 메서드를 재정의하는 방법
  • 아래 코드는 외부에서 주입하는 방법
public static void main(String[] args) {
  List<BaseballPlayer> list = new ArrayList<>();

  list.sort(new Comparator<BaseballPlayer>() {
    @Override
    public int compare(BaseballPlayer object1, BaseballPlayer object2) {
      return object1.getPlayerName().compoareTo(object2.getPlayerName());
    }
  });
}
  • 자바 자체의 언어적인 제약으로 인해 익명클래스는 중복코드를 많이 발생시킴
  • 메서드의 파라미터로 기본 데이터 타입과 객체형만 가능하기 때문
  • 행동 자체를 전달할 수 있는 방법의 필요함으로 인해 람다 표현식을 메서드 인수로 전달할 수 있도록 지원됨
  • sort 예제를 람다 표현식으로 변경하면 다음과 같음
list.sort(
  (BasepballPlayer object1, BasepballPlayer object2)
    -> object.getPlayerName().compareTo(object2.getPlayerName())
)
  • 축약된 형태로 변환되는것을 알 수 있음
  • 람다 표현식은 다음과 같은 장점이 존재
    • 이름 없는 함수를 선언가능
    • 소스코드 분량을 획기적으로 줄일 수 있음
    • 코드를 파라미터로 전달 가능

람다 표현식 이해하기

  • 람다 표현식은 익명 클래스를 단순화하여 그 표현식을 메서드의 인수로 전달하거나 인터페이스의 객체를 생성할 수 있는 기능을 제공
  • 람다 표현식의 특징
    • 이름이 없음
    • 종속되지 않음
    • 값의 특징을 갖음
    • 간단함
    • 새로운 것이 아님

함수형 인터페이스 기본

  • 한가지의 의문, 인터페이스는 통상적으로 여러개의 메서드를 포함하고 있는데 람다 표현식은 이름이 없고 단지 파라미터와 리턴타입만으로 식별하는데 어떻게 자바 컴파일러가 이를 인식하고 인터페이스의 구현체로 컴파일 할 수 있을까?
    • 람파 표현식은 오직 public 메서드 하나만 가지고 있는 인터페이스
    • 자바8 에서는 이러한 인터페이스를 함수형 인터페이스라고 부름
    • 함수형 인터페이스에서 제공하는 단 하나의 추상 메서드를 함수형 메서드라 부름
  • 자바8 이전 버전에서 만든 인터페이스도 람다 표현식으로 활용 가능
  • 하지만 함수형 인터페이스 여부를 @FunctionalInterface 어노테이션이 선언을 통해 실수로 함수형 인터페이스에 메서드를 추가하거나 하는 등의 사전 문제를 예방
    • 새로운 함수형 인터페이스를 추가할때는 @FunctionalInterface를 추가하는 것이 좋음
  • 대표 함수형 인터페이스
    • Consumer
    • Function
    • Predicate
    • Supplier

Consumer 인터페이스

  • Consumer 인터페이스는 요청받는 내용을 소비
  • 소비는 아무 리턴없이 요청을 처리한다는 것
public class ConsumerExample {
  public static void executeConsumer(List<String> nameList, Consumer<String> consumer) {
    for(String name : nameList) { 
      consumer.accept(name);
    }
  }

  public static void main(String[] args) {
    List<String> nameList = new ArrayList<>();
    // ...

    ConsumerExample.executeConsumer(nameList, (String name) -> System.out.println(name));
  }
}
  • 람다 표현식은 그 자체로 실행되는 것이 아니라 함수형 인터페이스에 포함되어 있는 함수형 메서드의 내부 코드를 정의하는 역할
  • 다음의 코드도 가능
public Consumer<String> getExpression() {
  return (String name) -> System.out.println(name);
}

ConsumerExample.executeConsumer(nameList, getExpression());

Function 인터페이스

  • 2개의 제네릭 타입(인수 타입, 리턴 타입)을 정의
public class FunctionExample {
  public static int executeFunction(String context, Function<String, Integer> function) {
    return function.apply(context);
  }

  public static void main(String[] args) {
    FunctionExample.executeFunction("Hello! Welcome to Java World.", (String context) -> context.length());
  }
}

Predicate 인터페이스

  • 리턴타입중 불 값을 처리가능한 인터페이스
public class PredicateExample {
  public static boolean isValid(String name, Predicate<String> predicate) {
    return predicate.test(name);
  }

  public static void main(String[] args) {
    PredicateExample.isValid("", (String name) -> !name.isEmpty());
  }
}

Supplier 인터페이스

  • 입력없이 출력만 존재
  • 파라미터 없이 리턴 타입만 있는 메서드는 주로 지정된 정보를 확인하거나 조회할 때 유용
public class SupplierExample {
  public static String executeSupplier(Supplier<String> supplier) {
    return supplier.get();
  }

  public static void main(String[] args) {
    String version = "java upgrade book, version 0.1 BETA";
    SupplierExample.executeSupplier(()-> {return version;});
  }
}
  • 책 저자는 네 가지 함수형 인터페이스를 잘 활용할 것을 권함
  • 거의 표준적 용어로 사용되고 있기 때문
  • 책 저자는 함수형 인터페이스를 이용해서 개발할 때 다음과 같은 세가지 기준을 적용
    • Function, Supplier, Consumer, Predicate를 표준 함수형 인터페이스로 설계에 반영
    • 명시적 함수형 인터페이스의 기능을 정의하고 싶으면 자바에서 제공하는 함수형 인터페이스보다는 프로젝트에 맞게 함수형 인터페이스를 직접 만들어 사용
    • 특별한 경우를 제외하고는 기본 데이터 타입을 지원하는 함수형 인터페이스는 가급적 사용하지 않음
      • 처리해야하는 데이터가 숫자이거나 성능에 매우 민감하다면 기본 데이터 타입을 위한 함수형 인터페이스를 사용

함수형 인터페이스 응용

메서드 참조

  • 함수를 메서드의 파라미터로 전달하는 것을 메서드 참조(method reference)라고 함
  • 메서드 참조의 장점은 람다 표현식과 달리 코드를 여러 곳에서 재사용할 수 있고 자바의 기본 제공 메서드 뿐만아니라 직접 개발한 메서드도 사용 가능하다는 것
  • 메서드 참조로 람다표현식을 한번더 축약 가능하고 가독성을 높일 수 있음
(String name) -> System.out.println(name) // 람다 표현식
System.out::println  // 메서드 참조

// 사용
list.stream().forEach(System.out::println);
  • 두 가지의 메서드 참조 정의
    • 클래스명::메서드명
    • 객체 변수명::메서드명

메서드 참조의 구분

  • 정적 메서드 참조
    • static 메서드에 대한 참조
    • Integer::parseInt
  • 비한정적 메서드 참조
    • static 메서드 참조와 유사하게 보이지만 특정한 객체를 참조하기 위한 변수를 지정하지 않음
    • String::toUpperCase
    • toUpperCase는 String의 public이며 static이 아닌 메서드
  • 한정적 메서드 참조
    • 참조 메서드가 특정 객체의 변수로 제한
    • Calandar.getInstance()::getTime
  • 생성자 참조
    • 클래스명::new

람다 표현식 조합

  • 함수형 인터페이스를 통해 람다 표현식을 조합 가능

Consumer 조합

public static void main(String[] args) {
  Consumer<String> consumer = 
    (String text) -> System.out.println("Hello : " + text);
  Consumer<String> consumerAndThen = 
    (String text) -> System.out.println("Text Length is : " + text.length());
  
  consumer.andThen(consumerAndThen).accept("Java");

  /*
    실행결과
    Hello : Java
    Text Length is 4
  */
}
  • andThen을 사용해 두 개의 인터페이스를 조합
  • accept 사용해 조합된 메서드를 호출
  • 여기서 조합의 의미는 첫 번째 람다 표현식의 결과를 두번째 람다 표현식의 입력으로 활용하는 것이 아닌 accept 메서드에 전달하는 파라미터를 각각 사용

Predicate 조합

  • Predicate 인터페이스는 참/거짓을 리턴하는 함수형 인터페이스
  • 테스트 성공/실패, 데이터 검증 결과 여부 등을 판단하는데 사용

Function 조합

public static void main(String[] args) {
  Function<String, Integer> parseIntFunction = 
    (String str) -> Integer.parseInt(str) + 1;
  Function<Integer, String> intToStrFunction = 
    (Integer str) -> "String : " + Integer.toString(i);
  
  System.out.println(parseIntFunction.apply("1000"));
  System.out.println(intToStrFunction.apply(1000));
  // Function 조합
  System.out.println(parseIntFunction.andThen(intToStrFunction).apply(1000));

  /*
    실행결과
    1001
    String: 1000
    String: 1001
  */

}
  • 실행결과를 보면 알 수 있듯이 apply로 전달되는 파라미터가 첫번째 람다표현식부터 순차적으로 적용되며 실행
  • Consumer는 각각 실행되지만 Function은 순차적으로 실행

References