javamPokaze || Co Java ma wspólnego z genetyką?  || Część 1 || Dziedziczenie
🛠️

javamPokaze || Co Java ma wspólnego z genetyką? || Część 1 || Dziedziczenie

notion image

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!
notion image

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.
 
notion image
 

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 jakoIS-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 klasie
Car. 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.
 
notion image
 

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 metody

Overload


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:
 
notion image

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 abstrakcyjnej KlasaB musimy użyć słowa kluczowego extends
    • 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