~~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'', ''List'' class Complex { private T real; private T imaginary; public T getReal() { return real; } public Complex add(Complex 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.