람다식은 자바 공부를 하며 한 번씩 보고 넘어갔었다. 넘어갈 때마다 이해했다고 생각했지만 실제로 코드를 짤 때는 잘 사용하지 않았다. 그저 착각이었다. 이번에는 꼭 람다식을 잡고 실제 프로젝트에서도 사용하리라..
JDK 1.8부터 추가된 람다식의 도입으로 인해 자바는 객체지향언어일 뿐만 아니라 함수형 언어가 되었다. 함수형 언어의 인기가 치솟았는데도 꽤 오래된(?) 자바가 여전히 경쟁력있는 언어인 이유 중 하나가 바로 람다식의 도입이라고 생각한다.
💡 메서드를 하나의 식(expression)으로 표현한 것
메서드를 람다식으로 표현하면 메서드의 이름이 없어지므로 익명 함수(anonymous function)라고도 한다.
시작하기에 앞서 람다식이 무엇인 지 예시코드를 보자.
int[] arr = new int[5];
Ararys.setAll(arr, (i) -> (int)(Math.random()*5)+1);
위 코드를 보면 JDK 1.7 까지 있어서는 안될 특이한 화살표가 하나 있다. 화살표로 이루어져있는 저 식을 람다식이라고 한다.
다들 알다시피 자바의 함수는 클래스 내에 메서드로만 선언할 수 있다. 아무데나 가서 void function(){}
한다고 함수 생성을 허락해주지 않는다.
그렇다면 람다식도 함수인데 원래 자바에서 사용되는 메서드였다면 어떻게 생겼을까?
int nimoh() {
return (int) (Math.random()*5)+1);
}
아마 이렇게 생겼을 것이다. 누가 지어줬는 지 예쁜 이름까지 생겼다. 매우 간단한 로직이라 람다식과 별 차이 없어보이지만 이 메서드를 사용하는 것은 자질구레하다. 앞서 말했듯이 자바에서 메서드를 사용하기 위해서는 클래스를 생성하고 그 안에 메서드를 선언한 뒤 사용하고자 하는 위치에서 객체를 생성해서 메서드를 호출해야한다.
얼마나 귀찮은가.. 그냥 함수하나 만들어서 쓰겠다는데 😭
이 자질구레함을 타파하기 위해 나온 것이 바로 람다식(두둥!!)이다.
(참고) 지금까지 함수와 메서드를 혼용해서 글을 적었다. 함수는 수학용어이며 개발에도 많이 쓰인다. 함수가 객체지향언어에서 사용되어 객체에 귀속된 경우 메서드라고 부른다. 이렇게 계속 혼용해서 부르면 헷갈리므로 클래스에 있든 람다식이든 함수라고 통일하겠다.
반환타입 메서드이름(매개변수 선언) { 문장들 }
반환타입메서드이름(매개변수 선언) -> { 문장들 }
기존 함수를 람다식으로 바꾸면 이렇게 바뀐다. 반환타입과 메서드이름을 생략하고 () -> {} 형식으로 만든다. 자바스크립트의 () => {} 화살표함수와 비슷하게 생겼다.
람다식을 작성할 때에는 여러가지 규칙과 생략이 있다.
반환값이 있다는 것은 return값이 있다는 것이다.
(int a, int b) -> { return a > b ? a : b; }
(int a, int b) -> a > b ? a : b
return과 {}을 생략하고 식만 남길 수 있다. 이 때, 식이기 때문에 ;은 적지 않아야한다.
(물론 뒤에 나오겠지만 람다식을 따로 선언할 때에는 ;를 해줘야한다.)
람다식은 대부분의 경우 매개변수를 추론해준다.
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b
따라서 이렇게 매개변수의 타입을 생략할 수 있다.
참고로 위 처럼 매개변수가 2개 이상인 경우, 둘 다 매개변수를 생략해주어야한다.
(int a, b)
는 안된다.
반환값이 있을 때도 {}를 생략했지만 없을 때도 생략할 수 있는 경우가 있다.
문장이 한 줄일 때이다.
(int a) -> { System.out.println(a); }
-> 생략
(int a) -> System.out.println(a);
이러한 규칙들을 잘 알아둬야 나중에 코드를 해석할 때 헷갈리지 않는다. 생략을 말도 없이 해버리니 가끔 헷갈릴 때가 있다.
자바에서 모든 메서드는 클래스 내에 포함되어야 한다. 람다식도 익명 함수로서 메서드일텐데 어떤 클래스에 포함되는 것일까?
다음 예제는 인터페이스의 추상메서드를 구현하는 익명클래스의 예제이다.
interface FunctionalInterface {
public abstract int max(int a, int b);
}
...
FunctionalInterface f = new FunctionalInterface() {
public int max(int a, int b){
return a > b ? a : b
}
}
고작 max 메서드를 사용하겠다고 new 부터 시작하는 거대한 익명 클래스를 작성하는 것은 귀찮다. max 메서드를 보면 (int a, int b) → a > b ? a : b 익명함수(람다식)와 메서드의 선언부가 일치한다.
따라서 다음과 같이 표현할 수 있다.
FunctionalInterface f = (int a, int b) -> a > b ? a : b;
int bigger = f.max(1,2); // 메서드 호출
위에서 보다시피 함수형 인터페이스는 하나의 추상 메서드만 가지고 있어야한다. 그리고 @FunctionalInterface 애노테이션을 사용해 함수형 인터페이스인 것을 알려주면, 컴파일러가 올바르게 정의되었는 지 체크해준다.
람다식 하나 사용할 때 마다 매번 함수형 인터페이스를 만들것인가? 썼던 걸 까먹고 어떤 상황에 쓰기로 했는 지 종종 기억하지 못할 수 있다.
java.util.function 패키지에는 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다. 우리는 가능한 여기 있는 인터페이스를 사용하도록 하자.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
java.lang.Runnable | void run() | 매개변수도 없고 반환값도 없음 |
Supplier | T get() -> T | 매개변수는 없고 반환값만 있음 공급자니까 어딘가에 공급만해줌 |
Consumer | T→ void accept(T t) | 매개변수만 있고 반환값이 없음 소비자니까 소비만 함. 공급없음 |
Function<T,R> | T→ R apply(T t) →R | 매개변수와 반환값 둘 다 있음. 우리가 아는 함수 함수니까 input output 다름 |
Predicate | T→ boolean test(T t) → boolean | 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean 수학에서 결과로 true 또는 false를 반환하는 함수를 predicate라 부른다. |
Predicate는 Function의 변형이다. 조건식을 람다로 표현하는데 사용된다.
Predicate<String> isEmptyStr = s -> s.length == 0;
인터페이스 이름 앞에 Bi 접두사가 붙는다 (Binary)
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
BiConsumer<T,U> | T,U→ void accept(T t, U u) | 두 개의 매개변수만 있고 반환값이 없음 |
BiFunction<T,U,R> | T,U→ R apply(T t, U u)→R | 두 개의 매개변수와 반환값 하나 있음 |
Predicate<T, U> | T, U→ boolean test(T t) → boolean | 조건식을 표현하는데 사용됨. 매개변수는 둘, 반환 타입은 boolean |
💡 매개변수 3개 이상인 람다식을 사용하려면 직접 만들어서 사용해야한다.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
UnaryOperator | T→ T apply(T t) → T | Function의 자손이며, Function과 달리 반환타입과 매개변수의 타입이 동일하다. |
BinaryOperator | T,T→ R apply(T t, T T)→ T | BiFunction의 자손이며, BiFunction과 달리 반환타입과 매개변수의 타입이 동일하다. |
인터페이스 | 메서드 | 설명 |
---|---|---|
Collection | boolean removeIf(Predicate filter) | 조건에 맞는 요소 삭제 |
List | void replaceAll(UnaryOperator operator) | 모든 요소를 변환하여 대체 |
Iterable | void forEach(Consumer action) | 모든 요소에 작업 action 수행 |
Map | V compute(K key, BiFunction<K,V,V> f) | 지정된 키의 값에 작업 f를 수행 |
V computeIfAbsent(K key, Function<K,V> f) | 키가 없으면, 작업 f 수행 후 추가 | |
V computeIfPresent(K key, BiFunction<K,V,V> f) | 지정된 키가 있을 때, 작업 f 수행 | |
V merge(K key, V value, BiFunction<V,V,V> f) | 모든 요소에 병합작업 f를 수행 | |
void forEach(BiConsumer<K,V> action) | 모든 요소에 작업 action을 수행 | |
void replaceAll(BiFunction<K,V,V> f) | 모든 요소에 치환작업 f를 수행 |
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
DoubleToIntFunction | double → int applyAsInt(double d) → int | AToB타입은 입력이 A타입이고 출력이 B타입이다. |
ToIntFunction | T → int applyAsIne(T value) → int | ToBFunction은 출력이 B타입이고 입력은 지네릭 타입 |
IntFunction | int → R apply(T t, U u) → R | Afunction은 입력이 A 타입이고 출력은 지네릭 타입 |
ObjIntConsumer | T, int → void accept(T t, U u) | ObjAFunction은 입력이 T, A타입이고 출력은 없음 |
java.util.function 패키지의 함수형 인터페이스에는 람다식 사용을 위한 추상메서드 외에도 디폴트 메서드와 static 메서드가 정의되어 있다.
// Function
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {}
static <T> Function<T, T> identity() {}
// Predicate
default Predicate<T> and(Predicate<? super T> other) {}
default Predicate<T> negate() {}
default Predicate<T> or(Predicate<? super T> other) {}
static <T> Predicate<T> isEqual(Object targetRef) {}
compose()
compose는 함수 f,g가 있을 때 f.compose(g)
라고 한다면 g 함수를 먼저 실행한 다음 f가 실행된다.
andthen()
compose와는 다르게 f.andthen(g)
의 경우 f를 먼저 실행한 뒤 g를 실행한다.
identity()
identity는 항등함수를 표현할 때 사용한다. x → x
와 같다.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150)); // 150이 위 조건을 만족하는 지 boolean 리턴
boolean result = Predicate.isEqual("hello").test("helo"); // false
람다식이 하나의 메서드만 호출하는 경우 ‘메서드 참조(method reference)’라는 방법으로 람다식을 간략히 할 수 있다.
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
->
Function<String, Integer> g = Integer::parseInt;
얼핏 보면 우변에 너무 정보가 없다. 하지만 람다식은 똑똑하기 때문에 Function의 지네릭 타입을 가지고 매개변수의 타입을 추정해준다. 또 다른 예를 보자.
BiFunction<String, String, Boolean> biFunction = (s1, s2) -> s1.equals(s2);
BiFunction<String, String, Boolean> biFunction1 = String::equals;
BiFunction 람다식은 input이 두 개, output이 하나이다. BiFunction<input, input, output>이기 때문에 우변에 람다식 매개변수로 (String, String)이 오는 것을 추정할 수 있다.
신세계이며 기가 막히다. 매우 간단해보이지만 타입 추론이 불가능 할 때에는 사용할 수 없다.
사용방법은 다음과 같다.
종류 | 람다식 | 메서드 참조 |
---|---|---|
static메서드 참조 | (x) → ClassName.method(x) | ClassName::method |
인스턴스메서드 참조 | (obj, x) → obj.method(x) | ClassName::method |
특정 객체 인스턴스메서드 참조 | (x) → obj.method(x) | obj::method |
생성자 메서드 참조 | () → new AClass() | AClass::new |
매개변수가 있는 생성자라면 매개변수 개수에 따라 적합한 함수형 인터페이스를 사용해야한다.
(BiFunction 등…)
배열을 생성할 때는 다음과 같다.
Function<Integer, int[]> f = x -> new int[x]; // 람다식 Function<Integer, int[]> f = int[]::new // 메서드 참조
람다식은 볼 때마다 헷갈리고 어려웠다. 하지만 한 번 제대로 잡고 공부하니 정말 재미있고 편리한 기능이다. 헷갈릴 때마다 여기 들어와서 정독해야겠다.
참조 : 자바의 정석 3rd Edition