다중정의 를 꼭 활용할 필요는 없다. 다중정의 대신 메서드 이름을 다르게 지어주는 방법이나 정적 팩터리 사용을 고민해보자. 왜냐하면 자바가 버전업 이 되가면서 개발자가 다중정의 규칙을 완전하게 이해하기는 쉽지 않기 때문이다. (다중정의 선택 규칙은 매우 매우 복잡하다.)
다중정의(overloading) 메서드 호출에 대한 결정은 컴파일 타임에 결정 된다.
public class OverloadingTest {
public static String classify(Set<?> s) {
return "집합";
}
public static String classify(List<?> list) {
return "리스트";
}
public static String classify(Collection<?> c) {
return "그 외";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<Integer>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections) {
// 집합, 리스트, 그외 X
// 그외, 그외, 그외 O
System.out.println(classify(c));
}
}
}
런타임에 변수 c 의 실제 인스턴스 타입은 매번 달라지지만, 호출할 다중정의(overloading) 메서드를 선택하는데는 영향을 주지 못한다.
컴파일 타임에 변수 c 의 타입이 Collection 이기 때문에 classify(Collection<?>) 만 호출 된다.
재정의(overriding) 은 동적으로 선택되고, 다중정의(overloading) 은 정적으로 선택된다.
public class OverridingTest {
static class Wine {
String name() { return "포도주"; }
}
static class SparklingWine extends Wine {
@Override
String name() { return "발포성 포도주"; }
}
static class Champagne extends Wine {
@Override
String name() { return "샴페인"; }
}
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());
}
}
}
재정의(overriding) 는 객체의 런타임 타입이 어떤 메서드를 호출할지 기준이 된다.
메서드 재정의한 다음 하위 클래스의 인스턴스 에서 호출하면 재정의한 메서드가 호출된다.
다중정의와 달리 컴파일 타임에 인스턴스의 타입이 무었이었냐는 기준이 되지 않는다.
다중정의 예시코드 해결방안
public class OverloadingResolveTest {
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그 외";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<Integer>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections) {
// 집합, 리스트, 그외
System.out.println(classify(c));
}
}
}
다중정의 하지 않고 하나의 메서드로 합친 후 instanceof 로 검사하면 간단하게 해결 된다.
다중정의가 혼란을 주는 상황은 피해야 한다.
매개변수 수가 같은 다중정의는 피해야 한다.
가변인수를 사용하는 메서드는 다중정의를 피해야 한다. (#53 )
우리에겐 다중정의 대신 메서드 이름을 다르게 지어주는 길이 있다.
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants
{
public void writeBoolean(boolean val) throws IOException {}
public void writeByte(int val) throws IOException {}
public void writeShort(int val) throws IOException {}
public void writeChar(int val) throws IOException {}
public void writeInt(int val) throws IOException {}
public void writeLong(long val) throws IOException {}
...
}
java.io.ObjectOutputStream 은 다중정의 대신 모두 다른 이름을 지어주는 방식을 택했다.
이는 java.io.ObjectInputStream 의 read 메소드들과 짝을 맞추기도 좋다.
readBoolean(), readInt(), readLong()
생성자는 정적 팩터리를 사용할 수 있다.
생성하는 이름을 다르게 줄 수 없는 문제가 있다.
이는 정적 팩터리(#1 )를 사용하면 어느정도 해결 할 수 있다.
하지만 생상자에서 매개변수 수가 동일한 다중정의는 피할 수 없는데 이는 아래에서 더 알아 본다.
매개변수 수가 같다면, 매개변수가 근본적으로 달라야 한다.
매개변수 중 하나 이상이 근본적으로 다르다(radically different) 면 보다 명확하게 구분이 가능하다.
근본적으로 다르다는 의미는 서로 형변환할 수 없는 것을 의미한다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public ArrayList(int initialCapacity) {}
public ArrayList(Collection<? extends E> c) {}
}
오토박싱.. 혼란의 서막
아래는 컬렉션에 -3 ~ 2 정수를 넣고 음이 아닌 0 ~ 2 를 지우는 목적으로 작성된 코드이다.
public class AutoboxingTest {
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);
}
// set : [-3, -2, -1, 0, 1, 2]
// list : [-3, -2, -1, 0, 1, 2]
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
// set : [-3, -2, -1]
// list : [-2, 0, 2] ???????
}
}
public interface Set<E> extends Collection<E> {
boolean remove(Object o);
}
public interface List<E> extends Collection<E> {
boolean remove(Object o);
E remove(int index);
}
Set 에서는 remove(Object o) 를 사용해서 정해진 원소를 지우는 메서드를 사용했기 때문에 의도한대로 동작했다.
하지만 List 에서 remove 메소드는 remove(Object), remove(int) 로 다중정의 돼있는 상태이므로, 실제 사용된 메소드는 지정된 위치의 원소를 지우는 remove(int) 이기 때문에 의도와 다르게 동작 했다.
이런 오토박싱으로 인한 혼란을 막기 위해서는 아래와 같이 명시적으로 형변환을 해주어야 한다.
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer)i); // or list.remove(Integer.valueOf(i));
}
List 인테페이스는 제네릭과 오토박싱이 추가됨에 따라 위와 같은 취약함이 존재한다.
람다, 메소드 참조.. 혼란 가중
public class LambdaMethodReferenceTest {
public static void main(String[] args) {
// ok
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
// compile error
exec.submit(System.out::println);
}
}
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
}
Reference to 'println' is ambiguous, both 'println()' and 'println(boolean)' match
new Thread(Runnable), submit(Runnable) 모두 존재하지만, submit 은 submit(Callable) 의 다중정의가 있다. (#44 )
println 은 반환 값이 모두 void 이니 당연히 submit(Runnable) 을 선택할 것 같지만, 다중정의 해소 알고리즘 은 우리의 의도처럼 동작하지 않는다.
그 이유는 println 또한 다중정의 되있기 때문에 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 다중정의 돼, 다중정의 해소 알고리즘 이 기대와 다르게 동작하는 것이다.
메소드 오버로딩을 할 때는 서로다른 함수형 인터페이스라도 같은 위치에 인수로 받으면 안된다. 서로 다른 함수형 인터페이스는 앞서 언급한 근복적으로 다르다고 할 수 없기 때문이다.
그렇다면 근본적으로 다르다는 것은 무엇인가
클래스 타입과 배열 타입(Object 제외)
인터페이스 타입과 배열 타입(Serializable, Cloneable 제외)
String과 Throwable 처럼 상/하위 관계가 아닌 두 클래스(관련 없다. unrelated)
String 너마저 ..
public final class StringBuffer
extends AbstractStringBuilder
implements Serializable, Comparable<StringBuffer>, CharSequence
{
// ...
}
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
public boolean contentEquals(CharSequence cs) {}
}
String 에 contentEquals 메서드는 개수, 위치가 동일하고 그리고 상/하위 관계에 있는 다중정의 메서드가 존재한다.
규칙은 어겼지만 모두 완전히 동일한 동작을 수행하는 경우라면 괜찮(어떤게 선택될지는 확실치 않음), contentEquals 는 더 특수한 다중정의 메서드(contentEquals(StringBuffer))에서 덜 특수한(contentEquals(CharSequence)) 다중정의 메서드로 일을 forwarding 한다.
public final class String
implements java.io.Serializable, Comparable<String>, 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 라는 서로 다른 동작을 수행하는 다중정의 메서드가 존재한다. (주의하자.)
다중정의 몇줄 요약
다중정의
를 꼭 활용할 필요는 없다.다중정의
대신 메서드 이름을 다르게 지어주는 방법이나정적 팩터리
사용을 고민해보자. 왜냐하면 자바가 버전업 이 되가면서 개발자가 다중정의 규칙을 완전하게 이해하기는 쉽지 않기 때문이다. (다중정의 선택 규칙은 매우 매우 복잡하다.)다중정의(overloading) 메서드 호출에 대한 결정은 컴파일 타임에 결정 된다.
c
의 실제 인스턴스 타입은 매번 달라지지만, 호출할 다중정의(overloading
) 메서드를 선택하는데는 영향을 주지 못한다.c
의 타입이Collection
이기 때문에classify(Collection<?>)
만 호출 된다.재정의(
overriding
) 은 동적으로 선택되고, 다중정의(overloading
) 은 정적으로 선택된다.overriding
) 는 객체의 런타임 타입이 어떤 메서드를 호출할지 기준이 된다.하위 클래스의 인스턴스
에서 호출하면 재정의한 메서드가 호출된다.다중정의 예시코드 해결방안
instanceof
로 검사하면 간단하게 해결 된다.다중정의가 혼란을 주는 상황은 피해야 한다.
우리에겐 다중정의 대신 메서드 이름을 다르게 지어주는 길이 있다.
java.io.ObjectOutputStream
은 다중정의 대신 모두 다른 이름을 지어주는 방식을 택했다.java.io.ObjectInputStream
의read
메소드들과 짝을 맞추기도 좋다.readBoolean()
,readInt()
,readLong()
생성자는 정적 팩터리를 사용할 수 있다.
매개변수 수가 같다면, 매개변수가 근본적으로 달라야 한다.
radically different
) 면 보다 명확하게 구분이 가능하다.오토박싱.. 혼란의 서막
-3 ~ 2
정수를 넣고 음이 아닌0 ~ 2
를 지우는 목적으로 작성된 코드이다.Set
에서는remove(Object o)
를 사용해서 정해진 원소를 지우는 메서드를 사용했기 때문에 의도한대로 동작했다.List
에서remove
메소드는remove(Object)
,remove(int)
로 다중정의 돼있는 상태이므로, 실제 사용된 메소드는 지정된 위치의 원소를 지우는remove(int)
이기 때문에 의도와 다르게 동작 했다.List
인테페이스는 제네릭과 오토박싱이 추가됨에 따라 위와 같은 취약함이 존재한다.람다, 메소드 참조.. 혼란 가중
new Thread(Runnable)
,submit(Runnable)
모두 존재하지만,submit
은submit(Callable)
의 다중정의가 있다. (#44 )println
은 반환 값이 모두void
이니 당연히submit(Runnable)
을 선택할 것 같지만,다중정의 해소 알고리즘
은 우리의 의도처럼 동작하지 않는다.println
또한 다중정의 되있기 때문에 참조된 메서드(println)와 호출한 메서드(submit) 양쪽 다 다중정의 돼,다중정의 해소 알고리즘
이 기대와 다르게 동작하는 것이다.근복적으로 다르다
고 할 수 없기 때문이다.그렇다면 근본적으로 다르다는 것은 무엇인가
클래스 타입
과배열 타입
(Object
제외)인터페이스 타입
과배열 타입
(Serializable
,Cloneable
제외)String
과Throwable
처럼 상/하위 관계가 아닌 두 클래스(관련 없다.unrelated
)String 너마저 ..
String
에contentEquals
메서드는 개수, 위치가 동일하고 그리고 상/하위 관계에 있는 다중정의 메서드가 존재한다.contentEquals
는 더 특수한 다중정의 메서드(contentEquals(StringBuffer)
)에서 덜 특수한(contentEquals(CharSequence)
) 다중정의 메서드로 일을forwarding
한다.String
에는 위와 동일한 상황이지만,valueOf
라는 서로 다른 동작을 수행하는 다중정의 메서드가 존재한다. (주의하자.)