핵심 요약
매개변수 수가 같을 경우 오버로딩을 피하자. 그러나 구현해야 할 경우, 형변환을 통해 명확히 선택하도록 하자. 그것이 불가능하다면 같은 객체을 입력받는 오버로딩 메소드들에 대해 모두 동일하게 동작하도록 하자.
재정의(Overriding)와 다중정의(Overloading)
: 재정의한 메소드는 동적으로, 다중정의한 메소드는 정적으로 선택된다.
재정의(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> wineList = List.of(
new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : wineList)
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)); // "그 외", "그 외", "그 외"
}
}
Collection<?>[] collections의 각 요소는 Collection<?> 타입이므로 컴파일 타임에는 어떤 구체적인 컬렉션 타입인지 알 수 없다. 따라서 classify(Set<?>), classify(List<?>), classify(Collection<?>) 중에서 Collection<?>을 매개변수로 받는 메서드가 항상 선택된다.
public class FixedCollectionClassifier {
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
...
}
의도한대로 동작하도록 하기 위해선 위처럼 classify 메소드를 하나로 합친 후 instanceof로 명시적으로 검사하면 해결할 수 있다. 이렇게 하면 컬렉션의 실제 타입에 따라서 적절한 문자열을 반환하게 된다.
다중정의(Overloading) 주의할 점
1. 매개변수가 같은 다중정의는 만들지 말아야 한다.
2. 가변인수를 사용하는 메소드는 다중정의를 아예 하지 말아야 한다. (아이템 53 예외 존재)
가변 인수 메소드를 다중 정의할 경우, 의도치 않은 다른 오버로딩 메소드가 호출될 수 있다.
public void foo(int i) {
System.out.println("int version");
}
public void foo(int... i) {
System.out.println("varargs version");
}
위 예시는 foo(1, 2)를 호출하면 "varargs version"이 출력되지만, foo(1)을 호출하면 foo(int i)가 호출된다.
3. 다중정의 대신 메소드 이름을 다르게 하는 방법을 고려하자.
ex. readBoolean(), readInt(), readLong()
4. 생성자를 오버로딩할 경우 정적 팩토리를 활용하자.(item 1)
5. 오토박싱과 다중정의가 상호작용할 때 주의하자.
(매개변수 타입을 명시적으로 지정하자)
아래 예시를 보자.
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]
}
}
오토박싱은 Java5에서 도입된 기능이다.
위 예시에서 List<Integer> 타입의 list 객체는 remove(int index)와 remove(Object o) 두 가지 메소드를 가진다. remove(int index) 메소드는 인덱스를 받아 해당 위치의 원소를 제거하는 메소드이고, remove(Object o) 메소드는 특정 객체를 받아 그 객체와 동일한 값을 가지는 원소를 제거하는 메소드이다.
list.remove(i)를 호출하면 i는 int 타입이므로 remove(int index)가 호출된다. 따라서 원소가 아닌 지정한 위치의 원소가 제거되며, 예상치 못한 결과가 발생한다.
for(int i = 0; i < 3; i++){
set.remove(i);
list.remove((Integer) i); // 혹은 list.remove(Integer.valueOf(i))
그러나 위처럼 수정하면 i가 Integer 타입으로 캐스팅되어 remove(Object o)가 호출된다. 즉, 의도한대로 동작한다.
즉, 오토박싱이 도입되지 않은 버전에서는 발생하지 않은 문제가 이후 버전에서는 발생할 수 있다. 자바 4까지는 Object와 int가 형변환될 수 없었기 때문이다.
✔ 근본적으로 다른 타입의 매개변수를 가진 메소드가 호출될 경우, 런타임에 결정된다.
근본적으로 다르다면(어느 쪽으로든 형변환할 수 없다면) 어느 다중정의 메소드를 호출할지 매개변수들의 런타임 타입만으로 결정된다. 즉, 컴파일 타임 타입에는 영향을 받지 않게 된다.
6. 람다/메소드 참조가 오버로딩과 같이 사용될 경우
메소드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.
// 1. Thread의 생성자 호출
new Thread(System.out::println).start();
// 2. ExecutorService의 submit 메서드 호출
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); // 컴파일 에러
new Thread(System.out::println).start()에서는 Thread의 생성자에 System.out::println이라는 메소드 참조를 전달하고 잇다. Thread 클래스의 생성자 중 하나는 Runnable 인터페이스를 인자로 받는데, System.out::println은 Runnable의 run() 메소드에 맵핑될 수 있다. 따라서 정상동작한다.
그러나 exec.submit(System.out::println)에서는, ExecutorService의 submit 메소드에 System.out::println 메소드 참조를 전달할 경우, 컴파일 에러가 발생한다. 이는 submit 메소드가 다중정의되어 있고, 그 중 하나는 Callable<T> 인터페이스를 인자로 받고, 다른 하나는 Runnable 인터페이스를 인자로 받기 때문이다. System.out::println 메소드 참조가 이 두 인터페이스 중 어느 것에 매핑될지 컴파일러가 판단할 수 없어서 발생한 문제이다.
이처럼 람다/메소드 참조를 사용할 경우 메소드나 생성자가 오버로딩된 경우에 주의해야 한다. 다른 타입의 함수형 인터페이스를 인수로 받는 다중정의된 메소드들이 있을 때, 람다/메소드 참조를 사용하면 컴파일러가 어떤 메소드를 호출할지 판단하기 어려울 수 있다.
해결 방법 중 하나는 람다 표현식 내에서 명시적으로 메소드를 호출하는 것이다. exec.submit(() -> System.out.println());와 같이 작성하면 Runnalbe 인터페이스를 기대하는 submit 메소드를 호출한다.
(Callable<T>는 T call() 메소드, Runnable은 void run() 메소드이므로, 인자를 받지 않고 반환하지 않는 run() 메소드에 대응된다)
7. 인수 포어딩하여 올바르게 동작시키기
상대적으로 더 특수한 다중정의 메소드에서 덜 특수한 다중정의 메소드로 넘기는 것
아래 예시를 보자.
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
String 클래스의 contentEquals 메소드는 StringBuffer와 CharSequence 두 가지 버전이 있다. StringBuffer 버전은 단순히 인자를 CharSequence로 캐스팅하고, 이를 CharSequence 버전에게 전달한다. 이런 방식으로 호출된 메소드는 런타임 타입에 따라 올바르게 동작하게 된다.
반면 아래 예시를 봐보자.
// 잘못된 설계 - 일관되지 않은 동작을 수행
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
String 클래스에는 valueOf 메소드가 다중정의되어 있다. Object와 char[] 타입의 두 가지 버전이 있는데, 같은 객체를 인수로 넘겨도 전혀 다른 동작을 수행한다.
Object 버전은 인수가 null일 경우 "null" 문자열을 반환하고, 아니라면 toString() 메소드를 호출하여 결과를 반환한다. 반면 char[] 버전은 인수를 새로운 String으로 변환하여 반환한다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라. (0) | 2023.07.17 |
|---|---|
| [effective java] 아이템 53. 가변인수는 신중히 사용하라. (0) | 2023.07.17 |
| [effective java] 아이템 51. 메서드 시그니처를 신중히 설계하라. (0) | 2023.07.17 |
| [effective java] 아이템 50. 적시에 방어적 복사본을 만들라. (0) | 2023.07.17 |
| [effective java] 아이템 49. 매개변수가 유효한지 검사하라. (0) | 2023.07.14 |