매개 변수의 실제 유형을 기반으로 오버로드 된 메소드 선택


115

이 코드를 실험하고 있습니다.

interface Callee {
    public void foo(Object o);
    public void foo(String s);
    public void foo(Integer i);
}

class CalleeImpl implements Callee
    public void foo(Object o) {
        logger.debug("foo(Object o)");
    }

    public void foo(String s) {
        logger.debug("foo(\"" + s + "\")");
    }

    public void foo(Integer i) {
        logger.debug("foo(" + i + ")");
    }
}

Callee callee = new CalleeImpl();

Object i = new Integer(12);
Object s = "foobar";
Object o = new Object();

callee.foo(i);
callee.foo(s);
callee.foo(o);

이것은 foo(Object o)세 번 인쇄 됩니다. 메서드 선택은 실제 (선언되지 않은) 매개 변수 유형을 고려할 것으로 예상합니다. 내가 뭔가를 놓치고 있습니까? 그것은 인쇄 해드립니다 있도록이 코드를 수정하는 방법이 foo(12), foo("foobar")그리고 foo(Object o)?

답변:


96

메서드 선택은 실제 (선언되지 않은) 매개 변수 유형을 고려할 것으로 예상합니다. 내가 뭔가를 놓치고 있습니까?

예. 당신의 기대가 잘못되었습니다. Java에서 동적 메소드 디스패치는 오버로드 된 메소드의 매개 변수 유형이 아니라 메소드가 호출 된 객체에 대해서만 발생합니다.

Java 언어 사양 인용 :

메서드가 호출 될 때 (§15.12) 실제 인수 (및 명시 적 형식 인수)의 수 및 의 컴파일 타임 형식이 컴파일 타임에 사용되어 호출 될 메서드의 서명을 결정합니다 ( §15.12.2). 호출 할 메서드가 인스턴스 메서드 인 경우 호출 할 실제 메서드는 동적 메서드 조회 (§15.12.4)를 사용하여 런타임에 결정됩니다.


4
인용 한 사양을 설명해 주시겠습니까? 두 문장이 서로 모순되는 것 같습니다. 위의 예제는 인스턴스 메서드를 사용하지만 호출되는 메서드는 런타임에 명확하게 결정되지 않습니다.
Alex Worden

15
@Alex Worden : 메서드 매개 변수 의 컴파일 시간 유형 호출 할 메서드의 서명을 결정하는 데 사용됩니다 (이 경우) foo(Object). 런타임에 메서드가 호출 되는 객체의 클래스는 메서드를 재정의하는 선언 된 유형의 하위 클래스 인스턴스 일 수 있다는 점을 고려하여 해당 메서드의 구현 결정합니다.
Michael Borgwardt

86

앞서 언급했듯이 오버로드 해결은 컴파일 타임에 수행됩니다.

Java Puzzlers 에는 이에 대한 좋은 예가 있습니다.

퍼즐 46 : 혼란스러운 생성자의 사례

이 퍼즐은 두 개의 Confusing 생성자를 제공합니다. 메인 메소드는 생성자를 호출하지만 어떤 생성자를 호출합니까? 프로그램의 출력은 답변에 따라 다릅니다. 프로그램은 무엇을 인쇄합니까? 아니면 합법적입니까?

public class Confusing {

    private Confusing(Object o) {
        System.out.println("Object");
    }

    private Confusing(double[] dArray) {
        System.out.println("double array");
    }

    public static void main(String[] args) {
        new Confusing(null);
    }
}

솔루션 46 : 혼란스러운 생성자의 경우

... Java의 과부하 해결 프로세스는 두 단계로 작동합니다. 첫 번째 단계에서는 액세스 가능하고 적용 가능한 모든 메서드 또는 생성자를 선택합니다. 두 번째 단계 에서는 첫 번째 단계에서 선택한 가장 구체적인 메서드 또는 생성자를 선택합니다. 하나의 메서드 또는 생성자가 덜 구체적입니다. 다른 전달 된 매개 변수를 받아 들일 수 있다면 다른 것보다 [JLS 15.12.2.5].

우리 프로그램에서 두 생성자는 모두 액세스 가능하고 적용 가능합니다. Confusing (Object) 생성자 는 Confusing (double [])에 전달 된 모든 매개 변수를 허용 하므로 Confusing (Object) 은 덜 구체적입니다. (모든 double 배열Object 이지만 모든 Objectdouble 배열 인 것은 아닙니다 .) 따라서 가장 구체적인 생성자는 프로그램의 출력을 설명하는 Confusing (double []) 입니다.

이 동작은 double [] 유형의 값을 전달하면 의미가 있습니다 . null 을 전달하면 직관적이지 않습니다 . 이 퍼즐을 이해하기위한 핵심은 어떤 메서드 나 생성자가 가장 구체적인 테스트가 실제 매개 변수를 사용하지 않는다는 것 입니다. 즉 호출에 나타나는 매개 변수입니다. 적용 가능한 과부하를 결정하는 데만 사용됩니다. 컴파일러가 적용 가능하고 액세스 가능한 오버로딩을 결정하면 공식 매개 변수 (선언에 나타나는 매개 변수) 만 사용하여 가장 구체적인 오버로딩을 선택합니다.

null 매개 변수를 사용 하여 Confusing (Object) 생성자 를 호출하려면 new Confusing ((Object) null)을 작성 합니다. 이렇게하면 Confusing (Object) 만 적용 할 수 있습니다. 보다 일반적으로 컴파일러가 특정 오버로딩을 선택하도록 강제하려면 실제 매개 변수를 선언 된 형식 매개 변수 형식으로 캐스팅합니다.


4
"SOF에 대한 최고의 설명 중 하나"라고 말하기에는 너무 늦지 않았 으면합니다. 감사합니다 :)
TheLostMind

5
생성자 'private Confusing (int [] iArray)'도 추가하면 컴파일에 실패 할 것입니다. 그렇지 않습니까? 이제 동일한 특이성을 가진 두 개의 생성자가 있기 때문입니다.
Risser 2014 년

동적 반환 유형을 함수 입력으로 사용하는 경우 항상 덜 구체적으로 사용됩니다. 가능한 모든 반환 값에 사용할 수있는 방법을 언급했습니다 ...
kaiser

16

인수 유형에 따라 메서드에 대한 호출을 디스패치하는 기능을 다중 디스패치 라고 합니다 . Java에서는 방문자 패턴으로 수행됩니다 .

그러나 Integers 및 Strings를 다루기 때문에이 패턴을 쉽게 통합 할 수 없습니다 (이 클래스를 수정할 수는 없습니다). 따라서 switch객체 런타임에 거인 이 선택의 무기가 될 것입니다.


11

Java에서 호출 할 메서드 (사용할 메서드 서명)는 컴파일 타임에 결정되므로 컴파일 타임 유형과 함께 사용됩니다.

이 문제를 해결하기위한 일반적인 패턴은 Object 서명을 사용하여 메서드의 개체 유형을 확인하고 캐스트를 사용하여 메서드에 위임하는 것입니다.

    public void foo(Object o) {
        if (o instanceof String) foo((String) o);
        if (o instanceof Integer) foo((Integer) o);
        logger.debug("foo(Object o)");
    }

유형이 많고 관리 할 수없는 경우 메서드 오버로딩이 올바른 접근 방식이 아닐 수 있습니다. 오히려 공용 메서드는 Object를 취하고 개체 유형별로 적절한 처리를 위임하는 일종의 전략 패턴을 구현해야합니다.


4

String, Integer, Boolean, Long 등과 같은 몇 가지 기본 Java 유형을 사용할 수있는 "Parameter"라는 클래스의 올바른 생성자를 호출하는 것과 비슷한 문제가있었습니다. 주어진 객체 배열이 있으면이를 배열로 변환하고 싶습니다. 입력 배열의 각 개체에 대해 가장 구체적인 생성자를 호출하여 내 Parameter 개체의. 또한 IllegalArgumentException을 throw하는 생성자 Parameter (Object o)를 정의하고 싶었습니다. 물론이 메서드가 배열의 모든 개체에 대해 호출되는 것을 발견했습니다.

내가 사용한 해결책은 리플렉션을 통해 생성자를 찾는 것입니다.

public Parameter[] convertObjectsToParameters(Object[] objArray) {
    Parameter[] paramArray = new Parameter[objArray.length];
    int i = 0;
    for (Object obj : objArray) {
        try {
            Constructor<Parameter> cons = Parameter.class.getConstructor(obj.getClass());
            paramArray[i++] = cons.newInstance(obj);
        } catch (Exception e) {
            throw new IllegalArgumentException("This method can't handle objects of type: " + obj.getClass(), e);
        }
    }
    return paramArray;
}

추악한 instanceof, switch 문 또는 방문자 패턴이 필요하지 않습니다! :)


2

Java는 호출 할 메소드를 판별 할 때 참조 유형을 확인합니다. 코드를 강제로 '올바른'방법을 선택하려면 필드를 특정 유형의 인스턴스로 선언 할 수 있습니다.

Integeri = new Integer(12);
String s = "foobar";
Object o = new Object();

매개 변수 유형으로 매개 변수를 캐스팅 할 수도 있습니다.

callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);

1

메서드 호출에 지정된 인수의 수와 유형과 오버로드 된 메서드의 메서드 서명 사이에 정확히 일치하는 것이 호출되는 메서드입니다. Object 참조를 사용하고 있으므로 Java는 컴파일 타임에 Object 매개 변수에 대해 Object를 직접 받아들이는 메서드가 있다고 결정합니다. 그래서 그 방법을 3 번 호출했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.