실제 서비스 데이터는 하나의 값보다 여러 개의 값과 그 관계로 이루어질 때가 많다. 유저의 관심 상품 목록, 상품 ID별 재고 수량, 중복 없는 방문자 ID는 각각 다른 자료구조가 잘 맞는다.
| 필요 조건 | 선택 후보 | 이유 |
|---|---|---|
| 순서대로 여러 값을 관리 | List | 인덱스 접근, 중복 허용 |
| 키로 값을 빠르게 찾기 | Map | 키-값 대응 |
| 중복을 제거하고 존재 여부 확인 | Set | 같은 원소를 중복 저장하지 않음 |
| 정해진 상태나 선택지 표현 | enum | 허용 가능한 값의 범위를 타입으로 표현 |
구현 클래스로는 ArrayList, HashMap, HashSet이 흔하다. 다만 정렬 순서나 삽입 순서가 필요하다면 TreeMap·TreeSet, LinkedHashMap·LinkedHashSet 등의 특성을 선택해야 한다. HashMap과 HashSet의 반복 순서는 보장되지 않는다.
List<String> productIds = new ArrayList<>();
<String>은 컬렉션에 담을 데이터 타입을 알려 준다. 잘못된 타입을 넣는 실수를 컴파일 단계에서 막는다. 제네릭 타입 인수에는 int 같은 원시 타입을 직접 쓰지 않으므로 Integer, Long 같은 래퍼 클래스를 쓴다.
import java.util.ArrayList;
import java.util.List;
public class ListExample {
public static void main(String[] args) {
List<String> recentSearches = new ArrayList<>();
recentSearches.add("java");
recentSearches.add("spring");
recentSearches.add("java");
System.out.println(recentSearches.get(0)); // java
System.out.println(recentSearches.size()); // 3
recentSearches.remove(1);
System.out.println(recentSearches); // [java, java]
}
}
List는 중복을 허용한다. get(index)로 조회할 수 있지만 인덱스가 범위를 벗어나면 예외가 발생한다. Arrays.asList(...)는 크기 변경이 제한된 리스트를, List.of(...)는 수정할 수 없는 리스트를 만든다는 차이도 알아 두자.
import java.util.HashMap;
import java.util.Map;
public class MapExample {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("P-101", 7);
stock.put("P-102", 0);
int count = stock.getOrDefault("P-999", 0);
stock.put("P-101", stock.get("P-101") - 1);
System.out.println(count); // 0
System.out.println(stock.get("P-101")); // 6
}
}
동일한 키로 put하면 기존 값이 덮어써진다. get은 키가 없을 때 null을 반환할 수 있으므로 주의한다. 키·값 전체 순회는 entrySet()을 사용하면 편리하다.
import java.util.HashSet;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
Set<String> visitors = new HashSet<>();
visitors.add("user-1");
visitors.add("user-2");
visitors.add("user-1");
System.out.println(visitors.size()); // 2
System.out.println(visitors.contains("user-2")); // true
}
}
집합 간 교집합은 retainAll, 합집합은 addAll, 차집합은 removeAll로 구할 수 있다. 모두 호출한 집합을 변경하므로 원본 데이터를 보존하려면 먼저 복사본을 만든다.
Set<String> first = new HashSet<>(Set.of("A", "B"));
Set<String> second = Set.of("B", "C");
first.retainAll(second);
System.out.println(first); // [B]
사용자 정의 객체를 HashSet의 원소나 HashMap의 키로 쓴다면 equals()와 hashCode()의 계약을 이해해야 한다. 같다고 판단하는 객체의 해시코드는 같아야 한다.
주문 상태를 "payed", "paid", "PAID"처럼 임의 문자열로 저장하면 오타가 버그로 이어지기 쉽다. enum으로 허용된 상태를 선언해 보자.
public class OrderStateExample {
enum OrderStatus {
CREATED, PAID, SHIPPED, CANCELLED
}
public static void main(String[] args) {
OrderStatus status = OrderStatus.PAID;
switch (status) {
case CREATED -> System.out.println("결제 전");
case PAID -> System.out.println("배송 준비");
case SHIPPED -> System.out.println("배송 중");
case CANCELLED -> System.out.println("주문 취소");
}
}
}
출력 결과는 배송 준비다. enum은 단순 상수 모음에 그치지 않고 필드와 메서드도 가질 수 있다.
List는 순서가 있지만 Set과 Map 인터페이스 전체가 특정 순서를 보장하는 것은 아니다.HashMap은 스레드 안전한 자료구조가 아니다. 여러 스레드에서 수정하면 별도 동기화가 필요하다.List.of, Set.of, Map.of가 반환하는 컬렉션은 수정 불가능하며 null 원소/키/값도 허용하지 않는다.Collections.unmodifiableList(original)은 변경을 막는 뷰일 수 있어 원본 리스트를 수정하면 뷰에도 반영된다.enum은 한정된 상태와 선택지를 타입으로 나타낸다.equals·hashCode를 고려한다.