Znasz już podstawy programowania strukturalnego oraz podstawowe algorytmy. Teraz czas przejść do bardziej skomplikowanych paradygmatów programowania obiektowego. Klasy abstrakcyjne są (abstrakcja) jednym z paradygmatów programowania obiektowego. Można ją wytłumaczyć jak stosowanie równań matematycznych w fizyce. Matematyczne obliczenia to pewnego rodzaju abstrakcja, która później przy podstawieniu odpowiednich wartości wylicza odpowiednie właściwości, które obserwujesz w przyrodzie. Klasy abstrakcyjne zachowują się podobnie. Są to zwykłe klasy (posiadają prawie wszystkie ich własności), ale umożliwiają też pisanie sygnatur metod, które później będziesz musieć zaimplementować w klasie potomnej. Spójrz na przykład poniżej.
public abstract class Player { public abstract void play(); public abstract void score(); public void run() { System.out.println("Running.."); } }
W tej mało skomplikowanej klasie użyto słowa kluczowego abstract. Oznacza ono, że klasa może posiadać metody, które nie są w niej zdefiniowane. Zauważ, że z trzech metod, jedna z nich to typowa metoda, która nie zwraca żadnej wartości, ale dwie kolejne nie posiadają treści (mówi się wtedy o sygnaturze metody). Możesz to sobie wytłumaczyć początkowym przykładem z matematyką używaną w fizyce. Pamiętasz na pewno słynny wzór Einsteina E=mc2. Jeśli nie pamiętasz, podpowiem Ci, że jest to wzór na energię, którą można wyrazić jako iloczyn masy i prędkości światła do kwadratu. Odnosząc go do mojego przykładu E to taka nasza sygnatura. Natomiast mc2 to jego jedna implementacja. Wzorów na energię w fizyce masz dużo więcej (np. na energię kinetyczną).
public class FootballPlayer extends Player{ @Override public void play() { System.out.println("Playing football.."); } @Override public void score() { System.out.println("Scoring goals.."); } }
Implementacja metod abstrakcyjnych
W kolejnym kroku użyłem klasy Player do rozszerzenia klasy FootballPlayer. W praktyce odziedziczy ona metodę running(), a także jest zmuszona do posiada implementacji metod, których sygnatury również zostały zdeklarowane w Player. W tym przypadku obiekt klasy FootballPlayer otrzyma unikatowe względem zwykłego obiektu klasy Player właściwości grania w piłkę i zdobywa bramki (chyba to dość jasne, ze nie każdy gracz będzie posiadał taką umiejętność). Dla porównania zobacz, na implementację poniżej.
public class BasketballPlayer extends Player{ @Override public void play() { System.out.println("Playing basketball.."); } @Override public void score() { System.out.println("Scoring points.."); } public void layUp() { System.out.println("Scoring points from lay-up.."); } }
Widzisz tu klasę BasketballPlayer, która rozszerzając klasę Player obowiązkowo posiada swoje implementacje metod play() i score(), ale ponad to ma dodatkową metodę layUp() (dwutakt). Pomimo, że koszykarz posiada funkcje gracza, to nic nie stoi na przeszkodzie, aby dodać mu umiejętności tylko charakterystyczne dla niego.
Cel tworzenia klas abstrakcyjnych
Po co w takim razie są klasy abstrakcyjne? Świetnie sprawdzają się jako klasy 'korzenie’ w drzewie dziedziczenia, ponieważ wymuszają na programiście, który je użyje, do napisania własnych metod, które powinny być charakterystyczne dla klasy danego typu. Programista planujący system w tym przypadku dostarcza kolejnemu programiście zestaw gotowych narzędzi, ale w zależności do czego one będą służyć, pozostawia mu otwartą furtkę do uszczegółowienia zachowania nowej klasy.
Napisz teraz swoją metodę main, aby zobaczyć efekt pracy koszykarza i piłkarza.
public class MainPlayer { public static void main(String[] args) { System.out.println("Football player."); FootballPlayer footballPlayer = new FootballPlayer(); footballPlayer.run(); footballPlayer.play(); footballPlayer.score(); System.out.println("Basketball player."); BasketballPlayer basketballPlayer = new BasketballPlayer(); basketballPlayer.run(); basketballPlayer.play(); basketballPlayer.score(); basketballPlayer.layUp(); } }
Teraz spójrz na efekt wywołania klas FootballPlayer i BasketballPlayer w konsoli.
Football player. Running.. Playing football.. Scoring goals.. Basketball player. Running.. Playing basketball.. Scoring points.. Scoring points from lay-up..
Obie klasy zachowały metodę run() w pierwotnym stanie, jak przypadku normalnego dziedziczenia. Obie klasy posiadały swoje implementacje metod score() i play(). Klasa BasketballPlayer dodatkowo posiadała swoją klasę layUp().
Instancja klasy abstrakcyjnej
Zadasz teraz pytanie, czemu nie przetestowałem klasę Player. Otóż ponieważ Player jest klasą abstrakcyjną, nie możesz stworzyć jej instancji (czyli Player player = new Player(); nie zadziała). Słowem abstract przed nazwą klasy sugerujesz, że to ma być klasa rodzic, która będzie później rozszerzana przez potomków i nie nadaje się ona do samoistnego funkcjonowania. Kolejne pytanie, które możesz zadać jest, czy może istnieć klasa abstrakcyjna bez metod abstrakcyjnych. Otóż może, po prostu w ten sposób blokujesz kolejnemu programiście użycie jej bezpośrednio w postaci jej instancji w innej metodzie (możesz jej użyć tylko przez dziedziczenie). Ostatnie pytanie, które warto zadać, to czym jest adnotacja @Override? Na to pytanie, odpowiem Ci, w kolejnym poście przy okazji lekcji związanej z polimorfizmem. 🙂
Link do lekcji: https://github.com/developeronthego/java-middle/tree/master/src/main/java/middle/lesson1
Lekcja o dziedziczeniu: https://developeronthego.pl/java-podstawy-9-dziedziczenie/