발단
아직 메서드 참조와 람다 표현식의 익숙하지 않다보니 메서드 참조 <-> 람다 표현식으로 바꿔보는 작업을 매번 진행하는데
stream 응용 문제를 풀다가 reversed() 메서드 사용에 문제가 발생했다.
거래내역(Transaction)을 특정 조건에 따라 정렬하는 과정에서
Transaction::getValue 를 t-> t.getValue()로 바꾸자 컴파일 에러 발생한 것
Obecjt 에서getValue() 메서드를 찾을 수 없다고 한다. reversed()를 붙이지 않았을 경우에는 멀쩡히 잘 찾아오던 것이
reversed() 메서드를 사용하니 메서드를 찾을 수 없다고 나오는 상황
메서드 참조와의 차이가 무엇인지 반환되는 타입을 확인해보자
메서드 참조를 사용한 경우에는 Comparator<Transaction> 으로 Transaction이 명시되어 있지만
람다를 사용한 경우에는 Comparator<Object>로 Object 타입으로 명시되어 있는 것을 확인 할 수 있다.
이는 컴파일러의 타입 추론에 문제가 있음을 나타낸다
자바의 제네릭 타입 추론 방법
기본적으로 자바 컴파일러는 제네릭 타입을 명시하지 않으면 Object 타입으로 간주한다.
다음과 같은 코드에서 제네릭 타입의 추론 동작을 확인 할 수 있다.
- Trader와 ArrayList Class 간의 접점은 오로지 Object 클래스이고 따라서 컴파일러는 동일한 타입의 T를 추론해야 하기에 Object Type 추론
- HashSet과 ArrayList의 경우 AbstractCollection을 상속 받은 AbstractSet과 AbstractList를 상속 받기 때문에 최하단 상속 클래스인 AbstractCollection<String>으로 Type 추론
- 두가지 모두 String이기에 String Type 추론
Comparator 코드의 분석
- sorted의 spec
Stream<T> sorted(Comparator<? super T> comparator);
여기서 T는 List에 담긴 객체의 타입을 의미한다. 즉, 객체 타입(혹은 객체 타입을 상속받는)의 비교 로직을 가진 Comparator를 사용해 정렬한다고 볼 수 있다.(compare의 구현)
또한, 중개연산이기에 파이프라인을 형성 할 수 있도록 Stream 타입을 반환한다.
- comparing의 spec
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
- 인자로 Function<T,R>(T->R를 반환하는 함수형 인터페이스)을 받음
- T와 U 두가지의 제네릭 타입을 가짐
- U extends Comparable<? super U> 👉 U가 Comparable<U> 인터페이스를 구현해야 함을 의미(compareTo 메서드 구현)
- ex) Integer, String의 경우 각 각 Comparable<Integer> , Comparable<String> 을 구현함
- Compartor<T> 타입을 리턴함으로써 비교하고자 하는 대상(T)에 해당하는 Comparator를 반환
정리하자면, comparing 내부에 람다식으로 어떤 객체를, 어떤 기준으로 비교할 건지 넘겨주면 (이 때, 기준에 해당하는 리턴 값의 타입은 위에서 본 제네릭 U의 타입을 따라야 하므로 compareTo를 구현한 클래스여야 함) 내부적으로 비교 로직이 완성된 Comparator가 반환된다는 것이다.
Compiler의 타입 추론이 왜 실패할까?
컴파일러의 추론은 두단계를 한번에 추론 할 수 없기 때문 그것을 단적으로 볼 수 있는 예제를 보자
Comparator<Integer> i = Comparator.naturalOrder();
Comparator<Integer> i2 = i.reversed();
Comparator<Integer> i3 = Comparator.naturalOrder().reversed();//에러
Comparator<Integer> i4 = Comparator.<Integer>naturalOrder().reversed();
전부 똑같은 것을 의미하지만 i3만 에러가 난다. 무엇이 다를까?
- i1에서 Comparator.naturalOrder()로 정상적으로 Integer Type을 추론했다.
- 그리고 정상적으로 추론 된 type을 기반으로 reveresed() 메서드를 호출 하는데 성공했다.
- i3에서 이것을 동시에 처리하려고 하자(2단계 추론)
- Comparator<Integer>이 필요한데 Comparator<T>가 공급되고 있다고 한다.
- naturalOrder()에서 반환되는 타입을 Integer로 명시해주자 다시 에러가 사라진 모습을 확인 할 수 있다.
결론
결론적으로 reversed()의 사용으로 인해 정상적인 추론 로직이 깨져 Object 형태로 컴파일러가 추론한다는 것.
이를 두고 컴파일러의 추론 매커니즘의 약점이라고도 한다.
이러한 약점을 해결하기 위한 여러가지 방법이 존재한다.
//1
.sorted(Comparator.comparing(Transaction::getValue).reversed())
//2
.sorted(Comparator.comparing((Transaction t)-> t.getValue()).reversed())
//3
.sorted(Comparator.comparing(t-> ((Transaction)t).getValue()).reversed())
//4
.sorted(Comparator.<Transaction,Integer>comparing(t-> t.getValue()).reversed())
//5
.sorted(Comparator.comparing(t-> t.getValue(),Comparator.reverseOrder()))
- 메서드 참조를 사용한 식으로 변경하여 작성 -> 가장 추천되는 방식
- 람다 표현식에서 Transaction을 explicit type (명시적 타입)으로 선언
- Object t를 Transaction으로 다운캐스팅
- 제네릭 메서드인 comparing의 explicit type 선언
- comapring 메서드의 2개의 인자를 받는 방식으로 변경
'Java' 카테고리의 다른 글
[Base64] 빼앗긴 Parameter 찾습니다. (3) | 2024.10.11 |
---|---|
[Transactional] 거래가 왜 없었을까요? (0) | 2024.08.18 |
[Transactional] 거래가 있었는데요.. 없었습니다 (2) | 2024.08.17 |
[Java의 동시성] - Java에서 동시성 문제를 해결하는 방법 (1) | 2023.05.07 |
[동시성 문제] - 동시성 문제란 무엇이며 어떻게 해결해야할까? (0) | 2023.05.01 |