CodeGym /Kursy /JAVA 25 SELF /Interfejsy ze standardowej biblioteki: Comparable, Serial...

Interfejsy ze standardowej biblioteki: Comparable, Serializable i inne

JAVA 25 SELF
Poziom 21, Lekcja 4
Dostępny

1. Interfejs Comparable<T>

Czy kiedykolwiek sortowałeś listę liczb lub łańcuchów? Oczywiście, że tak! A teraz wyobraź sobie, że masz listę swoich własnych obiektów — na przykład listę studentów, towarów albo kotów. Skąd Java ma wiedzieć, w jakiej kolejności je sortować? Właśnie do tego służy interfejs Comparable<T>.

Ten interfejs określa „naturalny porządek” obiektów — czyli taki porządek, który jest logiczny dla danego typu danych. Na przykład dla liczb — rosnąco, dla łańcuchów — alfabetycznie, dla studentów — według nazwiska albo wieku (wedle uznania).

Jak działa Comparable

Interfejs jest bardzo prosty: zawiera tylko jedną metodę:

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

Metoda compareTo powinna zwracać:

  • liczbę ujemną, jeśli bieżący obiekt jest „mniejszy” od drugiego;
  • 0, jeśli jest „równy”;
  • liczbę dodatnią, jeśli jest „większy”.

Przykład: sortujemy studentów według wieku

Dodajmy klasę Student i zaimplementujmy dla niej interfejs Comparable<Student>:

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

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

    // Gettery do przykładu
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public int compareTo(Student other) {
        // Sortujemy po wieku (rosnąco)
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Teraz możemy łatwo posortować tablicę lub listę studentów:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("John", 20));
        students.add(new Student("Peter", 18));
        students.add(new Student("Mary", 22));

        Collections.sort(students); // Działa dzięki Comparable!

        System.out.println("Posortowani studenci:");
        for (Student s : students) {
            System.out.println(s);
        }
    }
}

Wynik:

Peter (18)
John (20)
Mary (22)

Ważna uwaga

Jeśli implementujesz Comparable, dbaj o to, aby compareTo było spójne z equals. To znaczy: jeśli a.compareTo(b) == 0, to a.equals(b) powinno być true. W przeciwnym razie sortowanie i kolekcje mogą zachowywać się nieprzewidywalnie, a Ty zyskasz pretekst do filozoficznych rozważań o sensie życia programisty.

2. Interfejs Serializable

Serializacja to zdolność obiektu do przekształcenia się w sekwencję bajtów (na przykład po to, by zapisać się do pliku lub wysłać przez sieć), a następnie odtworzenia się z powrotem. Wyobraź sobie, że chcesz zapisać stan swojej gry albo wysłać obiekt na serwer — bez serializacji ani rusz.

W Javie służy do tego interfejs znakujący Serializable. „Znakujący” — to znaczy, że nie zawiera metod, a jedynie „oznacza” klasę jako serializowalną.

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;

    // ... reszta kodu
}

Jak zserializować obiekt

Do serializacji i deserializacji używa się klas ObjectOutputStream i ObjectInputStream. Przykład — zapisujemy obiekt do pliku i odczytujemy z powrotem:

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Student s = new Student("Kate", 19);

        // Zapisujemy obiekt do pliku
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
            out.writeObject(s);
        }

        // Odczytujemy obiekt z pliku
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
            Student loaded = (Student) in.readObject();
            System.out.println("Wczytano: " + loaded);
        }
    }
}

Uwaga: Wszystkie pola obiektu (oraz obiektów zagnieżdżonych) również muszą być serializowalne, w przeciwnym razie wystąpi błąd.

Po co jest interfejs znakujący

Interfejs Serializable nie wymaga implementowania metod — po prostu informuje JVM: „ten obiekt można serializować”. Jeśli zapomnisz go zaimplementować, próba serializacji zakończy się wyjątkiem NotSerializableException.

3. Inne ważne interfejsy standardowej biblioteki

Interfejs Cloneable

Jeszcze jeden interfejs znakujący. Jego zadanie to dać JVM do zrozumienia, że obiekt można klonować za pomocą metody Object.clone(). Bez niego próba wywołania clone() wyrzuci wyjątek.

Jednak klonowanie w Javie to temat z haczykiem. Domyślne klonowanie jest płytkie (shallow copy) i często lepiej napisać własne metody kopiujące.

public class Student implements Cloneable {
    private String name;
    private int age;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Interfejs AutoCloseable

Ten interfejs zawiera tylko jedną metodę close(). Każda klasa, która go implementuje, może być używana w konstrukcji try-with-resources — automatyczne zamykanie zasobów (na przykład plików, strumieni):

public class MyResource implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("Zasób zamknięty!");
    }
}

public class Main {
    public static void main(String[] args) {
        try (MyResource res = new MyResource()) {
            System.out.println("Pracujemy z zasobem");
        }
        // Tutaj res.close() zostanie wywołane automatycznie
    }
}

Interfejs Iterable<T>

Ten interfejs pozwala, aby Twój obiekt był „iterowalny” w pętli for-each. Zawiera tylko jedną metodę iterator(), która zwraca obiekt Iterator<T>.

public class MyList implements Iterable<String> {
    // ... wewnętrzne przechowywanie danych

    @Override
    public java.util.Iterator<String> iterator() {
        // Zwracamy iterator do przechodzenia po elementach
        return ...;
    }
}

Wszystkie standardowe kolekcje (ArrayList, HashSet itd.) implementują Iterable, dlatego można je przechodzić w for-each.

Interfejs Comparator<T>

Ten interfejs pozwala porównywać obiekty według różnych zasad, bez zmieniania ich samych. Na przykład sortować studentów po imieniu, a nie po wieku.

import java.util.Comparator;

Comparator<Student> byName = new Comparator<Student>() {
    @Override
    public int compare(Student a, Student b) {
        return a.getName().compareTo(b.getName());
    }
};

We współczesnej Javie robi się to zwykle za pomocą wyrażeń lambda:

Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());

Observer, EventListener

Te interfejsy są używane do budowania wzorców „obserwator” i „słuchacz zdarzeń” — kiedy jeden obiekt reaguje na zdarzenia zachodzące w innym. Na przykład w interfejsach graficznych (Swing, JavaFX) obsługa przycisków implementuje interfejs ActionListener.

4. Praktyka: implementujemy Comparable i serializujemy obiekt

Przykład 1. Comparable dla własnej klasy

Napiszmy klasę Book, którą można sortować według roku wydania:

public class Book implements Comparable<Book> {
    private String title;
    private int year;

    public Book(String title, int year) {
        this.title = title;
        this.year = year;
    }

    @Override
    public int compareTo(Book other) {
        return Integer.compare(this.year, other.year);
    }

    @Override
    public String toString() {
        return title + " (" + year + ")";
    }
}
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Book> books = Arrays.asList(
            new Book("Java dla bystrzaków", 2018),
            new Book("Wojna i pokój", 1869),
            new Book("Harry Potter", 1997)
        );
        Collections.sort(books);
        System.out.println(books);
    }
}

Wynik:

[Wojna i pokój (1869), Harry Potter (1997), Java dla bystrzaków (2018)]

Przykład 2. Serializacja obiektu

import java.io.*;

public class Book implements Serializable {
    private String title;
    private int year;
    // ... konstruktor, gettery, toString

    public Book(String title, int year) {
        this.title = title;
        this.year = year;
    }

    @Override
    public String toString() {
        return title + " (" + year + ")";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Book book = new Book("Java dla bystrzaków", 2018);

        // Zapisujemy obiekt do pliku
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
            out.writeObject(book);
        }

        // Odczytujemy obiekt z pliku
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
            Book loaded = (Book) in.readObject();
            System.out.println("Wczytano: " + loaded);
        }
    }
}

5. Tabela: najważniejsze interfejsy standardowej biblioteki

Interfejs Przeznaczenie Kluczowe metody Przykład użycia
Comparable<T>
Naturalny porządek obiektów
int compareTo(T o)
Sortowanie list
Comparator<T>
Niestandardowe porównywanie obiektów
int compare(T a, T b)
Sortowanie według różnych zasad
Serializable
Serializacja obiektów — (znakujący) Zapisywanie/odczytywanie obiektów
Cloneable
Klonowanie obiektów — (znakujący) Tworzenie kopii obiektów
AutoCloseable
Automatyczne zamykanie zasobów
void close()
try-with-resources
Iterable<T>
Iterowanie po elementach w kolekcjach
Iterator<T> iterator()
pętla for-each
Observer / EventListener Reakcja na zdarzenia
update(), actionPerformed
Obsługa zdarzeń w UI, wzorce projektowe

6. Typowe błędy przy pracy ze standardowymi interfejsami

Błąd nr 1: Nie zaimplementowano interfejsu, choć jest potrzebny.
Na przykład zapomniano zaimplementować Serializable, a próbujesz serializować obiekt — otrzymasz NotSerializableException. Podobnie z Cloneable i wywołaniem clone().

Błąd nr 2: Naruszenie kontraktu Comparable i equals.
Jeśli a.compareTo(b) == 0, a nie zachodzi a.equals(b), kolekcje mogą zachowywać się dziwnie. Na przykład TreeSet może „gubić” obiekty.

Błąd nr 3: Płytkie kopiowanie przy klonowaniu.
Metoda clone() domyślnie kopiuje tylko „górną warstwę” obiektu. Jeśli masz pola‑referencje do innych obiektów, nie są one kopiowane głęboko. To może prowadzić do zagadkowych bugów.

Błąd nr 4: Brak użycia try-with-resources.
Jeśli klasa implementuje AutoCloseable, ale nie używasz jej w try-with-resources, ryzykujesz zapomnienie o zamknięciu zasobu — i w efekcie wyciek pamięci lub blokadę pliku.

Błąd nr 5: Niepoprawna implementacja compareTo lub compare.
Jeśli zwracasz tylko 0 albo 1, zamiast ujemnej/zerowej/dodatniej liczby, sortowanie będzie działało niepoprawnie.

1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 4
Niedostępne
Porównanie numerycznych "kontenerów" 📦
Porównanie numerycznych "kontenerów" 📦
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 4
Niedostępne
Sortowanie miast według liczby ludności 🏙️
Sortowanie miast według liczby ludności 🏙️
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 4
Niedostępne
Alternatywne sortowanie produktów w sklepie 🏷️
Alternatywne sortowanie produktów w sklepie 🏷️
1
Zadanie
JAVA 25 SELF, poziom 21, lekcja 4
Niedostępne
Automatyczne zamykanie zasobów bezpieczeństwa 🔑
Automatyczne zamykanie zasobów bezpieczeństwa 🔑
1
Ankieta/quiz
Zaawansowane interfejsy, poziom 21, lekcja 4
Niedostępny
Zaawansowane interfejsy
Zaawansowane interfejsy i interfejsy funkcyjne
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION