Klasy abstrakcyjne a interfejsy
Część sekcji Programowanie obiektowe ścieżki Java w Coddy. Lekcja 38 z 87.
Teraz, gdy rozumiesz już zarówno klasy abstrakcyjne, jak i interfejsy, pojawia się częste pytanie: kiedy używać jednego, a kiedy drugiego? Wybór zależy od tego, co próbujesz zamodelować.
Używaj klasy abstrakcyjnej, gdy klasy mają wspólną bazę ze wspólnym stanem lub zachowaniem. Klasy abstrakcyjne mogą mieć zmienne instancji, konstruktory oraz zarówno metody abstrakcyjne, jak i konkretne:
public abstract class Animal {
protected String name; // wspólny stan
public Animal(String name) {
this.name = name;
}
public void sleep() { // wspólne zachowanie
System.out.println(name + " is sleeping");
}
public abstract void makeSound(); // musi zostać zaimplementowana
}Użyj interfejsu, gdy chcesz zdefiniować możliwość, którą mogą współdzielić niepowiązane ze sobą klasy. Interfejsy skupiają się na tym, co obiekt potrafi robić, a nie na tym, czym jest:
public interface Flyable {
void fly();
}
// Niespokrewnione klasy mogą współdzielić tę możliwość
class Bird extends Animal implements Flyable { ... }
class Airplane implements Flyable { ... }
class Drone implements Flyable { ... }Oto krótkie porównanie:
| Cecha | Klasa abstrakcyjna | Interfejs |
|---|---|---|
| Zmienne instancji | Tak | Tylko stałe |
| Konstruktory | Tak | Nie |
| Dziedziczenie wielokrotne | Nie (pojedyncze extends) | Tak (wielokrotne implements) |
| Modyfikatory dostępu | Dowolne | Tylko publiczne (dla metod abstrakcyjnych) |
Praktyczna wskazówka: jeśli tworzysz klasę abstrakcyjną zawierającą wyłącznie metody abstrakcyjne i nieprzechowującą żadnego stanu, interfejs będzie prawdopodobnie lepszym wyborem.
Wyzwanie
ŁatwyZbudujmy system pojazdów, który pokazuje, kiedy używać klas abstrakcyjnych, a kiedy interfejsów. Zamodelujesz pojazdy, które współdzielą stan i zachowanie dzięki klasie abstrakcyjnej, a opcjonalne możliwości dodasz za pomocą interfejsów.
Rozmieścisz swój kod w pięciu plikach:
Vehicle.java: Utwórz klasę abstrakcyjną, która będzie podstawą dla wszystkich pojazdów. Każdy pojazd ma polebrand(String) i poleyear(int). Dodaj konstruktor inicjalizujący oba pola, metody pobierające wartość każdego z nich oraz metodę abstrakcyjnąstartEngine(), która zwraca String. Dodaj też konkretną metodęgetInfo(), która zwraca:[brand] ([year]). To idealny przypadek użycia klasy abstrakcyjnej: pojazdy współdzielą stan (brand, year) i pewne zachowanie (getInfo), ale każdy uruchamia silnik w inny sposób.Convertible.java: Zdefiniuj interfejs dla pojazdów, których dach można złożyć. Ta możliwość nie jest związana z tym, czym pojazd jest: to coś, co niektóre pojazdy potrafią robić. Zadeklaruj dwie metody:openRoof()icloseRoof(), obie zwracające String.Car.java: Utwórz klasę, która rozszerzaVehiclei implementujeConvertible. Car ma dodatkowe polenumDoors(int). Użyjsuper, aby zainicjalizować odziedziczone pola. ZaimplementujstartEngine(), aby zwracała:[brand] car engine started. ZaimplementujopenRoof(), aby zwracała:[brand] roof opening, acloseRoof(), aby zwracała:[brand] roof closing.Motorcycle.java: Utwórz klasę, która rozszerzaVehicle, ale NIE implementujeConvertible: motocykle nie mają dachów! Motorcycle ma polehasSidecar(boolean). ZaimplementujstartEngine(), aby zwracała:[brand] motorcycle engine roaring.Main.java: Połącz wszystko, aby pokazać różnicę między klasami abstrakcyjnymi a interfejsami. Otrzymasz cztery dane wejściowe: markę samochodu, rok produkcji samochodu, markę motocykla i rok produkcji motocykla.Utwórz Car (z 4 drzwiami) i Motorcycle (bez wózka bocznego). Najpierw pokaż wspólne zachowanie klasy abstrakcyjnej, wypisując
getInfo()dla obu pojazdów. Oba dziedziczą tę metodę z Vehicle. Następnie wypisz wynikstartEngine()dla każdego z nich: zwróć uwagę, że każdy typ pojazdu implementuje tę metodę inaczej.Na koniec pokaż możliwości interfejsu: ponieważ tylko Car implementuje
Convertible, wywołaj i wypiszopenRoof(), a następniecloseRoof()tylko dla samochodu.
Otrzymasz cztery dane wejściowe: markę samochodu (String), rok produkcji samochodu (int), markę motocykla (String) i rok produkcji motocykla (int).
Wynik powinien zawierać łącznie sześć wierszy. Zwróć uwagę, że oba pojazdy współdzielą stan klasy abstrakcyjnej i metodę getInfo(), ale tylko Car ma możliwość składania dachu. To pokazuje, kiedy stosować każde z tych rozwiązań!
Spróbuj swoich sił
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Wczytaj dane wejściowe
String carBrand = scanner.nextLine();
int carYear = scanner.nextInt();
scanner.nextLine(); // pobierz znak nowej linii
String motorcycleBrand = scanner.nextLine();
int motorcycleYear = scanner.nextInt();
// TODO: Utwórz Car z 4 drzwiami
// TODO: Utwórz Motorcycle bez wózka bocznego (false)
// TODO: Wypisz wynik getInfo() dla obu pojazdów (pokazuje wspólne działanie klasy abstrakcyjnej)
// TODO: Wypisz wynik startEngine() dla obu pojazdów (pokazuje różne implementacje)
// TODO: Wypisz wyniki openRoof() i closeRoof() tylko dla samochodu (pokazuje możliwości interfejsu)
}
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneWprowadzenie do programowania obiektowegoKlasy a obiektySłowo kluczowe thisMetodyPola (atrybuty)Metoda konstruktoraPrzeciążanie konstruktoraPodsumowanie — prosty kalkulator4Dziedziczenie
Podstawy dziedziczenia (extends)Słowo kluczowe superPrzesłanianie metod (@Override)Łańcuch wywołań konstruktorówKlasa ObjectDziedziczenie jedno- i wielopoziomoweDlaczego Java nie obsługuje wielodziedziczenia klasPodsumowanie – hierarchia pracowników7Metody specjalne i klasa Object
Metoda toString()equals() i hashCode()Metoda clone()compareTo() i ComparableInterfejs ComparatorPodsumowanie — własne sortowanie10Obsługa wyjątków w programowaniu obiektowym
Hierarchia klas wyjątkówWłasne wyjątkiBłędy sprawdzane a niesprawdzaneWzorzec try-with-resourcesPodsumowanie – zweryfikowany użytkownik2Modyfikatory dostępu i enkapsulacja
Przegląd poziomów dostępuMetody pobierające i ustawiająceUkrywanie informacjiSłowo kluczowe finalPodsumowanie — menedżer kont bankowych5Polimorfizm
Podstawy przeciążania metodPrzesłanianie metod (w czasie wykonywania)Rzutowanie w górę i w dółOperator instanceofKlasy i metody abstrakcyjnePowtórka — kalkulator figur8Zaawansowane koncepcje OOP
Kompozycja a dziedziczenieAgregacja a kompozycjaKlasy wewnętrzne, zagnieżdżone i anonimoweEnumy i metody enumówRekordy (Java 16+)Klasy zapieczętowane (Java 17+)11Wzorce projektowe — część 1
Wprowadzenie do wzorców projektowychWzorzec SingletonWzorzec FactoryWzorzec BuilderWzorzec ObserverWzorzec Strategy3Właściwości klas i składowe statyczne
Zmienne instancji a zmienne statyczneMetody statyczneBloki statyczneStałe (static final)Powtórka – Counter i Utility6Interfejsy i klasy abstrakcyjne
Wprowadzenie do interfejsówImplementowanie interfejsówImplementacja wielu interfejsówMetody domyślne i statyczne w interfejsieKlasy abstrakcyjne a interfejsyInterfejsy funkcyjnePodsumowanie — system płatnościPoćwicz samodzielnie: Kompilator Java online