...
람다식 메소드::참조
자바의 람다표현식을 통해 코드 정의를 혁신적으로 줄여주었지만 이보다 더 간략하게 줄이는 문법이 있다.
메소드 참조(Method Reference)는 말 그대로 실행하려는 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 굳이 선언이 불필요한 부분을 생략하는 것을 말한다.
람다식은 복잡하고 길다란 로직보단 기존 메소드들을 단순히 호출만 하는 경우가 많다. 예를 들어 두 개의 값을 받아 작은 수를 리턴하는 Math 클래스의 max() 정적 메소드를 호출하는 람다식은 다음과 같다.
(x, y) -> Math.max(x, y)
이 자체로도 간략해 보이지만, 자바 개발진은 그러지 않은가 보다. 함수 형태를 보면 리턴값 자체가 또다른 Math 클래스의 메소드를 호출하는 것 뿐이고, 람다 함수의 매개변수 역시 그대로 max() 메서드의 매개변수로 들어가기 때문에 코드 중복이 발생하기 때문이다.
따라서 중복되는 매개변수를 없애고, 화살표를 없애고, 클래스가 메소드를 참조하는 기호인 . 기호를 :: 기호로 변환하면 다음과 같이 표현 할 수 있게 된다.
Math::max; // (x, y) -> Math.max(x, y)
메서드 참조 조건과 타입 추론
만약 Math::min 메서드 참조를 변수에 담고 싶다면, 함수형 인터페이스 타입인 IntBinaryOperator 인터페이스를 사용하면 된다. 두 개의 int 형 입력값을 받아 int 값을 리턴하는 함수 디스크립터를 갖고 있기 때문이다.
IntBinaryOperator b = Math::min;
b.applyAsInt(100, 200); // 100
이런식으로 문법 함축이 가능한 이유는 컴파일러가 람다식의 타입을 추론하기 때문이다. 이때 람다 타입 추론은 기존의 람다 익명 함수와는 약간 차이가 있다. 오히려 더 간단한데, 인터페이스의 추상 메서드 형태와 반환 메서드의 시그니처 형태가 같으면 된다.
위의 그림과 같이, IntBinaryOperator 인터페이스의 추상 메서드는 int형 매개변수 2개를 받으며 반환 타입이 int형이고, Math 클래스의 max 메서드 역시 타입이 똑같이 구성되어 있기 때문에, 이를 컴파일러가 추론해서 가능한 것이다.
정리하자면, 람다식의 메소드 참조 문법을 사용하기 위해서는 다음의 3가지 조건을 만족하면 된다.
- 함수형 인터페이스의 매개변수 타입 == 메소드의 매개변수 타입
- 함수형 인터페이스의 매개변수 개수 == 메소드의 매개변수 개수
- 함수형 인터페이스의 반환 타입 == 메소드의 반환 타입
메서드 참조 종류
어떠한 메서드를 참조하여 실행하느냐에 메소드 참조도 종류가 나뉘게 된다.
종류 | 람다 표현식 | 메서드 참조 |
정적 메서드 참조 | ||
인스턴스 메서드 참조 | ||
매개변수의 메서드 참조 | ||
생성자 참조 |
정적 메소드 참조
다음은 Interger 클래스의 parseInt() 정적 메서드를 호출하는 람다식을 메소드 참조로 리팩토링한 예제이다.
정적 메소드를 참조할때, 메서드 참조 :: 기호 앞부분에 클래스명을 그대로 기재하는 것이 특징이다.
Function<String, Integer> stringToInt;
// (x) -> ClassName.method(x)
stringToInt = (s) -> Integer.parseInt(s);
// ClassName::method
stringToInt = Integer::parseInt;
stringToInt.apply("100");
인스턴스 메소드 참조
다음은 ArrayList 클래스의 인스턴스를 만들고 인자로 Collection을 받아 리스트에 요소들을 추가하는 람다식을 메소드 참조로 리팩토링한 예제이다.
인스턴스의 메소드를 참조할때, 메서드 참조 :: 기호 앞부분에 상단에 선언한 인스턴스 변수를 기재하는 것이 특징이다.
ArrayList<Number> list = new ArrayList<>();
Consumer<Collection<Number>> addElements;
// (x) -> obj.method(x)
addElements = (arr) -> list.addAll(arr);
// obj::method
addElements = list::addAll;
addElements.accept(List.of(1, 2, 3, 4, 5));
System.out.println(list); // [1, 2, 3, 4, 5]
매개변수의 메서드 참조
다음은 String 클래스의 인스턴스를 받아 문자열의 길이를 구하는 람다식을 메소드 참조로 리팩토링한 예제이다.
매개변수의 메소드를 참조할때, 메서드 참조 :: 기호 앞부분에 정적 메소드 참조와 똑같이 매개변수의 클래스 타입명을 기재하는 것이 특징이다.
Function<String, Integer> size;
// (obj, x) -> obj.method(x)
size = (String s1) -> s1.length();
// ClassName::method
size = String::length;
size.apply("Hello World"); // 11
생성자 참조
생성자도 일종의 메소드 이기 때문에 메서드 참조가 가능하다. 리턴값이 단순히 객체를 생성하는 것이기 때문에 적용이 가능한 것이다. 대신 생성자는 고유 메서드명이 없기 때문에 new 로 표시한다.
BiFunction<Integer, Integer, Object> constructor;
// (x, y) -> new ClassName(x, y)
constructor = (x, y) -> new Object(x, y);
// ClassName::new
constructor = Object::new;
이때 생성자 오버로딩을 통해 매개 변수 개수에 따라서 인스턴스화를 구현하려고 할때, 람다식을 각기 다른 함수형 인터페이스에 대입하여 실행시켜야 한다.
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
class Customer {
String name;
int age;
public Customer() {
}
public Customer(String name) {
this.name = name;
}
public Customer(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
// 생성자 인자 없이 생성할때
Supplier<Customer> function1 = Customer::new;
Customer customer1 = function1.get();
// 생성자 인자 1개로만 생성할때 (입력타입, 생성자클래스)
Function<String, Customer> function2 = Customer::new;
Customer customer2 = function2.apply("홍길동");
// 생성자 인자 2개로만 생성할때 (입력타입1, 입력타입2, 생성자클래스)
BiFunction<String, Integer, Customer> function3 = Customer::new;
Customer customer3 = function3.apply("홍길동", 55);
}
이를 고급 응용하자면 Map 컬렉션에 생성자 람다를 넣어서 인스턴스 관리를 통해 '지연 인스턴스화' 기법을 구현할 수 있다. 이 기법은 다이나믹 팩토리 패턴으로서 리플렉션 대안으로 응용이 가능하다.
class Fruit {}
class Apple extends Fruit {
int count;
public Apple(int count) {
this.count = count;
}
}
class Banana extends Fruit {
int count;
public Banana(int count) {
this.count = count;
}
}
class Constructor {
public static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("banana", Banana::new);
}
public static Fruit makeFruit(String name, int count) {
if(map.containsKey(name)) {
return map.get(name.toLowerCase()).apply(count);
}
return null;
}
}
public static void main(String[] args) {
Apple apples = (Apple) Constructor.makeFruit("apple", 10);
Banana bananas = (Banana) Constructor.makeFruit("banana", 4);
}
부록) 자바스크립트의 메소드 참조
자바스크립트의 화살표 함수도 사실 람다 표현식의 일종이다. 그래서 자바와 같이 화살표 함수에 대한 메서드 참조가 가능하다.
List<Number> list = Arrays.asList(1, 2, 3, 4, 5, 6);
// 기존 람다 방식
list.forEach( (e) -> System.out.println(e) );
// 메소드 참조 방식
list.forEach(System.out::println);
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// 기존 콜백 방식
arr.forEach( (e) => console.log(e) );
// 메소드 참조 방식
arr.forEach(console.log);
그런데 사실 이 부분은 메서드 참조 같은 고급진 기능이 아니라, 자바스크립트의 함수 선언식 자체를 넘겼을 뿐이다.
자바스크립트로 브라우저의 이벤트를 등록할때 다음과 같이 addEventListener의 핸들러로 바로 화살표 함수를 정의해줘도 되지만 한단계 거쳐 먼저 함수를 선언하고 함수 자체를 넘겨줘본 경험이 있을 것이다.
const handler = () => {};
window.addEventListener('click', handler);
addEventListener 메서드가 매개변수로 callback 함수를 받아 내부에서 실행시켜주기 때문에 이러한 수식이 가능한 것이다.
자바스크립트의 forEach 구현
그래서 forEach를 간단하게 직접 구현해보면 아래와 같이 될 것이다. 자바스크립트 함수 매개변수로 함수 자체를 받아 함수 내부에서 매개변수를 callback() 괄호를 붙여 함수 호출을 해주는 것이다.
function forEach(arr, callback) {
for(const i of arr) {
callback(i); // console.log(i)
}
}
const arr = [0, 1, 2, 3, 4, 5];
forEach(arr, console.log);
자바의 forEach 구현
이러한 원리가 고대로 자바의 람다에서도 적용된다라고 생각하면 된다. 특정 함수형 인터페이스 타입으로 매개변수를 받고 매개변수의 추상 메서드를 호출함으로써 콜백이 실행되게 되는 것이다.
다음은 forEach 동작을 자바로 아주 간단하게 구현해본 코드이다.
public static void forEach(int[] arr, Consumer<? super Number> callback) {
for (int i : arr) {
callback.accept(i); // System.out.println(i)
}
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
forEach(arr, System.out::println);
}
# 참고자료
https://dev-kani.tistory.com/38
https://steady-coding.tistory.com/307
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.