~~NOCACHE~~
~~REVEAL theme=simple&disableLayout=0&transition=none&controls=1&show_progress_bar=1&build_all_lists=0&show_image_borders=0&horizontal_slide_level=2&enlarge_vertical_slide_headers=0&show_slide_details=1&open_in_new_window=1&size=1024x768~~
====== 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
}
===== Diagram klas UML =====
{{ zajecia:java_2026_1:uml-class-diagram-animal.png?500 |Diagram klas UML - dziedziczenie}}
===== 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 ''@Override'' jest 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()'' wyklucza ''this()'' - 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 nadklasy
* ''super(args)'' - wywołanie konstruktora nadklasy
* ''super.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ę obiektu
* ''clone()'' - tworzy kopię obiektu (wymaga implementacji interfejsu ''Cloneable'')
* ''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 =====
* ''[[https://docs.oracle.com/javase/8/docs/api/java/lang/Number.html|Number]]'' jest klasą abstrakcyjną, która reprezentuje liczby. Dziedziczą po niej klasy takie jak ''Integer'', ''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ślnie ''public 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 [[https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html|Comparable]] definiuje metodę ''compareTo'', która służy do porównywania obiektów tego samego typu.
* Obiekty implementujące ''Comparable'' mogą być sortowane przez ''Collections.sort()'' i ''Arrays.sort()''
interface Comparable {
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ć ''ClassCastException'' w 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 =====
* ''instanceof'' sprawdza, czy obiekt jest instancją danego typu lub jego podtypu (''true'' lub ''false'').
* 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 ''final'' lub 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 {
private T real;
private T imaginary;
public Complex add(Complex other) {
double realPart = this.real.doubleValue() + other.real.doubleValue();
// ...
}
}
* Można również użyć wielokrotnego ograniczenia: ''''
===== 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 {
private T real;
private T imaginary;
public T getReal() { return real; }
public Complex 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 ''@Override'' przy przesłanianiu i brak ''instanceof'' przy 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'' uogólniający operacje arytmetyczne ''add()'', ''substract()'', ''divide()'', ''multiply()''
* przesłon metodę ''equals()'' klasy ''Object'' dla liczb zespolonych
Utwórz klasę potomną ''PolarComplex'' rozszerzającą klasę ''Complex'' o następujące elementy:
* dodatkowe pola repezsentujace promień ''r'' oraz kąt ''phi''
* 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. [[zajecia:java:03_obiekty#zadanie_2_klasa_fraction|zadanie 2]])
Wymagania:
* ułamek jest liczbą, ma dziedziczyć po ''Number'' i 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/2'' i ''2/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 ''Fraction'' i 100 obiektów ''MixedFraction'' i umieść je w tablicy
* uporządkuj je w kolejności rosnącej (użyj ''Collections.sort()'' lub ''Arrays.sort()''),
* wypisz wynik (posortowane ułamki) na standardowe wyjście.