úterý 4. června 2013

Základy Javy - overriding a overloading

Přetěžování (overload) a překrývání (override) metody asi zná každej, co se kdy učil Javu. Otázkou je, jak dobře. Nedávno jsem si myslel, že to docela znám, ale ne-e. Popíšu jednoduchý problém.

Jaký je výstup následující metody?:

public static void main(String [] args) {
Object o = new A();
A a1 = new A();
A a2 = new B();
B b = new B();
a2.report(a1);
a2.report(b);
b.report(o);
b.report(a2);
}

Třídy A a B jsou definovány takto:


class A {
public void report(Object o) {
System.out.println("Object");
}
public void report(A a) {
System.out.println("A");
}
}


class B extends A {
public void report(A a) {
System.out.println("A again");
}
public void report(B b) {
System.out.println("B");
}
}


Pokud se vám to zdá easy, tak to ještě zkuste rozběhnout v nějakém IDE nebo pomocí příkaz. řádky. Jestli máte správně i odpovědi, tak jste v chillu a dále nemusíte číst.

Moje trochu zmateně formulovaná teorie

Přetěžování metod
Přetěžování metod je technika, kdy v jedné třídě se vyskytují metody se stejným jménem a odlišným počtem a/nebo typem argumentů. Ve skutečnosti to jsou zcela odlišné metody, jen mají stejné jméno.

Důležitější je ale, kdy se určí, která metoda je ta pravá? Odpověď je při psaná kódu podle argumentů metod (statická analýza kódu). Pokud existují více metod, které mají argumenty stejného a nadřazeného typu jako předávaný objekt, tak přednost má ta metoda, která je hierarchicky nejblíže tomuto předávanému objektu.

Překrytí metod
Překrytí metod je technika, kdy potomek znovu definuje metodu, která již existuje v předku. To znamená, že překrývající metoda v potomkovi má stejné jméno i počet a typ argumentů  jako metoda předka a vracející typ metody musí být buď stejného nebo podřazeného typu (např. pokud rodičovská metoda vrací Object, potomek může vracet i String).

Metoda, která se zavolá při překrytí, je určena až za běhu programu (late binding) podle typu objektu, na kterém je volána.

Řešení předchozího příkladu

Nyní zkusím aplikovat teorie na předchozím příkladu. Začnu postupně:

a) a2.report(a1);
Je jasné, že prvně probíhá psaní kódu (tedy statická analýza), až pak je běh napsaného kódu :) To tedy znamená, že prvně se uvažuje o přetěžování, poté o překrývání. Začnu od statické analýzy. Argument a1 je zde typu A. Objekt a2 při psaní kódu je také typu A. Třída B má přetěžovanou metodu report(). Protože je argument a1 typu A, prozatím se zavolá metoda report(A a).
Nyní nastupuje fáze běhu programu a překrytí metod. Za běhu je a1 pořád typu A, ale a2 je již typu B! Tudíž se stane to, že přednost dostane překrytá metoda ve třídě B. Metoda report(A a) ve třídě A má překrytou metodu ve třídě B, proto je výstup "A again".

b) a2.report(b);
Při psaní kódu jsou objekty a2 typu A a b typu B. Metoda report, která je volána na objektu a2, je přetěžovaná ve třídě A. Zde má přednost přetěžovaná metoda report(A a), protože je hierarchicky blíže objektu b než druhá metoda report(Objet o).
Při běhu programu nastupuje překrývání metod. Za běhu objekt b je pořád třídy B, ale a2 odkazuje na objekt třídy B. Třída B má metodu report(A a), která překryje stejnou metodu z rodičovské třídy A. Ošidný může být přítomnost metody report(B b) ve třídě B. Pokud si ale uvědomíme, že tato metoda je přetěžovanou metodou a přetížení metod probíhá za psaní kódu, tak je jasné, že je to jen trik pro zmatení studentů :) Proto je výstupem "A again".

c) b.report(o);
Při psaní kódu jsou objekty b typu B a objekt o typu Object. Jedním z vlastností dědičnosti je ten, že public a protected metody rodičů jsou děděny a tedy dostupné i pro potomky. Z tohoto důvodu má třída B ne 2, ale 4 různé metody report(). Nejvhodnější je v tomto případě metoda report(Object o).
Při běhu programu je objekt b pořád typu B, tedy nedochází k žádnému překrytí metod. Výstup je "Object".

d) b.report(a2);
Při psaní kódu jsou objekty b typu B a objekt a2 typu A. Třída B definuje vlastní metodu report(A a), proto se použije tato metoda.
Při běhu programu je a2 objektem třídy B. Jak jsme si ale řekli, při běhu dochází jen k překrytí metod. A protože je objekt b pořád objektem třídy B, k žádnému překrytí nedochází. Výstupem je tedy "A again".

Dynamické jazyky alà Groovy

Četl u Groovy, že typ objektu je přiřazen objektu až za běhu. Byl jsem tedy zvědav, co tento kód vypíše v Groovy. Výstupem je toto:

A again
B
A again
B

Protože je typ objektu přiřazen až za běhu, k přetížení i k překrytí metod dochází také za běhu! Co může být trochu zmatené u Groovy je fakt, že povoluje napsat stejný kód jako v Javě, tedy i s definicí typu jako:

A a1 = new A();

Přitom je to stejné, jako kdybyste napsali:

def a1 = new A();

Co si ještě matně pamatuji je fakt, že v Groovy volání metody toto volání nejde přímo k metodě, ale přes něco jako filtr (zapomněl jsem tu třídu), která až pak podle typu argumentů vybere tu správnou metodu. Vše probíhá za běhu, z tohoto důvodu je výstup takový, jaký je. Nevýhodou je rychlost, výhodou je, že můžete přidávat metody a parametry do tříd za běhu a vytvářet takové věci jako elegantně vypadající DSL (nemyslím internet, ale Domain-specific language).
Ve Groovy od verze 2 je možné přidávat anotace jako @CompileStatic, které dynamické vlastnosti zruší, tím se vše zrychlí. Nemám to ale vyzkoušené, jen jsem to četl zběžně.