Rozdział 10 - Dziedziczenie i polimorfizm - Konstruktory i tworzenie obiektów klas pochodnych

W tym rozdziale zobaczysz, jak istnienie konstruktorów wpływa na dziedziczenie klas. Najpierw jednak przypomnijmy sobie, czym są konstruktory.

Powtórka z konstruktorów

W rozdziale o klasach poznaliśmy specjalny rodzaj metod – konstruktory. Korzystamy z nich, gdy tworzymy obiekty danej klasy, podając nazwę konstruktora po słowie kluczowym new:

SamochodWyscigowy wyscigowy = new SamochodWyscigowy();

Konstruktory mogą przyjmować argumenty, które mogą posłużyć za wartości, którymi chcemy zainicjalizować tworzony obiekt:

Osoba osoba = new Osoba("Jan", "Nowak");

Jeżeli nie zdefiniujemy w naszej klasie konstruktora, otrzyma ona automatycznie domyślny konstruktor, który jest równoważny pustemu konstruktorowi bez argumentów. W poprzednim rozdziale, w klasie Pojazd, nie zdefiniowaliśmy konstruktora:

package pojazdy;

public class Pojazd {
  private String rejestracja;

  public void jedz() {
    System.out.println("Pojazd jedzie.");
  }
}

Powyższa klasa Pojazd pomimo, że nie definiuje konstruktora, posiada domyślny, pusty konstruktor, wygenerowany dla naszej wygody przez kompilator języka Java. Jest to równoważne z poniższą wersją tej klasy:

package pojazdy;

public class Pojazd {
  private String rejestracja;

  public Pojazd() {

  }

  public void jedz() {
    System.out.println("Pojazd jedzie.");
  }
}

Jeżeli klasa dostarcza chociaż jeden konstruktor, to domyślny konstruktor nie zostanie dla niej wygenerowany.

Jeżeli w Twojej klasie jest kilka konstruktorów, to możesz z jednego konstruktora wywołać inny, używając słowa kluczowego this. Musi to być jednak zawsze pierwsza instrukcja w kodzie konstruktora. Widzieliśmy taki przykład w rozdziale o konstruktorach:

Nazwa pliku: Rozdzial_09__Klasy\Film.java
public class Film {
  private String tytul;
  private String rezyser;
  private double cenaBiletu;

  public Film() {
    this("<nienazwany film>", "<brak rezysera>", 20.0);
  }

  public Film(String tytul) {
    this(tytul, "<brak rezysera>", 20.0);
  }

  public Film(String tytul, String rezyser) {
    this(tytul, rezyser, 20.0);
  }

  public Film(String tytul, String rezyser, double cenaBiletu) {
    this.tytul = tytul;
    this.rezyser = rezyser;
    this.cenaBiletu = cenaBiletu;
  }
}

Klasa Film zawiera kilka konstruktorów, które ustawiają różne pola obiektów tej klasy. Na początku każdego konstruktora, poza ostatnim, wywołujemy właśnie ten ostatni konstruktor, za pomocą słowa kluczowego this.

Konstruktory klas bazowych i kolejność tworzenia obiektów

Gdy tworzymy obiekt klasy, która rozszerza inną klasę, powinniśmy na samym początku konstruktora tej klasy pochodnej wywołać konstruktor z klasy bazowej.

Dlaczego do tej pory w przykładach dziedziczenia nigdy tego nie robiliśmy? Podobnie jak w przypadku generowania domyślnych konstruktorów, wywoływanie bezargumentowych konstruktorów z klas bazowych jest dla nas wykonywane automatycznie, jeżeli sami tego nie zrobimy. Dlatego wszystkie dotychczasowe przykłady działały – nie definiowaliśmy w klasach bazowych żadnego konstruktora, więc otrzymywały one bezargumentowe konstruktory domyślne. Nie wywoływaliśmy konstruktorów klas bazowych w klasach pochodnych, więc były one wywoływane za nas:

package pojazdy;

public class Pojazd {
  private String rejestracja;

  public void jedz() {
    System.out.println("Pojazd jedzie.");
  }
}
package pojazdy;

public class Samochod extends Pojazd {
  protected int liczbaKol;
}
package pojazdy;

public class SamochodWyscigowy extends Samochod {
  public SamochodWyscigowy() {
    this.liczbaKol = 4;
  }

  public String toString() {
    return "Samochod wyscigowy, liczba kol: " + liczbaKol;
  }
}

Żadna z klas Pojazd i Samochod nie definiuje konstruktora, więc otrzymują po jednym, bezargumentowym konstruktorze domyślnym. Konstruktor domyślny klasy Samochod jest automatycznie wywoływany na początku konstruktora klasy SamochodWyscigowy, gdy tworzymy obiekt tej klasy. Domyślny konstruktor klasy Pojazd jest z kolei wywoływany w domyślnym konstruktorze klasy Samochod. Żadnej z tych operacji nie zobaczymy powyżej, ponieważ te konstruktory są generowane i wywoływane automatycznie w momencie kompilacji przez kompilator.

Spójrzmy na inny przykład, który lepiej zobrazuje to zagadnienie. Trzy poniższe, proste klasy, dostarczają po jednym, bezargumentowym konstruktorze. Wypiszemy w każdym z nich informację, z której klasy dany konstruktor pochodzi:

Nazwa pliku: domyslneKonstruktory\A.java
package domyslneKonstruktory;

public class A {
  public A() {
    System.out.println("Tworzę A.");
  }
}
Nazwa pliku: domyslneKonstruktory\B.java
package domyslneKonstruktory;

public class B extends A {
  public B() { 					// 1
    System.out.println("Tworzę B.");
  }
}
Nazwa pliku: domyslneKonstruktory\C.java
package domyslneKonstruktory;

public class C extends B {
  public C() { 					// 2
    System.out.println("Tworzę C.");
  }

  public static void main(String[] args) {
    C c = new C(); 				// 3
  }
}

Każda klasa definiuje własny, bezargumentowy konstruktor. Klasa B dziedziczy po klasie A, a klasa C – po klasie B. Zauważ, że w konstruktorze klasy B (1) nie wywołujemy konstruktora z klasy bazowej, podobnie jak w klasie C (2). Jeżeli uruchomimy klasę C, w której tworzymy obiekt klasy C (3), to na ekranie zobaczymy:

Tworzę A. Tworzę B. Tworzę C.

Pomimo, że nie wywołaliśmy konstruktorów klas bazowych, to zobaczyliśmy na ekranie wypisywane przez nie komunikaty. Stało się tak, bo kompilator Java wywołał je za nas dla naszej wygody.

Zwróć uwagę na kolejność komunikatów. Tworzenie obiektu klasy C rozpoczyna się od konstruktora tej właśnie klasy, ale ponieważ na samym początku jej konstruktora musi zostać wywołany konstruktor klasy bazowej (w tym przypadku jest to klasa B), to wykonanie programu przechodzi do konstruktora klasy B. Tutaj ponownie musi zostać wywołany konstruktor klasy bazowej, czyli klasy A (bo tę klasę rozszerza klasa B), więc wykonanie programu przechodzi do konstruktora klasy A. Klasa A nie definiuje klasy, po której dziedziczy, więc domyślnie rozszerza specjalną klasę Object, o której już wspominałem, i do której wrócę w jednym z kolejnych podrozdziałów. Konstruktor klasy Object, zdefiniowanej w Bibliotece Standardowej Java, jest pusty. Po jego wykonaniu, wykonane zostanie ciało konstruktora klasy A, więc najpierw zobaczymy na ekranie komunikat Tworzę A. Następnie, wrócimy do konstruktora klasy B, a na końcu wykonany zostanie konstruktor klasy C. Jak więc widzisz, tworzenie obiektu zawsze zaczyna się od konstruktora „najstarszego” przodka danej klasy, a kończy się na wykonaniu konstruktora klasy, której obiekt chcieliśmy utworzyć.

Domyślne wywołanie konstruktora klasy bazowej odbędzie się tylko w przypadku, gdy klasa bazowa posiada bezargumentowy konstruktor. W przeciwnym razie kod się nie skompiluje:

domyslneKonstruktory\Produkt.java
package domyslneKonstruktory;

public class Produkt {
  private String nazwa;

  public Produkt(String nazwa) {
    this.nazwa = nazwa;
  }
}
domyslneKonstruktory\Produkt.java
package domyslneKonstruktory;

public class Monitor extends Produkt {
  private int przekatnaEkranu;

  public Monitor(int przekatnaEkranu) {
    this.przekatnaEkranu = przekatnaEkranu;
  }
}

Klasa Monitor rozszerza klasę Produkt, która definiuje jeden konstruktor, który przyjmuje jeden argument. Ponieważ klasa Produkt dostarcza konstruktor, to domyślny, bezargumentowy konstruktor nie zostanie dla niej wygenerowany przez kompilator Java.

W konstruktorze klasy Monitor nie wywołujemy konstruktora klasy bazowej, a zgodnie z zasadą tworzenia obiektów klas pochodnych, musimy to zrobić. Kompilator nam nie pomoże, bo klasa Produkt nie posiada żadnego bezargumentowego konstruktora, który mógłby on za nas wywołać automatycznie. Pozostaje nam jawnie wywołać ten konstruktor – w przeciwnym razie zobaczymy następujący błąd kompilacji klasy Monitor:

domyslneKonstruktory\Monitor.java:6: error: constructor Produkt in class Produkt cannot be applied to given types; public Monitor(int przekatnaEkranu) { ^ required: String found: no arguments reason: actual and formal argument lists differ in length 1 error

Kompilator próbował automatycznie wywołać konstruktor z klasy bazowej, ale wymaga on jednego argumentu typu String, którego kompilator nie jest w stanie sam dostarczyć. Musimy sami wywołać konstruktor z klasy bazowej Produkt. Do tego celu posłuży nam nowe słowo kluczowe: super.

Od opisanej na początku tego podrozdziału reguły, tzn. wymogu wywołania konstruktora klasy bazowej, jest mały wyjątek – możemy zamiast tego wywołać konstruktor tej samej klasy, o ile ten konstruktor wywoła konstruktor klasy bazowej. W kolejnym podrozdziale zobaczymy przykład tego zagadnienia.

Wywoływanie konstruktora klasy bazowej i słowo kluczowe super

Jeżeli klasa bazowa nie posiada bezargumentowego konstruktora, lub posiada kilka konstruktorów i chcesz wywołać jeden z nich, musisz skorzystać ze słowa kluczowego super. Działa ono na takich samych zasadach, jak używanie słowa kluczowego this w konstruktorze do wywołania innego konstruktora z tej samej klasy. Różnica polega jednak na tym, że za pomocą słowa kluczowego super odnosimy się do konstruktora klasy bazowej:

Nazwa pliku: produkty/Produkt.java
package produkty;

public class Produkt {
  private String nazwa;

  public Produkt(String nazwa) { // 1
    this.nazwa = nazwa;
  }
}
Nazwa pliku: produkty/Czeresnie.java
package produkty;

public class Czeresnie extends Produkt {
  private String gatunek;

  public Czeresnie(String gatunek) {
    super("Czeresnie");
    this.gatunek = gatunek;
  }
}

Klasa Czeresnie rozszerza klasę Produkt, która posiada jeden konstruktor, który oczekuje jednego argumentu typu String (1). Wywołujemy ten konstruktor w konstruktorze klasy Czeresnie, za pomocą słowa kluczowego super:

public Czeresnie(String gatunek) {
  super("Czeresnie");
  this.gatunek = gatunek;
}

Dzięki takiemu zapisowi, kod się kompiluje i działa bez problemów.

Wywołanie konstruktora klasy bazowej, jeżeli występuje w konstruktorze, musi być pierwszą instrukcją – w przeciwnym razie kompilator zaprotestuje:

public Czeresnie(String gatunek) {
  this.gatunek = gatunek;
  // błąd! super("Czeresnie"); musi być pierwszą instrukcją
  super("Czeresnie");
}
produkty\Czeresnie.java:6: error: constructor Produkt in class Produkt cannot be applied to given types; public Czeresnie(String gatunek) { ^ required: String found: no arguments reason: actual and formal argument lists differ in length produkty\Czeresnie.java:8: error: call to super must be first statement in constructor super("Czeresnie"); ^ 2 errors

Kompilator zgłasza dwa błędy – po pierwsze, z racji tego, że na początku konstruktora klasy Czeresnie nie wywołaliśmy konstruktora klasy bazowej, to kompilator próbuje wywołać ten konstruktor za nas. Nie jest to jednak możliwe, bo konstruktor ten oczekuje jednego argumentu, którego kompilator nie jest w stanie sam dostarczyć. Po drugie, kompilator informuje, że użycie super, by wywołać konstruktor klasy bazowej, musi być pierwszą instrukcją w konstruktorze.

Jeżeli klasa bazowa posiada kilka konstruktorów, to powinniśmy wywołać jeden z nich. To, który konstruktor klasy bazowej zostanie wywołany, zależy od argumentów, jakie przekażemy:

Nazwa pliku: produkty/Produkt.java
package produkty;

public class Produkt {
  private String nazwa;
  private double cena;

  public Produkt(String nazwa) {
    this(nazwa, 999999);
  }

  public Produkt(String nazwa, double cena) {
    this.nazwa = nazwa;
    this.cena = cena;
  }
}

Do klasy Produkt dodałem dodatkowe pole cena, a także drugi konstruktor, który przyjmuje, poza nazwą, także cenę produktu. Zmieniłem także pierwszy konstruktor, aby korzystał z drugiego, wywołując go za pomocą this – ten mechanizm znamy z podrozdziału o konstruktorach w rozdziale o klasach.

Użyjemy teraz każdego z powyższych konstruktorów w klasie Czeresnie:

Nazwa pliku: produkty/Czeresnie.java
package produkty;

public class Czeresnie extends Produkt {
  private String gatunek;

  public Czeresnie(String gatunek) {
    super("Czeresnie"); // 1
    this.gatunek = gatunek;
  }

  public Czeresnie(String gatunek, double cena) {
    super("Czeresnie", cena); // 2
    this.gatunek = gatunek;
  }
}

Pierwszy konstruktor (1) wywołuje ten konstruktor klasy bazowej, który przyjmuje jeden argument, natomiast drugi konstruktor (2) – ten, który przyjmuje dwa argumenty.

Na końcu poprzedniego podrozdziału napisałem, że od reguły wymogu wywołania konstruktora klasy bazowej na początku konstruktora klasy pochodnej jest pewien wyjątek. Otóż, zamiast wywoływać konstruktor klasy bazowej, możemy wywołać inny konstruktor tej samej klasy, o ile wywołuje on konstruktor klasy bazowej. Spójrz na poniższy przykład – do klasy Czeresnie dodałem jeszcze jeden konstruktor:

Nazwa pliku: produkty/Czeresnie.java
package produkty;

public class Czeresnie extends Produkt {
  private String gatunek;

  public Czeresnie() {
    this("nieznany gatunek"); // 1
  }

  public Czeresnie(String gatunek) { // 2
    super("Czeresnie");
    this.gatunek = gatunek;
  }

  public Czeresnie(String gatunek, double cena) {
    super("Czeresnie", cena);
    this.gatunek = gatunek;
  }
}

Zauważ, że konstruktor, który dodałem, nie wywołuje na samym początku konstruktora klasy bazowej. Zamiast tego, wywołuje on inny konstruktor z tej samej klasy (1) – w tym przypadku jest to konstruktor, który przyjmuje jeden argument (2). Zasada wywołania konstruktora klasy bazowej zostaje zachowana, ponieważ zostanie on wywołany, chociaż to wywołanie oddelegowujemy do innego konstruktora tej samej klasy.

Prywatne konstruktory a dziedziczenie

W podrozdziale o konstruktorach, w rozdziale o klasach, dowiedziałeś się, że konstruktory mogą być prywatne. Takie konstruktory mogą być używane jedynie w klasie, w której zostały zdefiniowane, a także w klasach zagnieżdżonych, które będą tematem osobnego rozdziału.

Jeżeli klasa posiada jedynie konstruktory prywatne, to takiej klasy nie możemy rozszerzyć za pomocą słowa kluczowego extends. Powodem jest to, że nie bylibyśmy w stanie wywołać konstruktora klasy bazowej, bo nie mamy dostępu do prywatnych pól i metody innych klas, a w tym – także konstruktorów:

public class PewnaKlasa {
  private PewnaKlasa() {
    System.out.println("Jestem prywatnym konstruktorem!");
  }
}
public class PewnaKlasaPochodna extends PewnaKlasa {

}

Powyższa klasa PewnaKlasaPochodna nie skompiluje się – kompilator zgłosi następujący błąd:

PewnaKlasaPochodna.java:1: error: PewnaKlasa() has private access in PewnaKlasa public class PewnaKlasaPochodna extends PewnaKlasa { ^ 1 error

Kompilator informuje nas, że klasa bazowa tej klasy posiada prywatny konstruktor, więc utworzenie obiektu tej klasy pochodnej byłoby niemożliwe, ponieważ, jak już wiemy z tego rozdziału, tworząc obiekt klasy pochodnej musimy wywołać konstruktor klasy bazowej.

Klasy zagnieżdżone są wyjątkiem od tej reguły, tzn. mogą rozszerzyć klasę z prywatnym konstruktorem, bo mają one dostęp do prywatnych pól i metod, co zobaczymy w jednym z dedykowanych rozdziałów. Dla dociekliwych: klasy zagnieżdżone to takie klasy, które zawarte są w innych klasach, a nie w osobnych plikach z rozszerzeniem .java.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.