Programowanie obiektowe

Wykład 10

Interfejsy

Dwa interfejsy do jednej klasy

W Javie można podpiąć (implementować) kilka interfejsów do jednej klasy. Jest to jedna z fundamentalnych cech języka Java, która pozwala na wielokrotną implementację interfejsów, co umożliwia wielokrotne dziedziczenie zachowań. W przeciwieństwie do dziedziczenia klas, gdzie Java pozwala na dziedziczenie tylko z jednej klasy bazowej, klasa może implementować dowolną liczbę interfejsów.

public interface InterfaceA {
    void methodA();
}

public interface InterfaceB {
    void methodB();
}

public class MyClass implements InterfaceA, InterfaceB {
    @Override
    public void methodA() {
        // Implementacja metody z InterfaceA
    }

    @Override
    public void methodB() {
        // Implementacja metody z InterfaceB
    }
}

Metody Abstrakcyjne o Tych Samych Sygnaturach

Jeśli dwie metody abstrakcyjne w różnych interfejsach mają tę samą sygnaturę, klasa implementująca te interfejsy musi dostarczyć tylko jedną implementację tej metody. Język Java traktuje je jako jedną i tę samą metodę.

public interface InterfaceA {
   void metoda();
}

public interface InterfaceB {
   void metoda();
}

public class MyClass implements InterfaceA, InterfaceB {
   @Override
   public void metoda() {
       // Implementacja metody
   }
}

Metody Domyślne o Tych Samych Sygnaturach

Jeśli dwa interfejsy definiują metody domyślne (default) o tej samej sygnaturze, klasa implementująca te interfejsy musi przesłonić tę metodę, aby rozwiązać konflikt.

public interface InterfaceA {
   default void metoda() {
       System.out.println("InterfaceA metoda");
   }
}

public interface InterfaceB {
   default void metoda() {
       System.out.println("InterfaceB metoda");
   }
}

public class MyClass implements InterfaceA, InterfaceB {
   @Override
   public void metoda() {
       InterfaceA.super.metoda(); // Wywołanie konkretnej metody domyślnej
       // lub własna implementacja
   }
}

“Dziedziczenie” interfejsów

interface MyInterfaceA {
    void methodA();
}

// Interfejs dziedziczący
interface MyInterfaceB extends MyInterfaceA {
    void methodB();
}

// Klasa implementująca InterfejsB
class Klasa implements MyInterfaceB {
    public void methodA() {
        // Implementacja metody z InterfejsA
    }

    public void methodB() {
        // Implementacja metody z InterfejsB
    }
}

Rzutowanie na interfejs

Rzutowanie obiektów na interfejs w Javie to proces, w którym obiekt klasy, która implementuje dany interfejs, jest traktowany jako instancja tego interfejsu. Jest to często używane, gdy chcemy skorzystać z metod określonych w interfejsie, nie zwracając uwagi na konkretną klasę obiektu. Rzutowanie jest szczególnie ważne w kontekście polimorfizmu, gdzie obiekty różnych klas mogą być traktowane jako instancje wspólnego interfejsu.

public interface Vehicle {
    void move();
}

public class Car implements Vehicle {
    @Override
    public void move() {
        System.out.println("Samochód jedzie");
    }
}

public class Bike implements Vehicle {
    @Override
    public void move() {
        System.out.println("Rower jedzie");
    }
}
Vehicle myCar = new Car(); 
Vehicle myBike = new Bike(); 
myCar.move(); 
myBike.move(); 

Inspiracja - metoda sort z klasy Arrays

import java.util.Arrays;

public class TestArray {

    public static void main(String[] args) {
        int[] intArray = {5, 2, 8, -3, 1};
        Arrays.sort(intArray);
        System.out.println(Arrays.toString(intArray));
        double[] doubleArray = {3.14, -1.59, 2.65, 3.58};
        Arrays.sort(doubleArray);
        System.out.println(Arrays.toString(doubleArray));
        String[] stringArray = {"Banana", "apple", "Cherry", "Date"};
        Arrays.sort(stringArray);
        System.out.println(Arrays.toString(stringArray));
    }
}

Interfejs Comparable w Javie jest używany do definiowania naturalnego porządku obiektów danej klasy. Kiedy klasa implementuje interfejs Comparable, oznacza to, że obiekty tej klasy mogą być porównywane ze sobą, co jest szczególnie użyteczne do sortowania.

Na wykładzie będzie omawiana generyczna wersja Comparable<T>.

Interfejs Comparable zawiera jedną metodę, którą należy zaimplementować:

public interface Comparable<T> {
    int compareTo(T o);
}

Metoda compareTo(T o) zwraca:

  • Liczbę mniejszą od zera, jeśli obiekt, na którym wywołano metodę, jest mniejszy od obiektu o.
  • Zero, jeśli oba obiekty są równe.
  • Liczbę większą od zera, jeśli obiekt, na którym wywołano metodę, jest większy od obiektu o.

Metoda sort z klasy Arrays może być użyta do sortowania tablic obiektów, które implementują interfejs Comparable. Sortowanie jest wtedy przeprowadzane zgodnie z naturalnym porządkiem określonym przez metodę compareTo.

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return this.age - other.age;
    }
    
    @Override
    public String toString() {
        return name + " - " + age;
    }

    // Gettery, settery, inne metody...
}
Person[] people = { new Person("Alice", 30), new Person("Bob", 25), new Person("Charlie", 35) };
Arrays.sort(people);

Dlaczego Comparable jest ważne?

  1. Definiowanie Naturalnego Porządku: Implementacja Comparable pozwala klasom określić, co znaczy, że jeden obiekt jest „większy”, „mniejszy” lub „równy” innemu.

  2. Ułatwia Sortowanie i Porównywanie: Dzięki Comparable, możemy używać metod takich jak Arrays.sort lub kolekcji, które automatycznie sortują elementy (np. TreeSet), bez konieczności określania dodatkowego komparatora.

  3. Większa Elastyczność i Czytelność Kodu: Implementacja Comparable w klasie sprawia, że porównanie i sortowanie obiektów tej klasy staje się bardziej intuicyjne i zintegrowane z naturalnymi operacjami Javy.

Sposoby porónywania dla pola typu int

public class TestMyNumber {
    public static void main(String[] args) {
        MyNumber[] numbers = {
                new MyNumber(5),
                new MyNumber(2),
                new MyNumber(8),
                new MyNumber(-3),
                new MyNumber(1)
        };
        Arrays.sort(numbers);
        System.out.println(Arrays.toString(numbers));
    }
}

class MyNumber implements Comparable<MyNumber> {
    private int value;

    public MyNumber(int value) {
        this.value = value;
    }
    
    @Override
    public String toString() {
        return String.valueOf(value);
    }

    @Override
    public int compareTo(MyNumber other) {
        //
    }
}
  1. Bezpośrednie Wyrzucanie -1, 0, 1
@Override
public int compareTo(MyNumber o) {
    if (this.value < o.value) return -1;
    if (this.value > o.value) return 1;
    return 0;
}
  1. Licząc Różnicę Między Polami
@Override
public int compareTo(MyNumber o) {
    return this.value - o.value;
}
  1. Za Pomocą Integer.compare
@Override
public int compareTo(MyNumber o) {
    return Integer.compare(this.value, o.value);
}
  1. Użycie Objects.compare: W Javie 7 i nowszych, można użyć Objects.compare w połączeniu z Comparator:
@Override
public int compareTo(MyNumber o) {
    return Objects.compare(this.value, o.value, Integer::compare);
}
  1. Korzystanie z Operatora Warunkowego:
@Override
public int compareTo(MyNumber o) {
    return (this.value < o.value) ? -1 : ((this.value == o.value) ? 0 : 1);
}

Sposoby porónywania dla pola typu double

import java.util.Arrays;

public class TestMyDouble {

    public static void main(String[] args) {
        MyDouble[] numbers = {
                new MyDouble(5),
                new MyDouble(2),
                new MyDouble(8),
                new MyDouble(-3),
                new MyDouble(1)
        };
        Arrays.sort(numbers);
        System.out.println(Arrays.toString(numbers));
    }
}


class MyDouble implements Comparable<MyDouble> {
    private double value;

    public MyDouble(double value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }

    @Override
    public int compareTo(MyDouble o) {
        return Double.compare(this.value, o.value);
    }
}

Sposoby porónywania dla pola typu String

import java.util.Arrays;

public class TestMyString {

    public static void main(String[] args) {
        MyString[] strings = new MyString[3];
        strings[0] = new MyString("Hello");
        strings[1] = new MyString("World");
        strings[2] = new MyString("Java");
        System.out.println(Arrays.toString(strings));
        Arrays.sort(strings);
        System.out.println(Arrays.toString(strings));
    }
}

class MyString implements Comparable<MyString> {
    private String value;

    public MyString(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return value;
    }

    @Override
    public int compareTo(MyString o) {
        return value.compareTo(o.value);
    }
}

Inne sposoby:

@Override
public int compareTo(MyString o) {
    return this.value.compareToIgnoreCase(o.value);
}
@Override
public int compareTo(MyString o) {
    Collator collator = Collator.getInstance();
    return collator.compare(this.value, o.value);
}
@Override
public int compareTo(MyString o) {
    return Objects.compare(this.value, o.value, String::compareTo);
}

Związek między equals i compareTo

W Javie, gdy implementujesz metody equals i compareTo w konkretnej klasie, istnieje ważna zasada, która powinna być zachowana, znana jako zgodność equals i compareTo. Zgodność ta oznacza, że wynik metody equals powinien być zgodny z wynikiem metody compareTo.

  1. Zgodność equals i compareTo:
    • Jeśli compareTo zwraca 0 (co wskazuje, że obiekty są równe pod względem porządku), to equals powinno również zwrócić true, sugerując, że obiekty są równoważne.
    • Jeśli equals zwraca true (wskazując, że obiekty są równoważne), to compareTo powinno zwrócić 0, wskazując, że są one równe pod względem porządku.
  2. Implementacja equals:
    • Musi sprawdzać, czy obiekt, z którym jest porównywany, jest tej samej klasy.
    • Następnie porównuje istotne pola, aby stwierdzić, czy obiekty są równoważne.
  3. Implementacja compareTo:
    • Musi porównywać te same pola, które są brane pod uwagę w metodzie equals.
    • Powinna zwracać 0, gdy obiekty są równe, wartość ujemną, gdy obiekt, na którym jest wywoływana, jest mniejszy, i wartość dodatnią, gdy jest większy.
public class Person implements Comparable<Person> {
   private String name;
   private int age;

   @Override
   public boolean equals(Object obj) {
       if (this == obj) return true;
       if (obj == null || getClass() != obj.getClass()) return false;
       Person person = (Person) obj;
       return age == person.age && Objects.equals(name, person.name);
   }

   @Override
   public int compareTo(Person other) {
       int nameComparison = name.compareTo(other.name);
       if (nameComparison != 0) {
           return nameComparison;
       }
       return Integer.compare(age, other.age);
   }
}

Uwaga! null nie należy do żadnej klasy. obj.compareTo(null) wyrzuca wyjątek, ale obj.equals(null) zwraca false.

Większość implementujących interfejs Comparable klas API Javy honoruje tę zasadę, ale jednym z ważnych wyjątków jest klasa BigDecimal.

import java.math.BigDecimal;
import java.util.Arrays;

public class TestBigDecimal {

    public static void main(String[] args) {
        BigDecimal[] numbers = new BigDecimal[3];
        numbers[0] = new BigDecimal("1.0");
        numbers[1] = new BigDecimal("1.00");
        numbers[2] = new BigDecimal("1.000");
        System.out.println(Arrays.toString(numbers));
        Arrays.sort(numbers);
        System.out.println(Arrays.toString(numbers));
    }
}

Interfejs Comparable<T> a dziedziczenie

Jeśli klasa bazowa X implementuje interfejs Comparable<X>, to klasa pochodna Y dziedzicząca po X nie może implementować interfejsu Comparable<Y>.

Można dodać w klasie pochodnej Y implementację interfejsu Comparable<X>.

Pierwszy sposób - rekomendowany

Wyrzucanie ClassCastException, jeśli typy są różne

public class Employee implements Comparable<Employee> {
    private String name;
    private int salary;

    // Konstruktory, gettery, settery itp.

    @Override
    public int compareTo(Employee other) {
        // Porównanie na podstawie nazwiska
        int nameComparison = this.name.compareTo(other.name);
        if (nameComparison != 0) {
            return nameComparison;
        }
        // Porównanie na podstawie wynagrodzenia
        return Integer.compare(this.salary, other.salary);
    }
}

public class Manager extends Employee {
    private int bonus;

    // Konstruktory, gettery, settery itp.

    @Override
    public int compareTo(Employee other) {
        if (other.getClass() != Manager.class) {
            throw new ClassCastException("Nie można porównać Managera z innym typem Employee");
        }
        Manager otherManager = (Manager) other;

        int baseComparison = super.compareTo(otherManager);
        if (baseComparison != 0) {
            return baseComparison;
        }

        // Porównanie na podstawie bonusu
        return Integer.compare(this.bonus, otherManager.bonus);
    }
}

Drugi sposób

Obiekty klasy pochodnej są uzupełniane o dodatkowe pole.

instanceof używane jest do zachowania hierarchii w porządku.

public class Employee implements Comparable<Employee> {
    private String name;
    private int salary;

    // Konstruktory, gettery, settery itp.

    @Override
    public int compareTo(Employee other) {
        // Porównanie na podstawie nazwiska
        int nameComparison = this.name.compareTo(other.name);
        if (nameComparison != 0) {
            return nameComparison;
        }
        // Porównanie na podstawie wynagrodzenia
        return Integer.compare(this.salary, other.salary);
    }
}

public class Manager extends Employee {
    private int bonus;

    // Konstruktory, gettery, settery itp.

    @Override
    public int compareTo(Employee other) {
        if (other instanceof Manager) {
            Manager otherManager = (Manager) other;
            int baseComparison = super.compareTo(otherManager);
            if (baseComparison != 0) {
                return baseComparison;
            }
            // Porównanie na podstawie bonusu
            return Integer.compare(this.bonus, otherManager.bonus);
        }

        return super.compareTo(other);
    }
}