람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함이다.
그런데 더 간단하게 만드는 방법이 있다.
바로 메서드 참조다.
map.merge(key, 1, (count, incr) -> count + incr);
map.merge(key, 1, Integer::sum);
메서드 참조를 사용하여 코드를 간결하게 만드는 예시이다.
매개변수인 count와 incr는 크게 하는일 없이 공간을 차지한다.
이 람다는 두 인수의 합을 단순히 반환할 뿐이다.
람다 대신 이 메서드의 참조를 전달하면 똑같은 결과를 더 보기 좋게 얻을 수 있다.
하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다.
이런 람다는 길이는 더 길지만 메서드 참조보다 읽기 쉽고 유지보수도 쉬울 수 있다.
람다로 할 수 없는 일이라면 제네릭 함수 타입 구현 외에는 메서드 참조로도 할 수 없다.
service.excute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
하지만 이렇게 메서드 참조보다 람다식이 간결한 경우도 있다.
메서드 참조 쪽은 더 짧지도, 더 명확하지도 않다.
따라서 람다 쪽이 낫다.
메서드 참조의 유형은 정적, 한정적( 인스턴스), 비한정적(인스턴스), 클래스 생성자, 배열 생성자가 있다.
메서드 참조에서 매개변수를 넣으려면 매개변수가 있는 메서드를 참조해야 한다.
예를 들어 리스트의 요소들을 정렬할 떄 Comparator를 사용하는 경우를 살펴보자.
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, (a, b) -> a.compareTo(b));
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, Integer::compare);
compareTo의 람다 표현식과 메서드 참조의 활용 방법이다.
람다 또는 메서드 참조를 사용할 때 컴파일러가 자동으로 매개변수를 매핑해준다.
람다 표현식은 함수형 인터페이스의 추상 메서드를 구현하는 방식으로 사용된다.
함수형 인터페이스는 단 하나의 추상 메서드만을 가지고 있는 인터페이스를 말한다.
람다 표현식에서는 해당 인터페이스의 추상 메서드와 일치하는 매개변수를 작성하고, 그 매개변수를 사용하여 해당 메서드의 구현을 정의한다.
예를들어, Comparator 인터페이스는 compare 라는 하나의 추상 메서드를 가지고 있다.
따라서 람다 표현식 (a,b) -> a.compareTo(b) 에서 매개변수 a와 b는 compare메서드의 매개변수와 일치한다.
컴파일러는 이를 인식하고 compare 메서드를 해당 람다 표현식으로 구현해준다.
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
});
위는 해당 메서드를 람다 표현식이나 메서드 참조를 사용하지 않고 구현한 코드이다.
Collections의 sort 메서드는 오버로딩으로 만들어져있다.
매개변수로 list만 넣어도 정렬이 된다.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
내부 구현은 List의 sort 메서드를 호출하도록 되어있다.
또한 지금 설명할 방식인 Comparator 인터페이스를 구현해서 사용하는 방식이 있다.
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
Comparator 인터페이스는 Java에서 객체들을 비교하는데 사용되는 비교자를 정의하는 인터페이스이다.
객체들의 정렬이나 순서를 결정하기 위해 사용된다.
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
compare과 equals를 정의할 수 있다.
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
});
다시 돌아와 보자.
Collections.sort의 매개변수로 List<T> , Comparaotr<T>가 들어간다.
여기서 람다식을 사용할 수 있는 이유는 Collections.sort 메서드의 두 번째 인자로 Comparator 인터페이스가 지정되어 있기 때문이다.
람다식은 함수형 인터페이스를 표현하는 표현식으로 함수형 인터페이스란 추상메서드를 단 하나만 가지고 있는 인터페이스...인데?...
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
뭐야 야발
그렇다.. Comparator 인터페이스는 Java에서 함수형 인터페이스로 분류되는 특별한 예외 중 하나이다.
이유는 Java8 이전엔 함수형 인터페이스의 개념이 없었지만 이미 많은 코드에서 해당 인터페이스를 사용하고 있기 때문에
이를 함수형 인터페이스로 취급하기로 결정했다.
이렇게해야 기존의 Comparator를 람다식이나 메서드 참조로 간단히 구현할 수 있게 된다.
람다식엔 Java8 이전의 호환성을 위해 로 타입 추가 외에도 해당 사항같은 예외가 있으니 알아두면 좋겠다.
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, (a, b) -> a.compareTo(b));
람다 표현식은 () 엔 인터페이스의 매개변수가, -> 후에는 인터페이스의 구현이 들어가면 된다.
근데 sort의 두번째 매개변수는 Comparator를 구현하는 객체를 넣어야하는데 어떻게 Integer::compareTo가 들어가지??
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Collections.sort(numbers, Integer::compare);
Integer 클래스는 이미 Comparable 인터페이스를 구현하고 있어서 compare 메서드를 가지고 있다.
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
....
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
...
}
Integer::compare는 Comparator 인터페이스의 compare 메서드와 시그니처가 동일하다.
즉 Integer::compare는 Comparator<Integer>의 인스턴스로 간주된다.
잠깐, 근데 Comparable과 Comparator의 관계가 뭔데 시그니쳐가 동일한거야?
Comparable과 Comparator는 자바에서 객체를 비교하는 데 사용되는 인터페이스이다.
public interface Comparable<T> {
int compare(T x, T y);
}
public interface Comparator<T> {
int compare(T o1, T o2);
}
이 두 인터페이스는 모두 int 값을 반환하고, 비교 결과에 따라 음수, 0, 양수를 반환한다.
따라서 시그니처가 동일하다는 의미는 Comparable과 Comparator 모두 두 객체를 비교하기 위해 int 값을 반환하는 compare 메서드를 가지고 있다는 것을 의미한다.
그러나 Comparable은 객체 가체에 비교 방법을 구현하고 있고, Comparator은 외부에서 별도의 비교 로직을 제공하는데 사용된다.
메서드 시그니처는 메서드의 이름, 매개변수의 개수, 매개변수의 타입, 리턴 타입으로 구성된다.
따라서 메서드의 이름이 다르더라도 매개변수의 개수와 타입, 그리고 리턴 타입이 모두 같다면 시그니처가 같은 메서드로간주된다.
이것이 정적 메서드 참조 유형이다.
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
// Integer의 compare 정적 메서드
// 메서드 참조는 클래스이름::정적메서드이름 으로 표현된다.
그럼 Integer::compareTo 는 매개변수 개수가 다른데 어떻게 사용 가능한 것일까?
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
이것은 (비한정적) 인스턴스 메서드 유형이다.
compareTo가 compare을 리턴하기 때문에 컴파일러가 매개변수를 식별해서 compare 메서드를 호출한다고 이해했다.
다만 정확하지 않으니 다음 기회에 다시 한번 다뤄보려고 한다.
메서드 참조 유형 | 예 | 같은 기능을 하는 람다 |
정적 | Integer::parseInt | str -> Integer.parseInt(str) |
한정적(인스턴스) | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
비한정적(인스턴스) | String::toLowerCase | str -> str.toLowerCase() |
클래스 생성자 | TreeMap<K,V>::new | () -> new TreeMap<K,V>() |
배열 생성자 | int[]::new | len -> new int[len] |
질문 1 : 람다와 메서드 참조의 차이점은?
람다는 익명 함수로 특정 기능을 표현하는 코드 블록이다. 주로 함수형 인터페이스의 구현체로 사용되며, 간단한 기능을 표현할 때 유용하다.
람다식은 직접 코드를 작성해야 하기 때문에 복잡한 구현식을 표현하기에는 제한적일 수 있다.
메서드 참조는 메서드의 참조를 직접 사용하는 방식으로, 람다식보다 간결하고 가독성이 좋다.
특정 메서드를 직접 호출하는 것이 아니라, 해당 메서드를 참조하여 람다식 같은 역할을 수행한다.
메서드 참조는 기존에 정의된 메서드를 재사용할 수 있기 때문에 재사용성과 유지보수성을 향상시킨다.
질문 2 : 어떤 경우에 람다식 대신 메서드 참조를 사용해야 할까?
메서드 참조는 람다식보다 더 간결하고 명확한 코드를 작성하는데 도움을 준다.
기존에 이미 구현된 메서드를 활용하여 코드를 간력화 할 때, 메서드가 길거나 복잡할때, 인터페이스의 메서드가 이미 정의되어 있을 때 등
상황에 맞춰 메서드 참조를 사용해야 한다.
단 무조건 적으로 메서드 참조를 사용해야 하는 것은 아니기 때문에 주의해야 한다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
Java. 불변 사용 시 적시에 방어적 복사본을 만들자 (0) | 2024.03.06 |
---|---|
Java. 스트림이란? (2) | 2024.01.02 |
42. 익명 클래스보다는 람다를 사용하라 ( 람다 표현식 ) (0) | 2023.07.31 |
41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 ( 마커 인터페이스 ) (0) | 2023.07.24 |
40. @Override 애너테이션을 일관되게 사용하라 ( @Override ) (0) | 2023.07.24 |