Janusz Jabłonowski
Związek między pojęciami Zasada podstawialności Podklasy muszą realizować kontrakt zawarty przez nadklasy
Przedefiniowywanie lub podmienianie (ang. overriding, czasami błędnie tłumaczone jako nadpisywanie) Umożliwia realizację polimorfizmu Realizacja kontraktu określonego w nadklasie Zasadniczo powinna być zachowana sygnatura (w sensie nagłówek metody ), ale
Rozważmy: class C{ private void priv() {} void pack() {} protected void prot() {} public void publ() {} }
Czy można zmienić zakresy widoczności? class subc extends C {? void priv() {}? void pack() {}? void prot() {}? void publ() {} }
Mamy zachować kontrakt Poszczególne zakresy widoczności wg rosnącego zasięgu: private (pakietowy) protected public Zaoferowanie wyższego zakresu widoczności nie narusza kontraktu Subtelny problem z private (czyli wręcz przeciwnie)
class PrivSubC extends C { private void priv() {} private void pack() {} private void prot() {} private void publ() {} } I dlaczego?
class PrivSubC extends C { private void priv() {} private void pack() {} private void prot() {} private void publ() {} } priv() się kompiluje, ale oznacza co innego metodę nie związaną z priv() z klasy C. Po dodaniu @Override priv też się nie kompiluje.
class PackSubC extends C { void priv() {} void pack() {} void prot() {} void publ() {} } I dlaczego?
class PackSubC extends C { void priv() {} void pack() {} void prot() {} void publ() {} } priv() jak poprzednio.
class ProtSubC extends C { protected void priv() {} protected void pack() {} protected void prot() {} protected void publ() {} } I dlaczego?
class ProtSubC extends C { protected void priv() {} protected void pack() {} protected void prot() {} protected void publ() {} } priv() jak poprzednio.
class PublSubC extends C { public void priv() {} public void pack() {} public void prot() {} public void publ() {} } I dlaczego?
class PublSubC extends C { public void priv() {} public void pack() {} public void prot() {} public void publ() {} } priv() jak poprzednio.
class A { public void mpubl() {System.out.println("A");} private void mpriv() {System.out.println("A");} void testuja(a a) { a.mpubl(); a.mpriv(); // Do prywatnych składowych innych obiektów // tej samej klasy można się odwoływać } void testujb(b b) { b.mpubl(); b.mpriv(); // Nie można, bo mpriv jest prywatne w B! // Definicja w A nie ma znaczenia. }}
class B extends A{ public void mpubl() private void mpriv() {System.out.println("B");} {System.out.println("B");} void testuja(a a) { a.mpubl(); a.mpriv(); // Błąd, mpriv jest prywatne w A! } }} void testujb(b b) { b.mpubl(); b.mpriv(); // OK.
A a = new A(); B b = new B(); a.testuja(a); // A A a.testuja(b); // B A <- tu jest najciekawiej, // prywatna metoda nie jest wirtualna a.testujb(b); // B b.testuja(a); // A b.testuja(b); // B b.testujb(b); // B B
Prywatne metody nie są wirtualne Ale można je zadeklarować w podklasie z tą samą sygnaturą i mogą wydawać się wirtualne Zatem należy zawsze używać adnotacji @Override!
Są przeznaczone do wykonywania na rzecz klas, a nie obiektów Można je wprawdzie wywołać na rzecz obiektów Ale nie jest to zalecane
class D { static private void a() { System.out.println("D.a"); } static void b() { System.out.println("D.b"); } static protected void c() { System.out.println("D.c"); } static public void d() { System.out.println("D.d"); } }
class PubSubD extends D { static public void a() { System.out.println("PublSubD.a"); } static public void b() { System.out.println("PublSubD.b"); } static public void c() { System.out.println("PublSubD.c"); } static public void d() { System.out.println("PublSubD.d"); } }
// Dodajmy do klasy D static public void test() { D d = new PubSubD(); }; d.a(); d.b(); d.c(); d.d(); // D.a // D.b // D.c // D.d
Kompilator dla wywołań w rodzaju d2.a() generuje ostrzeżenie o dostępie do metody klasowej Nie można podać @Override w metodach w podklasach W podklasie PrivSubD kompilator przyjmuje tylko prywatną metodę, w PackSubD dwie metody, w ProtSubD trzy (tych klas nie ma na slajdach)
Nie są Można je definiować w podklasach z taką samą sygnaturą i wtedy wydają się być przedefiniowane Dla uniknięcia wątpliwości najlepiej wywoływać je zawsze na rzecz klasy, np. D.a(), a nie obiektu, czyli np. nie d2.a(), this.a(), super.a(), choć składniowo te wszsytkie formy są poprawne.
Przy wołaniu przez obiekt metody klasowej sam obiekt nie ma znaczenia. Zupełnie nie ma znaczenia: D d = null; d.a(); // D.a d.b(); // D.b d.c(); // D.c d.d(); // D.d Wykona się z takim samym efektem jak poprzednio!
Zmienna klasowa jest wspólna dla całej klasy, więc występuje w dokładnie jednym egzemplarzu dla klasy (niezależnie od tego, czy obiektów tej klasy jest 0 czy 20 10 ) Ale co wtedy, gdy klasa ma podklasę czy jest jedna czy dwie zmienne klasowe (czyli czy zmienne klasowe się dziedziczą)? Czy można w podklasie mieć zmienną klasową o tej samej nazwie co w nadklasie?
class A { static int a1 = 1; static int a2 = 1;} class B extends A { static int b1 = 1; static int a1 = 1; // Ostrzeżenie kompilatora o przesłonięciu }
A.a1 += 10; A.a2 += 100; B.b1 += 1000; B.a1 += 10000; B.a2 += 100000; System.out.println(" A.a1=" + A.a1 + " A.a2=" + A.a2 + " B.b1=" + B.b1 + " B.a1=" + B.a1 + " B.a2=" + B.a2); A.a1=11 A.a2=100101 B.b1=1001 B.a1=10001 B.a2=100101
Zmienna klasowa jest jedna na klasę i wszystkie jej podklasy Można w podklasie zdefiniować zmienną o takiej samej nazwie, spowoduje to przesłonięcie zmiennej zadeklarowanej wyżej w hierarchii
Dotąd zawsze tak było Ale wiemy już, że może mieć inny (szerszy) zasięg widoczności Czy może mieć inny typ parametrów? Czy może mieć inny typ wyniku?
class Ea{} class Eb extends Ea{} class Ec extends Eb{} class E{ public Eb m(eb e){ return new Eb(); } }
class ESub1 extends E {. // Czy któraś deklaracja jest poprawna? @Override public Ea m(eb e){ return new Eb(); } @Override public Ec m(eb e){ return new Ec(); } @Override public Eb m(ea e){ return new Eb(); } @Override public Eb m(ec e){ return new Eb(); } Pamiętajmy o wypełnianiu kontraktu!
class ESub1 extends E {. // Czy któraś jest poprawna? @Override public Ea m(eb e){ return new Eb(); } @Override public Ec m(eb e){ return new Ec(); } @Override public Eb m(ea e){ return new Eb(); } @Override public Eb m(ec e){ return new Eb(); } Czemu dwie ostatnie metody się kompilują bez @Override?
Podklasa może nieco zmienić typ wyniku przedefiniowanej metody: zamiast typu klasy może być podany typ podklasy Jest to zgodne z zasadą kontraktu Mówimy wtedy o kowariantnym (typ wyniku zmienia się zgodnie ze zmianą typu klasy od nadklasy do podklasy) typie wyniku
Język Java pozwala też na inne odstępstwa: Lista wyjątków zgłaszanych przez metodę przedefiniowującą może być inna, ważne tylko, żeby każdy zgłaszany wyjątek (lub jego nadklasa) były w liście zgłaszanych wyjątków nadklasy (znów zasada zachowania kontraktu) Można dowolnie stosować modyfikatory synchronized, native, strictfp, bo nie mówią o kontrakcie, tylko o jego implementacji
Język Java pozwala też na inne odstępstwa: Metoda przedefiniowująca może być final (a przedefiniowywana?) Adnotacje nie muszą być zachowane Zarówno przedefiniowywana jak i przedefiniowana metoda mogą być abstrakcyjne (obie, jedna z nich, no i żadna oczywiście też)
Czemu zatem Java nie pozwala na kontrawariantne parametry? I czemu kompilator pozwala usunąć adnotację @Override w dwu ostatnich wariantach przedefiniowania metody m w klasie ESub1 (a z @Override nie chciał kompilować)? Spójrzmy ponownie na wcześniejszy slajd (tym razem z usuniętymi dwoma @Override)
class ESub1 extends E {. // Czy któraś z deklaracji jest poprawna? @Override public Ea m(eb e){ return new Eb(); } @Override public Ec m(eb e){ return new Ec(); } public Eb m(ea e){ return new Eb(); } public Eb m(ec e){ return new Eb(); } Czemu?
Wkroczyliśmy (niechcący) w zupełnie inny świat Wyszliśmy ze świata obiektowości Jesteśmy w świecie dowolnych języków programowania (z typami) Użyliśmy przeciążania (ang. overloading) To pożyteczne ale zarazem niebezpieczne narzędzie Przeciążanie nie ma nic wspólnego z przedefiniowywaniem!
Przeciążanie pozwala zdefiniować rodzinę funkcji (procedur, a w językach obiektowych metod) o tej samej nazwie, ale innej sygnaturze To która zostanie użyta kompilator rozpoznaje analizując wywołanie Zatem wybór wywoływanej funkcji (metody) odbywa się podczas kompilacji
Często wygodnie jest użyć tej samej nazwy do wielu operacji o podanym charakterze ale z innymi parametrami (np. wypisz, dodaj) Czasem jesteśmy zmuszeniu do użycia tego mechanizmu (np. w językach obiektowych konstruktor zwykle musi się nazywać tak jak klasa) Niektóre języki (Java nie) pozwalają przeciążać nazwy operatorów
Metody przeciążane muszą być widoczne w jednym zasięgu (inaczej nie byłoby o czym mówić), ale nie muszą być w nim zdefiniowane (np. jedna metoda może być odziedziczona, druga własna) Metody przeciążane muszą się dostatecznie różnić listą parametrów, by kompilator mógł wybrać właściwą
Wystarczające różnice: liczba parametrów (łatwe, nawet programista, nie tylko kompilator, się od razu zorientuje) typy parametrów (oj, bywa trudne) jedno i drugie
class G{} class GSub1 extends G{} class GSub2 extends G{}
class G1{ public void f(int i){system.out.println("f(int)");} public void f(char c){system.out.println("f(char)");} } class G2 extends G1{ public void f(long l){system.out.println("f(long)");} public void f(short s){system.out.println("f(short)");} public void f(byte b){system.out.println("f(byte)");} public void f(g g){system.out.println("f(g)");} public void f(gsub1 g){system.out.println("f(gsub1)");} public void f(gsub2 g){system.out.println("f(gsub2)");} public void f(g g1, G g2){system.out.println("f(g, G)");} public void f(gsub1 g1, G g2){system.out.println("f(gsub1, G)");} public void f(g g1, GSub2 g2){system.out.println("f(g, GSub2)");}}
G2 o = new G2(); G g = new GSub1(); o.f('a'); o.f(13); o.f(((byte)13)); o.f(256); o.f((short)256); o.f(-2147483648); o.f(1l); o.f(g); o.f(new GSub1()); o.f(new GSub2()); o.f(new G(), new G()); o.f(new GSub1(), new G()); o.f(new GSub1(), new GSub1()); o.f(new G(), new GSub2()); o.f(new GSub1(), new GSub2());
G2 o = new G2(); G g = new GSub1(); o.f('a'); f(char) o.f(13); f(int) o.f(256); f(int) o.f(-2147483648); f(int) o.f(1l); f(long) o.f(g); f(g)!!! o.f(new GSub1()); f(gsub1 o.f(new GSub2()); f(gsub2) o.f(new G(), new G()); f(g, G) o.f(new GSub1(), new G()); f(gsub1, G) o.f(new GSub1(), new GSub1()); f(gsub1, G) o.f(new G(), new GSub2()); f(g, GSub2) o.f(new GSub1(), new GSub2()); niejednoznaczne
Ostatni wariant nie skompilował się ze względu na niejednoznaczność. Do tego wywołania pasuje kilka metod, a dwie równie dobrze: public void f(gsub1 g1, G g2){ } public void f(g g1, GSub2 g2){ }
Żeby wywołały się metody oczekujące wartości typu byte albo short, można napisać tak: o.f(13); o.f((byte)13); o.f(256); o.f((short)256); G1.f(int) G1.f(byte) G1.f(int) G1.f(short)
Przykład o.f(g); G1.f(G)!!! Ten przykład jest bardzo ważny, bo pokazuje, że przeciążanie nie zależy od faktycznego typu obiektu!!!
Przeciążane metody mogą mieć dowolne zakresy widoczności i typy wyników (tzn. mogą mieć różne) Przeciążane metody nie mogą się różnić tylko zakresem widoczności (muszą być widoczne w tym samym miejscu)
Przeciążane metody nie mogą się różnić tylko typem wyniku Dlaczego? Popatrzmy: void m1(int i) {} void m1(string s) {} int m2() {return 1;} String m2() {return "";} m1(m2()); // co ma się policzyć?
Przedefiniowywanie i przeciążanie to zupełnie co innego (choć jest często mylone) Przedefiniowywanie odbywa się dynamicznie Przeciążanie jest rozstrzygane statycznie Przedefiniowywanie dotyczy tylko programowania obiektowego, przeciążanie dowolnych języków z typami
W przykładzie (wiele slajdów temu) po usunięciu @Override pojawiły się dwie przeciążone metody m, dlatego kompilacja się powiodła Morał (ponownie): zawsze piszmy @Override przy przedefiniowywaniu (IDE często samo podpowie i napisze)