« poprzedni punkt 

6. Pojęcie dziedziczenia

Zajmiemy się teraz krótko pojęciem dziedziczenia. Pełna dyskusja tej tematyki przewidziana jest na drugi semestr, Tutaj zwrócimy uwagę na te elementy, które będą nam potrzebne w najbliższych wykladach.

Dziedziczenie polega na przejęciu właściwości i funkcjonalności obiektów innej klasy i  ewentualnej ich  modyfikacji i/lub uzupelnieniu w taki sposób, by były one bardziej wyspecjalizowane.

Omawiana wyżej  klasa Publication opisuje właściwości publikacji, które kupuje i sprzedaje księgarnia.  Zauważmy, że za pomocą tej klasy nie możemy w pełni opisać książek. Książki są szczegołną, "wyspecjalizowaną" wersją publikacji, oprócz tytułu, wydawcy, ceny itd - mają jeszcze jedną właściwość - autora (lub autorów).
Gdybyśmy w programie chcieli opisywać zakupy i sprzedaż książek - to powinniśmy stworzyć nową klasę opisującą książki o nazwie np. Book.
Moglibyśmy to robić od podstaw (definiując w klasie Book pola author, title, ident, price i wszystkie metody operujące na nich, jak również metody sprzedaży i kupowania).
Ale po co? Przecież klasa Publication dostarcza już większość potrzebnych nam pól i metod.
Odziedziczymy ją zatem w klasie Book i dodamy tylko te nowe właściwości (pola i metody), których nie ma w klasie Publication, a powinny charakteryzować książki.


Słowo kluczowe extends służy do wyrażenia relacji dziedziczenia jednej klasy przez drugą.
Piszemy:
                class B extends A {
                    ...
                }
co oznacza, że klasa B dziedziczy (rozszerza) klasę A.
Mówimy:
  • klasa A jest bezpośrednią  nadklasą, superklasą, klasą bazową klasy B
  • klasa B jest bezpośrednią podklasą, klasą pochodną klasy A

Zapiszmy zatem:

public class Book extends Publication {
// definicja klasy Book
}

Co należy podać w definicji nowej klasy?
Takie właściwości jak tytuł, wydawca, rok wydania, identyfikator, cena, liczba publikacji "na stanie", metody uzyskiwania informacji o tych cechach obiektów oraz metody sprzedaży i zakupu - przejmujemy z klasy Publication. Zatem nie musimy ich na nowo definiować.
Pozostało nam tylko zdefiniować nowe pole, opisujące autora (niech nazywa się author) oraz metodę, która umożliwia uzyskanie informacji o autorze (powiedzmy getAuthor()).

class Book extends Publication {

  private String author;

  public String getAuthor() {
    return author;
  }
}

Czy to wystarczy?
Nie, bo jeszcze musimy powiedzieć w jaki sposób mają być inicjowane obiekty klasy Book. Aha, potrzebny jest konstruktor.
Naturalnie, utworzenie obiektu-książki wymaga podania:

  • autora,
  • tytułu,
  • wydawcy,
  • roku wydania,
  • identyfikatora (numeru ISBN),
  • ceny,
  • liczby książek aktualnie "na stanie".

Czyli konstruktor powinien mieć postać:

  public Book(String aut, String tit, String pub, int y, String id,
              double price, int quant) {
    ....
  }

Zwróćmy jednak uwagę: pola tytułu, wydawcy, roku, identyfikatora, ceny i ilości - są prywatnymi polami klasy Publication. Z klasy Book nie mamy do nich dostępu. Jak je zainicjowac?

Pola nadklasy (klasy bazowej) inicjujemy za pomocą wywołania z konstruktora klasy pochodnej konstruktora klasy bazowej (nadklasy)


Użycie  w konstruktorze następującej konstrukcji składniowej:

        super(lista_argumentów);

oznacza wywołanie konstruktora klasy bazowej z argumentami lista_argumentów .
Jeśli występuje - MUSI być pierwszą instrukcją konstruktora klasy pochodnej.
Jeśli nie występuje - przed utworzeniem obiektu klasy pochodnej zostanie wywołany konstruktor bezparametrowy klasy bazowej.

Konstruktor klasy Book musi więc wywołać konstruktor nadklasy, po to by zainicjować jej pola, a następnie zainicjować pole author.

  // Konstruktor klasy Book
  // argumenty: aut - autor, tit - tytuł, pub - wydawca, y - rok wydania
  //            id - ISBN, price - cena, quant - ilość   
  public Book(String aut, String tit, String pub, int y, String id,
              double price, int quant) {
    super(tit, pub, y, id, price, quant);
    author = aut;
  }

Teraz można podać już pełną definicję klasy Book.

public class Book extends Publication {

  private String author;

  public Book(String aut, String tit, String pub, int y, String id,
              double price, int quant) {
    super(tit, pub, y, id, price, quant);
    author = aut;
  }

 public String getAuthor() {
   return author;
 }

}

Zwróćmy uwagę: wykorzystanie klasy Publication (poprzez jej odziedziczenie) oszczędziło nam wiele pracy. Nie musieliśmy ponownie definiować pól i metod z klasy Publication w klasie Book.

Przy tak zdefiniowanej klasie Book możemy utworzyć jej obiekt:

    Book b = new Book("James Gossling", "Moja Java", "WNT", 2002,
                      "ISBN6893", 51.0, 0);

Ten obiekt zawiera:

  • elementy określane przez pola klasy dziedziczonej (Publication) - czyli: title, publisher, year, ident, price, quantity
  • element określany przez pole klasy Book - author
r

Podkreślmy: jest to jeden obiekt klasy Book.
Wiemy na pewno, że możemy użyć na jego rzecz metody z klasy Book - getAuthor().

Ale ponieważ klasa Book dziedziczy klasę Publication to obiekty klasy Book mają również wszelkie właściwości obiektów klasy Publication , a zatem możemy na ich rzecz używać również metod zdefiniowanych w klasie Publication.

Nic zatem nie stoi na przeszkodzie, by napisać taki program:

class TestBook {

  public static void main(String[] args) {

    Book b = new Book("James Gossling", "Moja Java", "WNT", 2002,
                      "ISBN6893", 51.0, 0);
    int n = 100;
    b.buy(n);
    double koszt = n * b.getPrice();
    System.out.println("Na zakup " + n + " książek:");
    System.out.println(b.getAuthor());
    System.out.println(b.getTitle());
    System.out.println(b.getPublisher());
    System.out.println(b.getYear());
    System.out.println(b.getIdent());
    System.out.println("---------------\nwydano: " + koszt);
    b.sell(90);
    System.out.println("---------------");
    System.out.println("Po sprzedaży zostało " + b.getQuantity() + " pozycji");
  }

}
Na zakup 100 książek:
James Gossling
Moja Java
WNT
2002
ISBN6893
---------------
wydano: 5100.0
---------------
Po sprzedaży zostało 10 pozycji

który skompiluje się i wykona poprawnie dając w wyniku pokazany listing.


Możemy powiedzieć, że obiekty klasy Book są również obiektami klasy Publication
(w tym sensie, że mają wszelkie właściwości obiektów klasy Publication)

Dzięki temu referencje do obiektów klasy Book możemy przypisywać zmiennym, oznaczającym obiekty klasy Publication (zawierającym referencje do obiektów klasy Publication). Np.

Book b = new Book(...);
Publication p = b;  

Nazywa się to referencyjną konwersją rozszerzającą (ang. widening reference conversion). Słowo konwersja oznacza, że dochodzi do przekształcenia  z jednego typu do innego typu (np. z typu Book do typu Publication). Konwersja jest rozszerzająca, bowiem, przekształcamy typ "pochodny" (referencja do obiektu podklasy) do typu "wyższego" (referencja do obiektu nadklasy). A ponieważ chodzi o typy referencyjne - mówimy o referencyjnej konwersji rozszerzającej,

Nieco mniej precyzyjnie, ale za to podkreślając, że chodzi o operowanie na obiektach, będziemy mówić o takich konwersjach jako o obiektowych konwersjach rozszerzających (ang. "upcasting" - up - bo w górę hierarchii dziedziczenia).

Obiektowe konwersje rozszerzające dokonywane są automatycznie przy:

  • przypisywaniu zmiennej-referencji  odniesienia do obiektu klasy pochodnej,
  • przekazywaniu argumentów metodzie, gdy parametr metody jest typu "referencja do obiektu nadklasy argumentu",
  • zwrocie wyniku, gdy wynik podstawiamy na  zmienną będącą  referencją do obiektu nadklasy zwracanego wyniku

Ta zdolność obiektów Javy do "stawania się" obiektem swojej nadklasy jest niesłychanie użyteczna.

Wyobraźmy sobie np. że oprócz klasy Book - z klasy Publication wyprowadziliśmy jeszcze klasę Journal  (czasopisma)
Klasa Journal dziedziczy klasę Publication i dodaje do niej - zamiast pola, opisującego autora - pola opisujące wolumin i numer wydania danego czasopisma.
Być może będziemy mieli jeszcze inne rodzaje publikacji - np. muzyczne, wydane na płytach CD (powiedzmy klasę CDisk, znowu dziedziczącą klasę Publication, i dodającą jakieś właściwe dla muzyki informacje, np. czas odtwarzania).

Możemy teraz np. napisać uniwersalną metodę pokazującą różnicę w dochodach ze sprzedaży wszystkich zapasów dowolnych dwóch publikacji.

public double incomeDiff(Publication p1, Publication p2) {
  double income1 = p1.getQuantity() * p1.getPrice();
  double income2 = p2.getQuantity() * p2.getPrice();
  return income1 - income2;
} 

i wywoływać ją dla dowolnych (różnych rodzajów) par publikacji

Book b1 = new Book(...);
Book b2 = new Book(...);
Journal j = new Journal(...);
CDisk cd1 = new CDisk(...);
CDisk cd2 = new CDisk(...);

double diff = 0;
diff = incomeDiff(b1, b2);
diff = incomeDifg(b1, j);
diff = inocmeDiff(cd1, b1);

Gdyby nie było obiektowych konwersji rozszerzających, to dla każdej mozliwej kombinacji "rodzajowej" par - musielibyśmy napisać inną metodę incomeDiff np.
double incomeDiff(Book, Book), double incomeDiff(Book, Journal), double incomeDiff(Book, CDisk) itd.

Zwróćmy uwagę, że w przedstawionej metodzie incomeDiff można wobec p1 i p2 użyć metod klasy Publication (bo tak są zadeklarowane parametry), ale nie można używać metod klas pochodnych, nawet wtedy, gdy p1 i p2 wskazują na obiekty klas pochodnych. Np.

....
{
Book b1 = new Book(...);
Book b2 = new Book(...);
jakasMetoda(b1,b2);
....
}

void jakasMetoda(Publication p1, Publication p2) {
String autor = p1.getAuthor();  // Błąd kompilacji - niezgodność typów
 ...                                          // na rzecz obiektu klasy Publication
 ...                                          // nie wolno użyć metody getAuthor()
}                                            // bo takiej metody nie ma w klasie Publication

Więcej na temat konwersji dowiemy się w przyszłych wykładach, a jeśli chodzi o pełne zrozumienia znaczenia dziedziczenia i roli konwersji referencyjnych - to uzyskamy je w drugim semestrze, gdzie zagadnienia obiektowości będą szczególnie akcentowane.

Na koniec krótkiego, wstępnego, mającego raczej instrumentalny dla dalszych wykładów tego semestru charakter, wprowadzenia do dziedziczenia, należy zaznaczyć bardzo ważną właściwość Javy.

W Javie każda klasa może bezpośrednio odziedziczyć tylko jedną klasę.
Ale pośrednio może mieć dowolnie wiele nadklas, co wynika z hierarchii dziedziczenia.
Ta hierarchia zawsze zaczyna się na klasie Object (której definicja znajduje się w zestawie stanardowych klas Javy).
Zatem w Javie wszystkie klasy pochodzą pośrednio od klasy Object.
Jeśli definiując klasę nie użyjemy słowa extends (nie zażądamy jawnie dziedziczenia), to i tak nasza klasa domyślnie będzie dziedziczyć klasę Object (tak jakbyśmy napisali class A extends Object).

Wobec tego hierarchia dziedziczenia omawianych tu klas wygląda następująco:

r

Z tego wynika, że:

referencję do obiektu dowolnej klasy można przypisać zmiennej typu Object (zawierającej referencję do obiektu klasy Object).

Z właściwości tej korzysta wiele "narzędziowych" metod zawartych w klasach standardu Javy.


« poprzedni punkt