람다식은 간단히 말해 메서드를 하나의 '식(expression)'으로 표현한 것이다.
람다식은 함수를 간략하면서도 명확하게 표현할 수 있게 해준다.
자바는 객체지향 언어이지만 JDK1.8부터 함수형 언어를 포함시켰다.
파이썬과 자바스크립트들도 객체지향 언어지만 함수형 언어를 가지고 있는것처럼 말이다.
하스켈이나 스칼라들 보다는 좀 부족하다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'이라고도 한다.
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int)(Math.random()*5)+1);
함수와 메서드의 차이
- 근본적으로는 동일. 함수는 일반적 용어, 메서드는 객체지향개념 용어
- 함수는 클래스에 독립적, 메서드는 클래스에 종속적
그렇다면 람다식은 어떻게 작성할까?
메서드(함수)를 람다식으로 바꾸는 방법은 아주 간단하다. 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->'를 넣어주면 된다.
int max(int a, int b) {
return a > b ? a : b;
}
// 람다식
(int a, int b) -> {
return a > b ? a : b;
}
반환 값이 있는 메서드의 경우, return문 대신 '식(expression)'으로 대신 할 수 있다. 식의 연산 결과가 자동적으로 반환값이 된다. 이때는 '문장(statement)'이 아닌 '식'이므로 끝에 ';'을 붙이지 않는다.
(int a, int b) -> { return a > b ? a : b; }
// 식
(int a, int b) -> a > b ? a : b
람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있다. 대부분의 경우 생략이 가능하다. 반환타입이 없는 이유도 마찬가지로 대부분 추론이 가능하기 때문이다.
(int a, int b) -> a > b ? a : b
// 매개변수 타입 제거
(a, b) -> a > b ? a : b
아래와 같이 선언된 매개변수가 하나뿐인 경우에는 ()도 생략할 수 있다. 하지만 매개변수의 타입이 있다면 괄호는 제거할 수 없다.
// 매개변수의 타입이 없는 경우
(a) -> a * a
// 괄호 제거 가능!
a -> a * a
// 매개변수의 타입이 있는 경우
(int a) -> a * a
// 괄호 제거 안됨!!
int a -> a * a // 에러!
매개변수와 마찬가지로 람다식의 괄호{}안에 들어갈 문장이 하나일 때는 괄호{}를 생략할 수 있다. 이 때도 ;를 붙이지 않는다.
(String name, int i) -> {
System.out.println(name+"="+i);
}
// {}제거
(String name, int i) ->
System.out.println(name+"="+i);
그러나 괄호{} 안의 문장이 한 줄 이더라도 그 문장이 return문이면 생략 불가능하다.
(a, b) -> { return a > b ? a : b; } // OK
(a, b) -> return a > b ? a : b // 에러
자바에서는 함수 혼자 존재할 수 없다.
따라서 아래와 같은식이 있다고 했을 때 이는 이 함수 그 자체로 존재하는 것이 아닌 하나의 객체 안에 메서드가 존재하는 것으로 보면 된다.
(int a, int b) -> a > b ? a : b
// 실제로는 아래처럼 존재.
new Object() {
int max(int a, int b) { // 이름은 임의로 설정.
return a > b ? a : b;
}
}
람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까? 참조변수가 있어야 객체의 메서드를 호출할 수 있으니 일단 이 익명 객체의 주소를 f라는 참조변수에 저장해 보자.
타입 f = (int a, int b) -> a > b ? a : b; // 참조변수의 타입이 무엇이어야 할까?
참조형이니 클래스 또는 인터페이스가 가능하다. 그리고 람다식과 동등한 메서드가 정의돼 있는 것이어야 한다. 그래야 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.
예로 아래와 같은 메서드 max가 선언된 MyFunction인터페이스가 정의돼 있다고 가정하고
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); // 익명 객체의 메서드를 호출
이렇듯 익명 클래스f를 만들어 사용할 수도 있고 max()의 람다식을 만들어 사용할 수도 있다.
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5, 3); // 익명 객체의 메서드를 호출
이처럼 MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction을 구현한 익명 객체의 메서드max()와 매개변수, 반환값이 일치하기 때문이다.
이렇게 하나의 메서드가 선언된 인터페이스를 람다식으로 다룰 수 있다. 이 때, 하나의 메서드가 선언된 인터페이스를 '함수형 인터페이스'라고 부르기로 했다.
@FunctionalInterface
interface MyFunction { // 함수형 인터페이스 MyFunction을 정의
public abstract int max(int a, int b);
}
함수형 인터페이스에는 람다식과 연결짓기 위해 꼭 하나의 추상 메서드만 존재해야 한다.
아래와 같이 함수형 인터페이스임을 알리는 어노테이션도 존재하니 컴파일타임때 오류를 캐치하기 위해 꼭 사용하자
@FunctionalInterface
interface MyFunction {
int max(int a, int b);
int min(int a, int b); // 에러! 두개 선언 불가, 어노테이션 없으면 에러아님.
}
아래와 같은 MyFunction인터페이스가 정의되었을 때,
@FunctionalInterface
interface MyFunction {
void myMethod(); // 추상 메서드
}
메서드의 매개변수가 MyFunction 타입이라면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다는 뜻이다.
void aMethod(MyFunction f) { // 매개변수의 타입이 함수형 인터페이스
f.myMethod(); // f의 myMethod를 호출할것이다.
}
..
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f); // myMethod() 출력될것.
또는 참조변수 없이 아래와 같이 직접 람다식을 매개변수로 지정하는 것도 가능하다.
마치 Arrays.sort(Collection c, Comparator ct)를 구현할 때 처럼 말이다.
amethod(() -> System.out.println("myMethod"));
// myMethod가 출력될 것
또한, 메서드의 반환타입이 함수형 인터페이스라면 이 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.
MyFunction myMethod() {
MyFunction f = () -> {};
return f; // 이 줄과 윗 줄을 한 줄로 합치면, return () -> {};
}
이렇게 메서드를 통해 람다식을 주고받을 수 있는것 처럼 보이지만 근본적으로는 객체를 주고받고 있는것이라 달라진 것은 아무것도 없다. 하지만 코드가 간결하고 이해하기 쉬워졌다고한다... (난 왜 더어렵지?)