W Javie, abstrakcja jest realizowana za pomocą klas abstrakcyjnych oraz interfejsów. Abstrakcja w programowaniu obiektowym polega na ukrywaniu szczegółów implementacyjnych przed użytkownikami końcowymi.
Dzięki abstrakcji, użytkownik ma świadomość dostępnych funkcjonalności, lecz nie wnika w ich wewnętrzne działanie.
Przeanalizujmy te dwa mechanizmy, aby lepiej zrozumieć ich zastosowanie.
Klasa Abstrakcyjna
Klasa abstrakcyjna w Javie to taka, której nie można bezpośrednio instancjonować (tworzyć obiektów). Może ona zawierać metody abstrakcyjne, jak i zwykłe. Metoda abstrakcyjna to taka, która ma zdefiniowaną sygnaturę (nazwę, parametry, zwracany typ), ale nie posiada ciała (implementacji).
Przykładem może być klasa abstrakcyjna `GraphicObject` z dokumentacji Oracle.
Klasę abstrakcyjną tworzymy, poprzedzając słowo kluczowe `class` słowem `abstract`.
abstract class AbstractClass { void run() { System.out.println("uruchomiono"); } }
Klasy abstrakcyjne można rozszerzać. Oznacza to, że inne klasy mogą dziedziczyć po klasie abstrakcyjnej.
abstract class AbstractClass { void run() { System.out.println("uruchomiono"); } } class ExtendingAbstractClass extends AbstractClass { void newMethod() { System.out.println("nowa metoda"); } @Override void run() { System.out.println("przesłonięto"); } }
Klasy abstrakcyjne są przydatne, gdy chcemy zaimplementować wspólne metody dla wielu klas dziedziczących. Dodatkowo, definiowanie metod abstrakcyjnych umożliwia określenie wspólnych interfejsów z różnymi implementacjami. Rozważmy przykład.
Załóżmy, że mamy samochód, który posiada funkcje takie jak uruchamianie, zatrzymywanie i cofanie. Są to funkcje wspólne dla wszystkich samochodów.
Jednak funkcje automatyczne, jak np. jazda autonomiczna, mogą różnić się w zależności od rodzaju samochodu. Zobaczmy, jak to zaimplementować obiektowo.
Najpierw utwórzmy abstrakcyjną klasę `Car`, po której będą dziedziczyć różne typy samochodów.
abstract class Car { void start() { // implementation System.out.println("samochód uruchomiony"); } void stop() { // implementation System.out.println("silnik zatrzymany"); } void reverse() { // implementation System.out.println("włączono tryb cofania"); } abstract void selfDrive(); }
Metody `start()`, `stop()` i `reverse()` są wspólne dla wszystkich samochodów i mają zdefiniowaną implementację. Metoda `selfDrive()` (jazda autonomiczna) jest abstrakcyjna, ponieważ jej implementacja zależy od typu samochodu.
class CarTypeA extends Car { @Override void start() { super.start(); } @Override void stop() { super.stop(); } @Override void reverse() { super.reverse(); } void selfDrive() { // custom implementation System.out.println("włączono tryb autonomicznej jazdy typu A"); } }
class CarTypeB extends Car { // ...podobne metody void selfDrive() { // custom implementation // inna implementacja niż w CarTypeB System.out.println("włączono tryb autonomicznej jazdy typu B"); } }
Ważne jest, aby pamiętać, że jeśli podklasa nie zaimplementuje wszystkich metod abstrakcyjnych z klasy abstrakcyjnej, sama również musi być zadeklarowana jako abstrakcyjna.
Interfejs
Interfejs to rodzaj kontraktu, który informuje klasę, jakie metody musi ona zaimplementować. W naszym przykładzie samochodu, posiada on podstawowe funkcje jak uruchamianie, poruszanie się i zatrzymywanie. Są one wspólne dla wszystkich samochodów.
Jeśli klasa implementuje interfejs `Car`, musi zaimplementować wszystkie zadeklarowane w nim metody, aby samochód mógł działać poprawnie i bezpiecznie.
Podobnie jak w przypadku klas abstrakcyjnych, nie można tworzyć obiektów interfejsu. Interfejs można traktować jako klasę w pełni abstrakcyjną, gdyż zawiera wyłącznie metody abstrakcyjne (bez implementacji).
Interfejs tworzymy za pomocą słowa kluczowego `interface`.
interface CAR { void start(); void stop(); void move(); }
Interfejs implementujemy za pomocą słowa kluczowego `implements` podczas definiowania klasy.
class CarTypeB implements CAR { public void start() { System.out.println("uruchomiono"); } public void stop() { System.out.println("zatrzymano"); } public void move() { System.out.println("jedzie"); } }
Podobieństwa
Jedyną wspólną cechą klas abstrakcyjnych i interfejsów jest to, że nie można tworzyć ich instancji.
Różnice
Cecha | Klasa abstrakcyjna | Interfejs |
---|---|---|
Dziedziczenie i implementacja | Klasa może dziedziczyć tylko po jednej klasie abstrakcyjnej. | Klasa może implementować wiele interfejsów. |
Typy zmiennych | Może posiadać zmienne `final`, nie-`final`, `static` i niestatyczne. | Może posiadać jedynie zmienne `static` i `final`. |
Typy metod | Może zawierać zarówno metody abstrakcyjne, jak i nieabstrakcyjne. | Może zawierać tylko metody abstrakcyjne (z wyjątkiem metod `static`). |
Modyfikatory dostępu | Klasa abstrakcyjna może mieć zdefiniowane modyfikatory dostępu. | Sygnatury metod zdefiniowane w interfejsie są domyślnie publiczne. Interfejs nie ma modyfikatora dostępu. |
Konstruktory i destruktory | Może deklarować konstruktory i destruktory. | Nie może deklarować konstruktorów ani destruktorów. |
Szybkość | Szybko | Wolno |
Kiedy Używać Klasy Abstrakcyjnej i Interfejsu?
Klasy abstrakcyjne stosuj gdy:
- Chcesz udostępnić wspólne metody i pola dla wielu klas.
- Chcesz modyfikować stan obiektów poprzez pola niestatyczne i niekońcowe.
Interfejsy stosuj gdy:
- Chcesz zdefiniować zachowanie klasy bez wnikania w szczegóły implementacyjne.
- Chcesz wymusić implementację określonych metod w klasach.
Podsumowanie
Interfejsy są często używane do tworzenia API, ponieważ definiują strukturę funkcjonalności bez narzucania konkretnej implementacji.
Klasy abstrakcyjne stosuje się, aby współdzielić wspólne metody abstrakcyjne i nieabstrakcyjne między wieloma klasami dziedziczącymi, co czyni kod bardziej modularnym i użytecznym.
Aby poszerzyć wiedzę o Javie, skorzystaj z dostępnych kursów online. Przygotowujesz się do rozmowy kwalifikacyjnej z Javy? Sprawdź przykładowe pytania dotyczące programowania obiektowego.