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
Hierarchia kolekcji w Javie
**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
ArrayList – najczęściej używana implementacja
import java.util.*; public class ListExample { public static void main(String[] args) { // Tworzenie ArrayList Listfruits = 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 ListlinkedList = 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]
Set – kolekcje unikalnych elementów
HashSet – najszybszy zbiór
import java.util.*; public class SetExample { public static void main(String[] args) { // Tworzenie HashSet SetuniqueColors = 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 SetsortedNames = 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ść
HashMap – najszybsza mapa
import java.util.*; public class MapExample { public static void main(String[] args) { // Tworzenie HashMap MapageMap = 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 Setsubjects; 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
Operacja | ArrayList | LinkedList | HashSet | TreeSet | HashMap |
---|---|---|---|---|---|
Dodawanie na końcu | O(1)* | O(1) | O(1) | O(log n) | O(1) |
Dodawanie na początku | O(n) | O(1) | O(1) | O(log n) | O(1) |
Wyszukiwanie | O(n) | O(n) | O(1) | O(log n) | O(1) |
Dostęp po indeksie | O(1) | O(n) | – | – | – |
Usuwanie | O(n) | O(1)*** | O(1) | O(log n) | O(1) |
*** O(1) tylko jeśli masz referencję do węzła
Iterowanie po kolekcjach
Różne sposoby iterowania
Listcolors = 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);
Bezpieczna modyfikacja podczas iteracji
Listnumbers = 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 Listnames = 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); } }
// PRZESTARZAŁE - nie używaj w nowym kodzie Vectorvector = 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
// DOBRZE - elastyczne Listnames = 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 Listnumbers = 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();
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.
Nie! HashMap nie gwarantuje kolejności. Użyj LinkedHashMap jeśli potrzebujesz zachować kolejność wstawiania, lub TreeMap dla kolejności sortowania.
HashMap pozwala na jeden klucz null i wiele wartości null. Ale uważaj – może to prowadzić do NullPointerException w późniejszym kodzie!
Użyj metody equals(): list1.equals(list2) dla List, lub set1.equals(set2) dla Set. Dla Map analogicznie.
Większość nie! ArrayList, HashMap, HashSet nie są thread-safe. Użyj Collections.synchronizedXxx() lub ConcurrentHashMap dla aplikacji wielowątkowych.
Shallow copy: new ArrayList<>(originalList). Deep copy wymaga własnej implementacji lub bibliotek jak Apache Commons Lang.
Domyślnie 10 elementów. Możesz podać własną pojemność w konstruktorze: new ArrayList<>(100) dla lepszej wydajności.
Przydatne zasoby:
- Oracle JavaDoc – Collections Framework
- Oracle Tutorial – Collections
- Oracle JavaDoc – Collections Utility Class
- Collections Framework Overview
🚀 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!