Podstawy programowania obiektowego Technologie internetowe Wykład 7
Program wykładu Dziedziczenie Wywołania konstruktorów w dziedziczeniu Hermetyzacja w dziedziczeniu Dziedziczenie typu, rzutowanie w górę i w dół Mechanizm RTTI Nadpisywanie pól Nadpisywanie metod polimorfizm Klasy i metody finalne Klasy i metody abstrakcyjne
Dziedziczenie technika konstruowania nowych klas z wykorzystaniem klas już istniejących (jak kompozycja!) opisuje relację między 2 klasami:... jest szególnym przypadkiem...,... jest rodzajem... rzadziej stosowana i trudniejsza technika od kompozycji Komputer Samochód Figura Trójkąt Czworokąt Notebook Ciężarówka Wyścigówka Prostokąt Kwadrat
Dziedziczenie w kodzie class Figura {... class Trojkat extends Figura {... class Czworokat extends Figura {... class Prostokat extends Czworokat {... class Kwadrat extends Prostokat {... Klasa bazowa Nadklasa Klasa dziedziczona Base class Super class Figura Klasa dziedzicząca Podklasa Derived class Base class Subclass Trójkąt Czworokąt Prostokąt Kwadrat
Dziedziczenie jako zawieranie się podzbiorów każdy kwadrat jest prostokątem wśród prostokątów niektóre są szczególne: kwadraty kwadrat zatem dziedziczy z prostokąta CZWOROKĄTY FIGURY Figura TROJKĄTY PROSTOKĄTY Trójkąt Czworokąt KWADRATY Prostokąt Kwadrat
Konsekwencje dziedziczenia klasa dziedzicząca przejmuje kod z klasy bazowej: class Figura { Punkt srodek; public void przesun(int dx, int dy) { srodek.przesun (dx, dy); class Kolo extends Figura { int r;... Kolo k = new Kolo(); k.srodek = new Punkt(5,5); k.r = 3; k.przesun(1,0); korzyści: mniej kodowania propagowanie zmian
Konstruktory w dziedziczeniu Konstruktory nie dziedziczą się!!! class Figura { Punkt srodek; public Figura(Punkt s) { srodek = new Punkt(s); class Kolo extends Figura { int r; public Kolo(Punkt s, int rr) { srodek=new Punkt(s); r=rr;... Figura f=new Figura( new Punkt(0,0)); Kolo k = new Kolo ( new Punkt(10,10), 5); ale to się nie skompiluje dlaczego?
Obiekty klas dziedziczących tworzą się po kolei aż 4 obiekty (5)!!! Kwadrat k = new Kwadrat(new Punkt(1,1), 10)); Figura środek 1 2 3 4 Czworokąt Figura środek Prostokąt Czworokąt Figura środek Kwadrat Prostokąt Czworokąt Figura środek new Figura(...) new Czworokat(...) new Prostokat(...) new Kwadrat(...) a każdy obiekt to wywołanie konstruktora który potrzebuje parametrów który konstruktor ma się wywołać (przeciążanie)?
Porządek wywołania konstruktorów każdy konstruktor klasy dziedziczącej musi wskazać konkretny konstruktor klasy bazowej, który wywoła się przed nim: nazwa klasy bazowej zastępowana jest słowem super konstruktor wybiera się podając jego parametry (przeciążanie!) wskazanie to musi być pierwszą instrukcją konstruktora jeśli tego nie zrobi kompilator wskaże konstruktor domyślny: class B extends A { public B() { public B() { super(); tu: zanim wywoła się domyślny konstruktor klasy B przed nim wywoła się domyślny konstruktor klasy A
Przykład wywołań konstruktorów class A { public A() { System.out.println("A.A()"); public A(int i) { System.out.println("A.A(int)"); class B extends A { public B() {super(0);system.out.println("b.b()"); public B(int i) {System.out.println("B.B(int)"); class C extends B { public C() { System.out.println("C.C()"); public C(int i) {super(i);system.out.println("c.c(int)"); A.A() A.A(int)... new C(); new C(1); A.A(int) B.B() C.C() A.A() B.B(int) C.C(int) B.B() C.C() B.B(int) C.C(int)
Konstruktory jeszcze raz brakuje konstruktora domyślnego Figury class Figura { Punkt srodek; public Figura(Punkt s) { srodek = new Punkt(s); class Kolo extends Figura { int r; public Kolo(Punkt s, int rr) { super(); srodek=new Punkt(s); r=rr; to wstawia kompilator... Figura f=new Figura( new Punkt(0,0)); Kolo k = new Kolo ( new Punkt(10,10), 5); lub wskazania innego konstruktora i jego parametrów
Konstruktory jeszcze raz wskazanie właściwego konstruktora class Figura { Punkt srodek; public Figura(Punkt s) { srodek = new Punkt(s); class Kolo extends Figura { int r; public Kolo(Punkt s, int rr) { super(s); srodek=new Punkt(s); r=rr;... Figura f=new Figura( new Punkt(0,0)); Kolo k = new Kolo ( new Punkt(10,10), 5); przy okazji inicjacja odziedziczonego pola który powinien być... prywatny?
Hermetyzacja w dziedziczeniu czy prywatne pola/metody się dziedziczą? class Figura { private Punkt srodek; public Figura(Punkt s) { srodek = new Punkt(s); class Kolo extends Figura { int r; public Kolo(Punkt s, int rr) { super(s); r=rr; public void przesun(int dx, int dy) { srodek.przesun(dx, dy); BŁĄD HERMETYZACJI dziedziczą się, ale są niedostępne! metoda 'przesun' powinna być zdefiniowana w Figurze albo należy użyć nowego poziomu ochrony... `
Poziom ochrony 'protected' pośredni poziom między 'public' a 'private' publiczny dla rodziny (w drzewie dziedziczenia) prywatny dla innych (obcych) klas class Figura { protected Punkt srodek; public Figura(Punkt s) { srodek = new Punkt(s); class Kolo extends Figura { int r; public Kolo(Punkt s, int rr) { super(s); r=rr; public void przesun(int dx, int dy) { srodek.przesun(dx, dy); OK `... Figura f=new Figura( new Punkt(0,0)); f.srodek=null; BŁĄD Kolo k = new Kolo ( new Punkt(10,10), 5); System.out.println(k.srodek); BŁĄD
Dziedziczenie nie dziedziczy się: konstruktorów (destruktorów, operatora= w C++) dziedziczy się: pola metody typ `
Po co typ? kompilator pilnuje zgodności typów: int i; float f; i = f; BŁĄD void f(int a) {... f( abc ); BŁĄD Samochód s = new Komputer(); BŁĄD ale czasami można go oszukać rzutowanie (casting) ` i = (int)f; OK Samochód s = (Samochód)new Komputer(); BŁĄD
Dziedziczenie typu klasa Kwadrat ma 4 typy (5): Figura Czworokat Prostokat Kwadrat więc kompilator dopuści podstawienie: Figura f = new Kwadrat(...); rzutowanie w górę!!! (upcasting) ` Figura Czworokąt Prostokąt Kwadrat
Rzutowanie w górę Figura[] figury = new Figura[100]; figura[0] = new Kwadrat(...); figura[1] = new Prostokat(...); figura[2] = new Trojkat(...); rzutowanie to zachodzi tylko w dziedziczeniu zawsze jest możliwe i bezpieczne upraszcza przechowywanie obiektów z 1 rodziny `
Klasa Object dlaczego Kwadrat ma 5 typów? każda klasa w Javie dziedziczy z klasy 'Object' (bezpośrednio lub pośrednio) class Figura {... class Figura extends Object {... pojedyncze drzewo dziedziczenia Object Figura składowe klasy Object: public String tostring(); public boolean equals(object o); metody związane z wątkami referencja typu 'Object' Object o = new... ` Czworokąt Prostokąt Kwadrat
Utrata dostępu do informacji Figura[] figury; for (Figura f: figury) f.przesun(1, 0); ale Figura f = new Kolo(new Punkt(1,2), 10); System.out.println(f.getR()); skąd wiadomo że 'f' to Koło? Figura f; if (random.nextint(2)%2 == 0) f = new Kolo(new Punkt(1,2), 10); else f = new Prostokat(new Punkt(1,2), 4, 6);
Utrata dostępu do informacji obiekt Koło cały czas jest w pamięci nie zmienia typu!!! (obiekt raz stworzony jako Koło zawsze będzie już Kołem) rzutowanie w górę zmienia tylko sposób patrzenia na obiekt kompilator mając referencję 'Figura f' nie zna rzeczywistego typu obiektu na który ona wskazuje wie, że ten obiekt to jakaś Figura i ma metodę 'przesun(int,int)' f - srodek:punkt +przesun(int,int) - r: int + getr(): int
Rzutowanie w dół (downcasting) rzutowanie z typu bazowego na typ pochodny ale to zły przykład: Kolo k = (Kolo)new Figura(...); dobry przykład: Figura f = new Kolo(...); Kolo k = (Kolo)f; poprawne rzutowanie w dół może nastąpić tylko po wcześniejszym rzutowaniu w górę odzyskanie utraconego dostępu do informacji Object Figura Czworokąt Prostokąt Kwadrat rzutowanie w dół jest ryzykowne
Mechanizm RTTI f? identyfikacja typu w czasie wykonania programu (Run-Time Type Identification) umożliwia świadome i poprawne rzutowanie w dół if (f instanceof Kolo) { Kolo k = (Kolo)f; System.out.println(k.getR()); operator 'instanceof' bada, czy 'f ' ma typ 'Kolo' jego bezpośrednie używanie (nadużywanie) jest uważane za mało eleganckie
Wykorzystanie 'instanceof' policzenie figur w tablicy Figura[] figury={ new Kwadrat(...), new Prostokat(...), new Czworokat(...) int lczw, lprost, lkw=0; for (Figura f: figury) { if (f instanceof Kwadrat) ++ lkw; if (f instanceof Prostokat) ++ lprost; if (f instanceof Czworokat) ++ lczw; System.out.println( Kwadraty: +lkw); System.out.println( Prostokąty: +lprost); System.out.println( Czworokąty: +lczw); wynik: 1 2 3!!! przecież każdy prostokąt jest też czworokątem!!!
Wykorzystanie 'instanceof' alternatywne policzenie figur w tablicy... if (f instanceof Kwadrat) ++ lkw; else if (f instanceof Prostokat) ++ lprost; else if (f instanceof Czworokat) ++ lczw;... lub: if (f instanceof Kwadrat) ++ lkw; if ( f instanceof Prostokat &&! f instanceof Kwadrat) ++ lprost; if ( f instanceof Czworokat &&! f instanceof Prostokat) ++ lczw; Object Figura Czworokąt Prostokąt Kwadrat
Nadpisywanie składowych w dziedziczeniu składowe dziedziczą się ale dopuszczalne jest ich nadpisywanie: class A { int x; class B extends A { int x; w takiej sytuacji obiekt B ma 2 zmienne 'x'!!! w przypadku pól to zwykle pomyłka x x 5 3 B A
Nadpisywanie pól class A { int x; class B extends A { int x; class C extends B { int x; public void showall() { System.out.println("A.x="+A.x+",B.x="+super.x+",C.x="+x); C c = new C(); c.x = 30; c.showall(); ((B)c).x = 20; ((A)c).x = 10; c.showall(); ważny jest typ referencji!!! x x x 30 20 10 B A C
Nadpisywanie metod w zasadzie nadpisywanie metod działa tak samo ale w Javie jest specjalny mechanizm, który komplikuje sprawę: POLIMORFIZM przy wywołaniu metod nie jest ważny typ referencji (jak w przypadku pól) ważny jest typ obiekty który się za nią ukrywa f? metody wywoływane w ten sposób nazywane są metodami/funkcjami wirtualnymi
Nadpisywanie a przeciążanie (overriding vs overloading) to nie jest nadpisywanie tylko przeciążanie: class A { String f() { return A.f ; class B extends A { String f(int a) { return B.f ; class C extends B { String f(char c) { return C.f ; C c = new C(); System.out.println(c.f()); System.out.println(c.f(0)); System.out.println(c.f('a')); nadpisywanie: + A.f() + B.f(int) + C.f(char) C B A class A { String f() { return A.f ; class B extends A { String f() { return B.f ; class C extends B { String f() { return C.f ; System.out.println(c.f());??? + B.f() + C.f() + A.f() C B A
Wiązanie metod (binding) class A { public String f() { return A.f ; class B extends A { public String f() { return B.f ; class C extends B { public String f() { return C.f ; A a = new C(); System.out.println(a.f()); obiekt klasy C ma 3 metody 'f()' C B A + A.f(): String + B.f(): String + C.f(): String 0x02ca4 0x02cc4 0x02ce4 każda ma inny adres w pamięci kodu kompilator musi wskazać, którą metodę wywołać skompilować znaczy przetłumaczyć na asembler: a.f(); jump 0x02cc4? wiązanie metody
Wiązanie wczesne i późne jeśli kompilator faktycznie wiąże metodę wiązanie wczesne a.f(); jump 0x02ca4 jaką metodę wybiera? z tej klasy, jakiego typu jest referencja 'a' (A) jeśli kompilator nie wiąże kodu wiązanie późne a.f(); jump kiedy wykona się te wiązanie? w czasie wykonania programu (run-time) jaka metoda zostanie wybrana? z tej klasy, jakiego typu jest obiekt
Funkcje/Metody wirtualne metody wiązane wcześnie zwykłe metody metody wiązane późno metody wirtualne mechanizm późnego wiązania metod polimorfizm w Javie (prawie) wszystkie metody są wirtualne nie trzeba ich oznaczać (jak w C++ 'virtual') metody wirtualne mogą nieznacznie spowalniać wykonanie programu
Typowe użycie f. wirtualnych class Figura { public String opis(){return Jestem Figurą ; Trójkąt class Trojkat extends Figura { public String opis(){return Jestem Trójkątem ; class Czworokat extends Figura { public String opis(){return Jestem Czworokątem ; Figura Czworokąt Prostokąt Kwadrat class Prostokat extends Czworokat { public String opis(){return Jestem Prostokątem ; class Kwadrat extends Prostokat { public String opis(){return Jestem Kwadratem ;... Figura f=new??? System.out.println(f.opis());... System.out.println(xxx.opis());??? xxx
Metody niewirtualne w Javie wyjątkami (zwykłymi metodami) są metody: finalne statyczne prywatne class A { private String f() { return A.f ; public String g() { return A.g; public String call() { return f()+ +g(); class B extends A { private String f() { return B.f ; public String g() { return B.g;... A a = new B(); System.out.println(a.call());
Metody niewirtualne w Javie wyjątkami (zwykłymi metodami) są metody: finalne statyczne prywatne class A { private String f() { return A.f ; public String g() { return A.g; public String call() { return f()+ +g(); class B extends A { private String f() { return B.f ; public String g() { return B.g;... A a = new B(); System.out.println(a.call()); A.f B.g wywołanie wirtualne wywołanie niewirtualne
Jeszcze metody niewirtualne class A { private String f() { return A.f ; public String g() { return A.g; class B extends A { private String f() { return B.f ; public String g() { return B.g; public String call() { return f()+ +g();... A a = new B(); System.out.println(a.call());??? metoda 'call' w klasie B
Jeszcze metody niewirtualne class A { private String f() { return A.f ; public String g() { return A.g; class B extends A { private String f() { return B.f ; wywołanie wirtualne public String g() { return B.g; public String call() { return this.f()+ +this.g();... wywołanie niewirtualne A a = new B(); System.out.println(a.call()); B.f B.g 'this' w kodzie klasy A jest typu A 'this' w kodzie klasy B jest typu B
Poziom kodu class A { protected int x = 10; public int getax() { return x; class B extends A { protected int x = 20; public int getbx() { return x; x 10 class C extends B { protected int x = 30; x 20 public int getcx() { return x;... C c= new C(); x 30 System.out.println( +c.getax()+c.getbx()+c.getcx()); C B A
Poziom kodu class A { typu A protected int x = 10; public int getax() { return this.x; class B extends A { typu B protected int x = 20; public int getbx() { return this.x; class C extends B { typu C protected int x = 30; C public int getcx() { return this.x; # x +getcx()... C c= new C(); System.out.println( +c.getax()+c.getbx()+c.getcx()); 10 20 30 kod sięga do pola ze swojemu poziomu potem do pól odziedziczonych (do góry) A # x +getax() B # x +getbx() nigdy nie sięga w dół (do klas dziedziczących)
Klasy i metody finalne z klasy finalnej nie można dziedziczyć finall class Ostateczna {... class Nowa extends Ostateczna {... metody finalnej nie można nadpisać jest też metodą niewirtualną (m.in. szybciej się wykonuje wiązanie wczesne) class Prostokat { protected double a, b; public final double pole() { return a*b; public Prostokat(double aa, double bb) { a=aa; b=bb; class Kwadrat extends Prostokat { public Kwadrat(int a) { super(a, a); public double pole() { return...
Klasy abstrakcyjne z klasy abstrakcyjne nie można utworzyć obiektu abstract class Figura {... new Figura(...); klasa abstrakcyjna służy tylko do dziedziczenia class Kolo extends Figura {... class Trojkat extends Figura {... ale można tworzyć referencje typu abstrakcyjnego Figura f = null; Figura[] figury = new Figura[100]; f=new Kolo(...); figury[0] = new Trojkat(...); klasa abstrakcyjna to zwykła klasa: może mieć pola, metody, konstruktory klasa abstrakcyjna ma 1 przywilej: tylko ona może posiadać metody abstrakcyjne...
Metody abstrakcyjne metoda abstrakcyjna to metoda niedokończona, zawiera tylko nagłówek abstract class Figura { protected Punkt srodek; public abstract double pole(); używana kiedy jej definicja nie miałaby sensu (jak policzyć pole jakiejś nieokreślonej figury?) klasa która dziedziczy z klasy abstrakcyjnej metodę abstrakcyjną musi ją zdefiniować do końca class Kolo extends Figura { protected double r; public double pole() { return Math.PI*r*r; metoda abstrakcyjna nigdy nie będzie wywoływana: jest wirtualna a nigdy nie stworzy się obiekt klasy Figura
Po co metody abstrakcyjne? kompilator ma wywołanie: Figura f =... f.pole(); musi sprawdzić, czy w klasie Figura metoda 'pole()' nie jest np.: statyczna, finalna, prywatna niewirtualna metoda abstrakcyjna narzuca na klasy dziedziczące obowiązek posiadania kompletnej metody 'pole()' (jeśli same nie chcą być abstrakcyjne): abstract class Figura2D extends Figura { metoda abstrakcyjna definiuje interfejs hierarchii klas
Wywoływanie metod zastępowanych konstruktor klasy dziedziczącej woła konstruktor klasy bazowej nowa metoda wirtualna całkowicie zastępuje przesłanianą metodę metoda zastępujące może po raz ostatni wywołać metodę zastępowaną class Figura { public String opis() {return Jestem Figurą ; class Kolo extends Figura { public String opis(){return super.opis()+ i Kołem ;... Figura f=new Koło(); System.out.println(f.opis()); Jestem Figurą i Kołem
Metoda 'public String tostring()' metoda zdefiniowana w klasie 'Object' oryginalna definicja wypisuje rzeczywistą nazwę klasy i nr referencji zwraca tekstową reprezentację obiektu często wykorzystywana class Punkt extends Object { private x, y;... Punkt p=new Punkt (1,2); System.out.print(p);<==>System.out.print(p.toString()); Punkt@0x23cd można ją nadpisać i w nowej metodzie wywołać oryginalną definicję: class Punkt extends Object { private x, y; public String tostring() { return super.tostring+ (x: +x+,y: +y+ ) ; System.out.print(p); Punkt@0x23cd(x:1,y:2)
Hermetyzacja a funkcje wirtualne przy nadpisywaniu metod wirtualnych nie można zmniejszyć poziomu dostępności (zwiększyć poziomu ochrony): class Figura { public String opis(){return Jestem Figurą ; class Kolo extends Figura { protected String opis(){return Jestem Kołem ; // BŁĄD kompilacji poziom ochrony (dostępności) należy co najmniej zachować class Kolo extends Figura { public String opis(){return Jestem Kołem ; // BŁĄD kompilacji