Kolekcje w Javie – List, Set, Map

TL;DR: Kolekcje w Javie to struktury danych do przechowywania grup obiektów. List przechowuje elementy w kolejności (może mieć duplikaty), Set przechowuje unikalne elementy, Map przechowuje pary klucz-wartość. ArrayList, HashSet i HashMap to najczęściej używane implementacje.

Dlaczego kolekcje to podstawa każdej aplikacji Java

**Bez kolekcji Twoje aplikacje byłyby ograniczone do pojedynczych zmiennych i statycznych tablic.** Kolekcje pozwalają na dynamiczne zarządzanie grupami danych – od listy użytkowników w systemie, przez zbiór unikalnych tagów, po mapowanie ID produktów na ich szczegóły. **To podstawowe narzędzie każdego Java developera**.

Co się nauczysz:

  • Różnice między List, Set i Map oraz kiedy używać każdej z nich
  • Najważniejsze implementacje: ArrayList, LinkedList, HashSet, HashMap
  • Praktyczne przykłady użycia w aplikacjach biznesowych
  • Wydajność różnych operacji na kolekcjach
  • Najlepsze praktyki i częste błędy
  • Iterowanie po kolekcjach i operations bulk
Wymagania wstępne: Podstawowa znajomość Java (klasy, obiekty, interfejsy), umiejętność korzystania z pętli for i while. Znajomość Generics będzie pomocna.

Hierarchia kolekcji w Javie

Java Collections Framework to zestaw interfejsów i klas do przechowywania i manipulowania grupami obiektów.

**Główne interfejsy:**
– **Collection** – podstawowy interfejs dla wszystkich kolekcji
– **List** – uporządkowana kolekcja (sekwencja) z duplikatami
– **Set** – kolekcja unikalnych elementów
– **Map** – mapowanie klucz-wartość (nie dziedziczy z Collection)

List – uporządkowane listy z duplikatami

List to jak lista zakupów – elementy mają określoną kolejność, możesz dodawać te same produkty wielokrotnie, i możesz odwoływać się do elementu po jego pozycji.

ArrayList – najczęściej używana implementacja

import java.util.*;

public class ListExample {
    public static void main(String[] args) {
        // Tworzenie ArrayList
        List fruits = new ArrayList<>();
        
        // Dodawanie elementów
        fruits.add("Jabłko");
        fruits.add("Banan");
        fruits.add("Pomarańcza");
        fruits.add("Jabłko"); // Duplikaty są dozwolone!
        
        System.out.println("Lista owoców: " + fruits);
        // Wynik: [Jabłko, Banan, Pomarańcza, Jabłko]
        
        // Dostęp po indeksie (zaczyna się od 0)
        String firstFruit = fruits.get(0);
        System.out.println("Pierwszy owoc: " + firstFruit);
        
        // Rozmiar listy
        System.out.println("Liczba owoców: " + fruits.size());
        
        // Sprawdzanie czy zawiera element
        if (fruits.contains("Banan")) {
            System.out.println("Mamy banany!");
        }
        
        // Usuwanie elementu
        fruits.remove("Banan");
        System.out.println("Po usunięciu banana: " + fruits);
        
        // Usuwanie po indeksie
        fruits.remove(0); // Usuwa pierwszy element
        System.out.println("Po usunięciu pierwszego: " + fruits);
    }
}

LinkedList – kiedy potrzebujesz częstych wstawień

// LinkedList lepszy dla częstych wstawień na początku/środku
List linkedList = new LinkedList<>();
linkedList.add("Element 1");
linkedList.add("Element 2");

// Wstawienie na początku - O(1) dla LinkedList vs O(n) dla ArrayList
linkedList.add(0, "Nowy pierwszy");

// LinkedList implementuje też interfejs Deque
LinkedList deque = new LinkedList<>();
deque.addFirst("Pierwszy");
deque.addLast("Ostatni");
deque.addFirst("Nowy pierwszy");

System.out.println(deque); // [Nowy pierwszy, Pierwszy, Ostatni]
Pro tip: Używaj ArrayList jako domyślnego wyboru. LinkedList tylko gdy często wstawiasz/usuwasz elementy ze środka listy.

Set – kolekcje unikalnych elementów

Set to jak zbiór w matematyce – każdy element może wystąpić tylko raz. Idealny do przechowywania tagów, kategorii, czy unikalnych identyfikatorów.

HashSet – najszybszy zbiór

import java.util.*;

public class SetExample {
    public static void main(String[] args) {
        // Tworzenie HashSet
        Set uniqueColors = new HashSet<>();
        
        // Dodawanie elementów
        uniqueColors.add("Czerwony");
        uniqueColors.add("Niebieski");
        uniqueColors.add("Zielony");
        uniqueColors.add("Czerwony"); // Duplikat - zostanie zignorowany!
        
        System.out.println("Unikalne kolory: " + uniqueColors);
        // Wynik: [Czerwony, Niebieski, Zielony] (kolejność może być inna!)
        
        System.out.println("Liczba unikalnych kolorów: " + uniqueColors.size());
        
        // Sprawdzanie czy zawiera element - bardzo szybkie O(1)
        if (uniqueColors.contains("Czerwony")) {
            System.out.println("Mamy czerwony kolor");
        }
        
        // Praktyczny przykład - usuwanie duplikatów z listy
        List numbersWithDuplicates = Arrays.asList(1, 2, 3, 2, 4, 3, 5);
        Set uniqueNumbers = new HashSet<>(numbersWithDuplicates);
        
        System.out.println("Lista z duplikatami: " + numbersWithDuplicates);
        System.out.println("Unikalne liczby: " + uniqueNumbers);
    }
}

TreeSet – posortowany zbiór

// TreeSet automatycznie sortuje elementy
Set sortedNames = new TreeSet<>();
sortedNames.add("Zenon");
sortedNames.add("Anna");
sortedNames.add("Bartosz");

System.out.println(sortedNames); // [Anna, Bartosz, Zenon] - automatycznie posortowane!

// Operacje na posortowanym zbiorze
TreeSet numbers = new TreeSet<>();
numbers.addAll(Arrays.asList(5, 2, 8, 1, 9, 3));

System.out.println("Pierwszy: " + numbers.first()); // 1
System.out.println("Ostatni: " + numbers.last());   // 9
System.out.println("Mniejsze niż 5: " + numbers.headSet(5)); // [1, 2, 3]

Map – mapowanie klucz-wartość

Map to jak słownik – każde słowo (klucz) ma swoje znaczenie (wartość). Lub jak książka telefoniczna – imię osoby to klucz, a numer telefonu to wartość.

HashMap – najszybsza mapa

import java.util.*;

public class MapExample {
    public static void main(String[] args) {
        // Tworzenie HashMap
        Map ageMap = new HashMap<>();
        
        // Dodawanie par klucz-wartość
        ageMap.put("Jan", 25);
        ageMap.put("Anna", 30);
        ageMap.put("Piotr", 28);
        ageMap.put("Jan", 26); // Nadpisuje poprzednią wartość!
        
        System.out.println("Mapa wieku: " + ageMap);
        
        // Pobieranie wartości po kluczu
        Integer janAge = ageMap.get("Jan");
        System.out.println("Wiek Jana: " + janAge);
        
        // Sprawdzanie czy klucz istnieje
        if (ageMap.containsKey("Anna")) {
            System.out.println("Anna ma " + ageMap.get("Anna") + " lat");
        }
        
        // Pobieranie z wartością domyślną (Java 8+)
        Integer mariaAge = ageMap.getOrDefault("Maria", 0);
        System.out.println("Wiek Marii: " + mariaAge); // 0, bo nie ma takiego klucza
        
        // Iterowanie po mapie
        System.out.println("\nIterowanie po wszystkich wpisach:");
        for (Map.Entry entry : ageMap.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
        
        // Tylko klucze
        Set names = ageMap.keySet();
        System.out.println("Wszystkie imiona: " + names);
        
        // Tylko wartości
        Collection ages = ageMap.values();
        System.out.println("Wszystkie wieki: " + ages);
    }
}

Praktyczny przykład – System zarządzania studentami

public class Student {
    private String firstName;
    private String lastName;
    private Set subjects;
    
    public Student(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.subjects = new HashSet<>();
    }
    
    // gettery, settery, toString()
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public Set getSubjects() { return subjects; }
    
    public void addSubject(String subject) {
        subjects.add(subject);
    }
    
    @Override
    public String toString() {
        return firstName + " " + lastName + " (przedmioty: " + subjects + ")";
    }
}

public class StudentManager {
    private List allStudents;           // Lista wszystkich studentów
    private Map studentById;    // Mapa ID -> Student
    private Map> studentsBySubject; // Mapa przedmiot -> studenci
    
    public StudentManager() {
        this.allStudents = new ArrayList<>();
        this.studentById = new HashMap<>();
        this.studentsBySubject = new HashMap<>();
    }
    
    public void addStudent(String id, String firstName, String lastName) {
        Student student = new Student(firstName, lastName);
        
        // Dodaj do listy wszystkich
        allStudents.add(student);
        
        // Dodaj do mapy ID -> Student
        studentById.put(id, student);
        
        System.out.println("Dodano studenta: " + student);
    }
    
    public void enrollStudentInSubject(String studentId, String subject) {
        Student student = studentById.get(studentId);
        if (student != null) {
            // Dodaj przedmiot do studenta
            student.addSubject(subject);
            
            // Dodaj studenta do mapy przedmiot -> studenci
            studentsBySubject.computeIfAbsent(subject, k -> new HashSet<>()).add(student);
            
            System.out.println(student.getFirstName() + " zapisał się na " + subject);
        }
    }
    
    public List getAllStudents() {
        return new ArrayList<>(allStudents); // Defensive copy
    }
    
    public Student findStudentById(String id) {
        return studentById.get(id);
    }
    
    public Set getStudentsInSubject(String subject) {
        return studentsBySubject.getOrDefault(subject, new HashSet<>());
    }
    
    public Set getAllSubjects() {
        return new HashSet<>(studentsBySubject.keySet());
    }
    
    public void printStatistics() {
        System.out.println("\n=== STATYSTYKI ====");
        System.out.println("Liczba studentów: " + allStudents.size());
        System.out.println("Liczba przedmiotów: " + studentsBySubject.size());
        
        // Najpopularniejszy przedmiot
        String mostPopular = "";
        int maxStudents = 0;
        for (Map.Entry> entry : studentsBySubject.entrySet()) {
            if (entry.getValue().size() > maxStudents) {
                maxStudents = entry.getValue().size();
                mostPopular = entry.getKey();
            }
        }
        System.out.println("Najpopularniejszy przedmiot: " + mostPopular + " (" + maxStudents + " studentów)");
    }
    
    public static void main(String[] args) {
        StudentManager manager = new StudentManager();
        
        // Dodawanie studentów
        manager.addStudent("001", "Jan", "Kowalski");
        manager.addStudent("002", "Anna", "Nowak");
        manager.addStudent("003", "Piotr", "Wiśniewski");
        
        // Zapisywanie na przedmioty
        manager.enrollStudentInSubject("001", "Java");
        manager.enrollStudentInSubject("001", "Spring");
        manager.enrollStudentInSubject("002", "Java");
        manager.enrollStudentInSubject("002", "JavaScript");
        manager.enrollStudentInSubject("003", "Java");
        
        // Wyświetlanie statystyk
        manager.printStatistics();
        
        // Studenci na Java
        System.out.println("\nStudenci na Java:");
        for (Student student : manager.getStudentsInSubject("Java")) {
            System.out.println("- " + student);
        }
    }
}

Wydajność operacji na kolekcjach

OperacjaArrayListLinkedListHashSetTreeSetHashMap
Dodawanie na końcuO(1)*O(1)O(1)O(log n)O(1)
Dodawanie na początkuO(n)O(1)O(1)O(log n)O(1)
WyszukiwanieO(n)O(n)O(1)O(log n)O(1)
Dostęp po indeksieO(1)O(n)
UsuwanieO(n)O(1)***O(1)O(log n)O(1)
* ArrayList może czasem potrzebować O(n) na dodawanie gdy musi powiększyć wewnętrzną tablicę
*** O(1) tylko jeśli masz referencję do węzła

Iterowanie po kolekcjach

Różne sposoby iterowania

List colors = Arrays.asList("Czerwony", "Niebieski", "Zielony");

// 1. Enhanced for loop (najczęściej używany)
for (String color : colors) {
    System.out.println(color);
}

// 2. Iterator (bezpieczny dla modyfikacji)
Iterator iterator = colors.iterator();
while (iterator.hasNext()) {
    String color = iterator.next();
    System.out.println(color);
    // iterator.remove(); // Bezpieczne usuwanie podczas iteracji
}

// 3. Tradycyjna pętla for (tylko dla List)
for (int i = 0; i < colors.size(); i++) {
    System.out.println(colors.get(i));
}

// 4. Java 8 Streams (nowoczesne podejście)
colors.stream()
      .filter(color -> color.startsWith("C"))
      .forEach(System.out::println);
Uwaga: Nigdy nie modyfikuj kolekcji podczas iterowania pętlą for-each! Użyj Iterator.remove() lub zbierz elementy do usunięcia w osobnej kolekcji.

Bezpieczna modyfikacja podczas iteracji

List numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));

// BŁĄD - ConcurrentModificationException!
/*
for (Integer number : numbers) {
    if (number % 2 == 0) {
        numbers.remove(number); // NIE RÓB TEGO!
    }
}
*/

// POPRAWNIE - używaj Iterator
Iterator iter = numbers.iterator();
while (iter.hasNext()) {
    Integer number = iter.next();
    if (number % 2 == 0) {
        iter.remove(); // Bezpieczne usuwanie
    }
}
System.out.println(numbers); // [1, 3, 5]

// ALTERNATYWNIE - zbierz do usunięcia
List toRemove = new ArrayList<>();
for (Integer number : numbers) {
    if (number % 2 == 0) {
        toRemove.add(number);
    }
}
numbers.removeAll(toRemove);

Częste błędy i najlepsze praktyki

Błąd #1: Używanie == zamiast equals() do porównywania obiektów w kolekcjach.
// BŁĄD
List names = Arrays.asList("Jan", "Anna");
if (names.contains(new String("Jan"))) { // może nie zadziałać!
    System.out.println("Znaleziono");
}

// POPRAWNIE - String ma zaimplementowany equals()
// Ale uważaj na własne klasy!
public class Person {
    private String name;
    
    // WAŻNE: zawsze implementuj equals() i hashCode() dla obiektów w kolekcjach!
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
Pułapka: HashMap i HashSet używają hashCode() – jeśli nie zaimplementujesz poprawnie, obiekty mogą „zniknąć” z kolekcji!
Błąd #2: Niepotrzebne używanie Vector czy Hashtable zamiast nowoczesnych alternatyw.
// PRZESTARZAŁE - nie używaj w nowym kodzie
Vector vector = new Vector<>(); // Używaj ArrayList
Hashtable hashtable = new Hashtable<>(); // Używaj HashMap

// NOWOCZESNE
List list = new ArrayList<>();
Map map = new HashMap<>();

// Jeśli potrzebujesz thread-safety:
List syncList = Collections.synchronizedList(new ArrayList<>());
Map syncMap = Collections.synchronizedMap(new HashMap<>());

Najlepsze praktyki

Pro tip: Zawsze deklaruj zmienne używając interfejsów (List, Set, Map) zamiast konkretnych klas (ArrayList, HashSet, HashMap).
// DOBRZE - elastyczne
List names = new ArrayList<>(); // Łatwo zmienić na LinkedList
Set numbers = new HashSet<>(); // Łatwo zmienić na TreeSet
Map people = new HashMap<>(); // Łatwo zmienić na TreeMap

// ŹLE - sztywne
ArrayList names = new ArrayList<>(); // Trudno zmienić implementację
HashSet numbers = new HashSet<>();
HashMap people = new HashMap<>();

// Inicjalizacja z danymi (Java 8+)
List fruits = Arrays.asList("Jabłko", "Banan", "Pomarańcza");
Set uniqueFruits = new HashSet<>(fruits);

// Sprawdzanie pustej kolekcji
if (names.isEmpty()) { // DOBRZE
    System.out.println("Lista jest pusta");
}

if (names.size() == 0) { // Mniej czytelne
    System.out.println("Lista jest pusta");
}

Utility klasy dla kolekcji

import java.util.*;

// Collections - przydatne metody statyczne
List numbers = Arrays.asList(3, 1, 4, 1, 5, 9);

// Sortowanie
Collections.sort(numbers);
System.out.println("Posortowane: " + numbers);

// Odwrócenie
Collections.reverse(numbers);
System.out.println("Odwrócone: " + numbers);

// Shuffling (tasowanie)
Collections.shuffle(numbers);
System.out.println("Wymieszane: " + numbers);

// Min i Max
System.out.println("Minimum: " + Collections.min(numbers));
System.out.println("Maksimum: " + Collections.max(numbers));

// Niemodyfikowalne kolekcje
List immutableList = Collections.unmodifiableList(
    Arrays.asList("A", "B", "C")
);
// immutableList.add("D"); // UnsupportedOperationException!

// Puste kolekcje
List emptyList = Collections.emptyList();
Set emptySet = Collections.emptySet();
Map emptyMap = Collections.emptyMap();
Kiedy używać ArrayList a kiedy LinkedList?

ArrayList to domyślny wybór – szybki dostęp losowy i ekonomiczne wykorzystanie pamięci. LinkedList używaj tylko gdy często wstawiasz/usuwasz elementy ze środka listy.

Czy HashMap zachowuje kolejność wstawiania?

Nie! HashMap nie gwarantuje kolejności. Użyj LinkedHashMap jeśli potrzebujesz zachować kolejność wstawiania, lub TreeMap dla kolejności sortowania.

Co się stanie jeśli użyję null jako klucza w HashMap?

HashMap pozwala na jeden klucz null i wiele wartości null. Ale uważaj – może to prowadzić do NullPointerException w późniejszym kodzie!

Jak sprawdzić czy dwie kolekcje mają te same elementy?

Użyj metody equals(): list1.equals(list2) dla List, lub set1.equals(set2) dla Set. Dla Map analogicznie.

Czy kolekcje są thread-safe?

Większość nie! ArrayList, HashMap, HashSet nie są thread-safe. Użyj Collections.synchronizedXxx() lub ConcurrentHashMap dla aplikacji wielowątkowych.

Jak skopiować kolekcję?

Shallow copy: new ArrayList<>(originalList). Deep copy wymaga własnej implementacji lub bibliotek jak Apache Commons Lang.

Jaka jest pojemność początkowa ArrayList?

Domyślnie 10 elementów. Możesz podać własną pojemność w konstruktorze: new ArrayList<>(100) dla lepszej wydajności.

Następne kroki:

  • Naucz się Java 8 Streams API dla funkcyjnego przetwarzania kolekcji
  • Poznaj Guava Collections – rozszerzenia kolekcji od Google
  • Dowiedz się o kolekcjach concurrent dla aplikacji wielowątkowych

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz prostą aplikację „Menedżer kontaktów” która używa wszystkich trzech typów kolekcji: List do przechowywania historii połączeń, Set do unikalnych tagów kontaktu, i Map do mapowania numeru telefonu na obiekt Contact. Dodaj funkcje: dodawanie kontaktu, wyszukiwanie po imieniu, wyświetlanie kontaktów z danym tagiem.

Które kolekcje najczęściej używasz w swoich projektach? Czy miałeś problemy z wydajnością kolekcji w dużych aplikacjach? Podziel się swoimi doświadczeniami!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry