Java — pytania rekrutacyjne z zadaniami o kolekcjach i wyjątkach

Java: pytania rekrutacyjne o kolekcje i wyjątki. Przećwicz klucze zamówień, equals/hashCode, kolejność, konflikty i przyczyny błędów w działającym kodzie.

Author: PracHub

Published: 10/11/2026

Java — pytania rekrutacyjne z zadaniami o kolekcjach i wyjątkach

October 11, 2026

Quick Overview

Polskie pytania rekrutacyjne Java z autorskim importerem zamówień: tożsamość, kolekcje, konflikt duplikatu i propagacja wyjątku, sprawdzone na OpenJDK17.

Software EngineerFree

Pytanie „czym różni się HashSet od ArrayList?” nabiera sensu, gdy dostajesz cztery wiersze zamówień i masz usunąć duplikaty. Czy identyczny numer w dwóch sklepach oznacza to samo zamówienie? Co zrobić, jeśli powtórzony numer ma inną kwotę? Wybór kolekcji wynika z odpowiedzi na te dwa pytania.

Ten zestaw pytań rekrutacyjnych Java prowadzi przez jeden autorski importer: od błędnego equals/hashCode, przez dobór kolekcji, do wyjątku, którego nie wolno ukryć. Zacznij od zadania Remove Global Duplicates While Preserving Order, a potem dodaj regułę konfliktu opisaną poniżej.

Granice dowodów: kontrakty języka i bibliotek pochodzą z oficjalnej dokumentacji Oracle Java SE 25. Oryginalny przykład uruchomiono na OpenJDK 17.0.20.1; nie testuje nowych funkcji Java 25. Dane są fikcyjne, a scenariusz nie jest relacją kandydata ani prognozą pytań konkretnej firmy. Wnioski dotyczą kodu w pamięci, bez bazy danych, płatności i wielowątkowości.

Klucz zamówienia i kwota decydują, czy rekord jest duplikatem, czy konfliktem

Jakie wymagania trzeba ustalić przed wyborem kolekcji?

Dane wejściowe mają uproszczony format sklep,numer,kwota_w_groszach; prezentowane fragmenty należą do klasy OrderImport z importem java.util.*, a fragment parsera używa lokalnych nazw fields i key. Przyjmujemy kwoty całkowite nieujemne, rozróżniamy wielkość liter i usuwamy spacje na brzegach pól. To ćwiczenie nie implementuje pełnego parsera CSV: nie obsługuje pól w cudzysłowie ani przecinków wewnątrz identyfikatora.

Tożsamością zamówienia jest para sklep–numer. A/1 i B/1 to różne zamówienia. Powtórzenie A/1 z tą samą kwotą ignorujemy, zachowując kolejność pierwszego pojawienia się klucza. Powtórzenie z inną kwotą odrzuca całą próbę zbudowania wyniku. Nie wybieramy po cichu wersji nowszej ani droższej.

WierszZnaczenie w kontrakcieDziałanie
A,1,100Pierwsze A/1Zachowaj
A,1,100Identyczny duplikatNie dodawaj kolejnego
A,2,50Inny numerZachowaj po A/1
B,1,70Inny sklepZachowaj po A/2
A,1,200Konflikt dla istniejącego kluczaZgłoś błąd

Dla pierwszych czterech wierszy wynik ma trzy zamówienia w kolejności A/1, A/2, B/1 i sumę 220 groszy. Piąty wiersz nie „aktualizuje” sumy do 320. Uruchomienie z konfliktem nie zwraca poprawnego wyniku.

Przed kodowaniem przedstaw te zasady rozmówcy. Jeśli rozmówca chce ostatnią wersję rekordu, ustal także kolejność wyniku: pierwsze pojawienie się klucza czy czas ostatniej aktualizacji. Zapytaj również, czy duplikat ma być widoczny w raporcie diagnostycznym i co oznacza brak numeru.

Dlaczego poprawne equals nie wystarcza do HashSet?

Oficjalny kontrakt Object wymaga, aby obiekty równe według equals miały ten sam hashCode. Odwrotność nie obowiązuje: kolizja skrótu nie oznacza równości. Przeanalizuj celowo błędną klasę:

static final class Broken {
    final String id;
    final int cents;
    Broken(String id, int cents) {
        this.id = id;
        this.cents = cents;
    }
    public boolean equals(Object o) {
        return o instanceof Broken b && id.equals(b.id);
    }
    public int hashCode() { return cents; }
}

new Broken("1", 100) i new Broken("1", 200) są tu równe, ale mają różne skróty. Lokalny test dodający je do HashSet zaobserwował rozmiar dwa. To demonstracja na wskazanym środowisku, nie gwarancja zachowania kolekcji dla klasy łamiącej kontrakt.

Naprawa polegająca na zwracaniu stałego skrótu może przywrócić zgodność z równością, lecz pogorszyć rozkład elementów. Nadal nie odpowiada też na pytanie, czy sam id wystarcza w systemie wielu sklepów. Poprawność domenowa klucza i poprawność techniczna kontraktu są osobnymi wymaganiami.

Oddziel kwotę od tożsamości, aby konflikt pozostał widoczny. Wtedy A/1 za 100 i A/1 za 200 staną się dwoma różnymi elementami, chociaż kontrakt importu wymaga wykrycia sprzecznych danych. Test powinien sprawdzać regułę biznesową, nie tylko rozmiar zbioru.

Kiedy wybrać listę, zbiór albo mapę?

W tym przykładzie potrzebujesz powiązać tożsamość z pełnym rekordem i porównać kwoty. Mapa daje bezpośredni dostęp do poprzedniej wartości. Zbiór kluczy wystarczyłby do samego pomijania powtórzeń, lecz nie przechowałby informacji potrzebnej do oceny konfliktu.

WymaganieKandydatOgraniczenie istotne dla importu
Zachować wszystkie wiersze, także powtórzeniaArrayListNie realizuje reguły unikalności
Sprawdzać obecność klucza, bez porządku wynikuHashSet<Key>Potrzebujesz osobnego miejsca na kwotę
Przechować rekord pod kluczem, bez wymagań kolejnościHashMap<Key, Order>Nie obiecuj kolejności pierwszego wystąpienia
Zachować rekord i kolejność pierwszych kluczyLinkedHashMap<Key, Order>Nadal trzeba jawnie wykryć konflikt
Sortować po kluczuTreeMap z odpowiednim porządkiemKolejność sortowania różni się od kolejności wejścia

Oracle opisuje HashMap jako mapę bez gwarancji kolejności. Podstawowe operacje mają stały czas przy odpowiednim rozproszeniu skrótów; nie jest to obietnica dla dowolnych danych i każdej operacji. Mapa nie jest automatycznie bezpieczna dla współdzielonych zapisów.

Domyślny LinkedHashMap utrzymuje kolejność wstawienia kluczy. Nie myl jej z opcjonalnym porządkiem dostępu. W naszym kodzie drugi identyczny rekord nie tworzy nowego klucza, więc nie przesuwa zamówienia na koniec.

Ten sam klucz i kwota oznaczają duplikat; różna kwota dla tego klucza odrzuca lokalną partię

Jak oddzielić niezmienny klucz od danych zamówienia?

Użyj dwóch małych rekordów Java, a politykę konfliktu pozostaw w importerze:

record Key(String store, String id) {}
record Order(Key key, int cents) {}

static List<Order> load(List<String> rows) {
    Objects.requireNonNull(rows, "rows");
    Map<Key, Order> unique = new LinkedHashMap<>();
    for (String row : rows) {
        Order order = parse(row);
        Order previous = unique.putIfAbsent(order.key(), order);
        if (previous != null && previous.cents() != order.cents()) {
            throw new IllegalStateException("conflicting order " + order.key());
        }
    }
    return List.copyOf(unique.values());
}

Tutaj pola klucza to String, a kwota to int, więc nie ma modyfikowalnej listy schowanej wewnątrz rekordu. Nie wyciągaj z tego wniosku, że każdy record zapewnia głęboką niezmienność. Rekord zawierający tablicę lub mutowalny obiekt wymaga osobnej analizy.

Zgodnie z kontraktem List.copyOf nie można modyfikować struktury zwróconej listy. Nie jest ogólną operacją głębokiego kopiowania. W tym konkretnym modelu elementy również nie mają zmiennego stanu używanego przez importer. Ta granica ma znaczenie, gdy rozmówca proponuje dodanie listy pozycji zamówienia.

putIfAbsent ułatwia rozróżnienie pierwszego i powtórzonego klucza. Nie traktuj jego nazwy jako dowodu atomowości całej metody. Lokalna mapa ma jednego właściciela; przykład nie zawiera współbieżnej publikacji ani wspólnego repozytorium zamówień.

Jak parser powinien przekazywać przyczynę błędu?

Parser najpierw sprawdza trzy pola oraz niepusty sklep i numer. Następnie parsuje kwotę. Gdy zapis liczby jest niepoprawny, dodaje kontekst klucza i zachowuje oryginalną przyczynę:

static Order parse(String row) {
    if (row == null) throw new IllegalArgumentException("null row");
    String[] fields = row.split(",", -1);
    if (fields.length != 3 || fields[0].isBlank() || fields[1].isBlank()) {
        throw new IllegalArgumentException("invalid fields");
    }
    Key key = new Key(fields[0].trim(), fields[1].trim());
    final int cents;
    try {
        cents = Integer.parseInt(fields[2].trim());
    } catch (NumberFormatException cause) {
        throw new IllegalArgumentException("invalid cents for " + key, cause);
    }
    if (cents < 0) throw new IllegalArgumentException("negative cents");
    return new Order(key, cents);
}

Mechanizm przyczyny jest częścią oficjalnego API Throwable. W naszym teście wyjątek zewnętrzny ma typ IllegalArgumentException, a getCause() zwraca NumberFormatException. Zachowana przyczyna pozwala odróżnić błąd zapisu liczby od konfliktu zamówienia bez zgadywania na podstawie ogólnego komunikatu.

Nie przechwytuj wszystkich wyjątków i nie zwracaj pustej listy. Puste wejście jest prawidłowym przypadkiem dającym pusty wynik; uszkodzony wiersz powinien pozostać błędem. Zrównanie tych stanów może spowodować, że odbiorca uzna awarię za brak zamówień.

W aplikacji rzeczywistej kontekst diagnostyczny wymaga też oceny danych poufnych. Nie musisz umieszczać całego surowego wiersza w komunikacie. Numer wiersza lub bezpieczny identyfikator może wystarczyć. Ćwiczenie używa fikcyjnych kluczy i nie ustanawia polityki logowania dla produkcji.

Czy finally może zgubić wyjątek?

Przewidź wynik tej krótkiej metody:

static int hidden() {
    try {
        throw new IllegalStateException("lost");
    } finally {
        return 7;
    }
}

Wynik lokalnego sprawdzenia to siedem; wyjątek nie dociera do wywołującego. JLS opisuje zakończenie finally, które może zastąpić wcześniejszy powód przerwania. Dlatego nie używaj return w finally jako wygodnego sposobu dostarczenia wyniku domyślnego.

To inny problem niż zachowanie przyczyny w konstruktorze wyjątku. W parserze chcesz przekazać błąd z dodatkowym kontekstem. W hidden usuwasz go przez zmianę przepływu sterowania. Najpierw ustal, czy wywołanie powinno zakończyć się wynikiem, czy błędem, a potem pokaż właściwą ścieżkę.

Jeżeli rozmowa przechodzi do zasobów, omów try-with-resources oraz wyjątki tłumione, ale nie twierdź, że ten importer je przetestował. Nie otwiera plików ani połączeń. Dopisanie słowa „finally” do odpowiedzi nie dowodzi poprawnego zarządzania zasobami.

Jak sprawdzić politykę importu, a nie tylko szczęśliwą ścieżkę?

Pełny plik OrderImport.java zakończył uruchomienie komunikatem PASS 19 checks; zapis wyniku jest częścią przykładu. Uruchomienie na OpenJDK 17 potwierdziło trzy klucze i sumę 220 dla poprawnych danych, odrzucenie konfliktu oraz zachowanie przyczyny parsowania. Zawiera też pusty import, kwotę zero, ujemną kwotę, brak numeru, nadmiarowe pole i wiersz NULL.

Przed uruchomieniem zapisz oczekiwania. Dla identycznego duplikatu wynik ma nadal trzy elementy; dla innego sklepu z numerem jeden powstaje osobny klucz. Dla konfliktu oczekujesz wyjątku, nie cichej zamiany wartości. Taki zestaw ujawnia zmianę wymagań lepiej niż sprawdzenie jednego wydruku mapy.

Kod buduje mapę wewnątrz metody i zwraca listę dopiero po zakończeniu pętli. Gdy parser zgłosi błąd, częściowo zbudowana lista nie zostaje zwrócona. To lokalne zachowanie „wynik albo wyjątek”, a nie transakcja obejmująca zewnętrzny system. Gdyby pętla wysyłała płatności lub zapisywała bazę, ta argumentacja przestałaby wystarczać.

Dalszy wariant może zbierać wszystkie błędy, zwracając osobny rezultat z rekordami i diagnostyką. Nie dodawaj go odruchowo do rozwiązania odrzucającego całą partię. Wyjaśnij, kto podejmuje decyzję o częściowym przyjęciu, jak rozliczane są konflikty i czy ponowne przetworzenie ma być bezpieczne.

Przy większych danych oszacuj pamięć na unikalne klucze i rekordy. Ten test nie jest benchmarkiem. Bez pomiaru nie podawaj liczby zamówień na sekundę ani przewagi konkretnej mapy. Najpierw zachowaj prawidłową semantykę, potem zaproponuj reprezentatywny pomiar.

Jak obronić model, gdy zmienią się wymagania?

Rozmówca może zapytać, czy HashSet pozostanie poprawny przy kolizji skrótów. Zacznij od kontraktu: dwa różne klucze mogą mieć ten sam skrót i nadal muszą być rozróżniane przez równość. Nasz błąd działa w przeciwną stronę: równe obiekty dostają różne skróty. Nie próbuj dowodzić jakości funkcji skrótu jednym przykładem bez kolizji.

Dla poprawnego klucza zaplanuj testy równości tych samych wartości, różnych sklepów i różnych numerów. Dodaj parę różnych obiektów o równych wartościach, zamiast porównywać obiekt tylko z nim samym. Jeśli tworzysz własną klasę, sprawdź również symetrię i przechodniość na reprezentatywnych danych. Rekord ogranicza ilość ręcznie pisanego kodu, ale nie wybiera za ciebie właściwych pól tożsamości.

Wariant z ignorowaniem wielkości liter wymaga normalizacji przed utworzeniem klucza albo spójnego modelu równości i skrótu. Samo użycie equalsIgnoreCase w jednym miejscu nie normalizuje danych w całej aplikacji. Musisz też ustalić, czy sklep a rzeczywiście jest tym samym sklepem co A; to decyzja domenowa, której nie wywnioskujesz z typu String.

Kolejny wariant to limit kwoty. Integer.parseInt odrzuci tekst poza zakresem int, lecz parser nie ustanawia dowolnego biznesowego maksimum. Jeżeli zamówienia mogą przekraczać ten zakres, wybierz reprezentację na podstawie wymagań i zaktualizuj testy. Nie przechodź bez namysłu na double: reprezentacja kwoty w najmniejszych jednostkach ma tutaj cel, a nowe operacje arytmetyczne wymagają analizy przepełnienia.

Pytanie o wyjątek checked lub unchecked nie ma odpowiedzi wynikającej wyłącznie z nazwy „import”. W tym małym API niepoprawny argument i konflikt są wyjątkami unchecked. Publiczna biblioteka może zamiast tego udostępnić typ błędu domenowego albo jawny wynik walidacji. Uzasadnij, co odbiorca może naprawić, czy ma ponowić próbę i czy musi odróżnić format od konfliktu. Nie zmieniaj klasy wyjątku bez opisania kontraktu dla wywołującego.

Jeżeli potrzebny jest numer wiersza, dodaj go w warstwie iteracji, która zna pozycję wejścia. Parser pojedynczego wiersza nie musi wiedzieć, skąd otrzymał tekst. Zachowaj przyczynę podczas wzbogacania komunikatu i testuj zarówno typ błędu, jak i informację przydatną do poprawienia danych. Wersja artykułu nie raportuje numerów wierszy; to konkretna propozycja rozszerzenia, nie ukryta funkcja istniejącego kodu.

Na końcu sprawdź, czy odbiorca chce wynik możliwy do modyfikacji. List.copyOf celowo odrzuca późniejsze add; lokalny test to potwierdza. Jeśli wymagany jest edytowalny bufor roboczy, zwróć nową listę zgodnie z nowym kontraktem. Nie usuwaj ograniczenia tylko dlatego, że test zgłosił UnsupportedOperationException: może właśnie wykrył naruszenie oczekiwanego sposobu użycia.

Które pytania PracHub pasują do tego zadania?

Poniższe ćwiczenia rozwijają konkretne fragmenty rozumowania. Są materiałem do treningu, a nie źródłem informacji o popularności pytań na polskim rynku. Przy każdym odpowiedz kontraktem, kontrprzykładem i sprawdzeniem wyniku.

Pełny tytuł pytania PracHubCo ćwiczysz
Remove Global Duplicates While Preserving OrderPorządek pierwszego wystąpienia i definicję duplikatu
Explain hash collisions and Java HashMap complexityRówność, skróty oraz warunki złożoności
Java and Object-Oriented Design FundamentalsTożsamość obiektu i odpowiedzialność modelu
Explain Java follow-ups after solving coding problemUzasadnienie kolekcji po działającym rozwiązaniu
Differentiate Java final, finalize, finallyRozdzielenie pojęć i przepływ błędów

Wróć do usuwania duplikatów z zachowaniem kolejności. Zanim napiszesz kod, podaj przypadek dwóch sklepów oraz konflikt kwoty. Jeśli potrafisz obronić te dwa przykłady i wskazać granicę lokalnego wyniku, odpowiedź wykracza poza definicję HashMap.

Sources and Further Reading

Źródła sprawdzone 11 października 2026. Autorski kod uruchomiono lokalnie na OpenJDK 17; 19 sprawdzeń przeszło. Brak raportów kandydatów, pomiaru produkcyjnego i gwarancji wyniku rekrutacji.


Comments (0)