- 람다식
- 스트림 [ Stream ]
람다식의 도입으로 자바는 객체지향언어인 동시에 함수형 언어가 되었다.
람다식은 간단히 말해서 메서드를 하나의 식(expression)
으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 익명 함수(anonymous functon)
이라고도 한다.
int[] arr = new int[5];
Arrays.setAll(arr,(i)->(int)(Math.randon()*5)+1);
위의 문장에서 ()->(int)(Math.random()*5)+1
이 람다식이다. 이 람다식이 하는 일을 메서드로 표현하면 다음과 같다.
int method(){
return (int)(Math.random()*5)+1;
}
위의 메서드보다는 람다식이 간결하면서도 이해하기 쉽다. 게다가 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다. 그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.
람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 변환될 수도 있다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.
람다식은 익명 함수
답게 메서드에서 이름과 반화타입을 제거하고 매개변수 선언부와 몸통{} 사이에 ->
를 추가한다.
반환타입 메서드이름(매개변수 선언) {
문장들
}
을 아래와 같이 변환한다.
(매개변수 선언) -> { 문장들 }
예를 들어 두 값 중에서 큰 값을 반환하는 메서드 max를 람다식으로 변환하면 아래와 같다.
(int a, int b) -> { return a > b ? a : b ; }
반환값이 있는 메서드의 경우 return 문 대신에 식(expression)
으로 대신 할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다. 이때는 문장(statement)
이 아닌 식(expression)
이므로 끝에 ;
를 붙이지 않는다.
(int a, int b) -> { a > b ? a : b }
람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있다. 대부분의 경우 생략가능하다. 람다식에 반환타입이 없는 이유도 추론이 가능하기 때문이다.
(a,b) -> { a > b ? a : b }
선언된 매개변수가 하나뿐인 경우에는 괄호()
를 생략할 수 있다 단, 매개변수의 타입이 있으면 괄호()
를 생략할 수 없다.
a -> a * a
마찬가지로 괄호{}
안에 문장이 하나일 때는 괄호{}
를 생략할 수 있다. 이 때 문장의 끝에 ;
를 붙이지 않아야 한다.
(String name, int i) -> System.out.println(name + "i)
그러나 괄호{}
안의 문장이 return 문일 경우 괄호{}
를 생략할 수 없다.
(int a, int b ) -> return a > b ? a : b; //에러
(int a, int b ) -> {return a > b ? a : b; } //OK
람다식은 익명 클래스의 객체와 동등하다. 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 함수형 인터페이스(functional interface)
라고 부르기로 했다.
@FunctionalInterface
interface MyFunction {
public abstract int max(int a,int b);
}
단, 함수형 인터페이스는 오직 하나의 추상 메서드만 정의되어 있어야 한다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default 메서드의 개수에는 제약이 없다.
함수형 인터페이스 MyFunction이 아래와 같이 정의되어 있을 때,
@FunctionalInterface
interface MyFunction{
void myMethod();
}
메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다는 뜻이다.
void aMethod(MyFunction f){
f.myMethod();
}
...
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
또는 참조변수 없이 아래와 같이 직접 람다식을 매개변수로 지정하는 것도 가능하다.
aMethod( ()->System.out.println("myMethod()") ); //람다식을 매개변수로 지정
메서드의 반환타입이 함수형 인터페이스타입이라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환한거나 람다식을 직접 반환할 수 도 있다.
MyFunction myMethod() {
MyFunctino f = () -> {};
return f;
}
람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것이다.
함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 아래와 같이 형변환이 필요하다.
MyFunction f = (MyFunction)( ()->{} ) ; //양변의 타입이 다르므로 형변환 필요
람다식은 이름이 없을 뿐 분명히 객체인데도, 아래와 같이 Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.
Object obj = (Object)(()->{}); //에러.
굳이 Object 타입으로 형변환하려면, 먼저 함수형 인터페이스로 변환해야 한다.
Object obj = (Object)(MyFunction)( () -> {} ) ;
String str = ( (Object)(MyFunction)( () -> {} ) ).toString();
람다식의 타입은 외부클래스이름$$Lamda$번호
와 같은 형식으로 되어 있다.
람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 앞서 익명클래스에서 본것과 동일하다.
대부분의 메서드는 타입이 비슷하다. 매개변수가 없거나 한 개 또는 두개, 반환값은 없거나 한개, 게다가 지네릭 메서드로 정의하면 매개변수나 반환타입이 달라도 문제가 되지 않는다.
그래서 java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
java.lang.Runnable | void run() | 매개변수도,반환값도 없음 |
Supplier<T> | T get() | 매개변수는 없고 반환값만 있음 |
Consumer<T> | void accept(T t) | 매개변수만 있고, 반환값이 없음 |
Function<T,R> | R apply(T t) | 일반적인 함수, 하나의 매개변수를 받아서 결과를 반환 |
Predicate<T> | boolean test(T t) | 조건식을 표현하는 데 사용, 매개변수는 하나 반환타입은 boolean |
Predicate는 Function의 변형으로 반환타입이 boolean이라는 것만 다르다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";
if(isEmptyStr.test(s)) //if(s.length()==0)
System.out.println("This is an empty String.");
접두사 Bi
가 붙는다.
두개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다.
Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
UnaryOperator<T> | T apply(T t) | Function의 자손, Function과 달리 매개변수와 반환의 타입이 같다. |
BinaryOperator<T> | T apply(T t,T t) | BiFunction의 자손, BiFunction과 달리 매개변수와 결과 타입이 같다. |
컬렉션 프레임웍의 인터페이스에 다수의 이폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용한다.
효율적으로 처리할 수 있도록 기본형을 사용하는 함수형 인터페이스들이 제공된다.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
DoubleToInFunction | int applyAsint(double d) | AToBFunction은 입력이 A타입 출력이 B타입 |
ToIntFunction<T> | int applyAsInt(T value) | ToBFunction은 출력이 B타입, 입력은 지네릭타입 |
IntFunction<R> | R apply(T t,U u) | AFunction은 입력이 A타입이고 출력은 지네릭 타입 |
ObjintConsumer<T> | void accept(T t,U u) | ObjAFunction 은 입력이 T,A타입이고 출력은 없다. |
java.util.function패키지의 함수형 인터페이스에는 추상메서드 외에도 디폴트 메서드와 static메서드가 정의되어 있다.
두 람다식을 합성해서 새로운 람다식을 만들 수 있다. 두 함수의 합성은 어느 함수를 먼저 적용하느냐에 따라 달라진다. f,g에서 f.andThen(g)는 함수 f를 먼저 적용하고 그 다음에 함수 g를 적용한다. 그리고 f.compose(g)는 반대로 g를 먼저 적용하고 f를 적용한다.
default <V> Function<T,V> andThen(Function<? super R,? extends V> after )
default <V> Function<V,R> compose(Function<? super V,? extends T> before)
여러 predicate를 and(),or(),negate()로 연결해서 하나의 새로운 predicate로 결합할 수 있다.
Predicate<Integer> all = notP.and(i->i<200).or(i->i%2==0);
static메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다. 먼저 isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.