자바에서는 함수형 프로그래밍(functional programming)
을 지원하기 위해서 Java 8
부터 람다식(Lambda Expressions)
을 지원하고 있습니다.
함수형 프로그래밍에대한 설명은 이 포스트를 참조해주세요.
람다식
은 다음과 같이 작성합니다. 매개변수를 전달하여 해당 데이터를 처리하는 부분으로만 이루어져 있습니다.
(매개변수, ...) -> {
//처리할 코드
}
만약 처리 코드가 한 줄이면 중괄호를 생략하고 표현할 수 있습니다.
(매개변수, ...) -> //처리할 코드
생긴 것을 보면 아시겠지만 익명 함수 형태로 함수를 아주 간단하게 표현하고 있습니다. 자바는 이러한 람다식을 익명 구현 객체로 변환해서 해석합니다.
예를들어 다음과 같은 인터페이스가 있다고 가정합니다.
public interface Calculable {
void calculate(int x, int y);
}
딱봐도 매개변수 두 개를 받아서 어떠한 계산을 하는 인터페이스죠? 이 인터페이스를 람다식으로 표현하면 다음과 같습니다.
(x, y) -> {
//calculate 처리 내용
}
위에서 람다식은 내부적으로
익명 구현 객체
로 변환한다고 했는데 위에서 본 람다식을 변환하면 아래 코드처럼 됩니다.new Calculable() { @Override public void calculate(int x, int y) { //구현된 calculate 처리 내용 } };
람다식과 익명 구현 객체를 비교해보면 정말 간단하게 표현이 되었음을 알 수 있죠?
익명 구현 객체를 람다식으로 표현할 때 주의할 점이 하나 있는데 바로 인터페이스가 단 하나의 추상 메소드만을 가져야합니다. 둘 이상의 추상 메소드를 가졌다면 람다식으로 표현할 수 없습니다. 왜냐하면 위에서 봤듯이 인터페이스 생성자나 메소드 선언부를 모두 생략하고, 메소드의 매개변수와 처리코드만 표현하기 때문에 둘 이상의 메소드라면 표현할 수 없기 때문입니다.
그래서 람다식으로 표현할 수 있는 단 하나의 추상 메소드를 가진 인터페이스를 함수형 인터페이스
라고 부릅니다.
컴파일러에게 인터페이스가 함수형 인터페이스를 보장하기 위한 @FunctionalInterface
어노테이션도 함께 지원하고 있습니다. @Override 처럼 어노테이션은 선택사항이지만 명확한 프로그래밍을 위해서는 붙여주는 것이 권장됩니다.
위에서 만든 인터페이스를 통해 람다식을 실제로 구현해보겠습니다.
@FunctionalInterface
public interface Calculable {
void calculate(int x, int y);
}
public class Main {
public static void main(String[] args) {
Calculable addition = (x, y) -> {
System.out.println(x + y);
};
Calculable subtraction = (x, y) -> {
System.out.println(x - y);
};
addition.calculate(5, 5);
subtraction.calculate(5, 5);
}
}
코드에서 보시다시피 람다식은 인터페이스 타입이기 때문에 다음 코드와 같이 인터페이스 타입의 매개변수로도 활용할 수 있습니다. 아래 코드는 완전히 동일한 결과를 가져다 줍니다.
public class Main {
public static void main(String[] args) {
getResult(((x, y) -> {
System.out.println(x + y);
}));
getResult(((x, y) -> {
System.out.println(x - y);
}));
}
public static void getResult(Calculable calculable) {
calculable.calculate(5, 5);
}
}
만약 함수형 인터페이스에 매개변수가 없다면 람다식은 다음과같이 표현됩니다.
() -> {
//처리할 코드
}
@FunctionalInterface
public interface Printable {
void printMessage();
}
public class Main {
public static void main(String[] args) {
Printable printable = () -> System.out.println("매개변수가 없는 람다식");
printable.printMessage();
}
}
매개변수가 있는 람다식은 처음 예제처럼 사용하시면 됩니다. 다만 두 가지 추가적인 기능이 있습니다.
첫번째는 매개변수 타입입니다. 타입을 생략하고 작성하는 것이 일반적이지만 타입을 적어줄수도 있습니다. 이때 기본형, 참조형 타입을 적을수도 있고 var
라는 타입 키워드를 붙일 수도 있습니다.
(타입 매개변수) -> {}
(var 매개변수) -> {}
(매개변수) -> {}
두번째는 만약 매개변수가 하나라면 괄호를 생략할 수 있다는 점입니다. 만약 매개변수 괄호를 쓰지 않는다면 타입을 적을 수 없게됩니다.
(매개변수) -> {}
매개변수 -> {}
지금까지는 모두 리턴이없는 void
로 예제를 만들었었는데요. 만약 리턴값이 있다면 어떻게 작성할까요?
이 역시도 간단합니다. return문만 적어주면됩니다. 만약 바로 리턴을 하는 명령이라면 return을 생략할수도 있습니다.
() -> {
//처리할 코드
return 값;
}
() -> 값;
아까의 인터페이스를 조금 수정한 코드입니다.
@FunctionalInterface
public interface Calculable {
int calculate(int x, int y);
}
public class Addition {
public void getResult(Calculable calculable) {
int result = calculable.calculate(5, 5);
System.out.println(result);
}
}
public class Main {
public static void main(String[] args) {
Addition addition = new Addition();
addition.getResult(((x, y) -> {
return x + y;
}));
}
}
다음과 같이 축약도 가능합니다.
public class Main { public static void main(String[] args) { Addition addition = new Addition(); addition.getResult(((x, y) -> x + y)); } }
람다식이 메소드를 참조한다는 것은 메소드를 참조해서 매개변수의 정보나 리턴 타입을 알아낸 후 불필요한 매개변수를 제거하는 것을 목적으로 참조합니다.
정적 메소드
의 참조는 클래스 이름 뒤에 ::
을 붙이고 정적 메소드 이름을 적습니다.
(x, y) -> Math.max(x, y);
//를 다음과 같이 참조해서 축약
Math :: max
인스턴스 메소드
의 참조는 객체를 생성한 후 동일하게 참조변수 뒤에 ::
를 붙이고 메소드를 이름을 적습니다.
참조변수 :: 메소드명
정적 메소드 참조는 Math.max()
를 통해 알아봤으니 인스턴스 메소드 참조를 예제 코드로 확인해보겠습니다. 아까 사용했던 Calculable과 Addition
을 재활용합니다.
public class Calculator {
public static int staticCalculation(int x, int y) {
return x + y;
}
public int instanceCalculation(int x, int y) {
return x + y;
}
}
public class Main {
public static void main(String[] args) {
Addition addition = new Addition();
addition.getResult(Calculator::staticCalculation);
Calculator calculator = new Calculator();
addition.getResult(calculator::instanceCalculation);
}
}
매개변수가 메소드를 가지고 있는 경우 매개변수의 메소드를 호출해서 다른 매개변수를 메소드의 인수로 사용할 수도 있습니다. 이 코드 또한 메소드 참조 코드로 변경할 수 있습니다.
(x, y) -> {x.method(y)}
클래스::method
생성자도 메소드의 일종이기에 생성자도 또한 메소드 참조로 만들 수 있습니다. 생성자를 참조하는 과정이 단순하게 객체 생성 후 리턴이라면 이를 생성자 참조 코드로 변경할 수 있습니다.
() -> new 생성자()
클래스::new
생성자는 보통 매개변수에 따라 오버로딩되어있는데, 이 경우 함수형 인터페이스의 추상 메소드와 동일한 매개변수 갯수, 타입을 가진 생성자를 따라서 생성자 참조를 합니다.