본문 바로가기

STUDY/이펙티브자바

8-4) 메소드 오버로딩은 신중히 사용하라

Collection 분류기

 

public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    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));
    }
}

// 하나로 합쳐진 classify 메소드
public static String classify(Collection<?> c) {
    return c instanceof Set  ? "Set" :
            c instanceof List ? "List" : "Unknown Collection";
}

위 코드를 보면 Collection 배열에 Set, List, Map을 담아두고 각 요소들로 classify를 호출한다. classify 함수는 각각 Set, List, Collection 타입의 매개변수를 받도록 오버로딩 되어있다. 얼핏 생각하면 Set, List, Map 요소가 각각의 classify 함수를 호출하여 "Set" "List" "Unknown Collection"이 출력될 것 같지만 "Unknown Collection" 만 3번 출력된다. 그 이유는 오버로딩된 메소드중 어떤 메소드를 호출할 지 정하는 것은 컴파일 타임에 결정되기 때문이다. (오버라이딩된 메소드는 런타임에 결정된다). 이처럼 매개변수의 개수가 같은 메소드 오버로딩은 개발자에게 혼동을 일으킬 수 있으므로 사용을 피하는것이 좋다.

 

// 메소드 오버라이딩
class Wine {
    String name() { return "wine"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "sparkling wine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

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());
    }
}

 

 


List의 메소드 오버로딩

 

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);

위 코드를 보면 set와 list에 -3, -2, -1, 0, 1, 2, 3이 들어간다. 그리고 set.remove와 list.remove가 0 ~ 3까지 호출된다. 개발자는 set와 list에 -3, -2, -1가 남아있기를 기대했을 수 있다. 하지만 실행 결과를 보면 set에는 -3, -2, -1이 남아있지만 list에는 -2, 0, 2가 남아있다. 이는 List가 remove(Object obj)와 remove(int index)와 같이 메소드 오버로딩 되어있기 때문이다. 개발자는 remove(Object obj)를 의도했겠지만 실제로 실행된건 remove(int index)이다. 이는 메소드 오버로딩이 개발자의 실수를 유발하는 대표적인 사례이다.

 

 


함수형 인터페이스와 메소드 오버로딩

 

List<String> list = List.of("Peter", "Thomas", "Edvard", "Gerhard");
list.forEach(item -> System.out.println(item));
// 메소드를 람다식으로 바꿔주는 메소드 레퍼런스
list.forEach(System.out::println);

 

ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(() -> System.out.println());
// 컴파일 에러 발생
exec.submit(System.out::println);

메소드 레퍼런스(::)는 클래스의 메소드를 간단한 람다식으로 변환해주는 편리한 기능이다. 하지만 메소드 레퍼런스와 메소드 오버로딩이 만나면 의도치 않은 문제가 발생할 수 있다. exec.submit(() -> System.out.println());와 exec.submit(System.out::println);는 둘 모두 submit 메소드에 println를 람다식으로 전달한것과 같은데 하나는 정상동작하고 하나는 컴파일 에러가 발생한다. 이는 submit 메소드가 submit(Runnable task), submit(Callable<T> task)로 둘 모두함수형 인터페이스를 파라미터로 받도록 오버로딩 되있기 때문이다. () -> System.out.println()는 무난하게 submit(Runnable task) 메소드를 호출하도록 맵핑되지만, System.out::println는 안타깝게도 submit(Callable task)를 호출하도록 맵핑되어 컴파일 에러가 발생한다. 따라서 메소드를 오버라이딩할 때, 함수형 인터페이스를 동일 위치로 하여 구현하면 안된다.

728x90