Czym jest klasa abstrakcyjna
Interfejs deklaruje zachowanie bez stanu. Zwykła klasa jest w pełni zaimplementowana i można tworzyć jej instancje. Klasa abstrakcyjna leży pomiędzy nimi: może mieć pola, konstruktory i gotowe metody jak zwykła klasa, ale może też zostawić niektóre metody bez implementacji i nie pozwala tworzyć swoich instancji bezpośrednio. Oznacza się ją słowem kluczowym abstract.
Chodzi o to, by w jednym miejscu zebrać wszystko, co podklasy mają wspólnego, i jednocześnie zmusić każdą podklasę do uzupełnienia tych części, które naprawdę się różnią.
Animal definiuje getName() raz dla wszystkich podklas i deklaruje sound() jako abstract: metodę z sygnaturą, ale bez ciała, którą musi dostarczyć Dog.
Nie można utworzyć instancji klasy abstrakcyjnej
Klasa abstrakcyjna może mieć niedokończone metody, więc utworzenie jej bezpośrednio dałoby niekompletny obiekt. Kompilator na to nie pozwala:
Animal a = new Animal("???"); // error: Animal is abstract; cannot be instantiated
Zawsze tworzysz instancję konkretnej podklasy, czyli takiej, która zaimplementowała wszystkie metody abstrakcyjne. Tę instancję możesz potem trzymać w zmiennej typu abstrakcyjnego i właśnie tak korzysta się z abstrakcji.
Metody abstrakcyjne zmuszają podklasy do decyzji
Metoda abstract to obietnica, której podklasa musi dotrzymać. Jeśli podklasa nie zaimplementuje którejś z nich, sama staje się abstrakcyjna, a kompilator to zgłasza. To główna dźwignia klasy abstrakcyjnej: gwarantuje, że pewne zachowanie istnieje, nie narzucając, co ono robi.
describe() jest napisane raz w Shape, a mimo to wywołuje własne area() każdej podklasy. Klasa abstrakcyjna dostarcza wspólny szkielet, a podklasy szczegóły.
Wspólny stan i konstruktory
W przeciwieństwie do tradycyjnego interfejsu klasa abstrakcyjna może mieć pola instancji i definiować konstruktory. Konstruktor nigdy sam nie tworzy obiektu Animal ani Shape: uruchamia się przez super(...) przy tworzeniu podklasy i inicjalizuje wspólny stan.
Pole balance, deposit i applyInterest są w jednym miejscu. Abstrakcyjna zostaje tylko ta reguła, która naprawdę się zmienia, czyli interestRate(). Konstruktor podklasy musi wywołać super(...), aby zainicjalizować odziedziczony stan.
Pułapka: łączenie abstract i final
abstract i final to przeciwieństwa. Metoda abstract wymaga nadpisania, a metoda final go zabrania. Oznaczenie tego samego elementu na oba sposoby albo utworzenie klasy abstrakcyjnej final to błąd kompilacji. Pamiętaj też, że klasa abstrakcyjna może mieć zero metod abstrakcyjnych: zadeklarowanie klasy jako abstract tylko po to, by nie dało się tworzyć jej instancji, jest dozwolone i czasem przydatne dla typów bazowych, które chcesz wyłącznie rozszerzać.
abstract final class Bad { } // error: abstract and final conflict
abstract class Base {
abstract final void f(); // error: an abstract method can't be final
}
Klasa abstrakcyjna a interfejs
Te mechanizmy się pokrywają, więc wybór zależy od tego, co chcesz współdzielić:
- Klasa abstrakcyjna: używaj jej dla blisko powiązanych klas, które mają wspólny stan i kod.
SavingsiCheckingrozszerzająAccount, dziedzicząc polebalancei logikędeposit. Klasa rozszerza tylko jedną klasę. - Interfejs: używaj go dla zdolności, którą mogą dzielić niepowiązane klasy.
BirdiAirplanemogą byćFlyablebez żadnej wspólnej implementacji. Klasa może implementować wiele interfejsów.
Częsty wzorzec łączy oba: interfejs definiuje kontrakt, a klasa abstrakcyjna implementuje powtarzalny kod, dzięki czemu konkretne podklasy uzupełniają tylko to, co unikalne.
Dalej: polimorfizm
Zauważ, że w każdym przykładzie powyżej trzymaliśmy instancję podklasy w zmiennej typu abstrakcyjnego, a wywołanie metody automatycznie dawało zachowanie podklasy. Ta jedna zdolność, czyli jeden typ referencji i wiele zachowań w czasie działania programu, to polimorfizm. To dzięki niemu klasy abstrakcyjne i interfejsy naprawdę się opłacają. Tym zajmiemy się na następnej stronie.
Najczęściej zadawane pytania
Czym jest klasa abstrakcyjna w Javie?
Klasa abstrakcyjna to klasa zadeklarowana ze słowem kluczowym abstract, której instancji nie da się utworzyć bezpośrednio. Służy do rozszerzania. Może łączyć w pełni zaimplementowane metody i pola (wspólny stan i kod dla podklas) z metodami abstract, które nie mają ciała: ich implementacja jest obowiązkiem każdej podklasy.
Czy można utworzyć instancję klasy abstrakcyjnej w Javie?
Nie. new AbstractType() to błąd kompilacji, bo klasa abstrakcyjna może mieć niezaimplementowane metody (abstract), więc obiekt byłby niekompletny. Tworzy się instancję konkretnej podklasy, która uzupełnia wszystkie metody abstrakcyjne, a potem zapisuje ją w zmiennej typu abstrakcyjnego.
Czym różni się klasa abstrakcyjna od interfejsu w Javie?
Klasa abstrakcyjna może mieć pola instancji, konstruktory i częściowo zaimplementowaną logikę, ale klasa może rozszerzać tylko jedną. Interfejs deklaruje zachowanie bez stanu instancji, a klasa może implementować wiele interfejsów. Użyj klasy abstrakcyjnej, aby współdzielić stan i kod między blisko powiązanymi podklasami; użyj interfejsu, aby dać niepowiązanym klasom wspólną zdolność.