→ Slide 1

Dziedziczenie w języku Java

→ Slide 2

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.
→ Slide 3

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
}
→ Slide 4

Diagram klas UML - dziedziczenie

→ Slide 5
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;
    }
}
→ Slide 6

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 { }
→ Slide 7
  • 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
→ Slide 8

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.
→ Slide 9
  • 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");
    }
}
→ Slide 10
  • 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) { }
}
→ Slide 11
  • 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");
    }
}
→ Slide 12
  • 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.");
    }
}
↓ Slide 13

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)
→ Slide 14
  • 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");
    }
}
→ Slide 15
  • 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(); }
→ Slide 16
  • 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"; }
}
→ Slide 17
  • 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");
    }
}
→ Slide 18
  • 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.
→ Slide 19
  • Interfejs Comparable<T> 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<T> {
    int compareTo(T o);
}
  • zwraca wartość dodatnią, gdy this < o
  • zwraca zero, gdy this == o
  • zwraca wartość ujemną, gdy this > o
→ Slide 20
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
→ Slide 21
  • 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();
→ Slide 22
  • 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.
→ Slide 23
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
  }
}
→ Slide 24
  • 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();
}
→ Slide 25

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();
    }
}
→ Slide 26
  • 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
→ Slide 27

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+).

→ Slide 28
  • 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>
→ Slide 29
  • ? 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();
        // ...
    }
}
→ Slide 30
  • 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
→ Slide 31
  • 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.
→ Slide 32

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 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
→ Slide 33

Rozbuduj klasę ułamków Fraction z poprzedniego zadania (zob. 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.