1. Klasy i metody abstrakcyjne
Czasem w życiu (i w programowaniu) chce się powiedzieć: „No cóż, nie wiem, jak dokładnie to się robi, ale wiem na pewno, że to musi być!”. Na przykład wszystkie zwierzęta powinny umieć wydawać dźwięk, ale jaki dokładnie — zależy od konkretnego zwierzęcia. Właśnie na takie przypadki w Javie wymyślono klasy abstrakcyjne i metody abstrakcyjne.
Klasa abstrakcyjna — to klasa, której nie można utworzyć bezpośrednio (nie można napisać new Animal(), jeśli Animal jest abstrakcyjna), ale po której można dziedziczyć. Taka klasa może zawierać zarówno zwykłe (zaimplementowane) metody, jak i abstrakcyjne — czyli zadeklarowane, ale niezaimplementowane.
Metoda abstrakcyjna — to metoda bez ciała. Deklaruje się ją za pomocą słowa kluczowego abstract i musi zostać zaimplementowana w podklasach (chyba że podklasa sama jest abstrakcyjna).
Przykład z życia
Załóżmy, że mamy aplikację dla zoo. Chcemy, aby wszystkie zwierzęta miały metodę makeSound(), ale nie wiemy, jaki dokładnie dźwięk wydają. Wtedy tworzymy klasę abstrakcyjną:
public abstract class Animal {
public abstract void makeSound(); // Metoda abstrakcyjna
}
I konkretne zwierzęta implementują tę metodę po swojemu:
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Hau hau!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miau!");
}
}
Teraz, jeśli ktoś spróbuje utworzyć new Animal(), kompilator od razu powie: „Wybacz, ale abstraktnych zwierząt w naturze nie ma!” To jest przydatne: gwarantujesz, że w programie będą istnieć tylko konkretne zwierzęta z konkretnym zachowaniem.
2. Polimorfizm poprzez abstrakcję
Abstrakcja — to wyodrębnienie wspólnego interfejsu dla grupy obiektów. Klasa abstrakcyjna właśnie wyznacza taki wspólny interfejs: mówi, jakie metody muszą zostać zaimplementowane przez wszystkich potomków.
Polimorfizm i abstrakcja działają w parze: klasa abstrakcyjna gwarantuje, że wszyscy potomkowie mają potrzebne metody, a polimorfizm pozwala wywoływać te metody przez referencję typu bazowego.
Przykład: tworzymy zoo
Zbierzmy małe zoo. Mamy abstrakcyjną klasę Animal i kilka jej potomków:
public abstract class Animal {
public abstract void makeSound();
}
public class Cow extends Animal {
@Override
public void makeSound() {
System.out.println("Muuu!");
}
}
public class Duck extends Animal {
@Override
public void makeSound() {
System.out.println("Kwa kwa!");
}
}
Teraz możemy utworzyć tablicę zwierząt:
Animal[] zoo = {
new Dog(),
new Cat(),
new Cow(),
new Duck()
};
for (Animal animal : zoo) {
animal.makeSound(); // Dla każdego zwierzęcia wywoła się "właściwa" metoda
}
Każdy obiekt w tablicy — to konkretne zwierzę, ale dla kodu to po prostu Animal. Dzięki polimorfizmowi i abstrakcji możemy być pewni, że każdy obiekt ma metodę makeSound() i że zadziała ona poprawnie.
3. Wykorzystanie klas abstrakcyjnych do polimorfizmu
Przyjrzyjmy się bardziej praktycznemu przykładowi. Wyobraźmy sobie, że tworzymy aplikację do zarządzania pracownikami firmy. Mamy różne typy pracowników: menedżerowie, programiści, testerzy. Wszyscy mają wspólną metodę work(), ale realizują ją na różne sposoby.
Abstrakcyjna klasa Employee
public abstract class Employee {
protected String name;
public Employee(String name) {
this.name = name;
}
public abstract void work();
}
Konkretne podklasy
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " kieruje zespołem.");
}
}
public class Developer extends Employee {
public Developer(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " pisze kod.");
}
}
public class Tester extends Employee {
public Tester(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " testuje aplikację.");
}
}
Zastosowanie polimorfizmu
Teraz możemy utworzyć tablicę pracowników i dla każdego wywołać metodę work():
Employee[] employees = {
new Manager("Anna"),
new Developer("Ivan"),
new Tester("Mariya")
};
for (Employee e : employees) {
e.work();
}
Wynik:
Anna kieruje zespołem.
Ivan pisze kod.
Mariya testuje aplikację.
Zwróć uwagę: nie wiemy (i nie chcemy wiedzieć!) w pętli, jaki dokładnie mamy typ pracownika. Po prostu wywołujemy work(), a każdy obiekt robi swoje.
4. Przydatne niuanse
Gwarancja implementacji metod
Klasa abstrakcyjna wymusza na wszystkich potomkach implementację wymaganych metod. Jeśli zapomnisz zaimplementować metodę abstrakcyjną w podklasie, kompilator od razu cię upomni: „Musisz to zrobić!”
Uniwersalny interfejs
Kod, który działa na tablicy lub liście typu abstrakcyjnego (Employee[], List<Animal>), może być całkowicie uniwersalny. Możesz dodawać nowe podklasy — a kod główny nie będzie wymagał zmian.
Ochrona przed „lewymi” obiektami
Ponieważ klasy abstrakcyjnej nie można utworzyć bezpośrednio, nikt przypadkiem nie stworzy obiektu „niejasnego” typu, który nie implementuje wymaganych metod.
Teoria i składnia: jak zadeklarować klasę i metodę abstrakcyjną
- Klasę abstrakcyjną deklaruje się za pomocą słowa kluczowego abstract przed class.
- Metodę abstrakcyjną deklaruje się za pomocą słowa kluczowego abstract i nie ma ona ciała (tylko średnik).
- Klasa z co najmniej jedną metodą abstrakcyjną musi być abstrakcyjna.
- Klasa dziedzicząca po klasie abstrakcyjnej musi zaimplementować wszystkie jej metody abstrakcyjne, albo sama powinna być abstrakcyjna.
Schemat
public abstract class Animal {
public abstract void makeSound();
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Miau!");
}
}
5. Typowe błędy przy pracy z klasami abstrakcyjnymi
Błąd nr 1: próba utworzenia obiektu klasy abstrakcyjnej.
Kod w rodzaju new Animal() się nie skompiluje. Klasy abstrakcyjne — to jak instrukcja montażu mebli bez samych części: dopóki nie pojawi się konkretny podklas, obiektu złożyć nie można.
Błąd nr 2: zapomniano zaimplementować metodę abstrakcyjną w podklasie.
Jeśli zadeklarujesz metodę abstrakcyjną, ale nie zaimplementujesz jej w potomku (i nie uczynisz tej klasy również abstrakcyjną), kompilator się „zasmuci” i pokaże błąd.
Błąd nr 3: zapomniano o modyfikatorach dostępu.
Nadpisana metoda nie może mieć bardziej restrykcyjnego modyfikatora dostępu niż w klasie bazowej. Na przykład jeśli metoda abstrakcyjna była public, to implementacja również musi być public (a nie protected ani private).
Błąd nr 4: próba zdefiniowania metody abstrakcyjnej z ciałem.
abstract-metoda nie może mieć ciała, inaczej kompilator przewróci oczami i powie: „Zdecyduj się — albo abstrakcja, albo implementacja!”
Błąd nr 5: polimorfizm nie działa dla metod statycznych.
Polimorfizm działa tylko dla metod niestatycznych. Metody statyczne nie są nadpisywane — są ukrywane, dlatego zachowanie przy wywołaniu zależy od typu zmiennej, a nie od faktycznego obiektu.
GO TO FULL VERSION