✨ 핵심 정리
• 일반적으로 매개변수 수가 같을 때는 다중 정의를 피하는 게 좋다.
• 어쩔 수 없는 경우(ex. 생성자)라면 헷갈릴만한 매개변수는 형변환하라.
• 불가능하다면 같은 객체를 입력받는 다중 정의 메서드들이 모두 동일하게 동작하게 만들어라.
📌 Overriding
💡 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택된다.
class Wine {
String name() { return "포도주"; }
}
class SparklingWine extends Wine {
@Override String name() { return "발포성 포도주"; }
}
class Champagne extends SparklingWine {
@Override String name() { return "샴페인"; }
}
public class Overriding {
public static void main(String[] args) {
List<Wine> windList = List.of(
new Wine(), new SparklingWine(), new Champagne()
);
for (Wine wine : windList)
System.out.println(wine.name());
}
}
포도주
발포성 포도주
샴페인
- 메서드를 재정의했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지 기준이 된다.
- 컴파일 타임에 인스턴스 타입이 무엇이었는지는 상관 없다.
- 언제나 '가장 하위에서 정의한' 재정의 메서드가 실행된다.
📌 Overloading
💡 다중 정의가 어느 메서드를 호출할 지는 컴파일 타임에 정해진다.
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> lst) {
return "리스트";
}
public static String classify(Collection<?> c) {
return "그 외";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
그 외
그 외
그 외
- 컴파일 타임에서 for문의 c는 항상 Collection<?> 타입이다.
- 런타임에서 매번 타입이 달라지지만, 호출할 메서드를 선택하는데 영향을 주지 못한다.
- 다중 정의에선 오직 매개변수의 컴파일타임 타입에 결정된다.
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
- 위 문제는 세 개의 다중 정의 메서드를 하나로 합친 후, instanceof로 명시적 검사를 수행하면 해결된다.
📌 다중 정의 주의 사항
💡 다중 정의가 혼동을 일으키는 상황을 피해라
1️⃣ 안전하고 보수적으로 가려면 매개변수 수가 같은 다중 정의는 만들지 마라
- 가변인수(varargs)를 사용하는 메서드라면 다중 정의를 아예 사용하면 안 된다.
2️⃣ 다중 정의하는 대신 메서드 이름을 다르게 지어주는 게 나을 수도 있다
- ObjectOutputStream 클래스의 write 메서드는 다중 정의가 아닌, 모든 메서드에 다른 이름을 지어주었다.
- writeBoolean(boolean), writeInt(int), writeLong(long), ...
- 명확할 뿐더러, read 메서드의 이름과 짝을 맞추기 좋다.
3️⃣ 생성자와 같은 경우엔 정적 팩터리 메서드를 활용할 수 있다
- 여러 생성자가 같은 수의 매개변수를 받는 경우를 완전히 피하기 힘들 때는 대비책을 마련하자.
- 매개변수 중 하나 이상이 근본적으로 다르다(radically different)면 된다.
- 두 타입의 null이 아닌 값을 서로 어느 쪽으로든 형변환할 수 없어야 한다.
- 컴파일타임 타입에 영향을 받지 않고, 매개변수들의 런타임 타입만으로 결정되게 된다.
- ex. ArrayList에 int를 받는 생성자와 Collection을 받는 생성자
4️⃣ AutoBoxing과 Generic에 주의하라
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] [-2, 0, 2]
}
}
public interface List<E> extends Collection<E> {
...
boolean remove(Object o);
E remove(int index);
...
}
- List의 경우엔 i를 특정 원소 값이 아닌 index로 취급하여, 예상과 다른 결과가 반환되었다.
- Java 5에서 오토 박싱이 도입되면서, 둘의 구분이 명확했던 이전과 달라 발생하는 문제
- 즉, 제네릭과 오토박싱으로 인하여 두 메서드 매개변수 타입이 더는 근본적으로 다르지 않다.
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i); // 혹은 remove(Integer.valueOf(i))
}
// [-3, -2, -1] [-3, -2, -1]
- 위 문제는 인수를 Integer로 형변환하여 올바른 다중 정의 메서드를 선택하게 만들어주면 된다.
5️⃣ 메서드를 다중 정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다
public class LambdaThread {
public static void main(String[] args) {
// 1. Thread 생성자 호출
new Thread(System.out::println).start();
// 2. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newSingleThreadExecutor();
// exec.submit(System.out::println); // compile error
}
}
- 둘다 System.out::println을 인자로 받고, Runnable을 받는 형제 메서드를 다중 정의하지만 2번만 컴파일 에러가 발생한다.
- ExecutorService의 submit 다중 정의 메서드 중 Callable<T>를 받는 메서드가 있기 때문이다.
- println이 void를 반환하므로, 반환값이 있는 Callable과 헷갈릴 수 없다고 생각하지만 다중정의 해소(resolution; 적절한 다중 정의 메서드를 찾는 알고리즘)는 이렇게 동작하지 않는다.
- 만약 println이 다중정의 없이 하나만 존재했다면 submit 메서드 호출이 제대로 컴파일 됐을 것이다.
- 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 의미를 가진다.
✒️ 부정확한 메서드 참조(inexact method reference) [JLS, 15.13.1]
책에서도 그렇고, 다른 곳에 질문을 해봐도 컴파일러 제작자를 위한 설명이라 그냥 넘어가라고 나와있다.
그래도 간략하게 찾아서 이해한 내용을 정리하자면 다음과 같다.
System.out::println은 다중 정의되어 있어서 부정확한 메서드 참조다.
ExecutorService.submit 또한 다중 정의 되어 있어, 이 경우 자바의 다중 정의 해소 알고리즘이 기대대로 동작하지 못할 수 있다.
즉, 컴파일 단계에서 다중 정의된 메서드 중에 무엇을 사용할지 정해야 하는데, 결정 알고리즘들이 답을 찾지 못한다. (그러려니 하고 받아들여라...근데 그 그러려니 받아들이는 게 너무 힘들다 ㅠ)
추가적인 내용으로 Lambda나 Method reference는 익명 클래스로 구현되어 있지 않다.
실제 구현은 전혀 익명 클래스가 아니며, 내부적으로 invokedynamic 명령, MethodHandle, CallSite 객체 등을 이용해 구현한다.
그런데 자바 사용자 입장에서는 그냥 같다고 봐도 무방하다.