Poruszone zagadnienia
- Dziedzieczenie
- Testy IS-A oraz HAS-A
- Overload
- Override
- Klasa abstrakcyjna
- Klasa o konkretnej implementacji
Wstęp
Czytając pierwszą książkę na temat Javy, która wpadła mi w ręce, moją uwagę natychmiast przykuła jedna rzecz, a mianowicie termin “dziedziczenie”. Nie mogłem oprzeć się porównaniom pomiędzy dziedziczeniem w Javie, a dziedziczeniem w genetyce. Biologia zawsze miała w moim sercu szczególne miejsce.
Jak wiemy, w genetyce dziedziczenie polega na przekazywaniu cech z pokolenia na pokolenie. Geny zawierają informacje o naszych cechach, takich jak kolor oczu czy kształt nosa. A co z Javą? Okazuje się, że w świecie programowania także mamy coś, co przypomina dziedziczenie genetyczne.
Dziedziczenie w Javie pozwala na tworzenie hierarchii klas, gdzie jedna klasa może dziedziczyć cechy innej klasy. Podobnie jak w genetyce, gdzie cechy przekazywane są z pokolenia na pokolenie, w Javie klasy potomne dziedziczą cechy po swoich "rodzicach".
To porównanie wydaje się zabawne i jednocześnie fascynujące. Czy nasz kod może mieć "genetyczne cechy"?
Te wszystkie przemyślenia skłoniły mnie do tego, aby stworzyć kilku odcinkową serię “Co Java ma wspólnego z genetyką”, w której poruszę tematy dziedziczenia oraz polimorfizmu, a także innych kwestii z tym związanych, które będą się ze sobą stale przeplatały.
W dalszej części tego inaugurującego wpisu zanurzymy się w świecie dziedziczenia w Javie i poszukamy analogii z dziedziczeniem genetycznym. Odkryjemy, jak cechy są przekazywane w kodzie.
Czy programiści są jak twórcy nowych gatunków? Czy może istnieć "kodowe DNA"? 😂 Zapraszam do lektury!
Dziedziczenie
W najprostrzych słowach: dziedziczenie to mechanizm, w którym właściwości klasy A (jej zmienne instancji oraz metody) mogą być dziedziczone przez inną klasę B. W tym przypadku klasa A będzie nazywana klasą nadrzędną (bazową), a klasa B podrzędną. Nazwy wywodzą się oczywiście z języka angielskiego (
superclass
oraz subclass
). Najlepiej będzie to wyjaśnić na prostym przykładzie.:// Klasa nadrzędna Car (superclass) public class Car { String brand; String model; public Car(String brand, String model) { this.brand = brand; this.model = model; } public void startEngine() { System.out.println("Uruchamiam silnik samochodu " + brand + ", model: " + model); } } ------------------------------------------------------------------------------------------- // Klasa podrzędna Combi dziedzicząca po klasie Car (subclass) public class Combi extends Car { public Combi(String brand, String model) { super(brand, model); } public void openTrunk() { System.out.println("Otwieram bagażnik w samochodzie " + brand + ", model: " + model); } } ------------------------------------------------------------------------------------------- // Klasa podrzędna SUV dziedzicząca po klasie Car (subclass) public class SUV extends Car { public SUV(String brand, String model) { super(brand, model); } public void activate4WD() { System.out.println("Aktywuję napęd na cztery koła w " + brand + ", model: " + model); } } ------------------------------------------------------------------------------------------- public class TestCar { public static void main(String[] args) { Combi combi = new Combi("Opel", "Astra"); SUV suv = new SUV("Jeep", "Wrangler"); combi.startEngine(); // Uruchamiam silnik samochodu Opel, model: Astra combi.openTrunk(); // Otwieram bagażnik w samochodzie Opel, model: Astra suv.startEngine(); // Uruchamiam silnik samochodu marki Jeep, model: Wrangler suv.activate4WD(); // Aktywuję napęd na cztery koła w Jeep, model: Wrangler } }
Jak pewnie zauważyłeś/aś, przy klasach podrzędnych znalazło się magiczne słowo
extends
. Dziedziczenie w Javie jest realizowane właśnie za pomocą tego słowa kluczowego. Onacza to nic innego jak np:class Combi extends Car{ // auto combi rozszerza właściwości auta standardowego } class Tiger extends Animals{ // t tygrys rozszerza swoje cechy o cechy klasy Animals } class Fastfood extends Food{ // śmieciowe żarcie rozszerza właściwości żywności } // hmm.. to chyba zły przykład...
Poprzez dziedziczenie klasy podrzędne (
Combi
) oraz (SUV
) dziedziczą wszystkie publiczne pola i metody z klasy nadrzędnej Car
.W tym przykładzie klasa
Car
jest klasą nadrzędną
, a klasy Combi
i SUV
dziedziczą po niej cechy. Klasa Car
posiada podstawowe właściwości i metody dotyczące samochodu, takie jak marka i model, oraz metodę startEngine()
do uruchamiania silnika. Klasy podrzędne Combi
i SUV
dziedziczą pola klasy (zmienne instancji
) oraz metodę startEngine()
z klasy Car
, a także posiadają swoje własne unikalne metody. Klasa Combi
posiada własną metodę openTrunk()
, a klasa SUV
metodę activate4WD()
, które rozszerzają funkcjonalność podstawowej klasy Car
.Konstruktory do klas podrzędnych
Combi
i SUV
, mogą wywoływać konstruktor klasy nadrzędnej Car
za pomocą słowa kluczowego super
. Oczywiście będzie to możliwe tylko wtedy, kiedy klasa Combi
, przedłuży funkcjonalność klasy Car
wykorzystując słowo kluczowe extends
. W naszym kodzie wygląda to tak:public Combi(String brand, String model) { super(brand, model); //super oznacza wywołanie konsturtowa klasy Car }
Poprzez dziedziczenie klasy podrzędne (
Combi
oraz SUV
) dziedziczą wszystkie publiczne pola i metody z klasy nadrzędnej Car
.Testy IS-A oraz HAS-A, czyli jak tworzć genetyczne drzewo dziedziczenia
Masz problem z hierarchią klas? Nie potrafisz poukładać sobie w głowie, która klasa powinna być nadrzędna, a która po niej dziedziczyć? Nie łapiesz jak to dobrze uporządkować w głowie?
Zatem mam dla Ciebie perełkę! Chyba nie ma lepszego i szybszego sposóbu na objaśnienie relacji pomiędzy klasami i ich składowymi w dziedziczeniu. Weźmy na tapetę taki przykład:
//Klasa nadrzędza public class Dog { public void bark() { System.out.println("Woof! Woof!"); } } -------------------------------------------------------------------------------------- //Klasa podrzędna public class Puppy extends Dog { public void play() { System.out.println("The puppy is playing"); } } -------------------------------------------------------------------------------------- public class TestDog { public static void main(String[] args) { Dog dog = new Dog(); dog.bark(); // Output: Woof! Woof! Puppy puppy = new Puppy(); puppy.bark(); // Output: Woof! Woof! puppy.play(); // Output: The puppy is playing.
IS-A (jest)
Klasa, która dziedziczy po innej klasie, jest określana jako
IS-A
klasy nadrzędnej. Co to oznacza?Przykład 1:
Jeśli mamy relację
Puppy extends Dog
, możemy powiedzieć, że:Puppy IS-A Dog
(Szczeniak jest Psem)
W ten sposób można łatwo odróżnić, która klasa ma być nadrzędna, a która podrzędna (szczeniak jest psem, ale pies nie jest szczeniakiem)
Przypadek 2:
Jeśli mielibyśmy tutaj 3 klasy
Puppy
, Dog
, Rex
, w których występuje zależność :Puppy extends Dog
Rex extends Puppy
Tutaj pomiędzy klasami
Rex
, a Dog
również występuje taka sama zależność (Rex IS-A Dog
). Rex
rozszerza (extends
) klasę Puppy
, co oznacza, że jest podklasą (subclass
) klasy Puppy
. Ponieważ Puppy
dziedziczy po klasie Dog
, to Rex
dziedziczy również po klasie Dog
. W ten sposób można stwierdzić, że Rex IS-A Dog
HAS-A (ma/posiada)
Wyobraźmy sobie, że mamy klasę
Car
oraz Engine
. Stworzymy sobie obiekt typu Engine
w klasieCar
. Inaczej mówiąc, klasa Car
posiadać będzie instancję klasy Engine
w swoim ciele. Zależność taka opisana może być w ten sposób:Car HAS-A Engine
(Samochód ma/posiada Silnik w swoim ciele)
Dzięki temu w klasie
Car
mamy dostęp to zmiennych instancji lub metod tej klasy Engine
. Relacja HAS-A
umożliwia klasie posiadanie dostępu do funkcjonalności innej klasy, której obiekt posiada. Instancja klasy Engine
, udostępni swoje pola i metody klasie Car
, jedynie w przypadku, jeśli są oznaczone jako jako public
lub package/default
(default jeśli obie klasy są w tym samym pakiecie
). Ale o tym kiedy pogadamy kiedy indziej.Overload oraz @Override
Ok, znamy już mechanizm dziedziczenia i dwie fajne metody, które pozwolą nam łatwo odnaleść się w relacjach pomiędzy klasami. Wiemy też, że klasa dziedzicząca rozszerza swoje funkcjonalności, wykorzystując odziedziczone metody z klasy nadrzędnej.
A co w przypadku, jeśli taką odziedziczoną metodę chcielibyśmy jakoś zmodyfikcować? Czy to możliwe? Rzućmy okiem na taki przykład:
public class Car { public void acceletarte(){ System.out.println("Car is acceletating"); } } ------------------------------------------------------------------------------------------- public class ElectricCar extends Car { // Wykonujemy override metody acceletarte() @Override public void acceletarte(){ System.out.println("Electric car is acceletating"); } } ------------------------------------------------------------------------------------------- public class GasolineCar extends Car { // Wykonujemy overload metody acceletarte() public void acceletarte(int speed){ System.out.println("Gasoline car is acceletating at " + speed + " km/h"); } }
W tym przykładzie klasy podrzędne
ElectricCar
oraz GasolineCar
dziedziczą z klasy Car
. Oznacza to, że mają dostęp do podstawowej funkcjonalności metody accelerate()
.W Javie mamy dwie fajne funkcjonalności, które nazywają się
overload
(przeładowanie) oraz override
(przesłonięcie) metody. Przedstawmy je zatem wykorzystując powyższy kod.Override
Metoda
accelerate()
została przesłonięta @override
, a to oznacza, że mogliśmy zmienić jej funkcjonalność poprzez modyfikcję jej ciała dodając własną instukcję wyświetlania w konsoli napisu ("Electric car is acceletating"
).WAŻNE:
Oveveride
wykonujemy, jeśli nie wprowadzamy zmian w typie zwracanym oraz parametrach metodyOverload
Metoda
accelerate()
została przeładowana overload
, co oznacza, że dokonaliśmy zmian nie tylko w ciele metody, ale także w parametrach i przy okazji w typie zwracanym.WAŻNE:
Overload
wykonujemy, kiedy chcemy dokonać zmian w liście parametrów (dodać/zmniejszyć ich ilość lub typ zwracany)📎 Odeślę Cię do mojego poprzedniego wpisu, w którym porónuje te dwie funkcjonalności:
Klasa abstrakcyjna
Abstrakcja kojarzy mi się z czymś oderwanym od rzeczywistości, np. z niebieską pomarańczą. A jak to rozumieją twórcy Javy?
Zacznijmy od tego, że w kontekście dziedziczenia klasy w Javie możemy podzielić na dwa rodzaje (na razie dwa). My skupmy się teraz na tych dwóch rodzajach:
Abstract clas - klasy abstrakcyjne:
- Klasy abstrakcyjne w Javie są oznaczane słowem kluczowym
abstract
- Nie można tworzyć obiektów (instancji) klasy abstrakcyjnej (jest to tzw. klasa szablonowa)
- Klasa abstrakcyjna może zawierać zarówno abstrakcyjne metody (bez implementacji), jak i metody z implementacją (z ciałem)
- Jeśli klasa abstrakcyjna zawiera abstrakcyjne metody, klasy dziedziczące (
subclass
) są zobowiązane do zaimplementowania tych metod (przez słowo kluczowe@Override
) lub też same muszą być oznaczona jako abstrakcyjne, jeśli nie chcemy zaimplementować w nich tych metod (abstract methods must be implemented by the first concrete subclass
). To, co umieścisz w cele tych metod zależy już tylko od Ciebie
- Klasa abstrakcyjna może służyć jako punkt wyjścia (szablon) dla dziedziczących po niej klas, które w ten sposób mogą dziedziczyć pewne cechy i metody. Aby nasza
KlasaA
odziedziczyła pewne cechy od nadrzędnej klasy abstrakcyjnejKlasaB
musimy użyć słowa kluczowegoextends
public class KlasaA extends KlasaB { //Pole klasy }
Concrete class - klasy o konkretnej implementacji:
- Klasy konkretne to zwykłe klasy, które nie są oznaczone jako abstrakcyjne
- Z takich klas można tworzyć obiekty (instancje), gdyż mają one pełną implementację swoich metod
- Klasy konkretne mogą dziedziczyć po klasach abstrakcyjnych, ale zobowiązane są do dostarczenia implementacji dla wszystkich abstrakcyjnych metod dziedziczonej klasy
Klasy abstrakcyjne służą jako punkty wspólne dla klas dziedziczących i nie mogą tworzyć obiektów, podczas gdy klasy konkretne oferują pełną implementację i mogą być instancjonowane bezpośrednio.
Rzućmy okiem na poniższy kod, w którym przedstawiam hierarchię dziedziczenia na przykładzie samochodów:
Klasy abstrakcyjne:
- Car
- SportsCar
- Combi
Klasy o konkretnej implementacji
- Lamborghini
- Ferrari
- Volvo
- Renault
Klasa abstrakcyjna
Car
:abstract class Car { String brand; String model; public Car(String brand, String model) { this.brand = brand; this.model = model; } // Abstrakcyjna metoda do opisu, która musi być zaimplementowana w klasach podrzędnych public abstract String getDescription(); // metoda abstrakcyjna na ma ciała }
Klasy abstrakcyjne
SportsCar
oraz Combi
(dziedziczące po klasie Car
):abstract class SportsCar extends Car { int maxSpeed; public SportsCar(String brand, String model, int maxSpeed) { super(brand, model); this.maxSpeed = maxSpeed; } // Własna implementacja metody getDescription w SportsCar @Override public String getDescription() { return "SportsCar - Brand: " + brand + ", Model: " + model + ", Max Speed: " + maxSpeed + " km/h"; } } -------------------------------------------------------------------------------------- abstract class Combi extends Car { int trunkCapacity; public Combi(String brand, String model, int trunkCapacity) { super(brand, model); this.trunkCapacity = trunkCapacity; } // Własna implementacja metody getDescription w Combi @Override public String getDescription() { return "Combi - Brand: " + brand + ", Model: " + model + ", Trunk Capacity: " + trunkCapacity + " liters"; } }
Klasy o konkretnej implementacji
Lamborghini
oraz Ferrari
(dziedziczące po klasieSportsCar
):class Lamborghini extends SportsCar { public Lamborghini(String model, int maxSpeed) { super("Lamborghini", model, maxSpeed); } class Ferrari extends SportsCar { public Ferrari(String model, int maxSpeed) { super("Ferrari", model, maxSpeed); } }
Klasy o konkretnej implementacji
Volvo
oraz Renault
(dziedziczące po klasie Combi
):class Volvo extends Combi { public Volvo(String model, int trunkCapacity) { super("Volvo", model, trunkCapacity); } class Renault extends Combi { public Renault(String model, int trunkCapacity) { super("Renault", model, trunkCapacity); } }
W tym kodzie zaimplemenotwaliśmy hierarchię klas abstrakcyjnych reprezentujących różne typy samochodów. Na szczycie hierarchii znajduje się abstrakcyjna klasa
Car
, która zawiera podstawowe właściwości każdego samochodu, takie jak brand
oraz model
Następnie mamy kolejne dwie klasy abstrakcyjne
SportsCar
i Combi
dziedziczące po klasie Car
. Zawierają dodatkowe właściwości specyficzne dla samochodów sportowych i samochodów kombi, odpowiednio maxSpeed
(maksymalna prędkość) i trunkCapacity
(pojemność bagażnika).Na końcu mamy klasy konkretne
Lamborghini
, Ferrari
, Volvo
i Renault
dziedziczące z odpowiednich klas abstrakcyjnych (SportsCar
lub Combi
) W klasach Lamborghini
, Ferrari
, Volvo
i Renault
nie musimy ponownie nadpisywać metody getDescription()
, ponieważ nie dodajmy żadnych dodatkowych funkcji. Teraz klasy Lamborghini
, Ferrari
, Volvo
i Renault
będą dziedziczyły metodę getDescription()
z klas nadrzędnych, więc możemy tworzyć instancje tych klas (np. w klasie TestClass
) i wywoływać getDescription()
aby uzyskać ich opisy.Rezultatem działania programu jest wyświetlenie opisów czterech różnych samochodów, w tym informacje o marce, modelu i specyficznych cechach dla danego typu pojazdu, takich jak maksymalna prędkość dla samochodów sportowych i pojemność bagażnika dla samochodów kombi.
Podsumowując, klasy abstrakcyjne zapewniają ogólny szkielet, a dziedziczące po nich klasy konkretne muszą dostarczyć konkretnych implementacji ich metod. Abstrakcyjne klasy dostarczają pewnego rodzaju "szablonu", który musi być uzupełniony w klasach konkretnych, aby stworzyć kompletną funkcjonalność.
Śledź mnie na LinkedIn
Newsletter
Jeśli masz jakieś sugestie lub pytania, proszę napisz do mnie wiadomość: kuba@javampokaze.pl