자바에서 JDK1.8부터 람다식(lambda expression)을 도입함으로써, 객체지향언어였던 자바가 동시에 함수형 언어가 되었습니다. 이번 포스팅은 람다식에 관해서 작성하였습니다.
람다식이란?
메서드를 하나의 식(expression)으로 표현한 것입니다. 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지게 됩니다. 그래서 람다식을 '익명 함수(anonymous function)'라고도 합니다.
람다식은 메서드를 변형한 형태이지만, 변수처럼 다룰 수 있습니다. 즉, 메서드의 매개변수로 전달되는 것도 가능하고, 메서드의 결과로 반환될 수도 있습니다.
람다식의 형태
람다식은 아래와 같이 메서드를 식(expression)으로 바꿀 수 있습니다.
메서드: 반환타입 메서드이름(매개변수 리스트) { 구현부 }
↓
람다식: (매개변수 리스트) -> {구현부}
예시를 통해 일반 메서드를 람다식 형태로 바꾸어 보겠습니다.
int max(int a, int b) {
return a > b ? a : b;
}
↓
(int a, int b) -> { return a > b ? a : b; }
반환값이 있는 메서드는 return 문 대신 '식(expression)'으로 대신할 수도 있습니다. 그리고 식이 한 줄이라면 중괄호 { }도 생략가능합니다. 만약 식이 아닌 return 문으로 작성하면 중괄호 { }를 생략할 수 없습니다.
식(expression)의 연산결과가 자동으로 반환되고, 식(expression)이기 때문에 ';'는 생략합니다.
(int a, int b) -> a > b ? a : b
(int a, int b) -> { return a > b? a: b; }
(String name, int i) -> System.out.println(name + "=" i)
람다식에 선언된 매개변수의 타입을 컴파일러가 추론할 수 있는 경우 타입을 생략할 수 있습니다. 생략할 경우, 모든 매개변수 타입을 생략해야 합니다. 람다식의 경우 대부분 매개변수 타입을 추론할 수 있어서 대부분 생략하는 편입니다.
(a, b) -> a > b ? a : b
만약 괄호 ( )안에 매개변수가 1개라면 ( ) 또한 생략 가능합니다. 단, 매개변수 타입이 있으면 생략이 불가능합니다.
(a) -> a * a
a -> a * a
(int a) -> a * a
자바에서는 모든 메서드가 특정 클래스 내에 포함되었어야 했습니다. 그렇다면 람다식 또한 클래스에 포함되어 있을 것입니다. 어떤 클래스에 포함되어 있을까요?
사실 람다식은 익명 클래스의 객체와 동등합니다. 즉 아래와 같이 표현할 수 있습니다.
new Object() {
int max(int a, int b) {
return a > b ? a : b;
}
}
그렇다면, 이 람다식은 객체이므로 람다식을 참조하려면 아래와 같이 참조변수가 있어야 할 것입니다.
타입 참조변수 = (int a, int b) -> a > b ? a : b;
이때, 참조변수의 타입은 무엇으로 해야 할까요? 참조형이니까 클래스나 인터페이스가 가능할 것입니다. 또한 람다식과 동등한 메서드가 정의되어 있는 것이어야 할 것입니다. 예를 들어 아래와 같이 max() 메서드가 정의된 인터페이스가 있다고 가정할 때, 이 인터페이스를 구현한 익명 클래스의 객체를 생성해 보겠습니다.
interface MyFunction {
public abstract max(int a, int b);
}
// 익명 클래스의 객체 생성
MyFunction f = new MyFunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
};
int bigNumber = f.max(5,3);
위의 익명 객체를 람다식으로 대체해 보겠습니다.
MyFunction f = (int a, int b) -> a > b ? a : b;
int bigNumber = f.max(5, 3);
이렇게 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체할 수 있는 이유는, 람다식도 실제로는 익명 객체이고 MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수, 반환값이 일치하기 때문입니다.
이렇게, 하나의 추상 메서드가 선언된 인터페이스 (= 함수형 인터페이스)를 정의해서 람다식을 다룰 수 있습니다. 즉, 람다식을 다루기 위한 인터페이스가 함수형 인터페이스입니다.
함수형 인터페이스
아래와 같이 @FunctionalInterface를 붙여 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인할 수 있습니다.
@FunctionalInterface //함수형 인터페이스임을 나타냄
interface MyFunction {
void myMethod();
}
위의 max() 예제에서 살펴보았듯이, 함수형 인터페이스의 추상 메서드를 람다식을 통해 구현할 수 있습니다.
[예시 1]
@FunctionalInterface
interface MyFunction {
void myMethod();
}
public class Main {
public static void main(String[] args) {
MyFunction f = () -> System.out.println("MyMethod");
f.myMethod();
}
}
[결과]
MyMethod
[예시 2]
@FunctionalInterface
interface MyFunction {
void myMethod();
}
public class Main {
public static void main(String[] args) {
MyFunction f = () -> System.out.println("MyMethod");
f.myMethod();
execute(() -> System.out.println("Execute!!"));
MyFunction f2 = getMyFunction();
f2.myMethod();
}
public static void execute(MyFunction mf) {
mf.myMethod();
}
public static MyFunction getMyFunction() {
return () -> System.out.println("Get MyFunction!!");
}
}
[결과]
MyMethod
Execute!!
Get MyFunction!!
외부 변수를 참조하는 람다식
아래와 같이 람다식에서 외부에 선언된 변수에 접근을 한다면, 그 변수는 final로 변할 수 없는 상수여야 합니다.
또한, 람다식의 매개변수는 외부 지역변수와 같은 이름을 가질 수 없습니다.
자바 8 API에서 제공하는 함수형 인터페이스
지금까지는 저희가 직접 함수형 인터페이스를 개발하여 사용하였습니다. 자바 8에서는 개발자들이 최소한의 코드로 개발할 수 있도록 함수형 인터페이스를 여러 패키지에서 제공하고 있습니다. 아래 표는 자바 8 API에서 제공하는 대표적 함수형 인터페이스입니다.
public class Main {
public static void main(String[] args) {
Runnable r = () -> System.out.println("hello");
Supplier<Integer> sup = () -> 3 * 3;
Consumer<Integer> con = num -> System.out.println(num);
Function<Integer, String> fun = num -> "input: " + num;
Predicate<Integer> pre = num -> num > 10;
UnaryOperator<Integer> uOp = num -> num * num;
BiConsumer<String, Integer> bCon = (str, num) -> System.out.println(str + num);
BiFunction<Integer, Integer, String> bFun = (num1, num2) -> "add result: " + (num1 + num2);
BiPredicate<Integer, Integer> bPre = (num1, num2) -> num1 > num2;
BinaryOperator<Integer> bOp = (num1, num2) -> num1 - num2;
}
}
함수형 인터페이스 | 추상 메서드 | 용도 |
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) | 입력을 받아 참/거짓을 단정할 수 있는 인터페이스 |
UnaryOperator<T> | T apply(T t) | 단항(Unary) 연산할 수 있는 인터페이스 |
BiConsumer<T, U> |
void accept(T t, U u) | 이항 소비자 인터페이스 |
BiFunction<T ,U, R> | R apply(T t, U u) | 이항 함수 인터페이스 |
BiPredicate<T, U> | Boolean test(T t, U u) | 이항 단정 인터페이스 |
BinaryOperator<T, T> | T apply(T t, T t) | 이항 연산 인터페이스 |
메서드 참조
람다식을 더욱 간결하게 표현할 수 있는 방법이 있습니다. 람다식이 하나의 메서드만 호출하는 경우에만 가능한데, '메서드 참조(method reference)'를 사용하는 방법입니다.
메서드 참조를 이용하면 람다식을 아래와 같이 표현할 수 있습니다.
class MyClass{
public MyClass() {}
public MyClass(int i){}
public MyClass(int i, String s) {}
}
public class Main {
public static void main(String[] args) {
Function<String, Integer> f= (String s) -> Integer.parseInt(s);
Function<String, Integer> f2 = Integer::parseInt; // 메서드 참조
BiFunction<String, String, Boolean> f3 = (s1, s2) -> s1.equals(s2);
BiFunction<String, String, Boolean> f4 = String::equals; // 메서드 참조
MyClass obj = new MyClass();
Function<String, Boolean> f5 = x -> obj.equals(x);
Function<String, Boolean> f6 = obj::equals; // 메서드 참조
}
}
이렇게 바꿀 수 있는 이유는, 컴파일러가 생략된 부분을 우변의 메서드 선언부로부터 혹은 좌변의 Function 인터페이스로부터 쉽게 알아낼 수 있기 때문입니다.
위 코드에서부터, 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있음을 볼 수 있습니다. 엄밀하게 이야기하면 아래 3가지 유형으로 메서드 참조를 이용할 수 있습니다.
메서드 참조 유형 | 람다식의 인자 | 예제 |
클래스::정적 메서드 | 정적 메서드의 인자가 됩니다. | Integer::parseInt s -> Integer.parseInt(s) |
인스턴스::인스턴스메서드 | 인스턴스 메서드의 인자가 됩니다. | obj::equals, System.out::println x -> obj.equals(x), num -> System.out.println(num) |
클래스::인스턴스메서드 | 첫 번째 인자는 인스턴스가 되고 그 다음 인자(들)는 인스턴스 메서드의 인자(들)가 됩니다. | String::equals (s1, s2) -> s1.equals(s2) |
또한 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있습니다: 클래스::new
Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s2 = MyClass::new; // 메서드 참조
Function<Integer, MyClass> s3 = i -> new MyClass(i);
Function<Integer, MyClass> s4 = MyClass::new; // 메서드 참조
BiFunction<Integer, String, MyClass> s5 = (i, str) -> new MyClass(i,str);
BiFunction<Integer, String, MyClass> s6 = MyClass::new; // 메서드 참조
람다식의 메서드 참조로 아래와 같이 배열을 선언할 수 있습니다.
Function<Integer, int[]> f = x -> new int[x];
Function<Integer, int[]> f2 = int[]::new;
[참고자료]
남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016
김종민, [스프링 입문을 위한 자바 객체 지향의 원리와 이해], 위키북스, 2015
'Java' 카테고리의 다른 글
Networking (네트워킹): IP주소와 URL (0) | 2023.11.05 |
---|---|
자바의 스트림(stream) (0) | 2023.11.05 |
volatile 키워드 (2) | 2023.10.29 |
Lock과 Condition (await()과 signal(),signalAll()) (2) | 2023.10.29 |
wait()과 notify(), notifyAll() (2) | 2023.10.29 |