1. Składnia klasy abstrakcyjnej
Dajmy sobie wyjaśnić, czym jest klasa abstrakcyjna w Javie i po co w ogóle jest potrzebna. Wyobraź sobie, że masz zestaw LEGO: masz zestaw elementów (na przykład „kółka”, „klocki”) oraz specjalne elementy dla konkretnych modeli. Klasa abstrakcyjna to jak ogólna instrukcja obchodzenia się z elementami bez opisu składania konkretnego modelu: klocki łączą się wypustkami, kółka mocuje się do osi, elementy można dokładać warstwami. Klasa abstrakcyjna opisuje właśnie te wspólne zasady i obowiązkowe kroki, ale nie rozpisuje montażu konkretnego modelu. Konkretnymi modelami są podklasy: biorą ogólną instrukcję i dodają brakujące kroki.
W Javie klasa abstrakcyjna to klasa, której nie można utworzyć bezpośrednio (nie da się zrobić new AbstractClass()), ale można po niej dziedziczyć i implementować jej „niedopisane” metody.
Klasy abstrakcyjne są potrzebne, gdy:
- Grupa obiektów ma wspólne zachowanie lub stan, ale część logiki się różni.
- Chcesz zrealizować część funkcjonalności „domyślnie”, a część pozostawić do implementacji przez potomków.
- Chcesz zabronić bezpośredniego tworzenia obiektów tej klasy (na przykład „Zwierzę” samo w sobie nie występuje, ale „Kot” – jak najbardziej).
Jak zadeklarować klasę abstrakcyjną
To proste: przed słowem class piszemy słowo kluczowe abstract.
public abstract class Animal {
// Pola (np. imię zwierzęcia)
protected String name;
// Konstruktor
public Animal(String name) {
this.name = name;
}
// Metoda abstrakcyjna – tylko deklaracja, bez implementacji!
public abstract void makeSound();
// Zwykła (zaimplementowana) metoda
public void sleep() {
System.out.println(name + " śpi: Zzz...");
}
}
Charakterystyka:
- Klasa abstrakcyjna może zawierać zarówno metody zaimplementowane, jak i abstrakcyjne.
- Klasa abstrakcyjna może zawierać pola, konstruktory, a nawet metody static.
- Nie można utworzyć obiektu klasy abstrakcyjnej bezpośrednio:
-
Animal a = new Animal("Ktoś"); // Błąd!
Jak zadeklarować metodę abstrakcyjną
Metoda abstrakcyjna to metoda bez ciała, czyli bez nawiasów klamrowych i kodu w środku. Deklaruje się ją słowem kluczowym abstract i koniecznie kończy średnikiem ;.
public abstract void makeSound();
- Metody abstrakcyjne można deklarować tylko wewnątrz klasy abstrakcyjnej.
- Klasa, która dziedziczy po klasie abstrakcyjnej, musi zaimplementować wszystkie metody abstrakcyjne, w przeciwnym razie sama staje się abstract.
2. Dziedziczenie klas abstrakcyjnych: jak to działa
Spójrzmy na konkretny przykład. Niech mamy klasę abstrakcyjną Animal z metodą abstrakcyjną makeSound(). Teraz utworzymy klasę potomną Dog, która implementuje tę metodę:
public class Dog extends Animal {
public Dog(String name) {
super(name); // Wywołanie konstruktora klasy bazowej
}
@Override
public void makeSound() {
System.out.println(name + " szczeka: Hau-hau!");
}
}
I jeszcze jedna klasa potomna:
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " miauczy: Miau!");
}
}
Teraz możemy użyć tych klas w programie:
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Rex");
Animal cat = new Cat("Kitty");
dog.makeSound(); // Rex szczeka: Hau-hau!
cat.makeSound(); // Kitty miauczy: Miau!
dog.sleep(); // Rex śpi: Zzz...
cat.sleep(); // Kitty śpi: Zzz...
}
}
Zwróć uwagę:
Możemy deklarować zmienne typu Animal, ale tworzyć obiekty tylko konkretnych (nieabstrakcyjnych) potomków.
Schemat: jak to wygląda
. Animal (abstract)
/ \
Dog Cat
(implementuje makeSound) (implementuje makeSound)
3. Kiedy użyć klasy abstrakcyjnej, a kiedy interfejsu?
To jedno z najczęstszych pytań na rozmowach kwalifikacyjnych – i słusznie! Przyjrzyjmy się temu:
- Klasa abstrakcyjna – gdy obiekty mają wspólny stan (na przykład pola), wspólną logikę (metody z implementacją) i chcesz dać „szkielet” zachowania z możliwością dopracowania.
- Interfejs – gdy chcesz zdefiniować tylko zestaw metod (kontrakt), bez implementacji i stanu. Od Java 8 interfejsy otrzymały metody default/static, ale wciąż interfejs dotyczy „tego, co obiekt ma umieć”, a nie „jak to robi”.
Przykład z życia:
„Ptak” – klasa abstrakcyjna: wszystkie ptaki mają dziób, skrzydła i potrafią latać (choć różnie).
„Latający” – interfejs: nie tylko ptaki potrafią latać, ale też samoloty i superbohaterowie! Latają wszyscy inaczej, ale ważne, że to potrafią.
4. Praktyczne przykłady
Przykład 1: Klasa abstrakcyjna z częściową implementacją
Załóżmy, że tworzysz grę, w której są różne rodzaje transportu. Wszystkie potrafią jechać, ale robią to na różne sposoby. Jednak wszystkie mają prędkość, nazwę i standardowy sposób zatrzymywania się.
public abstract class Transport {
protected String name;
protected int speed;
public Transport(String name, int speed) {
this.name = name;
this.speed = speed;
}
// Metoda abstrakcyjna – implementacja w klasach potomnych
public abstract void move();
// Zaimplementowana metoda
public void stop() {
System.out.println(name + " zatrzymał się.");
}
}
public class Car extends Transport {
public Car(String name, int speed) {
super(name, speed);
}
@Override
public void move() {
System.out.println(name + " jedzie po drodze z prędkością " + speed + " km/h.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name, int speed) {
super(name, speed);
}
@Override
public void move() {
System.out.println(name + " kręci pedałami z prędkością " + speed + " km/h.");
}
}
Użycie:
Transport car = new Car("Toyota", 120);
Transport bike = new Bicycle("Stels", 25);
car.move(); // Toyota jedzie po drodze z prędkością 120 km/h.
bike.move(); // Stels kręci pedałami z prędkością 25 km/h.
car.stop(); // Toyota zatrzymał się.
bike.stop(); // Stels zatrzymał się.
Przykład 2: Metoda abstrakcyjna z parametrem
public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
Użycie:
Shape c = new Circle(3);
Shape r = new Rectangle(4, 5);
System.out.println("Pole koła: " + c.area());
System.out.println("Pole prostokąta: " + r.area());
5. Cechy i niuanse klas abstrakcyjnych
- Klasa abstrakcyjna może mieć dowolne modyfikatory dostępu: public, protected, private (na przykład dla pól).
- Można zadeklarować klasę abstrakcyjną bez metod abstrakcyjnych. Bywa to przydatne, jeśli chcesz jedynie zabronić tworzenia egzemplarzy klasy bazowej.
- Jeśli klasa dziedziczy po klasie abstrakcyjnej, ale nie implementuje wszystkich metod abstrakcyjnych, również musi być zadeklarowana jako abstract.
- Klasa abstrakcyjna może mieć konstruktory. Są one wywoływane przy tworzeniu obiektu potomnego (przez super(...)).
- Metody abstrakcyjne nie mogą być private (w przeciwnym razie nie da się ich zaimplementować w klasie potomnej).
- Klasy abstrakcyjne mogą zawierać metody i pola static.
- Klasa abstrakcyjna może implementować interfejsy, ale nie musi implementować ich metod – mogą to zrobić potomkowie.
Tabela: porównanie klasy abstrakcyjnej i interfejsu
| Klasa abstrakcyjna | Interfejs | |
|---|---|---|
| Słowo kluczowe | |
|
| Może zawierać pola | Tak (dowolne) | Od Java 8 – tylko static/final |
| Metody z implementacją | Tak | Od Java 8 – default/static |
| Metody abstrakcyjne | Tak | Tak |
| Dziedziczenie wielokrotne | Nie | Tak (można implementować wiele interfejsów) |
| Konstruktory | Tak | Nie |
| Można utworzyć obiekt | Nie | Nie |
6. Typowe błędy przy pracy z klasami i metodami abstrakcyjnymi
Błąd nr 1: próba utworzenia instancji klasy abstrakcyjnej.
Jeśli napiszesz new Animal("Ktoś"), kompilator od razu przypomni, że klasy abstrakcyjne nie są do tego przeznaczone. Pamiętaj: abstrakcja to „szkielet”, a nie „żywy organizm”.
Błąd nr 2: zapomniano zaimplementować wszystkie metody abstrakcyjne w klasie potomnej.
Jeśli Twoja klasa nie implementuje choć jednej metody abstrakcyjnej klasy bazowej, sama musi być zadeklarowana jako abstract, w przeciwnym razie otrzymasz błąd kompilacji.
Błąd nr 3: zadeklarowano metodę abstrakcyjną poza klasą abstrakcyjną.
W Javie nie można zadeklarować metody abstrakcyjnej w zwykłej (nieabstrakcyjnej) klasie – kompilator od razu zaprotestuje.
Błąd nr 4: próba uczynienia metody abstrakcyjnej private lub static.
Metody abstrakcyjne nie mogą być private (bo nie da się ich nadpisać) ani static (bo metody statyczne się nie nadpisują). No i nie mogą być też final, ponieważ final zabrania nadpisania.
Błąd nr 5: zapomniano o modyfikatorze dostępu przy metodzie abstrakcyjnej.
Jeśli jawnie nie podasz modyfikatora, metoda będzie miała widoczność pakietową (package-private), co nie zawsze jest tym, o co Ci chodziło.
GO TO FULL VERSION