람다와 함수형 인터페이스
- 자바에 람다 표현식이 추가됨으로써 함수형 인터페이스, 메서드 참조가 등장
- 이를 바탕으로 스트림 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은 순차적으로 실행