1. Polimorfizm: co to jest i po co jest potrzebny
Jeśli myślisz, że polimorfizm to coś ze świata mutantów Marvela, muszę rozczarować: w programowaniu wszystko jest dużo spokojniejsze, ale nie mniej magiczne. Polimorfizm to zdolność obiektów o różnej implementacji reagować na te same wywołania metod w różny sposób.
Przykład z życia:
Masz klasę Book i klasę Magazine, obie dziedziczą po klasie abstrakcyjnej LibraryItem. Chcesz, aby dla dowolnego elementu biblioteki można było wywołać metodę printInfo(), a ona wypisze potrzebne informacje — dla książki będzie to autor i tytuł, dla magazynu — numer wydania i data.
Przykład kodu:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Książka: " + title + ", autor: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Magazyn: " + title + ", wydanie: " + issueNumber);
}
}
Teraz można utworzyć tablicę z różnych elementów i wywołać printInfo() dla każdego:
LibraryItem[] items = {
new Book("Władca much", "William Golding"),
new Magazine("Nauka i życie", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Wyświetli:
// Książka: Władca much, autor: William Golding
// Magazyn: Nauka i życie, wydanie: 5
Tak właśnie działa polimorfizm!
2. Typowe błędy z polimorfizmem
Próba wywołania metod, których nie ma w typie bazowym
Jednym z najczęstszych błędów jest próba odwołania się do metody, która jest zadeklarowana tylko w klasie potomnej, przez referencję typu bazowego.
LibraryItem item = new Book("Harry Potter", "J.K. Rowling");
// item.getAuthor(); // Błąd kompilacji! W LibraryItem nie ma metody getAuthor()
Java kompiluje kod, wychodząc od tego, co widzi w typie zmiennej (LibraryItem), a nie w rzeczywistym obiekcie (Book). Dlatego jeśli potrzebujesz wywołać metodę specyficzną dla książki, musisz dokonać rzutowania:
if (item instanceof Book) {
Book book = (Book) item;
// Teraz można wywołać book.getAuthor()
}
Rzutowanie bez sprawdzenia
Jeśli jesteś przekonany, że obiekt to Book, ale w rzeczywistości tak nie jest, dostaniesz ClassCastException w czasie działania programu. Na przykład:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // Bum! ClassCastException
Prawidłowy sposób — zawsze sprawdzać typ:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("To nie jest książka!");
}
Nieużywanie zalet polimorfizmu
Czasem programiści piszą kod tak, że jest on sztywno związany z konkretnymi typami, chociaż można by użyć abstrakcji. Na przykład, jeśli piszesz:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
To działa tylko dla książek. A jeśli jutro pojawią się magazyny, gazety, komiksy? Lepiej używać tablicy LibraryItem[] i pracować z metodami klasy bazowej lub interfejsu.
3. Abstrakcje: po co są i jak ich nie zepsuć
Klasy abstrakcyjne i interfejsy
Abstrakcja to sztuka wyodrębnienia tego, co najważniejsze, i ukrycia szczegółów. W Javie służą do tego klasy abstrakcyjne i interfejsy.
- Klasa abstrakcyjna — klasa, której nie można utworzyć bezpośrednio, a jedynie po niej dziedziczyć.
- Interfejs — kontrakt: co klasa ma umieć, ale nie jak to robi.
Błąd 1: Utworzenie klasy abstrakcyjnej bez metod abstrakcyjnych
Jeśli w twojej klasie abstrakcyjnej nie ma żadnej metody abstrakcyjnej, zastanów się — czy na pewno powinna być abstrakcyjna? Może prościej zrobić zwykłą klasę?
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Lepiej zrobić zwykłą klasę, jeśli nie ma metod abstrakcyjnych
Błąd 2: Brak implementacji obowiązkowych metod w klasach potomnych
Jeśli klasa dziedziczy po klasie abstrakcyjnej lub implementuje interfejs, ma obowiązek zaimplementować wszystkie metody abstrakcyjne. Jeśli o tym zapomnisz — kompilator przypomni, ale bywa, że metody są zaimplementowane „na odczepnego” i nic nie robią. To utrudnia utrzymanie kodu.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Pusto! Źle!
}
}
Błąd 3: Zbyt głęboka lub zagmatwana hierarchia abstrakcji
Gdy klasy dziedziczą po sobie w pięciu–dziesięciu poziomach, zrozumienie ich staje się bardzo trudne. Lepiej tworzyć „płaskie” hierarchie, w których wszystko jest jasne.
Zły przykład:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Skomplikowane, prawda? Lepiej ograniczyć się do dwóch–trzech poziomów.
4. Praktyka: Zastosowanie polimorfizmu i abstrakcji w aplikacji ćwiczeniowej
Dopracujmy twoją ćwiczeniową aplikację biblioteczną. Wcześniej były tylko książki. Teraz dodamy magazyny i zrealizujemy wspólny interfejs dla wydań drukowanych.
Ogłośmy klasę abstrakcyjną:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Dodajmy klasy potomne:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Książka: " + title + ", autor: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Magazyn: " + title + ", wydanie: " + issueNumber);
}
}
Używamy polimorfizmu:
LibraryItem[] items = {
new Book("Czysty kod", "Robert Martin"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Dodajmy interfejs dla wydań elektronicznych
Załóżmy, że niektóre wydania można czytać online. Wprowadźmy interfejs:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Otwieramy e-książkę pod adresem: " + url);
}
}
Teraz można pracować z e-książkami przez interfejs:
ReadableOnline ebook = new EBook("Java dla początkujących", "Barry Burd", "https://example.com/java");
ebook.openOnline();
5. Jak uniknąć problemów z polimorfizmem i abstrakcjami: najlepsze praktyki
- Używaj interfejsów i klas abstrakcyjnych do opisu zachowań, a nie stanu.
Na przykład interfejs Printable dobrze opisuje możliwość „drukowania”, ale trzymanie w interfejsie pola String title to już zły pomysł. - Sprawdzaj typ obiektu za pomocą instanceof przed rzutowaniem.
Zwłaszcza jeśli obiekt może mieć różny typ. To uchroni przed ClassCastException. - Dąż do „płaskich” i zrozumiałych hierarchii.
Im prostsze drzewo dziedziczenia — tym łatwiej utrzymywać i rozbudowywać kod. - Unikaj „bezsensownych” abstrakcji.
Jeśli klasa nie zawiera metod abstrakcyjnych i nie jest przeznaczona do dziedziczenia — nie rób jej abstrakcyjną. - Używaj adnotacji @Override zawsze, gdy nadpisujesz metodę.
To pomaga kompilatorowi wyłapywać błędy w sygnaturze.
6. Typowe błędy przy pracy z polimorfizmem i abstrakcjami
Błąd nr 1: Rzutowanie typu bez sprawdzenia
Czasem kusi, by „pójść na skróty” i rzutować typ bez sprawdzenia. Może to zadziałać, ale może też doprowadzić do nagłego upadku programu. Zawsze używaj instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Błąd nr 2: Próba wywołania metody klasy potomnej przez referencję typu bazowego
LibraryItem item = new Book("Java", "Autor");
item.getAuthor(); // Błąd kompilacji: w LibraryItem nie ma takiej metody!
Rozwiązanie — albo rzutować typ, albo dodać potrzebną metodę do klasy bazowej (jeśli to ma sens).
Błąd nr 3: Niepełna implementacja interfejsu lub klasy abstrakcyjnej
Jeśli zapomnisz zaimplementować wszystkie metody interfejsu — kompilator nie pozwoli zbudować projektu. Ale jeśli zaimplementujesz „wydmuszki”, które nic nie robią, doprowadzi to do nieoczekiwanego zachowania.
Błąd nr 4: Zbyt głęboka hierarchia dziedziczenia
Jeśli masz więcej niż trzy poziomy dziedziczenia — zastanów się, czy nie da się uprościć architektury.
Błąd nr 5: Naruszenie zasady pojedynczej odpowiedzialności
Jeśli abstrakcja opisuje zbyt wiele obowiązków, staje się trudna w utrzymaniu. Lepiej rozbijać na kilka interfejsów lub klas.
GO TO FULL VERSION