// 예제
public class CollectionClassifier {
public static String classify(Set<?> set) {
return "Set";
}
public static String classify(List<?> list) {
return "List";
}
public static String classify(Collection<?> collection) {
return "Etc";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> collection : collections) {
System.out.println(classify(collection));
}
}
}
// 출력 결과
Etc
Etc
Etc
Collection<?>타입이다.Whopper인 것과 무관하게, 가장 하위에서 정의한 재정의 메서드가 실행 됨 //오버라이딩 예시
public class Whopper {
String patty() {
return "패티 1장";
}
static class TwoStackWhopper extends Whopper {
@Override
String patty() {
return "패티 2장";
}
}
static class ThreeStackWhopper extends Whopper {
@Override
String patty() {
return "패티 3장";
}
}
public static void main(String[] args) {
List<Whopper> whoppers = List.of(
new Whopper(),
new TwoStackWhopper(),
new ThreeStackWhopper()
);
for (Whopper whopper : whoppers) {
System.out.println(whopper.patty());
}
}
}
// 출력 결과
패티 1장
패티 2장
패티 3장
null이 아닌 두 타입의 값을 어느 쪽으로든 서로 형변환이 불가public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
// 예상 결과
[-3, -2, -1] [-3, -2, -1]
// 실제 결과
[-3, -2, -1] [-2, 0, 2]
왜 위와 같은 일이 발생할까?
set.remove(i)의 시그니처는 remove(Object)이다.list.remove(i)는 다중정의된 remove(int index)를 선택하게 된다.list.remove(Integer.valueOf(i))를 하면 remove(Object)로써 동작한다. 해당 예시가 혼란스러웠던 이유는 List<E>인터페이스가 remove(Object)와 remove(int)를 다중정의했기 때문이다.
// 컴파일 오류 안남
new Thread(System.out::println).start();
// 컴파일 오류 남
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(System.out::println);
ExecutorService의 submit이 다중정의 되어 있어 컴파일 오류가 난다.
submit은 Runnable뿐만 아니라, Callable<T>를 받는 메서드도 있다. 하지만, println은 void를 반환할텐데, 왜 Runnable과 Callable을 구분하지 못할까?
println이 다중정의 없이 단 하나만 존재했다면, 해당 submit메서드는 컴파일 에러가 나지 않았을 것이다.위 예시는, 참조된 메서드인 println과 호출한 메서드인 submit이 모두 다중정의되어, 다중정의 해소 알고리즘이 우리의 기대처럼 동작하지 않는 상황이다.
System.out::println은 부정확한 메서드 참조이기 때문이다.부정확한 메서드 참조 & 암시적 타입 람다식
- 목표 타입이 선택되기 전에는 그 의미가 정해지지 않는다.
- 이는 적용성 테스트(applicability test) 때 무시된다. -> 이것이 문제의 원인.
[뇌피셜 주의] 저의 지식 한계로 인한 뇌피셜입니다 ㅠㅠ 틀린부분 있을 시 지적 부탁드립니다!
아무래도..println이 많은 오버로딩 메서드가 있어서 "부정확하다"고 표현하는 것 같은데...
이를 알아보기 위해 조금 더 찾아보던 중, 해당 내용에 대해 조슈아 블로크 선생님이 트위터를 남겨둔걸 발견했다.(해당 트위터 글)
뭐 명확한 답은 얻질 못했지만, 하나의 힌트는 얻을 수 있었다. 바로, println메서드에는 인수를 받지 않는 메서드도 오버로딩되어 구현되어 있다는 점이다.

그러면 System.out::println이 모호한 메서드 참조인 이유는 "인수를 받지 않는 경우"와 "인수를 받는 경우"가 구분이 안가기 때문인 것으로 추측된다.
실제로, 컴파일러도 아래와 같이 println()과 println(boolean)이 서로 구별이 되지 않는다고 경고하고 있다.

그런데, 사실 System.out::println과 같은 메서드 참조는 foreach같은 메서드에서는 잘 쓰고 있지 않았던가..? 왜 위 예시에서는 컴파일 에러가 발생하고, foreach같은 곳에서는 컴파일 에러가 발생하지 않는걸까?
List<String> hi = List.of("하잉", "바잉", "뀨잉");
hi.stream().forEach(System.out::println);
위의 foreach예시를 보자. 이는 컴파일 에러가 발생하지 않는다. 이유는 다음과 같다고 추측된다.
System.out::println이 모호하지 않고 확실히 인자를 받는 메서드이다.즉, 위에서 사용된 System.out::println은 모호하지 않고 확실하다고 볼 수 있다. stream()에서 인자가 넘어오기 때문에, 확실하게 (object) -> System.out.println(object)라고 할 수 있다.
반면에, excutorService.submit(System.out::println)에 사용된 System.out::println은 모호하다고 볼 수 있다.
해당 메서드 참조가 인수를 받는 경우인지, 인수를 받지 않는 경우인지 불확실하기 때문이다.
위에서 언급된 "목표 타입이 선택되기 전에는 그 의미가 정해지지 않는다." 가 여기에 적용되는 말이 아닐까?
submit()의 인수로 넘겨진 System.out::println은 아직 목표 타입이 정해지지 않았다. (Runnable or Callable)System.out::println이 인수를 받는 경우인지, 인수를 받지 않는 경우인지도 정해지지 않았다.따라서, 정해진게 아무것도 없으므로 아주 모호한 상태라는 것이다. 때문에, System.out::println은 submit()에 사용될 수 없다는 뜻이 아닐까? 라고 결론지었다.
아직 한번에 이해하기는 너무 어려운 내용이었던 것 같다. 일단은 조슈아 선생님이 컴파일러 개발자가 아니면 굳이 알지 않아도 된다 했으니.. 일단은 여기까지만 하고 넘어가기로 했다. (생각이 꼬리에 꼬리를 물고 해결이 안되는 지경에 이르렀기 때문 ㅠㅠ)
또 다른 생각
하지만,
submit()은Runnable혹은Callable<T>만 받는다. 그렇다면System.out::println은Runnable로 인식되면 그만 아닌가..? 라는 생각이 든다.위와 같이 생각한 이유는,
System.out::println은Callable<T>의 시그니처를 만들 수 없기 때문이다. 모호하지 않을 수도 있다고 생각했지만, 자바 컴파일러는 아닌가보다 ㅠ
추가 참고 자료
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html
javac -Xlint:overloads FunctionalEx.java
FunctionalEx.java:16: warning: [overloads] <T#1>functionalTest(Consumer<T#1>) in FunctionalEx is potentially ambiguous with <T#2>functionalTest(Predicate<T#2>) in FunctionalEx
static <T> void functionalTest(Consumer<T> consumer) {
^
where T#1,T#2 are type-variables:
T#1 extends Object declared in method <T#1>functionalTest(Consumer<T#1>)
T#2 extends Object declared in method <T#2>functionalTest(Predicate<T#2>)
1 warning
Object외의 클래스 타입과 배열 타입Serializable과 Cloneable외의 인터페이스 타입과 배열 타입String과 Throwable(관련 없는 관계)public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
valueOf(char[])와 valueOf(Object)는 같은 객체를 건네도 전혀 다른일을 수행함결론은 메서드 오버로딩은 조심해서 쓰라는 것 같다. 컴파일러가 구분하지 못하는 이유를 찾아 헤맸으나, 이렇다 할 성과는 보지 못한 것 같다.. 나중에 지식을 더 많이 쌓은 후에 깊은 뜻을 헤아려보도록 해야곘다. (아니면 기선님 강의 존버라도..ㅋㅋㅋ)
참고
https://github.com/SeolYoungKim/effective_java_study/tree/main