Record, equals/hashCode i niemodyfikowalne kolekcje
- Kolekcje i strumienie
- poziom: średni
- aktualizacja: 16 sierpnia 2026
record to zwięzły typ na dane: kompilator pisze za Ciebie konstruktor, akcesory, equals, hashCode i toString. Dzięki temu rekord bez żadnych ceregieli nadaje się na element zbioru i klucz mapy. Nie daj się jednak zwieść słowu „niemodyfikowalny" — rekord zamraża wyłącznie swoje referencje. Jeśli komponentem jest zwykła lista, ten kto ją przekazał wciąż trzyma do niej uchwyt i może ją opróżnić minutę po utworzeniu rekordu. Lekarstwem jest obronna kopia, czyli List.copyOf w konstruktorze kompaktowym.
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Record, equals/hashCode i niemodyfikowalne kolekcje — skryptyJAVA.pl
* Zapisz jako: RekordyIKolekcje.java
*/
public class RekordyIKolekcje {
/** Rekord z normalizacją i walidacją w konstruktorze kompaktowym. */
public record Produkt(String kod, String nazwa, BigDecimal cena) {
public Produkt {
kod = Objects.requireNonNull(kod, "kod").strip().toUpperCase(Locale.ROOT);
nazwa = Objects.requireNonNull(nazwa, "nazwa").strip();
cena = Objects.requireNonNull(cena, "cena");
if (kod.isEmpty() || nazwa.isEmpty()) {
throw new IllegalArgumentException("Kod i nazwa nie mogą być puste.");
}
if (cena.signum() < 0) {
throw new IllegalArgumentException("Cena nie może być ujemna.");
}
}
}
/** Jedna pozycja zamówienia. */
public record Pozycja(Produkt produkt, int ilosc) {
public Pozycja {
Objects.requireNonNull(produkt, "produkt");
if (ilosc < 1) {
throw new IllegalArgumentException("Ilość musi być dodatnia.");
}
}
public BigDecimal wartosc() {
return produkt.cena().multiply(BigDecimal.valueOf(ilosc));
}
}
/** Rekord z obronną kopią listy — zawartości nie można zmienić z zewnątrz. */
public record Zamowienie(String numer, List<Pozycja> pozycje) {
public Zamowienie {
numer = Objects.requireNonNull(numer, "numer").strip();
pozycje = List.copyOf(Objects.requireNonNull(pozycje, "pozycje"));
if (numer.isEmpty()) {
throw new IllegalArgumentException("Numer nie może być pusty.");
}
}
public BigDecimal wartosc() {
return pozycje.stream()
.map(Pozycja::wartosc)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// ── PRZYKŁADY ───────────────────────────────────────────
public static void main(String[] args) {
Produkt kawa = new Produkt(" ka-01 ", "Kawa ziarnista", new BigDecimal("24.90"));
Produkt takaSamaKawa =
new Produkt("KA-01", "Kawa ziarnista", new BigDecimal("24.90"));
Produkt herbata = new Produkt("HE-02", "Herbata", new BigDecimal("14.50"));
System.out.println("Rekord: " + kawa);
System.out.println("Równe: " + kawa.equals(takaSamaKawa));
System.out.println("Ten sam hashCode: " + (kawa.hashCode() == takaSamaKawa.hashCode()));
// LinkedHashSet (odmiana HashSet z zapamiętaną kolejnością) rozpoznaje duplikat
// dzięki wygenerowanym equals i hashCode.
Set<Produkt> katalog = new LinkedHashSet<>(List.of(kawa, takaSamaKawa, herbata));
System.out.println("Unikalnych produktów: " + katalog.size());
// Rekord może bezpiecznie pełnić rolę klucza mapy.
Map<Produkt, Integer> koszyk = new LinkedHashMap<>();
koszyk.merge(kawa, 2, Integer::sum);
koszyk.merge(takaSamaKawa, 1, Integer::sum);
koszyk.merge(herbata, 3, Integer::sum);
System.out.println("Koszyk: " + koszyk);
List<String> zrodlo = new ArrayList<>(List.of("A", "B"));
List<String> widok = Collections.unmodifiableList(zrodlo);
List<String> migawka = List.copyOf(zrodlo);
zrodlo.add("C");
System.out.println("Widok po zmianie źródła: " + widok);
System.out.println("Niezależna migawka: " + migawka);
// Set.copyOf i Map.copyOf gubią kolejność LinkedHash* — jest ona nawet losowana
// przy każdym starcie JVM, dlatego pokazujemy tu wyłącznie rozmiary.
Set<Produkt> stalyKatalog = Set.copyOf(katalog);
Map<Produkt, Integer> stalyKoszyk = Map.copyOf(koszyk);
System.out.println("Stały zbiór ma elementów: " + stalyKatalog.size());
System.out.println("Stała mapa ma kluczy: " + stalyKoszyk.size());
List<Pozycja> roboczePozycje = new ArrayList<>();
roboczePozycje.add(new Pozycja(kawa, 2));
roboczePozycje.add(new Pozycja(herbata, 1));
Zamowienie zamowienie = new Zamowienie(" ZAM-2026-001 ", roboczePozycje);
roboczePozycje.clear();
System.out.println("Pozycji w zamówieniu: " + zamowienie.pozycje().size());
System.out.println("Wartość zamówienia: " + zamowienie.wartosc() + " zł");
}
}
Jak użyć tego skryptu
- Zapisz przykład jako
RekordyIKolekcje.java— nazwę pliku bierzesz wprost z klasy publicznej — i uruchom poleceniemjava RekordyIKolekcje.java. Wynik pokaże deduplikację rekordów, działanie kluczy mapy oraz różnicę między widokiem i migawką listy. - Zdefiniuj rekord przez listę komponentów, na przykład
record Punkt(int x, int y) {}. Odczyt odbywa się metodamix()iy(), a nie getteramigetX()igetY(). - Dodaj konstruktor kompaktowy, gdy dane wymagają walidacji lub normalizacji. Nie wpisuje się w nim listy parametrów; wartości przypisane do parametrów zostaną po zakończeniu konstruktora zapisane w komponentach rekordu.
- Dla komponentów będących kolekcjami wykonuj w konstruktorze
List.copyOf,Set.copyOflubMap.copyOf. Dzięki temu późniejsza zmiana kolekcji przekazanej przez wywołującego nie zmieni rekordu. Te fabryki odrzucają jednaknullw elementach nagimNullPointerExceptionbez żadnego komunikatu — jeśli chcesz błędu równie czytelnego jak „Numer nie może być pusty.", sprawdź listę własnym warunkiem, zanim ją skopiujesz.
Warto wiedzieć
- Automatyczne
equalsihashCodeuwzględniają wszystkie komponenty rekordu. Nie używaj w rekordzie-kluczu danych, które logicznie nie należą do jego tożsamości, ani modyfikowalnych obiektów, których hash może się zmienić. Collections.unmodifiableList(zrodlo)tworzy niemodyfikowalny widok, ale zmiany wykonane przez referencjęzrodlosą w nim widoczne.List.copyOf(zrodlo)tworzy niezależną migawkę — i to zwykle jej potrzebuje rekord. Uważaj przy tym na kolejność:Set.copyOfiMap.copyOfnie zachowują porządkuLinkedHashSetaniLinkedHashMap, a kolejność iteracji takiej kopii jest losowana przy każdym starcie JVM — ten sam kod wypisze zbiór inaczej dziś i inaczej jutro. Jeśli kolejność ma dla Ciebie znaczenie, opakujLinkedHash*wCollections.unmodifiableSetlubunmodifiableMapi nie oddawaj nikomu referencji do źródła.- Kopie
List.copyOf,Set.copyOfiMap.copyOfsą płytkie: nie pozwalają zmieniać struktury kolekcji, ale nie kopiują jej elementów. Elementy też powinny być niemodyfikowalne, najlepiej same będąc rekordami. Najgorszym komponentem rekordu jest tablica —equalsporównuje ją przez tożsamość, więc dwa rekordy o identycznej zawartości nigdy nie będą równe, atoStringwypisze coś w rodzaju[I@45ee12a7. Tablicę kopiuj przezclone()i w konstruktorze, i w akcesorze — albo po prostu trzymajList.copyOfzamiast tablicy. BigDecimal.equalsuwzględnia skalę, więcnew BigDecimal("24.9")inew BigDecimal("24.90")nie są równe mimo tej samej wartości liczbowej. Jeśli cena ma należeć do tożsamości rekordu, ujednolicaj jej skalę.
Podobne skrypty
Stream API w praktyce: filtrowanie, grupowanie i podsumowania
Dziewięć gotowych sekcji Stream API na liście pracowników: filter i map, sortowanie z thenComparing, groupingBy z counting i summingDouble, statystyki, Optional, joining i partitioningBy.
średni Zobacz kod → Kolekcje i strumienieNajczęstsze operacje na List, Map i Set
Operacje, które robisz na kolekcjach codziennie: dodawanie i usuwanie elementów listy, kasowanie duplikatów, część wspólna i różnica zbiorów oraz zliczanie i grupowanie danych w mapie.
początkujący Zobacz kod → Kolekcje i strumienieSortowanie obiektów przez Comparator i Collator
Sortowanie rekordów po wielu polach naraz: rosnąco i malejąco, z bezpieczną obsługą wartości null i polskim porządkiem liter — Comparator, thenComparing i Collator w praktyce.
średni Zobacz kod →