Przeciążony wybór metody oparty na rzeczywistym typie parametru

115

Eksperymentuję z tym kodem:

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

To drukuje foo(Object o)trzy razy. Oczekuję, że wybór metody będzie uwzględniał rzeczywisty (a nie deklarowany) typ parametru. Czy coś mi brakuje? Czy istnieje sposób na zmodyfikowanie tego kodu, aby był drukowany foo(12), foo("foobar")i foo(Object o)?

Sergey Mikhanov
źródło

Odpowiedzi:

96

Oczekuję, że wybór metody będzie uwzględniał rzeczywisty (a nie deklarowany) typ parametru. Czy coś mi brakuje?

Tak. Twoje oczekiwanie jest błędne. W Javie dynamiczne wysyłanie metod ma miejsce tylko dla obiektu, dla którego metoda jest wywoływana, a nie dla typów parametrów przeciążonych metod.

Cytując specyfikację języka Java :

Gdy wywoływana jest metoda (§15.12), liczba rzeczywistych argumentów (i dowolnych argumentów typu jawnego) oraz typy argumentów w czasie kompilacji są używane w czasie kompilacji, aby określić podpis metody, która zostanie wywołana ( §15.12.2). Jeśli wywoływana metoda jest metodą instancji, faktyczna metoda, która ma zostać wywołana, zostanie określona w czasie wykonywania przy użyciu dynamicznego wyszukiwania metod (§15.12.4).

Michael Borgwardt
źródło
4
Czy może Pan wyjaśnić cytowaną specyfikację? Te dwa zdania wydają się sobie zaprzeczać. W powyższym przykładzie zastosowano metody instancji, ale wywoływana metoda nie jest wyraźnie określana w czasie wykonywania.
Alex Worden
15
@Alex Worden: typ czasu kompilacji parametrów metody jest używany do określenia podpisu metody, która ma zostać wywołana, w tym przypadku foo(Object). W czasie wykonywania klasa obiektu, dla którego wywoływana jest metoda, określa, która implementacja tej metody jest wywoływana, biorąc pod uwagę, że może to być instancja podklasy zadeklarowanego typu, która przesłania metodę.
Michael Borgwardt
86

Jak wspomniano wcześniej, rozpoznawanie przeciążenia jest wykonywane w czasie kompilacji.

Java Puzzlers ma na to dobry przykład:

Zagadka 46: Przypadek zagubionego konstruktora

Ta łamigłówka przedstawia dwóch konstruktorów Mylących. Główna metoda wywołuje konstruktora, ale który? Wynik programu zależy od odpowiedzi. Co program drukuje, czy w ogóle jest to legalne?

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

Rozwiązanie 46: Przypadek mylącego konstruktora

... Proces rozwiązywania problemów w Javie przebiega w dwóch fazach. Pierwsza faza wybiera wszystkie metody lub konstruktory, które są dostępne i mają zastosowanie. Druga faza wybiera najbardziej szczegółowe metody lub konstruktory wybrane w pierwszej fazie. Jedna metoda lub konstruktor jest mniej specyficzna niż inna, jeśli może akceptować parametry przekazane do drugiej [JLS 15.12.2.5].

W naszym programie oba konstruktory są dostępne i mają zastosowanie. Konstruktor Confusing (Object) akceptuje każdy parametr przekazany do Confusing (double []) , więc Confusing (Object) jest mniej konkretny. (Każda podwójnie tablica jest obiekt , jednak nie każdy obiekt jest podwójna tablica ). Od najbardziej konstruktor więc kłopotliwe (dwukrotnie []) , co wyjaśnia wyjście programu.

To zachowanie ma sens, jeśli przekażesz wartość typu double [] ; jeśli przekażesz null, jest sprzeczne z intuicją . Kluczem do zrozumienia tej zagadki jest to, że test, dla której metoda lub konstruktor jest najbardziej specyficzny, nie wykorzystuje rzeczywistych parametrów : parametrów pojawiających się w wywołaniu. Służą one jedynie do określenia, które przeciążenia mają zastosowanie. Gdy kompilator ustali, które przeciążenia mają zastosowanie i są dostępne, wybiera najbardziej specyficzne przeciążenie, używając tylko parametrów formalnych: parametrów pojawiających się w deklaracji.

Aby wywołać konstruktor Confusing (Object) z parametrem null , napisz nowy Confusing ((Object) null) . Gwarantuje to, że ma zastosowanie tylko Confusing (Object) . Mówiąc bardziej ogólnie, aby zmusić kompilator do wybrania określonego przeciążenia, należy rzutować rzeczywiste parametry na zadeklarowane typy parametrów formalnych.

denis.zhdanov
źródło
4
Mam nadzieję, że nie jest za późno, aby powiedzieć - „jedno z najlepszych wyjaśnień na temat SOF”. Dzięki :)
TheLostMind
5
Sądzę, że gdybyśmy dodali również konstruktor „private Confusing (int [] iArray)”, kompilacja nie powiodłaby się, prawda? Ponieważ teraz mamy dwóch konstruktorów o tej samej specyficzności.
Risser
Jeśli używam dynamicznych typów zwracanych jako danych wejściowych funkcji, zawsze używa mniej szczegółowych ... powiedział, że metoda, której można użyć dla wszystkich możliwych wartości zwracanych ...
kaiser
16

Możliwość wysłania wywołania metody opartej na typach argumentów nazywana jest wysyłaniem wielokrotnym . W Javie odbywa się to za pomocą wzorca Visitor .

Jednakże, ponieważ masz do czynienia z Integers i Strings, nie możesz łatwo włączyć tego wzorca (po prostu nie możesz modyfikować tych klas). Tak więc olbrzym switchw czasie wykonywania obiektów będzie Twoją ulubioną bronią.

Anton Gogolev
źródło
11

W Javie metoda do wywołania (jak w przypadku której sygnatury metody użyć) jest określana w czasie kompilacji, więc jest zgodna z typem czasu kompilacji.

Typowym sposobem obejścia tego problemu jest sprawdzenie typu obiektu w metodzie z podpisem Object i delegowanie do metody za pomocą rzutowania.

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

Jeśli masz wiele typów i jest to niemożliwe do zarządzania, to przeciążenie metod prawdopodobnie nie jest właściwym podejściem, a raczej metoda publiczna powinna po prostu przyjąć Object i zaimplementować pewien wzorzec strategii, aby delegować odpowiednią obsługę dla typu obiektu.

Yishai
źródło
4

Miałem podobny problem z wywołaniem odpowiedniego konstruktora klasy o nazwie „Parameter”, która mogłaby przyjmować kilka podstawowych typów Java, takich jak String, Integer, Boolean, Long itp. Biorąc pod uwagę tablicę obiektów, chcę je przekształcić w tablicę moich obiektów Parameter, wywołując najbardziej specyficzny konstruktor dla każdego obiektu w tablicy wejściowej. Chciałem również zdefiniować parametr konstruktora (obiekt o), który będzie zgłaszał IllegalArgumentException. Oczywiście zauważyłem, że ta metoda jest wywoływana dla każdego obiektu w mojej tablicy.

Rozwiązaniem, którego użyłem, było wyszukanie konstruktora poprzez odbicie ...

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

Nie są wymagane żadne brzydkie instancje, instrukcje przełączania ani wzorce odwiedzających! :)

Alex Worden
źródło
2

Java sprawdza typ referencyjny, próbując określić, którą metodę wywołać. Jeśli chcesz wymusić swój kod, wybierasz `` właściwą '' metodę, możesz zadeklarować swoje pola jako wystąpienia określonego typu:

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

Możesz również rzutować swoje parametry jako typ parametru:

callee.foo(i);
callee.foo((String)s);
callee.foo(((Integer)o);
akf
źródło
1

Jeśli istnieje dokładne dopasowanie między liczbą i typami argumentów określonymi w wywołaniu metody a sygnaturą metody przeciążonej metody, to jest to metoda, która zostanie wywołana. Używasz odwołań do Object, więc java decyduje w czasie kompilacji, że dla parametru Object istnieje metoda, która akceptuje bezpośrednio Object. Więc wywołał tę metodę 3 razy.

Ashish Thukral
źródło