Dziedziczenie w języku Java
Dziedziczenie
Dziedziczenie pozwala klasie potomnej (podklasie, subclass) odziedziczyć pola i metody klasy bazowej (nadklasy, superclass).
Korzyści:
- ponowne wykorzystanie kodu,
- łatwiejsze modelowanie relacji między klasami,
- rozszerzalność,
- łatwiejsze utrzymanie, mniejsza duplikacja.
Dziedziczenie w Javie
Klasa potomna rozszerza (extends) klasę bazową, dodając nowe funkcjonalności lub modyfikując istniejące.
class Superclass { // pola i metody } class Subclass extends Superclass { // dodatkowe pola i metody }
Przykład
public class Animal { protected String name; public Animal(String name) { this.name = name; } public void sound() { System.out.println("Jakiś dźwięk"); } }
public class Dog extends Animal { private boolean isTrained; public Dog(String name) { this.name = name; // dostęp do pola z klasy bazowej this.isTrained = false; } public void train() { isTrained = true; } }
Dziedziczenie a wielodziedziczenie
W Javie dozwolone jest tylko pojedyncze dziedziczenie klas
class A { } class B extends A { } // poprawne // class C extends A, B { } // niepoprawne
Dziedziczenie wielopoziomowe (A → B → C)
class A { } class B extends A { } class C extends B { }
Modyfikatory dostępu
- public - pola i metody dostępne wszędzie
- protected - dostępne w klasach potomnych
- private - niedostępne w klasach potomnych, dostępne tylko w klasie, w której zostały zadeklarowane
- default (brak modyfikatora) - dostępne w obrębie tego samego pakietu
Polimorfizm
Polimorfizm - ten sam interfejs (ta sama nazwa metody) może reprezentować różne zachowania, zależne od typu obiektu, na którym metoda jest wywoływana
- przeciążanie metod (overloading) — realizowane w czasie kompilacji. Różne sygnatury metod.
- przesłanianie metod (overriding) — realizowane w czasie wykonania. Dynamiczny wybór metody na podstawie typu obiektu.
Przesłanianie metod
- Klasa potomna może dostarczyć własną implementację metody odziedziczonej z klasy bazowej
- Użycie adnotacji
@Overridejest zalecane, aby wskazać, że metoda jest przesłonięciem, co pomaga uniknąć błędów.
class Animal { void sound() { System.out.println("Jakiś dźwięk"); } }
class Dog extends Animal { @Override void sound() { System.out.println("Hau hau"); } }
Przeciążenie metody
- metody o tej samej nazwie, ale o innej liście parametrów
- w zależności od przekazanych argumentów, wywoływana jest odpowiednia wersja metody
- może wystąpić w tej samej klasie lub w klasie potomnej
class Parent { void method() { } } class Child extends Parent { void method(int a) { } void method(int a, int b) { } }
Dziedziczenie konstruktorów
- domyślnie wywoływany jest domyślny konstruktor klasy bazowej (jeśli istnieje), a następnie konstruktor klasy potomnej
super(args)pozwala wywołać jawnie konstruktor klasy bazowej- użycie
super()wykluczathis()- instrukcja musi być pierwszą instrukcją w konstruktorze
public class Parent { public Parent() { System.out.println("Konstruktor klasy bazowej"); } } public class Child extends Parent { public Child() { super(); // Wywołanie konstruktora klasy bazowej System.out.println("Konstruktor klasy potomnej"); } }
Dostęp do składowych nadklasy
super.method()- wywołanie metody nadklasysuper(args)- wywołanie konstruktora nadklasysuper.field- dostęp do pól nadklasy
class Parent { int value = 10; void display() { System.out.println("To jest klasa nadrzedna."); } } class Child extends Parent { @Override void display() { super.display(); super.value = 20; // dostęp do pola klasy nadrzędnej System.out.println("To jest klasa potomna."); } }
Java Object
Każda klasa dziedziczy pośrednio lub bezpośrednio po klasie Object, która jest korzeniem hierarchii klas.
Dzięki temu wszystkie klasy mają dostęp do metod zdefiniowanych w Object:
toString()- zwraca reprezentację tekstową obiektu (domyślnie zawiera nazwę klasy i hash code),equals(Object obj)- porównuje obiekty pod względem zawartości (domyślnie porównuje referencje)hashCode()- zwraca hash code obiektu (domyślnie oparty na adresie pamięci),getClass()- zwraca obiekt klasy reprezentujący klasę obiektuclone()- tworzy kopię obiektu (wymaga implementacji interfejsuCloneable)finalize()- metoda wywoływana przez garbage collector przed usunięciem obiektu (niezalecana do użycia)
Klasy abstrakcyjne
- Klasa abstrakcyjna nie może być bezpośrednio tworzona jako obiekt.
- Może zawierać zarówno metody abstrakcyjne, jak i zwykłe metody z implementacją.
- Ukrywa szczegóły implementacji, eksponując tylko interfejs.
abstract class Animal { abstract void sound(); void sleep() { System.out.println("Zwierze spi"); } }
class Dog extends Animal { @Override void sound() { System.out.println("Hau hau"); } }
Klasa abstrakcyjna Number
Numberjest klasą abstrakcyjną, która reprezentuje liczby. Dziedziczą po niej klasy takie jakInteger,Double,Float,BigDecimal, itp.- Definiuje metody do konwersji na różne typy liczbowe a także dostarcza metody wbudowane do konwersji między typami liczbowymi.
Metody abstrakcyjne:
public abstract int intValue(); public abstract long longValue(); public abstract float floatValue(); public abstract double doubleValue();
Metody wbudowane:
public byte byteValue() { return (byte) intValue(); } public short shortValue() { return (short) intValue(); }
Interfejsy
- interfejs zawiera metody abstrakcyjne (domyślnie
public abstract) i stałe pola (domyślniepublic static final) - interfejs służy jako wzorzec, który klasa implementuje
- klasa może implementować wiele interfejsów
public interface Animal { int PAWS_COUNT = 4; // pole statyczne i finalne String sound(); // metoda abstrakcyjna }
public class Dog implements Animal { public String food = "Dog food"; // pole niestatyczne @Override public String sound() { return "Hau hau"; } }
Interfejsy w Java 8 i nowszych
- od Java 8 interfejsy mogą zawierać metody domyślne (
default) z implementacją oraz metody statyczne - metody domyślne - pozwalają na dodawanie nowych metod do interfejsów bez łamania istniejących implementacji. Można je nadpisywać.
public interface MyInterface { void abstractMethod(); // metoda abstrakcyjna default void defaultMethod() { System.out.println("To jest metoda domyślna"); } static void staticMethod() { System.out.println("To jest metoda statyczna"); } }
Interfejsy funkcyjne
- Interfejs funkcyjny - interfejs zawierający dokładnie jedną metodę abstrakcyjną (może mieć wiele metod domyślnych lub statycznych).
- Używany jako typ docelowy dla wyrażeń lambda i referencji do metod.
- Przykłady:
Runnable,Callable,Comparator,Comparable,Function,Predicate.
Interfejs Comparable
- Interfejs Comparable<T> definiuje metodę
compareTo, która służy do porównywania obiektów tego samego typu. - Obiekty implementujące
Comparablemogą być sortowane przezCollections.sort()iArrays.sort()
interface Comparable<T> { int compareTo(T o); }
- zwraca wartość dodatnią, gdy
this < o - zwraca zero, gdy
this == o - zwraca wartość ujemną, gdy
this > o
Klasa abstrakcyjna vs Interfejs
| Cecha | Klasa abstrakcyjna | Interfejs |
|---|---|---|
| Metody | metody abstrakcyjne i konkretne | metody abstrakcyjne (od Java 8 także default i static) |
| Deklaracja | abstract class | interface |
| Konstruktor | może mieć konstruktor (nie można instancjonować) | brak |
| Pola | dowolne | domyślnie public static final |
| Dziedziczenie | brak wielodziedziczenia | można implementować wiele interfejsów |
Rzutowanie w górę (Upcasting)
- Rzutowanie w górę - traktowanie referencji typu potomnego jako typu bazowego
- Jest domyślne i bezpieczne (nie wymaga jawnego rzutowania).
- Umożliwia polimorficzne wywoływanie metod, które mogą być przesłonięte w klasie potomnej.
Animal a = new Dog();
Rzutowanie w dół (Downcasting)
- Rzutowanie w dół - traktowanie referencji typu bazowego jako typu potomnego
- Celem jest dostęp do metod/pól specyficznych dla klasy potomnej.
- Wymaga jawnego rzutowania
Rose r = (Rose) flower;
- Może rzucić
ClassCastExceptionw czasie wykonywania, jeśli obiekt nie jest instancją docelowego typu.
Przykład
class Flower {} class Rose extends Flower { public void bloom() { System.out.println("Rose is blooming"); } } public class Main { public static void main(String[] args) { Flower f = new Rose(); Rose r = (Rose) f; // downcasting r.bloom(); // dostęp do metody specyficznej dla Rose } }
Operator instanceof
instanceofsprawdza, czy obiekt jest instancją danego typu lub jego podtypu (truelubfalse).- pozwala na bezpieczną weryfikację typu przed rzutowaniem aby uniknąć
ClassCastException.
Flower f = new Rose(); if (f instanceof Rose) { Rose r = (Rose) f; /* bezpieczne */ r.bloom(); }
Klasy anonimowe
Klasa anonimowa to klasa definiowana „w locie”, mająca dokładnie jedną instancję. Klasa anonimowa jest zawsze klasą wewnętrzną.
public interface Animal { void sound(); }
public class Main { public static void main(String[] args) { Animal cat = new Animal() { @Override public void sound() { System.out.println("Miauuu..."); } }; cat.sound(); } }
Klasy anonimowe - uwaga
- klasy anonimowe są często używane do implementacji interfejsów lub klas abstrakcyjnych, gdy potrzebujemy jedynie jednej instancji tej klasy i nie chcemy tworzyć osobnej klasy o nazwie dla tego celu
- mechanizm wykorzystywany do przekazywania metod jako argumentów przed wprowadzeniem wyrażeń lambda w Javie 8
- zmienna używana wewnątrz klasy anonimowej powinna być oznaczona jako
finallub być efektywnie finalna (Java 8+), czyli jej wartość jest niezmienna
Klasy anonimowe i lambda
Przykład klasy anonimowej:
Runnable r = new Runnable() { @Override public void run() { System.out.println("Uruchomiono"); } }; // Lambda (krótsze, Java 8+) Runnable r2 = () -> System.out.println("Uruchomiono");
Uwaga: zmienne używane w klasach anonimowych muszą być final lub efektywnie finalne (Java 8+).
Typy parametryczne ograniczone
- Można ograniczyć typy generyczne do tych, które implementują określony interfejs lub dziedziczą po określonej klasie.
- Pozwala na korzystanie z metod zdefiniowanych w klasie lub interfejsie ograniczenia.
public class Complex<T extends Number> { private T real; private T imaginary; public Complex<T> add(Complex<T> other) { double realPart = this.real.doubleValue() + other.real.doubleValue(); // ... } }
- Można również użyć wielokrotnego ograniczenia:
<T extends ClassA & InterfaceB>
Argumenty wieloznaczne (?)
?to symbol używany w generykach do reprezentowania nieznanego typu (wildcard).- Może być używany do definiowania metod lub klas, które mogą operować na różnych typach generycznych,
- np.:
List<?>,List<? extends Number>,List<? super Integer>
class Complex<T extends Number> { private T real; private T imaginary; public T getReal() { return real; } public Complex<T> add(Complex<? extends Number> other) { double realPart = (this.real.doubleValue() + other.getReal().doubleValue(); // ... } }
Najczęstsze błędy OOP:
- nadużywanie dziedziczenia (używaj kompozycji zamiast głębokich hierarchii).
- ignorowanie enkapsulacji (pola powinny mieć odpowiednie modyfikatory dostępu).
- brak
@Overrideprzy przesłanianiu i brakinstanceofprzy rzutowaniach - głębokie hierarchie i niejasny polimorfizm
Zasady SOLID:
- Single Responsibility Principle (SRP) - klasa powinna mieć tylko jedną odpowiedzialność.
- Open/Closed Principle (OCP) - klasy powinny być otwarte na rozszerzenia, ale zamknięte na modyfikacje.
- Liskov Substitution Principle (LSP) - obiekty klasy bazowej powinny być zastępowalne obiektami klasy pochodnej bez zmiany poprawności programu.
- Interface Segregation Principle (ISP) - wyspecjalizowane interfejsy, klienci nie powinni być zmuszani do zależności od interfejsów, których nie używają.
- Dependency Inversion Principle (DIP) - zależności powinny być odwrócone, czyli moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu, oba powinny zależeć od abstrakcji.
Ćwiczenie:
Rozbuduj klasę Complex z poprzednich:
- część rzeczywista i urojona to typy sparametryzowane, które muszą być liczbami (
Number) - wyodrębnij interfejs generyczny
ArithemeticOperations<T>uogólniający operacje arytmetyczneadd(),substract(),divide(),multiply() - przesłon metodę
equals()klasyObjectdla liczb zespolonych
Utwórz klasę potomną PolarComplex rozszerzającą klasę Complex o następujące elementy:
- dodatkowe pola repezsentujace promień
roraz kątphi - konstruktor przyjmujący postać biegunową
- przesłoń
toString()tak, by pokazywała postać biegunową, np.r=2.00, phi=1.57
Zadanie 3. Klasy Fraction i MixedFraction
Rozbuduj klasę ułamków Fraction z poprzedniego zadania (zob. zadanie 2)
Wymagania:
- ułamek jest liczbą, ma dziedziczyć po
Numberi implementować metody abstrakcyjne z tej klasy - ułamki można porządkować, niech implementują interfejs
Comparable - Przesłoń metody
equals(Object)tak, aby ułamki o tej samej wartości były równe (np.1/2i2/4).
Zaimplementuj klasę MixedFraction, która rozszerza Fraction:
- Reprezentuje liczbę mieszaną: część całkowita + ułamek właściwy, np.
-3 1/3. - Przesłoń
toString(), aby wypisywała wartość w postaci mieszanej. - Dodaj geter i setter dla części całkowitej (część całkowita nie musi być przechowywana w polu ale może być dynamicznie wyznaczana)
Utwórz interfejs Randomizable z metodą:
generate(Random r)- wypełnia obiekt losową zawartością i zwraca referencjęthis.- opcjonalnie: interfejs powinien być generyczny, aby umożliwić implementację dla różnych typów.
Zaimplementuj Randomizable dla Fraction i MixedFraction.
Napisz program testowy:
- wygeneruj losowo 100 obiektów
Fractioni 100 obiektówMixedFractioni umieść je w tablicy - uporządkuj je w kolejności rosnącej (użyj
Collections.sort()lubArrays.sort()), - wypisz wynik (posortowane ułamki) na standardowe wyjście.

