Overloading dei metodi in Java
Definisci più metodi con lo stesso nome ma parametri diversi in Java per creare API flessibili.
L'overloading dei metodi consiste nel dichiarare due o più metodi nella stessa classe con lo stesso nome ma liste di parametri diverse. Il compilatore sceglie quale chiamare in base ai tipi degli argomenti passati.
Hai già usato metodi in overloading senza rendertene conto. System.out.println(...) dispone di versioni che accettano un int, un double, una String, un Object, un char[] e così via. Tutti si comportano in modo simile — stampano il valore e una nuova riga — ma l'implementazione varia in base al tipo.
Cosa significa "lista di parametri diversa"
Due overload devono differire per arità (numero di parametri) oppure per i tipi di quei parametri nell'ordine. Non possono differire solo per:
- Nomi dei parametri
- Tipo di ritorno
- Se i parametri sono
final
public static int square(int n) { return n * n; }
// VALID overloads — different parameter types
public static double square(double n) { return n * n; }
public static long square(long n) { return n * n; }
// VALID overload — different arity
public static int sum(int a, int b) { return a + b; }
public static int sum(int a, int b, int c) { return a + b + c; }
// INVALID — only the return type differs
// public static long square(int n) { return (long) n * n; } // won't compileCome Java seleziona un overload
Quando scrivi square(3), il compilatore esegue questo procedimento nell'ordine:
- Corrispondenza esatta. Esiste un overload i cui parametri corrispondono esattamente ai tipi degli argomenti?
square(3)→square(int). Fatto. - Widening. In caso contrario, è possibile ampliare gli argomenti (ad es.
int → long,int → double)? Viene scelto l'overload che richiede il widening minimo. - Autoboxing / unboxing. Altrimenti, prova a incapsulare o a disincapsulare (
int ↔ Integer). - Varargs. Come ultima risorsa, ricade su un overload varargs (vedi il capitolo sui varargs).
Se due overload si equivalgono allo stesso passaggio, la chiamata è ambigua e non compilerà.
public static void show(int n) { System.out.println("int: " + n); }
public static void show(long n) { System.out.println("long: " + n); }
public static void show(double n) { System.out.println("double: " + n); }
show(3); // exact match → int
show(3L); // exact match → long
show(3.0); // exact match → double
show((short) 3); // widens short → int (closest), picks show(int)Ambiguità
Quando il compilatore non riesce a scegliere tra due overload ugualmente validi, si ottiene un errore:
public static void f(int a, long b) { /* ... */ }
public static void f(long a, int b) { /* ... */ }
f(1, 2); // ERROR: reference to f is ambiguousEntrambi gli overload richiedono il widening di un argomento da int a long. Nessuno dei due è "migliore". La soluzione è disambiguare nel punto di chiamata con un cast esplicito — f(1, 2L) oppure f(1L, 2) — oppure aggiungere un terzo overload f(int, int) che gestisca il caso in modo esatto.
Overloading su tipi oggetto
I tipi riferimento effettuano l'overloading nello stesso modo, ma con relazioni di sottotipo al posto del widening:
public static void log(Object o) { System.out.println("Object: " + o); }
public static void log(String s) { System.out.println("String: " + s); }
log("hello"); // exact match → String
log(42); // autobox to Integer, then Integer is-a Object → log(Object)
log((Object) "hi"); // forces the Object overloadSe passi una String e il solo overload disponibile accetta Object, Java è soddisfatto — String è un Object. Ma se esiste un overload più specifico, Java lo preferisce.
Quando l'overloading è utile
L'obiettivo dell'overloading è offrire ai chiamanti un'API pulita e naturale rispetto ai tipi. I due schemi più comuni sono:
Valori predefiniti. Un overload breve chiama quello lungo con valori predefiniti sensati:
public static void greet(String name) {
greet(name, 1); // delegate
}
public static void greet(String name, int times) {
for (int i = 0; i < times; i++) {
System.out.println("Hello, " + name);
}
}Convertitori di convenienza. Tipi di input diversi, stessa operazione logica:
public static int lengthOf(String s) { return s == null ? 0 : s.length(); }
public static int lengthOf(int[] xs) { return xs == null ? 0 : xs.length; }
public static int lengthOf(int n) { return Integer.toString(n).length(); }Il chiamante scrive lengthOf(x) senza preoccuparsi; il compilatore instrada verso il corpo corretto.
Quando non usare l'overloading
Se due overload farebbero cose sostanzialmente diverse, è meglio assegnare loro nomi diversi. Chi legge format(x, y) non dovrebbe dover cercare quale overload è stato selezionato per capire cosa fa la chiamata. Gli overload dovrebbero essere varianti della stessa idea, non idee diverse che condividono un nome.
Overloading vs. overriding
Questi due termini suonano simili ma risolvono problemi diversi, e confonderli è la fonte di confusione più comune.
- L'overloading è stesso nome, liste di parametri diverse, all'interno di una classe. Quale metodo viene eseguito è deciso dal compilatore, guardando solo ai tipi statici (dichiarati) degli argomenti. Questo si chiama binding statico (a tempo di compilazione).
- L'overriding è stesso nome, stessa lista di parametri, in una sottoclasse — sostituisce un metodo ereditato. Quale metodo viene eseguito è deciso dalla JVM a runtime, in base al tipo effettivo dell'oggetto. Questo è il binding dinamico (a runtime).
Object x = "hello";
log(x); // calls log(Object), NOT log(String) — chosen from x's DECLARED typeAnche se x contiene una String a runtime, il compilatore vede solo Object x, quindi sceglie l'overload Object. La selezione dell'overload non considera mai il tipo a runtime — quello è compito dell'overriding. Consulta l'overriding dei metodi per il lato runtime della questione.
Un esempio pratico
Prossimi passi
L'overloading permette a un nome di puntare a più metodi. A volte si vuole che un singolo metodo chiami se stesso — risolvendo un problema staccando un pezzo e chiedendo a una versione più piccola dello stesso problema di gestire il resto. Questo è la ricorsione.