Jedna nazwa, kilka wersji
Na poprzedniej stronie widać było, jak parametry metody określają, co ona przyjmuje. Przeciążanie metod idzie dalej: możesz nadać tę samą nazwę kilku metodom, o ile ich listy parametrów się różnią. Kompilator traktuje je jako osobne metody i wybiera właściwą na podstawie przekazanych argumentów.
Dlatego System.out.println bez problemu wypisuje int, String, boolean albo double: nie ma jednego println, jest wiele przeciążeń o tej samej nazwie. Piszesz wywołanie, o które ci chodzi, a kompilator dopasowuje je do pasującej wersji.
Obie metody nazywają się square, ale jedna przyjmuje int, a druga double. Literał 5 to int, więc uruchamia się pierwsze przeciążenie; 2.5 to double, więc uruchamia się drugie.
Co liczy się jako inne przeciążenie
Przeciążenia muszą różnić się listą parametrów, czyli przynajmniej jednym z poniższych:
- inną liczbą parametrów,
- innymi typami parametrów,
- inną kolejnością typów.
Lista parametrów w każdym wywołaniu pasuje dokładnie do jednej z trzech metod join, więc nie ma żadnych wątpliwości.
Typ zwracany się nie liczy
Częsta pułapka początkujących: próba przeciążenia tylko po typie zwracanym. Typ zwracany nie należy do sygnatury, której używa kompilator, więc ten kod się nie kompiluje:
// NIE kompiluje się: ta sama nazwa, te same parametry, różni się tylko typ zwracany
static int value() { return 1; }
static double value() { return 1.0; } // error: value() jest już zdefiniowana
Kompilator nie potrafi ich odróżnić, bo w wywołaniu takim jak value() nic w argumentach nie wskazuje, o którą wersję chodzi. Przeciążenia mogą mieć różne typy zwracane, ale tylko wtedy, gdy ich listy parametrów już się różnią.
Jak Java wybiera przeciążenie
Gdy argumenty pasują do więcej niż jednego przeciążenia, Java wybiera najbardziej szczegółowe i woli dokładne dopasowanie typu od konwersji rozszerzającej. Zobacz, co się dzieje z argumentem int:
show(7) pasuje dokładnie do int, choć long i double też pomieściłyby 7 po rozszerzeniu. Dopiero gdyby usunąć dokładne przeciążenie, kompilator rozszerzyłby int do long, a potem do double. O wyborze decyduje w całości kompilator, na podstawie zadeklarowanych typów argumentów.
Uważaj na niejednoznaczne wywołania
Jeśli żadne przeciążenie nie jest wyraźnie najlepsze, kompilator nie zgaduje, tylko zgłasza błąd. Najczęściej zdarza się to z null, który pasuje do każdego typu referencyjnego:
static void handle(String s) { }
static void handle(StringBuilder b) { }
handle(null); // error: odwołanie do handle jest niejednoznaczne
Oba przeciążenia przyjmują null i żadne nie jest bardziej szczegółowe, więc wywołanie się nie skompiluje. Napraw to, podając typ jawnie przez rzutowanie, handle((String) null), albo przeprojektuj kod tak, by przeciążenia się nie zderzały. Ta sama ostrożność obowiązuje przy mieszaniu autoboxingu i rozszerzania; utrzymuj zestawy przeciążeń na tyle proste, by każde wywołanie miało jednego oczywistego zwycięzcę.
Przeciążanie konstruktorów
Przeciążanie nie ogranicza się do zwykłych metod. Konstruktory korzystają z niego stale, by oferować kilka sposobów tworzenia obiektu. Konstruktor bez argumentów może przekazać pracę pełniejszemu przez this(...):
Dwa konstruktory mają wspólną nazwę Point, ale różnią się liczbą parametrów, dokładnie jak przeciążone metody. Delegowanie przez this(...) trzyma logikę inicjalizacji w jednym miejscu.
Przeciążanie a nadpisywanie
Te dwa pojęcia brzmią podobnie, ale nie mają ze sobą związku:
- Przeciążanie (overloading): ta sama nazwa, różne listy parametrów, w tej samej klasie. Kompilator wybiera wersję w czasie kompilacji. Chodzi o oferowanie wariantów operacji.
- Nadpisywanie (overriding): podklasa definiuje na nowo odziedziczoną metodę o tej samej nazwie i tych samych parametrach. Java wybiera wersję w czasie działania na podstawie rzeczywistego typu obiektu. Chodzi o zastąpienie zachowania (spotkasz je przy dziedziczeniu i polimorfizmie).
Jeśli listy parametrów są identyczne, nadpisujesz (albo powodujesz błąd zduplikowanej metody w tej samej klasie); jeśli się różnią, przeciążasz.
Dalej: varargs
Przeciążanie pozwala napisać join(a, b) i join(a, b, c) jako osobne metody. Co jednak, jeśli chcesz przyjąć dowolną liczbę argumentów bez deklarowania przeciążenia dla każdej liczby? Składnia varargs w Javie pozwala jednej metodzie przyjąć listę argumentów o zmiennej długości i o tym jest następna strona.
Najczęściej zadawane pytania
Czym jest przeciążanie metod w Javie?
Przeciążanie metod to definiowanie w jednej klasie kilku metod o tej samej nazwie, z których każda ma inną listę parametrów (inną liczbę parametrów, inne typy albo inną kolejność typów). Kompilator decyduje, którą wersję wywołać, dopasowując przekazane argumenty do parametrów każdego przeciążenia. To decyzja podejmowana w czasie kompilacji, a nie w czasie działania programu.
Czy dwie metody mogą różnić się w Javie tylko typem zwracanym?
Nie. Typ zwracany nie należy do sygnatury metody używanej przy przeciążaniu, więc int total() i double total() w tej samej klasie to błąd kompilacji. Przeciążenia muszą różnić się listą parametrów: liczbą, typami albo kolejnością parametrów. Typ zwracany może się różnić, ale tylko dodatkowo obok różnicy w parametrach, a nie sam.
Czym różni się przeciążanie od nadpisywania w Javie?
Przeciążanie (overloading) to kilka metod o tej samej nazwie, ale innych parametrach w jednej klasie, rozstrzygane przez kompilator w czasie kompilacji. Nadpisywanie (overriding) to podklasa definiująca na nowo odziedziczoną metodę o tej samej nazwie i tych samych parametrach, rozstrzygane w czasie działania na podstawie rzeczywistego typu obiektu. Przeciążanie służy do oferowania wariantów; nadpisywanie do zastępowania zachowania.