Chapter 14. 람다와 스트림
1. 람다식(Lambda Expression)
(1) 람다식이란?
- 메서드를 하나의 식으로 표현한 것
- 람다식은 함수를 간략하면서 명확한 식으로 표현할 수 있게 해짐
- 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 '익명 함수' 라고도 함
int method() {
return (int) (Math.random() * 5) * 1;
}
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random() * 5) +1);
- 메서드는 클래스에 포함되어야 하고 객체도 생성해야 메서드를 호출할 수 있으나, 람다식은 이 모든 과정 없이 오직 람다식 자체만으로도 메서드의 역할을 대신할 수 있음
- 또한 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고 메서드의 결과로 반환될 수도 있음. 즉, 메서드를 변수처럼 다루는 것이 가능해짐
- 메서드와 함수의 차이
- 객체지향 개념에서는 함수(function) 대신 메서드라는 용어를 사용
- 메서드와 함수는 같은 의미이지만 메서드는 특정 클래스에 반드시 속해야 한다는 제약이 있음
- 그러나 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에 이 경우엔 함수라는 용어를 사용할 수 있음
(2) 람다식 작성하기
- 익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 사이에 ->를 추가
반환타입 메서드이름 (매개변수 선언) {
문장들
}
(매개변수 선언) -> {
문장들
}
- 반환값이 있는 메서드의 경우 return 문 대신 식으로 대신할 수 있음. 이 경우 ;를 붙이지 않음
int max(int a, int b) {
return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b
- 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략이 가능하며 대부분 생략이 가능. 반환 타입이 없는 이유도 항상 추론이 가능하기 때문. 대신, 생략을 하려면 모든 타입을 생략해야 함 (한 타입만 생략 x)
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
- 선언된 매개변수가 하나뿐인 경우에는 괄호 생략이 가능. 단, 매개변수의 타입이 있을 경우 괄호를 생략할 수는 없음
(a) -> a * a
(int a) -> a * a
- {} 안의 문장이 하나인 경우 {}를 생략할 수 있음. ;를 붙이지 말아야 함. 단, {} 안의 문장이 return 문일 경우엔 생략이 불가
(String name, int i) -> {
System.out.println(name + "=" + i);
}
(String name, int i) ->
System.out.println(name + "=" + i)
(int a, int b) -> {
return a > b ? a : b;
}
(int a, int b) -> a > b ? a : b
(3) 함수형 인터페이스(Functional Interface)
- 람다식이 메서드와 동등한 것처럼 설명해왔으나 사실 익명 클래스의 객체와 동등
(int a, int b) -> a > b ? a : b
new Object() {
int max(int a, int b) {
return a > b ? a : b;
}
}
- 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출 할 수 있는가?
- 참조 변수가 있어야 객체의 메서드를 호출할 수 있는데 참조 변수의 타입은 클래스 또는 인터페이스가 가능하고 람다식과 동등한 메서드가 정의되어 있어야 함 -> 인터페이스가 적합
interface MyFunction {
public abstract int max(int a, int b);
}
MyFunction f = new MyFunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
};
int big = f.max(5, 3);
- 인터페이스에 정의된 메서드 max()는 람다식
(int a, b) -> a > b ? a : b
과 메서드의 선언부가 일치, 따라서 위의 코드의 익명 객체를 람다식으로 대체할 수 있음
MyFunction f = (int a, int b) -> a > b ? a : b;
int big = f.max(5, 3);
- 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유
- 람다식도 실제로는 익명 객체이고 인터페이스를 구현한 익명 객체의 메서드와 람다식의 매개변수의 타입과 개수, 반환값이 일치하기 때문
- 하나의 메서드가 선언된 인터페이스를 정의해 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스러움
- 따라서 인터페이스를 통해 람다식을 다루기로 결정됨
- 함수형 인터페이스 : 람다식을 다루기 위한 인터페이스
- 오직 하나의 추상 메서드만 정의되어 있어야 하는 제약이 있음
- static, default 메서드의 개수에는 제약이 없음
- @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해줌
@FunctionalInterface
interface MyFunction {
void myMethod();
}
- 함수형 인터페이스 타입의 매개변수와 반환 타입
- 메서드의 매개변수가 함수형 인터페이스 타입일 경우 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 함
void aMethod(MyFunction f) {
f.myMethod();
}
...
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
aMethod(() -> System.out.println("myMethod()"));
- 메서드의 반환 타입이 함수형 인터페이스 타입일 경우, 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있음
MyFunction myMethod() {
MyFunction f = () -> {};
return f;
}
- 람다식의 타입과 형변환
- 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아님. 람다식은 익명 객체이며 익명 객체는 컴파일러가 타입의 이름을 임의로 정하기 때문에 알 수 없음
- 또한 Object 타입으로 형변환이 불가능하며 오직 함수형 인터페이스로만 형변환이 가능. 굳이 하려면 함수형 인터페이스로 먼저 변환을 하고 형변환을 진행해야 함
MyFunction f = (MyFunction) (() -> {});
Object obj = (Object) (() -> {});
Object obj = (Object) (MyFunction) (() -> {});
(4) java.util.function 패키지
- java.util.function 패키지
- 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의
- 매번 새로운 함수형 인터페이스를 정의하지 말고 해당 패키지의 인터페이스를 활용하는 것이 좋음
java.lang.Runnable - void run()
: 매개변수도, 반환값도 없음
Supplier<T> - T get() -> T
: 매개변수는 없고 반환값만 있음
Consumer<T> - T -> void accept(T t)
: 매개변수만 있고 반환값은 없음
Function<T, R> - T -> R apply(T t) -> R
: 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
Predicate<T> - T -> boolean test(T t) -> boolean
: 조건식을 표현하는데 사용. 매개변수는 하나, 반환 타입은 boolean
(5) Function의 합성과 Predicate의 결합
- Function의 합성
- 수학에서 두 함수를 합성해 하나의 새로운 함수를 만들어내는 것 처럼 두 람다식을 합성해 새로운 람다식을 만들어낼 수 있음
- 두 함수의 합성은 함수의 적용 순서에 따라 결과가 달라짐
- A.andThen(B) : A 적용 후 B 적용
- A.compose(B) : B 적용 후 A 적용
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
Function<Integer, String> g = (i) -> Interger.toBinaryString(i);
Function<String, String> h = f.andThen(g);
- Predicate의 결합
- 여러 조건식을 논리 연산자인 &&(and), ||(or) 등으로 연결해서 하나의 식을 구성할 수 있는 것처럼 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있음
- 두 대상을 비교할 때엔 isEqual()을 사용
(6) 메서드 참조
- 메서드 참조(method reference) : 람다식이 하나의 메서드만 호출하는 경우 람다식을 더욱 더 간결히 작성할 수 있음
- 클래스이름::메서드이름 또는 참조변수::메서드이름 으로 바꿀 수 있음
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
Function<String, Integer> f = Integer::parseInt;
- 생성자의 메서드 참조
- 생성자를 호출하는 람다식 역시 메서드 참조로 변환이 가능
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;
Funtion<Integer, MyClass> f = (i) -> new MyClass(i);