Selezione del metodo sovraccaricato in base al tipo reale del parametro


115

Sto sperimentando questo codice:

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

Viene stampato foo(Object o)tre volte. Mi aspetto che la selezione del metodo prenda in considerazione il tipo di parametro reale (non dichiarato). Mi sto perdendo qualcosa? C'è un modo per modificare questo codice in modo che venga stampato foo(12), foo("foobar")e foo(Object o)?

Risposte:


96

Mi aspetto che la selezione del metodo prenda in considerazione il tipo di parametro reale (non dichiarato). Mi sto perdendo qualcosa?

Sì. La tua aspettativa è sbagliata. In Java, l'invio del metodo dinamico avviene solo per l'oggetto su cui viene chiamato il metodo, non per i tipi di parametro dei metodi sovraccaricati.

Citando le specifiche del linguaggio Java :

Quando viene invocato un metodo (§15.12), il numero di argomenti effettivi (ed eventuali argomenti di tipo esplicito) ei tipi di argomenti in fase di compilazione vengono utilizzati, in fase di compilazione, per determinare la firma del metodo che sarà invocato ( §15.12.2). Se il metodo che deve essere invocato è un metodo di istanza, il metodo effettivo da invocare sarà determinato in fase di esecuzione, utilizzando la ricerca del metodo dinamico (§15.12.4).


4
Puoi spiegare le specifiche che hai citato per favore. Le due frasi sembrano contraddirsi a vicenda. L'esempio precedente utilizza metodi di istanza, tuttavia il metodo invocato chiaramente non viene determinato in fase di esecuzione.
Alex Worden

15
@Alex Worden: il tipo di tempo di compilazione dei parametri del metodo viene utilizzato per determinare la firma del metodo da chiamare, in questo caso foo(Object). In fase di esecuzione, la classe dell'oggetto su cui viene chiamato il metodo determina quale implementazione di quel metodo viene chiamata, tenendo conto che potrebbe essere un'istanza di una sottoclasse del tipo dichiarato che sovrascrive il metodo.
Michael Borgwardt

86

Come accennato prima, la risoluzione del sovraccarico viene eseguita in fase di compilazione.

Java Puzzlers ha un bell'esempio per questo:

Puzzle 46: il caso del costruttore confuso

Questo puzzle ti presenta due costruttori confusi. Il metodo principale invoca un costruttore, ma quale? L'output del programma dipende dalla risposta. Cosa stampa il programma o è addirittura legale?

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

Soluzione 46: caso del costruttore confuso

... Il processo di risoluzione del sovraccarico di Java opera in due fasi. La prima fase seleziona tutti i metodi o costruttori che sono accessibili e applicabili. La seconda fase seleziona il più specifico dei metodi o costruttori selezionati nella prima fase. Un metodo o un costruttore è meno specifico di un altro se può accettare qualsiasi parametro passato all'altro [JLS 15.12.2.5].

Nel nostro programma, entrambi i costruttori sono accessibili e applicabili. Il costruttore Confusing (Object) accetta qualsiasi parametro passato a Confusing (double []) , quindi Confusing (Object) è meno specifico. (Ogni double array è un Object , ma non ogni Object è un double array .) Il costruttore più specifico è quindi Confusing (double []) , che spiega l'output del programma.

Questo comportamento ha senso se si passa un valore di tipo double [] ; è controintuitivo se si passa a null . La chiave per comprendere questo puzzle è che il test per quale metodo o costruttore è più specifico non utilizza i parametri effettivi : i parametri che appaiono nell'invocazione. Vengono utilizzati solo per determinare quali sovraccarichi sono applicabili. Una volta che il compilatore ha determinato quali sovraccarichi sono applicabili e accessibili, seleziona il sovraccarico più specifico, utilizzando solo i parametri formali: i parametri che compaiono nella dichiarazione.

Per richiamare il costruttore Confusing (Object) con un parametro null , scrivere new Confusing ((Object) null) . Ciò garantisce che sia applicabile solo Confusing (Object) . Più in generale, per forzare il compilatore a selezionare un sovraccarico specifico, eseguire il cast dei parametri effettivi sui tipi dichiarati dei parametri formali.


4
Spero che non sia troppo tardi per dire: "una delle migliori spiegazioni su SOF". Grazie :)
TheLostMind

5
Credo che se aggiungessimo anche il costruttore 'private Confusing (int [] iArray)' non riuscirebbe a compilare, no? Perché ora ci sono due costruttori con la stessa specificità.
Risser

Se utilizzo i tipi di ritorno dinamico come input della funzione, utilizza sempre il metodo meno specifico ... ha detto che il metodo può essere utilizzato per tutti i possibili valori di ritorno ...
kaiser

16

La capacità di inviare una chiamata a un metodo in base ai tipi di argomenti è chiamata invio multiplo . In Java questo viene fatto con il pattern Visitor .

Tuttavia, poiché hai a che fare con Integers e Strings, non puoi incorporare facilmente questo modello (semplicemente non puoi modificare queste classi). Quindi, un gigante switchdel tempo di esecuzione degli oggetti sarà la tua arma preferita.


11

In Java il metodo da chiamare (come la firma del metodo da utilizzare) è determinato in fase di compilazione, quindi va con il tipo in fase di compilazione.

Il modello tipico per aggirare questo problema è controllare il tipo di oggetto nel metodo con la firma Object e delegare al metodo con un cast.

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

Se si hanno molti tipi e questo non è gestibile, allora il sovraccarico del metodo probabilmente non è l'approccio giusto, piuttosto il metodo pubblico dovrebbe semplicemente prendere Object e implementare un qualche tipo di modello di strategia per delegare la gestione appropriata per tipo di oggetto.


4

Ho avuto un problema simile chiamando il giusto costruttore di una classe chiamata "Parameter" che poteva accettare diversi tipi Java di base come String, Integer, Boolean, Long, ecc. Dato un array di oggetti, voglio convertirli in un array dei miei oggetti Parameter chiamando il costruttore più specifico per ogni Object nella matrice di input. Volevo anche definire il parametro del costruttore (Object o) che avrebbe generato un'eccezione IllegalArgumentException. Ovviamente ho trovato questo metodo invocato per ogni oggetto nel mio array.

La soluzione che ho usato è stata cercare il costruttore tramite riflessione ...

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

Non sono richieste brutte istanze, istruzioni switch o pattern del visitatore! :)


2

Java esamina il tipo di riferimento quando tenta di determinare quale metodo chiamare. Se vuoi forzare il tuo codice scegli il metodo 'giusto', puoi dichiarare i tuoi campi come istanze del tipo specifico:

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

Puoi anche lanciare i tuoi parametri come tipo di parametro:

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

1

Se esiste una corrispondenza esatta tra il numero e il tipo di argomenti specificati nella chiamata al metodo e la firma del metodo di un metodo sovraccarico, questo è il metodo che verrà richiamato. Stai usando i riferimenti a oggetti, quindi java decide in fase di compilazione che per Object param, esiste un metodo che accetta direttamente Object. Quindi ha chiamato quel metodo 3 volte.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.