Przejdź do treści

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.

JAVA RekordyIKolekcje.java 112 linii Pobierz .java
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

  1. Zapisz przykład jako RekordyIKolekcje.java — nazwę pliku bierzesz wprost z klasy publicznej — i uruchom poleceniem java RekordyIKolekcje.java. Wynik pokaże deduplikację rekordów, działanie kluczy mapy oraz różnicę między widokiem i migawką listy.
  2. Zdefiniuj rekord przez listę komponentów, na przykład record Punkt(int x, int y) {}. Odczyt odbywa się metodami x() i y(), a nie getterami getX() i getY().
  3. 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.
  4. Dla komponentów będących kolekcjami wykonuj w konstruktorze List.copyOf, Set.copyOf lub Map.copyOf. Dzięki temu późniejsza zmiana kolekcji przekazanej przez wywołującego nie zmieni rekordu. Te fabryki odrzucają jednak null w elementach nagim NullPointerException bez ż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 equals i hashCode uwzglę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ę zrodlo są w nim widoczne. List.copyOf(zrodlo) tworzy niezależną migawkę — i to zwykle jej potrzebuje rekord. Uważaj przy tym na kolejność: Set.copyOf i Map.copyOf nie zachowują porządku LinkedHashSet ani LinkedHashMap, 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, opakuj LinkedHash* w Collections.unmodifiableSet lub unmodifiableMap i nie oddawaj nikomu referencji do źródła.
  • Kopie List.copyOf, Set.copyOf i Map.copyOf są 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 — equals porównuje ją przez tożsamość, więc dwa rekordy o identycznej zawartości nigdy nie będą równe, a toString wypisze coś w rodzaju [I@45ee12a7. Tablicę kopiuj przez clone() i w konstruktorze, i w akcesorze — albo po prostu trzymaj List.copyOf zamiast tablicy.
  • BigDecimal.equals uwzględnia skalę, więc new BigDecimal("24.9") i new 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ę.
#record #kolekcje #list #map #set #walidacja

Podobne skrypty