Stream 연산들은 매개변수로 함수형 인터페이스(Functional Interface)를 받도록 되어 있다.
그리고 람다식은 반환 값으로 함수형 인터페이스를 반환하고 있다.
그렇기 때문에 우리는 Stream API를 정확히 이해하기 위해 람다식과 함수형 인터페이스에 대해 알고 있어야 한다.
람다식(Lambda Expression)이란 함수를 하나의 식(expression)으로 표현한 것이다.
함수를 람다식으로 표현하면 메소드의 이름이 필요 없기 때문에, 람다식은 익명 함수(Annonumous Function)의 한 종류로 볼 수 있다.
익명함수(Annonumous Function)란, 함수의 이름이 없는 함수로 익명함수들은 모두 일급 객체이다.
일급 객체인 함수는 변수처럼 사용 가능하며 매개변수로 전달이 가능하다는 특징을 가지고 있다.
// 기존의 방식
반환타입 메소드명 (매개변수, ...){
실행문
}
// 예시
public String hello(){
return "Hello World!";
}
// 람다 방식
(매개변수, ...) -> { 실행문 }
// 예시
() -> "Hello World!";
불필요한 코드를 줄이고 가독성을 높이기 위함이다.
그렇기 때문에 함수형 인터페이스의 인스턴스를 생성하여 함수를 변수처럼 선언하는 람다식에서는,
메소드의 이름이 불필요하다고 여겨져서 이를 사용하지 않는다. (어차피 메소드가 한 개만 존재하므로)
대신 컴파일러가 문맥을 살펴 타입을 추론한다.
또한 람다식으로 선언된 함수는 1급 객체이기 때문에 Stream API의 매개변수로 전달이 가능해진다.
결국 무조건 람다식이 좋다는 보장은 없다. 상황에 따라 필요에 맞는 방법을 사용하는 것이 중요하다.
함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 한다.
함수형 인터페이스를 사용하는 이유는 Java의 람다식이 함수형 인터페이스를 반환하기 때문이다.
public class Lambda{
public static void main(String[] args){
// 기존의 익명 함수
System.out.println(new MyLambdaFunction(){
public int max(int a, int b) return a>b? a:b;
}.max(3, 5));
}
}
public class Lambda{
public static void main(String[] args){
// 람다식을 이용한 익명함수
MyLambdaFunction lambdaFunction = (int a, int b) ->a>b? a:b;
System.out.println(lambdaFunction.max(3, 5));
}
}
람다식을 통해 Java8 이전에 사용했던 익명함수들을 람다식으로 변경해 코드를 줄일 수 있다.
여기서 놓치지 말아야하는 것은 람다식으로 생성된 순수 함수는 함수형 인터페이스로만 선언이 가능하다는 점이다.
또한 @FunctionalInterface는 해당 인터페이스가 1개의 함수만을 갖도록 제한하기 때문에, 여러 개의 함수를 선언하면 컴파일 에러가 발생한다는 점이다.
Java에서 자주 사용될 것 같은 함수형 인터페이스가 이미 정의되어 있으며, 총 4가지 인터페이스를 지원하고 있다.
Supplier<T>
Consumer<T>
Function<T, R>
Predicate<T>
Supplier<T>
Supplier는 매개변수 없이 반환값만을 갖는 함수형 인터페이스이다.
Supplier는 T get()을 추상메소드로 가지고 있다.
// 정의
@FunctionalInterface
public interface Supplier<T>{
T get();
}
// 사용 예시
Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());
Consumer<T>
Consumer는 객체 T를 매개변수로 받아서 사용하며, 반환값은 없는 함수형 인터페이스이다.
Consumer는 void accept(T t)를 추상메소드로 갖는다.
또한 Consumer는 andThen이라는 함수를 제공하고 있는데, 이를 통해 하나의 함수가 끝난 다음 Consumer를 연쇄적으로 이용할 수 있다.
아래의 예제에서는 먼저 accept으로 받아들인 Consumer를 먼저 처리하고, andThen으로 받은 두 번째 Consumer를 처리하고 있다. 함수형에서는 함수는 값의 대입 또는 변경 등이 없기 때문에 첫 번째 Consumer가 split으로 데이터를 변경하였다 하더라도 원본의 데이터는 유지된다.
// 정의
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after){
Objects.requireNonNull(after);
return (T t) -> { accpet(t); after.accept(t); };
}
}
// 예시
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(system.out::println).accpet("Hello World");
Function<T, R>
Function은 객체 T를 매개변수로 받아서 처리한 후 R로 반환하는 함수형 인터페이스이다.
Function은 R apply(T t)를 추상 메서드로 갖는다.
또한 Function은 Consumer와 마찬가지로 andThen을 제공하고 있으며, 추가적으로 compose를 제공하고 있다.
앞에서 andThen은 첫 번째 함수가 실행된 이후에 다음 함수를 연쇄적으로 실행하도록 연결해준다고 했지만,
compose는 첫 번째 함수 실행 이전에 먼저 함수를 실행하여 연쇄적으로 연결해준다는 점에서 차이가 있다.
또한 identity 함수가 존재하는데, 이는 자기 자신을 반환하는 static 함수이다.
// 정의
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
// 예시, 메소드 참조로 간소화 가능(String::length;)
Function<String, Integer> function = str -> str.length();
function.apply("Hello World");
Predicate<T>
Predicate는 객체 T를 매개 변수로 받아 처리한 후 Boolean을 반환한다.
Predicate는 Boolean test(T t)를 추상 메소드로 갖고 있다.
// 정의
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
// 예시
Predicate<String> predicate = (str) -> str.equals("Hello World");
predictate.test("Hello World");
메소드 참조란 함수형 인터페이스를 람다식이 아닌 일반 메소드를 참조시켜 선언하는 방법이다.
일반 메소드를 참조하기 위해서는 다음의 3가지 조건을 만족해야한다.
참조 가능한 메소드는 일반 메소드, static 메소드, 생성자가 있으며 클래스이름::메소드이름
으로 참조할 수 있다.
이렇게 참조하면 함수형 인터페이스로 반환이 된다.
String의 length 함수는 매개변수가 없으며, 반환형이 int로 동일하기 때문에 String::length
로 다음과 같이 메소드 참조를 적용할 수 있다.
// 기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello World");
// 메소드 참조로 변경
Function<String, Integer> function = String::length;
function.apply("Hello World");
// 일반 메소드를 참조하여 Consumer를 선언한다.
Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!!");
// 메소드 참조를 통해 Consumer를 매개변수로 받는 forEach를 쉽게 사용할 수 있다.
List<String> list = Arrays.asList("red", "orange", "yellow", "green", "blue");
list.forEach(System.out::println);
//interface Iterable<T>
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Predicate<Boolean> predicate = Objects::isNull;
// isNull 함수
public static boolean isNull(Object obj){
return obj == null;
}
클래스이름::new
로 참조할 수 있다Supplier<String> supplier = String::new;