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 |
|---|---|---|---|
|
Naturalny porządek obiektów | |
Sortowanie list |
|
Niestandardowe porównywanie obiektów | |
Sortowanie według różnych zasad |
|
Serializacja obiektów | — (znakujący) | Zapisywanie/odczytywanie obiektów |
|
Klonowanie obiektów | — (znakujący) | Tworzenie kopii obiektów |
|
Automatyczne zamykanie zasobów | |
try-with-resources |
|
Iterowanie po elementach w kolekcjach | |
pętla for-each |
| Observer / EventListener | Reakcja na zdarzenia | |
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.
GO TO FULL VERSION