Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- Spring
- 플러스 백엔드
- 티스토리챌린지
- 아키텍처
- Unity
- Kotlin
- 백준
- stack
- 오블완
- Kafka
- Gradle
- MSA
- event
- docker
- 트리
- 이진트리
- JPA
- 코딩테스트
- 연습문제
- 삽입
- EDA
- jdk
- bean
- jre
- code blocks
- 탐색
- redis
- 알고리즘
- 프로그래머스
- Java
Archives
- Today
- Total
Repository
3단계: Java 컬렉션 & 제네릭 본문
반응형
3단계: Java 컬렉션 & 제네릭
실무에서 가장 많이 사용하는 컬렉션 프레임워크와 제네릭을 내부 동작 원리부터 성능 최적화까지 완벽하게 정리했습니다.
3.1 컬렉션 프레임워크
List 완벽 가이드: ArrayList vs LinkedList
내부 구조와 동작 원리
ArrayList: 동적 배열
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 내부적으로 배열 사용
private Object[] elementData;
private int size;
// 기본 용량
private static final int DEFAULT_CAPACITY = 10;
// 생성자
public ArrayList() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
public ArrayList(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
// 추가
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 용량 확인
elementData[size++] = e;
return true;
}
// 용량 확장 (기존 용량의 1.5배)
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5배
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 조회
public E get(int index) {
rangeCheck(index);
return (E) elementData[index]; // O(1)
}
// 삭제
public E remove(int index) {
rangeCheck(index);
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
// 배열 이동 (느림!)
System.arraycopy(elementData, index + 1,
elementData, index, numMoved);
}
elementData[--size] = null;
return oldValue;
}
}
메모리 레이아웃
ArrayList: 연속된 메모리 공간
┌────┬────┬────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │null│null│ ← elementData
└────┴────┴────┴────┴────┴────┴────┘
idx0 idx1 idx2 idx3 idx4
장점: 인덱스 접근 빠름 (O(1))
단점: 중간 삽입/삭제 느림 (O(n))LinkedList: 이중 연결 리스트
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
// 내부 노드 클래스
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
transient Node<E> first; // 첫 번째 노드
transient Node<E> last; // 마지막 노드
transient int size = 0;
// 추가 (끝에 추가)
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
}
// 조회 (순차 탐색)
public E get(int index) {
checkElementIndex(index);
return node(index).item; // O(n)
}
// 노드 찾기 (최적화: 반으로 나눠 탐색)
Node<E> node(int index) {
if (index < (size >> 1)) {
// 앞에서부터 탐색
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 뒤에서부터 탐색
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// 삭제 (노드 연결만 변경)
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
return element;
}
}
메모리 레이아웃
LinkedList: 노드들이 링크로 연결
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ data:10│◄──►│ data:20│◄──►│ data:30│◄──►│ data:40│
│prev:null │prev:○ │prev:○ │prev:○ │next:null
│next:○ │ │next:○ │ │next:○ │
└────────┘ └────────┘ └────────┘ └────────┘
장점: 삽입/삭제 빠름 (O(1) - 노드만 찾으면)
단점: 인덱스 접근 느림 (O(n))
메모리 오버헤드 (prev, next 포인터)성능 비교 (JMH 벤치마크)
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@Fork(1)
public class ListBenchmark {
@Param({"1000", "10000", "100000"})
private int size;
private List<Integer> arrayList;
private List<Integer> linkedList;
@Setup
public void setup() {
arrayList = new ArrayList<>();
linkedList = new LinkedList<>();
for (int i = 0; i < size; i++) {
arrayList.add(i);
linkedList.add(i);
}
}
// 1. 끝에 추가
@Benchmark
public void arrayListAddLast() {
arrayList.add(999);
}
@Benchmark
public void linkedListAddLast() {
linkedList.add(999);
}
// 2. 처음에 추가
@Benchmark
public void arrayListAddFirst() {
arrayList.add(0, 999);
}
@Benchmark
public void linkedListAddFirst() {
linkedList.add(0, 999);
}
// 3. 중간에 추가
@Benchmark
public void arrayListAddMiddle() {
arrayList.add(size / 2, 999);
}
@Benchmark
public void linkedListAddMiddle() {
linkedList.add(size / 2, 999);
}
// 4. 인덱스 접근
@Benchmark
public int arrayListGet() {
return arrayList.get(size / 2);
}
@Benchmark
public int linkedListGet() {
return linkedList.get(size / 2);
}
// 5. 순회
@Benchmark
public long arrayListIterate() {
long sum = 0;
for (int i = 0; i < arrayList.size(); i++) {
sum += arrayList.get(i);
}
return sum;
}
@Benchmark
public long linkedListIterate() {
long sum = 0;
for (int i = 0; i < linkedList.size(); i++) {
sum += linkedList.get(i);
}
return sum;
}
// 6. Enhanced for 순회
@Benchmark
public long arrayListForEach() {
long sum = 0;
for (Integer num : arrayList) {
sum += num;
}
return sum;
}
@Benchmark
public long linkedListForEach() {
long sum = 0;
for (Integer num : linkedList) {
sum += num;
}
return sum;
}
}
/**
* 벤치마크 결과 (예시, 환경마다 다름):
*
* size = 10,000 기준
*
* 끝에 추가:
* - ArrayList: 0.003 µs/op ✅ 빠름
* - LinkedList: 0.003 µs/op ✅ 빠름
*
* 처음에 추가:
* - ArrayList: 25.5 µs/op ❌ 느림 (전체 복사)
* - LinkedList: 0.003 µs/op ✅ 빠름
*
* 중간에 추가:
* - ArrayList: 12.8 µs/op ❌ 느림
* - LinkedList: 1.2 µs/op ✅ 더 빠름
*
* 인덱스 접근:
* - ArrayList: 0.002 µs/op ✅ 매우 빠름 (O(1))
* - LinkedList: 1.5 µs/op ❌ 느림 (O(n/2))
*
* 인덱스 순회:
* - ArrayList: 15 µs/op ✅ 빠름
* - LinkedList: 15,000 µs/op ❌ 1000배 느림!
*
* Enhanced for 순회:
* - ArrayList: 15 µs/op ✅ 빠름
* - LinkedList: 18 µs/op ✅ 비슷 (Iterator 사용)
*/
실전 사용 시나리오
ArrayList를 사용하는 경우
public class ArrayListUseCases {
// 1. 조회가 많은 경우
public User findUserById(List<User> users, Long id) {
// 랜덤 액세스가 빈번함 → ArrayList
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getId().equals(id)) {
return users.get(i);
}
}
return null;
}
// 2. 끝에 추가만 하는 경우
public void collectLogs(List<String> logs, String newLog) {
logs.add(newLog); // O(1) amortized
}
// 3. 크기를 미리 아는 경우
public List<Integer> generateNumbers(int count) {
List<Integer> numbers = new ArrayList<>(count); // 초기 용량 지정
for (int i = 0; i < count; i++) {
numbers.add(i); // 재할당 없음!
}
return numbers;
}
// 4. Stream 처리
public List<String> processNames(List<User> users) {
return users.stream() // ArrayList의 stream()이 효율적
.map(User::getName)
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
}
// 5. 정렬
public void sortUsers(List<User> users) {
users.sort(Comparator.comparing(User::getAge)); // ArrayList는 정렬이 빠름
}
}
LinkedList를 사용하는 경우
public class LinkedListUseCases {
// 1. Queue/Deque로 사용
public class TaskQueue {
private Deque<Task> queue = new LinkedList<>();
public void enqueue(Task task) {
queue.addLast(task); // O(1)
}
public Task dequeue() {
return queue.pollFirst(); // O(1)
}
public Task peek() {
return queue.peekFirst(); // O(1)
}
}
// 2. 양쪽 삽입/삭제가 빈번한 경우
public class RecentlyViewed {
private LinkedList<String> items = new LinkedList<>();
private static final int MAX_SIZE = 10;
public void addItem(String item) {
// 이미 있으면 제거
items.remove(item);
// 맨 앞에 추가
items.addFirst(item); // O(1)
// 크기 제한
if (items.size() > MAX_SIZE) {
items.removeLast(); // O(1)
}
}
}
// 3. LRU 캐시 구현
public class LRUCache<K, V> {
private int capacity;
private Map<K, Node<K, V>> map = new HashMap<>();
private DoublyLinkedList<K, V> list = new DoublyLinkedList<>();
public V get(K key) {
Node<K, V> node = map.get(key);
if (node == null) return null;
// 최근 사용으로 이동
list.moveToHead(node); // O(1)
return node.value;
}
public void put(K key, V value) {
Node<K, V> node = map.get(key);
if (node != null) {
node.value = value;
list.moveToHead(node); // O(1)
} else {
Node<K, V> newNode = new Node<>(key, value);
map.put(key, newNode);
list.addFirst(newNode); // O(1)
if (map.size() > capacity) {
Node<K, V> tail = list.removeLast(); // O(1)
map.remove(tail.key);
}
}
}
}
// 4. 중간 삽입/삭제가 많은 경우
public void processEvents(LinkedList<Event> events) {
Iterator<Event> iterator = events.iterator();
while (iterator.hasNext()) {
Event event = iterator.next();
if (event.isExpired()) {
iterator.remove(); // O(1) - 현재 위치에서 삭제
} else if (event.needsUpdate()) {
// 현재 위치 뒤에 새 이벤트 추가
// LinkedList에서 효율적
}
}
}
}
의사결정 가이드
/**
* ArrayList 선택:
* ✅ 조회가 많다 (get() 빈번)
* ✅ 끝에 추가만 한다
* ✅ 크기를 미리 안다
* ✅ 메모리 효율이 중요하다
* ✅ Cache locality가 중요하다
* ✅ 일반적인 경우 (90%)
*
* LinkedList 선택:
* ✅ 처음/끝 삽입/삭제가 많다
* ✅ Queue/Deque가 필요하다
* ✅ 중간 삽입/삭제가 많다 (Iterator 사용)
* ✅ 순회만 한다 (Enhanced for)
*
* ❌ LinkedList 피해야 할 경우:
* - 인덱스 접근이 필요하다
* - 랜덤 액세스가 많다
* - 정렬이 필요하다
* - Stream 처리가 많다
*/
public class ListDecisionTree {
public List<String> chooseList() {
// 대부분의 경우: ArrayList
return new ArrayList<>();
// Queue가 필요: LinkedList 또는 ArrayDeque
// (ArrayDeque가 더 빠름!)
Deque<String> queue = new ArrayDeque<>();
// 동시성: CopyOnWriteArrayList
List<String> concurrentList = new CopyOnWriteArrayList<>();
}
}
ArrayList 최적화 팁
public class ArrayListOptimization {
// 1. 초기 용량 지정
public List<User> loadUsers(int expectedSize) {
// ✅ 재할당 방지
List<User> users = new ArrayList<>(expectedSize);
// 데이터 로드...
return users;
}
// 2. ensureCapacity 사용
public void addManyItems(List<String> list) {
if (list instanceof ArrayList) {
((ArrayList<String>) list).ensureCapacity(list.size() + 10000);
}
for (int i = 0; i < 10000; i++) {
list.add("Item " + i);
}
}
// 3. trimToSize로 메모리 절약
public List<String> buildList() {
ArrayList<String> list = new ArrayList<>(1000);
// 실제로는 100개만 추가됨
for (int i = 0; i < 100; i++) {
list.add("Item " + i);
}
list.trimToSize(); // 900개 공간 반환
return list;
}
// 4. subList 주의
public void subListPitfall() {
List<String> original = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
// ❌ subList는 원본을 참조! (메모리 누수 위험)
List<String> sub = original.subList(0, 2); // [A, B]
// ✅ 새 리스트 생성
List<String> newList = new ArrayList<>(original.subList(0, 2));
}
// 5. 삭제 최적화
public void efficientRemove(List<String> list) {
// ❌ 느림: 매번 배열 이동
for (int i = 0; i < list.size(); i++) {
if (list.get(i).startsWith("remove")) {
list.remove(i);
i--; // 인덱스 조정
}
}
// ✅ 빠름: Iterator 사용
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
if (iter.next().startsWith("remove")) {
iter.remove();
}
}
// ✅✅ 가장 빠름: removeIf
list.removeIf(s -> s.startsWith("remove"));
}
}
Set의 모든 것: HashSet, TreeSet, LinkedHashSet
중복 제거의 원리
HashSet: 해시 테이블 기반
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 내부적으로 HashMap 사용!
private transient HashMap<E, Object> map;
// 더미 값
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
// 추가
public boolean add(E e) {
return map.put(e, PRESENT) == null; // 새로 추가되면 true
}
// 포함 확인
public boolean contains(Object o) {
return map.containsKey(o); // O(1)
}
// 삭제
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
}
중복 제거 원리: hashCode()와 equals()
public class HashSetInternals {
public static void main(String[] args) {
Set<User> users = new HashSet<>();
User user1 = new User(1L, "Alice");
User user2 = new User(1L, "Alice");
users.add(user1);
users.add(user2); // 중복 제거될까?
System.out.println(users.size()); // ???
}
}
// ❌ hashCode(), equals() 미구현
class User {
private Long id;
private String name;
// 생성자...
}
// 결과: 2 (중복 제거 안됨!)
// ✅ hashCode(), equals() 구현
class User {
private Long id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// 결과: 1 (중복 제거됨!)
HashSet 동작 과정
/**
* HashSet에 요소 추가 과정:
*
* 1. hashCode() 호출
* └─> 해시 값 계산
*
* 2. 해시 값으로 버킷 찾기
* └─> bucket = hashCode & (capacity - 1)
*
* 3. 버킷 내에서 equals() 비교
* └─> 같은 객체가 있으면 추가 안함
* └─> 없으면 추가
*/
public class HashSetExample {
// hashCode() 계약
public void hashCodeContract() {
User u1 = new User(1L, "Alice");
User u2 = new User(1L, "Alice");
// equals()가 true면 hashCode()도 같아야 함
assert u1.equals(u2);
assert u1.hashCode() == u2.hashCode();
// hashCode()가 다르면 equals()는 false (역은 성립 안함)
User u3 = new User(2L, "Bob");
assert u1.hashCode() != u3.hashCode();
assert !u1.equals(u3);
}
// 나쁜 hashCode() 구현
class BadUser {
private Long id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BadUser)) return false;
BadUser user = (BadUser) o;
return Objects.equals(id, user.id);
}
// ❌ 항상 같은 값 반환 (모든 객체가 같은 버킷에!)
@Override
public int hashCode() {
return 42;
}
}
// 좋은 hashCode() 구현
class GoodUser {
private Long id;
private String name;
private String email;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GoodUser)) return false;
GoodUser user = (GoodUser) o;
return Objects.equals(id, user.id);
}
// ✅ 여러 필드 조합 (Objects.hash 사용)
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
// ✅✅ 수동 구현 (더 효율적)
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (id != null ? id.hashCode() : 0);
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
}
}
TreeSet: 정렬된 Set
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
// 내부적으로 TreeMap 사용 (Red-Black Tree)
private transient NavigableMap<E, Object> m;
private static final Object PRESENT = new Object();
public TreeSet() {
this(new TreeMap<>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
// 추가 - O(log n)
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
// 특별한 메서드들
public E first() {
return m.firstKey(); // 최소값
}
public E last() {
return m.lastKey(); // 최대값
}
public E lower(E e) {
return m.lowerKey(e); // e보다 작은 최대값
}
public E higher(E e) {
return m.higherKey(e); // e보다 큰 최소값
}
public SortedSet<E> subSet(E fromElement, E toElement) {
return m.subMap(fromElement, toElement).keySet();
}
}
TreeSet 활용 예제
public class TreeSetExamples {
// 1. 자동 정렬
public void autoSort() {
Set<Integer> numbers = new TreeSet<>();
numbers.add(5);
numbers.add(2);
numbers.add(8);
numbers.add(1);
System.out.println(numbers); // [1, 2, 5, 8] - 자동 정렬!
}
// 2. 커스텀 정렬
public void customSort() {
// 역순 정렬
Set<String> names = new TreeSet<>(Comparator.reverseOrder());
names.addAll(Arrays.asList("Charlie", "Alice", "Bob"));
System.out.println(names); // [Charlie, Bob, Alice]
// 길이순 정렬
Set<String> words = new TreeSet<>(Comparator.comparing(String::length));
words.add("apple");
words.add("pie");
words.add("banana");
System.out.println(words); // [pie, apple, banana]
}
// 3. 범위 검색
public void rangeSearch() {
TreeSet<Integer> scores = new TreeSet<>();
scores.addAll(Arrays.asList(85, 90, 75, 95, 88, 92));
// 90점 이상
SortedSet<Integer> above90 = scores.tailSet(90);
System.out.println(above90); // [90, 92, 95]
// 85점 이하
SortedSet<Integer> below85 = scores.headSet(85, true);
System.out.println(below85); // [75, 85]
// 85 ~ 92 사이
SortedSet<Integer> between = scores.subSet(85, true, 92, true);
System.out.println(between); // [85, 88, 90, 92]
}
// 4. 순위 기반 검색
public void rankBasedSearch() {
TreeSet<Integer> set = new TreeSet<>(Arrays.asList(10, 20, 30, 40, 50));
System.out.println(set.first()); // 10 (최소)
System.out.println(set.last()); // 50 (최대)
System.out.println(set.lower(30)); // 20 (30보다 작은 최대)
System.out.println(set.higher(30)); // 40 (30보다 큰 최소)
System.out.println(set.floor(35)); // 30 (35 이하 최대)
System.out.println(set.ceiling(35)); // 40 (35 이상 최소)
}
// 5. 실무 예제: 순위표
public class LeaderBoard {
private TreeSet<Player> rankings = new TreeSet<>(
Comparator.comparing(Player::getScore).reversed()
.thenComparing(Player::getName)
);
public void addPlayer(Player player) {
rankings.add(player);
}
public Player getTopPlayer() {
return rankings.first();
}
public Set<Player> getTop10() {
return rankings.stream()
.limit(10)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
public int getRank(Player player) {
return rankings.headSet(player, false).size() + 1;
}
}
}
LinkedHashSet: 삽입 순서 유지
public class LinkedHashSet<E> extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 내부적으로 LinkedHashMap 사용
public LinkedHashSet() {
super(16, 0.75f, true); // true = LinkedHashMap 사용
}
}
LinkedHashSet 활용
public class LinkedHashSetExamples {
// 1. 삽입 순서 유지
public void insertionOrder() {
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("C");
linkedSet.add("A");
linkedSet.add("B");
System.out.println(linkedSet); // [C, A, B] - 삽입 순서!
Set<String> hashSet = new HashSet<>();
hashSet.add("C");
hashSet.add("A");
hashSet.add("B");
System.out.println(hashSet); // [A, B, C] 또는 임의 순서
}
// 2. LRU 캐시
public class LRUSetCache<E> {
private final int capacity;
private final LinkedHashSet<E> cache;
public LRUSetCache(int capacity) {
this.capacity = capacity;
this.cache = new LinkedHashSet<>(capacity, 0.75f);
}
public void add(E element) {
// 이미 있으면 제거 후 재추가 (순서 변경)
if (cache.contains(element)) {
cache.remove(element);
} else if (cache.size() >= capacity) {
// 가장 오래된 것 제거
E oldest = cache.iterator().next();
cache.remove(oldest);
}
cache.add(element);
}
public boolean contains(E element) {
return cache.contains(element);
}
}
// 3. 중복 제거하면서 순서 유지
public List<String> removeDuplicatesKeepOrder(List<String> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
}
Set 비교 및 선택 가이드
/**
* Set 구현체 비교:
*
* ┌─────────────────┬──────────┬──────────┬────────────┬─────────┐
* │ 특성 │ HashSet │ TreeSet │LinkedHashSet│ 성능 │
* ├─────────────────┼──────────┼──────────┼────────────┼─────────┤
* │ 추가 │ O(1) │ O(log n) │ O(1) │ Hash 빠름│
* │ 삭제 │ O(1) │ O(log n) │ O(1) │ Hash 빠름│
* │ 검색 │ O(1) │ O(log n) │ O(1) │ Hash 빠름│
* │ 순서 보장 │ ❌ │ ✅ 정렬 │ ✅ 삽입순 │ - │
* │ null 허용 │ ✅(1개) │ ❌ │ ✅(1개) │ - │
* │ 메모리 │ 보통 │ 많음 │ 많음 │ Hash 적음│
* │ Thread-Safe │ ❌ │ ❌ │ ❌ │ - │
* └─────────────────┴──────────┴──────────┴────────────┴─────────┘
*/
public class SetSelectionGuide {
// HashSet: 일반적인 경우 (빠른 성능)
public Set<String> generalUse() {
return new HashSet<>();
}
// TreeSet: 정렬이 필요한 경우
public Set<Integer> sortedSet() {
return new TreeSet<>();
}
// TreeSet: 범위 검색이 필요한 경우
public NavigableSet<LocalDate> dateRange() {
return new TreeSet<>();
}
// LinkedHashSet: 삽입 순서가 중요한 경우
public Set<String> orderedSet() {
return new LinkedHashSet<>();
}
// EnumSet: Enum 타입 (가장 효율적!)
public Set<DayOfWeek> weekdays() {
return EnumSet.range(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
}
// ConcurrentSkipListSet: 동시성 + 정렬
public Set<String> concurrentSorted() {
return new ConcurrentSkipListSet<>();
}
// CopyOnWriteArraySet: 읽기가 많고 쓰기가 적은 경우
public Set<String> readHeavy() {
return new CopyOnWriteArraySet<>();
}
}
Set 연산
public class SetOperations {
public void basicOperations() {
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
Set<Integer> set2 = new HashSet<>(Arrays.asList(4, 5, 6, 7, 8));
// 합집합 (Union)
Set<Integer> union = new HashSet<>(set1);
union.addAll(set2);
System.out.println(union); // [1, 2, 3, 4, 5, 6, 7, 8]
// 교집합 (Intersection)
Set<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
System.out.println(intersection); // [4, 5]
// 차집합 (Difference)
Set<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2);
System.out.println(difference); // [1, 2, 3]
// 대칭 차집합 (Symmetric Difference)
Set<Integer> symDiff = new HashSet<>(set1);
symDiff.addAll(set2);
Set<Integer> tmp = new HashSet<>(set1);
tmp.retainAll(set2);
symDiff.removeAll(tmp);
System.out.println(symDiff); // [1, 2, 3, 6, 7, 8]
// 부분집합 확인
Set<Integer> subset = new HashSet<>(Arrays.asList(1, 2, 3));
boolean isSubset = set1.containsAll(subset);
System.out.println(isSubset); // true
}
// Stream을 사용한 Set 연산
public void streamOperations() {
Set<String> set1 = Set.of("A", "B", "C");
Set<String> set2 = Set.of("B", "C", "D");
// 합집합
Set<String> union = Stream.concat(set1.stream(), set2.stream())
.collect(Collectors.toSet());
// 교집합
Set<String> intersection = set1.stream()
.filter(set2::contains)
.collect(Collectors.toSet());
// 차집합
Set<String> difference = set1.stream()
.filter(e -> !set2.contains(e))
.collect(Collectors.toSet());
}
}
Map 마스터하기: HashMap 내부 구조
Hash 충돌과 체이닝
HashMap 내부 구조
public class HashMap<K, V> extends AbstractMap<K, V>
implements Map<K, V>, Cloneable, Serializable {
// 내부 배열 (버킷)
transient Node<K,V>[] table;
// 노드 클래스
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 충돌 시 다음 노드
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// 기본 용량과 로드 팩터
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 저장
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 해시 계산 (중요!)
static final int hash(Object key) {
int h;
// null은 0, 아니면 hashCode() ^ (hashCode() >>> 16)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 실제 저장 로직
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 테이블이 없으면 초기화
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 버킷 인덱스 계산: (n - 1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 빈 버킷
else {
// 3. 충돌 발생
Node<K,V> e; K k;
// 3-1. 첫 번째 노드와 키가 같으면
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3-2. TreeNode면 트리에 추가
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 3-3. 링크드 리스트에 추가
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 8개 이상이면 트리로 변환!
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 4. 기존 값이 있으면 교체
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
// 5. 크기 증가 및 리사이즈 확인
if (++size > threshold)
resize();
return null;
}
// 조회
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1. 버킷 찾기
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2. 첫 번째 노드 확인
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3. 나머지 노드 탐색
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 링크드 리스트 순회
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
}
메모리 구조 시각화
HashMap 내부 구조 (capacity=16, size=6):
table (배열):
┌───┬─────────────────────────────────┐
│ 0 │ null │
├───┼─────────────────────────────────┤
│ 1 │ → [key1, val1] → [key9, val9] │ (충돌! 체이닝)
├───┼─────────────────────────────────┤
│ 2 │ null │
├───┼─────────────────────────────────┤
│ 3 │ → [key3, val3] │
├───┼─────────────────────────────────┤
│ 4 │ → [key4, val4] → [key12, val12] │ (충돌! 체이닝)
│ │ → [key20, val20] │ (3개 충돌)
├───┼─────────────────────────────────┤
│...│ ... │
├───┼─────────────────────────────────┤
│15 │ null │
└───┴─────────────────────────────────┘
로드 팩터 = size / capacity = 6 / 16 = 0.375
threshold = capacity * loadFactor = 16 * 0.75 = 12
→ size가 12를 넘으면 resize (capacity를 32로 증가)Java 8의 Tree 변환 최적화
충돌이 많을 때: 링크드 리스트 → 레드-블랙 트리
public class HashMapTreeOptimization {
/**
* Java 8+ 최적화:
*
* 같은 버킷에 8개 이상 충돌 시:
* - 링크드 리스트 → 레드-블랙 트리 변환
* - 조회 성능: O(n) → O(log n)
*
* 6개 이하로 줄어들면:
* - 트리 → 링크드 리스트 복원
*/
static final int TREEIFY_THRESHOLD = 8; // 트리화 임계값
static final int UNTREEIFY_THRESHOLD = 6; // 트리 해제 임계값
public void demonstrateTreeConversion() {
// 같은 해시 코드를 가지는 키들
Map<BadKey, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.put(new BadKey(i), "Value " + i);
}
// 내부적으로 트리로 변환됨!
}
// 나쁜 키 (항상 같은 해시 코드)
static class BadKey {
private int value;
BadKey(int value) {
this.value = value;
}
@Override
public int hashCode() {
return 1; // 항상 같은 값!
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BadKey)) return false;
return value == ((BadKey) o).value;
}
}
}
성능 비교
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class HashMapPerformanceBenchmark {
@Param({"100", "1000", "10000"})
private int size;
private Map<Integer, String> hashMap;
private Map<Integer, String> treeMap;
private Map<Integer, String> linkedHashMap;
@Setup
public void setup() {
hashMap = new HashMap<>();
treeMap = new TreeMap<>();
linkedHashMap = new LinkedHashMap<>();
for (int i = 0; i < size; i++) {
String value = "Value " + i;
hashMap.put(i, value);
treeMap.put(i, value);
linkedHashMap.put(i, value);
}
}
@Benchmark
public String hashMapGet() {
return hashMap.get(size / 2);
}
@Benchmark
public String treeMapGet() {
return treeMap.get(size / 2);
}
@Benchmark
public String linkedHashMapGet() {
return linkedHashMap.get(size / 2);
}
@Benchmark
public void hashMapPut() {
hashMap.put(size + 1, "New Value");
hashMap.remove(size + 1);
}
@Benchmark
public void treeMapPut() {
treeMap.put(size + 1, "New Value");
treeMap.remove(size + 1);
}
@Benchmark
public void linkedHashMapPut() {
linkedHashMap.put(size + 1, "New Value");
linkedHashMap.remove(size + 1);
}
}
/**
* 벤치마크 결과 (예시):
*
* size = 10,000 기준
*
* Get:
* - HashMap: 8 ns/op ✅ 가장 빠름
* - LinkedHashMap: 10 ns/op ✅ 거의 같음
* - TreeMap: 45 ns/op ❌ 느림
*
* Put:
* - HashMap: 12 ns/op ✅ 가장 빠름
* - LinkedHashMap: 15 ns/op ✅ 거의 같음
* - TreeMap: 65 ns/op ❌ 느림
*/
HashMap vs ConcurrentHashMap
ConcurrentHashMap: 동시성 안전
public class ConcurrentHashMapExample {
// ❌ HashMap (Thread-unsafe)
public void unsafeHashMap() {
Map<String, Integer> map = new HashMap<>();
// 여러 스레드가 동시에 접근하면 문제 발생!
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int value = i;
executor.submit(() -> {
map.put("key" + value, value); // ConcurrentModificationException!
});
}
}
// ✅ ConcurrentHashMap (Thread-safe)
public void safeConcurrentHashMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int value = i;
executor.submit(() -> {
map.put("key" + value, value); // 안전!
});
}
}
// ConcurrentHashMap의 고급 기능
public void advancedFeatures() {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 1. putIfAbsent (원자적 연산)
map.putIfAbsent("key1", 100);
map.putIfAbsent("key1", 200); // 무시됨
// 2. compute (원자적 연산)
map.compute("key2", (k, v) -> v == null ? 1 : v + 1);
// 3. merge (원자적 연산)
map.merge("key3", 1, Integer::sum); // 없으면 1, 있으면 +1
// 4. forEach (병렬 처리 가능)
map.forEach(1, (k, v) -> {
System.out.println(k + ": " + v);
});
}
// 실무 예제: 카운터
public class ConcurrentCounter {
private final ConcurrentHashMap<String, AtomicInteger> counters =
new ConcurrentHashMap<>();
public void increment(String key) {
// ✅ 원자적 연산
counters.computeIfAbsent(key, k -> new AtomicInteger(0))
.incrementAndGet();
}
public int getCount(String key) {
AtomicInteger count = counters.get(key);
return count != null ? count.get() : 0;
}
}
}
실전 캐싱 전략
public class CachingStrategies {
// 1. 단순 캐시
public class SimpleCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public V get(K key, Supplier<V> supplier) {
return cache.computeIfAbsent(key, k -> supplier.get());
}
}
// 2. 시간 기반 캐시
public class TimedCache<K, V> {
private final Map<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
private final long ttlMillis;
static class CacheEntry<V> {
final V value;
final long expiryTime;
CacheEntry(V value, long ttl) {
this.value = value;
this.expiryTime = System.currentTimeMillis() + ttl;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}
public TimedCache(long ttlMillis) {
this.ttlMillis = ttlMillis;
}
public V get(K key, Supplier<V> supplier) {
CacheEntry<V> entry = cache.get(key);
if (entry == null || entry.isExpired()) {
V value = supplier.get();
cache.put(key, new CacheEntry<>(value, ttlMillis));
return value;
}
return entry.value;
}
public void cleanup() {
cache.entrySet().removeIf(e -> e.getValue().isExpired());
}
}
// 3. LRU 캐시 (LinkedHashMap 활용)
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LRUCache(int maxSize) {
super(16, 0.75f, true); // true = accessOrder
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
// 사용 예제
public void useLRUCache() {
Map<String, String> cache = new LRUCache<>(3);
cache.put("1", "A");
cache.put("2", "B");
cache.put("3", "C");
cache.get("1"); // 1을 최근 사용으로 이동
cache.put("4", "D"); // 2가 제거됨 (가장 오래 사용 안함)
System.out.println(cache); // {3=C, 1=A, 4=D}
}
// 4. Caffeine 라이브러리 (권장)
public void caffeineCache() {
// Gradle: implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
/*
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.recordStats()
.build(key -> loadFromDatabase(key));
String value = cache.get("key");
CacheStats stats = cache.stats();
*/
}
}
컬렉션 선택 가이드: 어떤 상황에 무엇을?
/**
* 컬렉션 선택 의사결정 트리:
*
* 1. 중복을 허용하는가?
* ├─ Yes → List
* │ ├─ 조회가 많은가? → ArrayList ✅
* │ ├─ 처음/끝 삽입/삭제가 많은가? → ArrayDeque
* │ └─ 중간 삽입/삭제가 많은가? → LinkedList
* │
* └─ No → Set
* ├─ 순서가 필요한가?
* │ ├─ Yes, 정렬 순서 → TreeSet
* │ ├─ Yes, 삽입 순서 → LinkedHashSet
* │ └─ No → HashSet ✅
* │
* └─ Enum인가? → EnumSet
*
* 2. Key-Value 쌍인가?
* ├─ Yes → Map
* │ ├─ 순서가 필요한가?
* │ │ ├─ Yes, 정렬 순서 → TreeMap
* │ │ ├─ Yes, 삽입 순서 → LinkedHashMap
* │ │ └─ No → HashMap ✅
* │ │
* │ ├─ 동시성이 필요한가? → ConcurrentHashMap
* │ └─ Enum 키인가? → EnumMap
* │
* └─ No → 1번으로
*
* 3. 동시성이 필요한가?
* ├─ Yes, 읽기가 많다 → CopyOnWriteArrayList/Set
* ├─ Yes, 쓰기도 많다 → ConcurrentHashMap
* └─ Yes, Queue → ConcurrentLinkedQueue
*/
public class CollectionSelectionGuide {
// 시나리오별 추천
// 1. 순서가 있는 데이터 (조회 많음)
public List<User> userList() {
return new ArrayList<>(); // ✅
}
// 2. 중복 제거
public Set<String> uniqueEmails() {
return new HashSet<>(); // ✅
}
// 3. 정렬이 필요한 고유 데이터
public Set<Integer> sortedScores() {
return new TreeSet<>(); // ✅
}
// 4. 키-값 매핑
public Map<String, User> usersByEmail() {
return new HashMap<>(); // ✅
}
// 5. 정렬된 키-값
public Map<LocalDate, List<Event>> eventsByDate() {
return new TreeMap<>(); // ✅
}
// 6. Queue (FIFO)
public Queue<Task> taskQueue() {
return new ArrayDeque<>(); // ✅ (LinkedList보다 빠름)
}
// 7. Stack (LIFO)
public Deque<String> navigationStack() {
return new ArrayDeque<>(); // ✅ (Stack 클래스는 deprecated)
}
// 8. 동시성 List
public List<String> concurrentList() {
return new CopyOnWriteArrayList<>(); // ✅
}
// 9. 동시성 Map
public Map<String, Object> concurrentMap() {
return new ConcurrentHashMap<>(); // ✅
}
// 10. Enum 기반
public Set<DayOfWeek> workdays() {
return EnumSet.of(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY); // ✅
}
public Map<Month, String> monthNames() {
Map<Month, String> map = new EnumMap<>(Month.class); // ✅
map.put(Month.JANUARY, "1월");
return map;
}
}
성능 비교표
/**
* 시간 복잡도 비교:
*
* ┌────────────┬─────────┬─────────┬─────────┬─────────┐
* │ 자료구조 │ 추가 │ 삭제 │ 조회 │ 검색 │
* ├────────────┼─────────┼─────────┼─────────┼─────────┤
* │ ArrayList │ O(1)* │ O(n) │ O(1) │ O(n) │
* │ LinkedList │ O(1)** │ O(1)** │ O(n) │ O(n) │
* │ ArrayDeque │ O(1)* │ O(1)* │ - │ - │
* ├────────────┼─────────┼─────────┼─────────┼─────────┤
* │ HashSet │ O(1) │ O(1) │ - │ O(1) │
* │ TreeSet │ O(log n)│ O(log n)│ - │ O(log n)│
* │LinkedHashSet│ O(1) │ O(1) │ - │ O(1) │
* ├────────────┼─────────┼─────────┼─────────┼─────────┤
* │ HashMap │ O(1) │ O(1) │ O(1) │ O(1) │
* │ TreeMap │ O(log n)│ O(log n)│ O(log n)│ O(log n)│
* │LinkedHashMap│ O(1) │ O(1) │ O(1) │ O(1) │
* └────────────┴─────────┴─────────┴─────────┴─────────┘
*
* * 끝에 추가/삭제, amortized
* ** 위치를 알 때
*/
3.2 제네릭
제네릭 기초부터 고급까지
제네릭이 필요한 이유
제네릭 이전 (Java 5 이전)
public class BeforeGenerics {
public void oldWay() {
// ❌ 타입 안전성 없음
List list = new ArrayList();
list.add("Hello");
list.add(123);
list.add(new Date());
// 런타임 에러 발생!
String str = (String) list.get(1); // ClassCastException!
}
}
제네릭 사용 (Java 5+)
public class WithGenerics {
public void newWay() {
// ✅ 컴파일 타임 타입 체크
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 컴파일 에러!
// 캐스팅 불필요
String str = list.get(0); // 안전!
}
}
타입 파라미터 <T, E, K, V>
기본 제네릭 클래스
// 단일 타입 파라미터
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
// 사용
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer num = intBox.get();
다중 타입 파라미터
// Key-Value 쌍
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// 사용
Pair<String, Integer> pair = new Pair<>("Age", 30);
String key = pair.getKey();
Integer value = pair.getValue();
제네릭 메서드
public class GenericMethods {
// 제네릭 메서드 정의
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
// 여러 타입 파라미터
public static <K, V> Map<K, V> createMap(K[] keys, V[] values) {
Map<K, V> map = new HashMap<>();
for (int i = 0; i < keys.length; i++) {
map.put(keys[i], values[i]);
}
return map;
}
// 제한된 타입 파라미터
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// 사용
public void use() {
String[] strings = {"A", "B", "C"};
printArray(strings); // 타입 추론
Integer maxNum = max(10, 20); // 20
String maxStr = max("apple", "banana"); // "banana"
}
}
와일드카드 (? extends, ? super)
상한 경계 와일드카드 (? extends T)
public class UpperBoundWildcard {
// Number 또는 그 하위 타입 (Integer, Double 등)
public static double sum(List<? extends Number> numbers) {
double sum = 0.0;
for (Number num : numbers) {
sum += num.doubleValue();
}
return sum;
}
public void use() {
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
double sum1 = sum(integers); // OK
double sum2 = sum(doubles); // OK
// ❌ 추가는 불가능! (어떤 타입인지 모르므로)
List<? extends Number> list = integers;
// list.add(10); // 컴파일 에러!
}
// 실무 예제: 복사
public static <T> void copy(List<? extends T> source, List<T> dest) {
for (T item : source) {
dest.add(item);
}
}
}
하한 경계 와일드카드 (? super T)
public class LowerBoundWildcard {
// Integer 또는 그 상위 타입 (Number, Object 등)
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
// ✅ 추가 가능!
// ❌ 조회는 Object로만 가능
Object obj = list.get(0);
// Integer num = list.get(0); // 컴파일 에러!
}
public void use() {
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(integers); // OK
addIntegers(numbers); // OK
addIntegers(objects); // OK
}
// 실무 예제: Collections.sort()
public static <T extends Comparable<? super T>> void sort(List<T> list) {
// T가 Comparable<T> 또는 상위 타입의 Comparable 구현
}
}
PECS 원칙 (Producer Extends, Consumer Super)
public class PECSPrinciple {
/**
* PECS: Producer Extends, Consumer Super
*
* - Producer (읽기만): ? extends T
* - Consumer (쓰기만): ? super T
*/
// Producer: 데이터를 제공 (읽기)
public static double sum(List<? extends Number> numbers) {
double sum = 0.0;
for (Number num : numbers) { // 읽기만 함
sum += num.doubleValue();
}
return sum;
}
// Consumer: 데이터를 소비 (쓰기)
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 쓰기만 함
list.add(2);
list.add(3);
}
// 둘 다 필요하면: 와일드카드 사용 안함
public static <T> void processList(List<T> list) {
T item = list.get(0); // 읽기
list.add(item); // 쓰기
}
// 실무 예제: Collections.copy()
public static <T> void copy(
List<? extends T> src, // Producer (읽기)
List<? super T> dest // Consumer (쓰기)
) {
for (T item : src) {
dest.add(item);
}
}
}
타입 소거(Type Erasure)
컴파일 타임 vs 런타임
public class TypeErasure {
// 컴파일 전
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
// 컴파일 후 (타입 소거)
public class Box {
private Object item;
public void set(Object item) {
this.item = item;
}
public Object get() {
return item;
}
}
// 사용 코드 변환
// 컴파일 전:
Box<String> box = new Box<>();
box.set("Hello");
String value = box.get();
// 컴파일 후:
Box box = new Box();
box.set("Hello");
String value = (String) box.get(); // 자동 캐스팅 삽입
}
타입 소거의 제약
public class TypeErasureConstraints {
// ❌ 제네릭 타입의 인스턴스 생성 불가
public <T> T create() {
// return new T(); // 컴파일 에러!
return null;
}
// ✅ 해결: Class<T> 전달
public <T> T create(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
// ❌ 제네릭 타입의 배열 생성 불가
public <T> T[] createArray(int size) {
// return new T[size]; // 컴파일 에러!
return null;
}
// ✅ 해결: Class<T> 전달 + Array.newInstance()
@SuppressWarnings("unchecked")
public <T> T[] createArray(Class<T> clazz, int size) {
return (T[]) Array.newInstance(clazz, size);
}
// ❌ static 필드에 타입 파라미터 사용 불가
public class Container<T> {
// private static T staticItem; // 컴파일 에러!
}
// ❌ instanceof에 제네릭 타입 사용 불가
public <T> boolean check(Object obj) {
// return obj instanceof T; // 컴파일 에러!
return false;
}
// ✅ 해결: Class<T> 전달
public <T> boolean check(Object obj, Class<T> clazz) {
return clazz.isInstance(obj);
}
}
실무 패턴: Super Type Token
public class SuperTypeToken {
// Type Token 패턴
public abstract class TypeReference<T> {
private final Type type;
protected TypeReference() {
Type superclass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// 사용
public void use() {
TypeReference<List<String>> listType = new TypeReference<List<String>>() {};
Type type = listType.getType();
System.out.println(type); // java.util.List<java.lang.String>
}
// 실무 예제: JSON 역직렬화
public class JsonParser {
public <T> T parse(String json, TypeReference<T> typeRef) {
// Type 정보를 사용하여 역직렬화
// Jackson, Gson 등이 이 패턴 사용
return null;
}
}
// 사용
public void parseJson() {
String json = "[{\"name\":\"Alice\"}, {\"name\":\"Bob\"}]";
// List<User> 타입 정보 전달
List<User> users = new JsonParser().parse(json,
new TypeReference<List<User>>() {}
);
}
}
제네릭 베스트 프랙티스
public class GenericsBestPractices {
// 1. 원시 타입 사용 금지
// ❌
List list = new ArrayList();
// ✅
List<String> list = new ArrayList<>();
// 2. 다이아몬드 연산자 사용 (Java 7+)
// ❌
Map<String, List<String>> map = new HashMap<String, List<String>>();
// ✅
Map<String, List<String>> map = new HashMap<>();
// 3. 제네릭 메서드 활용
// ❌
public void process(List list) { }
// ✅
public <T> void process(List<T> list) { }
// 4. 경계 지정
// ❌
public <T> T max(T a, T b) {
// compareTo() 사용 불가
return a;
}
// ✅
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// 5. 와일드카드 사용
// ❌ 너무 구체적
public void print(List<Object> list) { }
// ✅ 유연함
public void print(List<?> list) { }
// 6. PECS 원칙 준수
// Producer Extends
public <T> void addAll(List<T> dest, List<? extends T> src) {
dest.addAll(src);
}
// Consumer Super
public <T> void copyAll(List<? super T> dest, List<T> src) {
dest.addAll(src);
}
}
마치며
이번 글에서는 Java 컬렉션 프레임워크와 제네릭을 깊이 있게 다뤄보았습니다.
핵심 요약:
List
- ArrayList: 대부분의 경우 (조회 중심)
- LinkedList: Queue/Deque로 사용
- ArrayDeque: Stack/Queue 최적 (LinkedList보다 빠름)
Set
- HashSet: 일반적인 고유 집합
- TreeSet: 정렬이 필요할 때
- LinkedHashSet: 순서가 중요할 때
Map
- HashMap: 일반적인 키-값 매핑
- TreeMap: 정렬된 키 필요
- LinkedHashMap: 순서 유지
- ConcurrentHashMap: 동시성 필요
제네릭
- 타입 안전성과 코드 재사용성
- 와일드카드로 유연성 확보
- PECS 원칙 준수
- 타입 소거의 제약 이해
실무 팁:
- 대부분의 경우: ArrayList, HashSet, HashMap
- 성능이 중요하면: 벤치마크로 확인
- 동시성이 필요하면: Concurrent 컬렉션
- 명확한 의도 전달: 적절한 인터페이스 사용
다음 단계에서는 Stream API와 함수형 프로그래밍을 다룰 예정입니다.
반응형
'Java' 카테고리의 다른 글
| 5단계: Java 동시성 & 멀티스레딩 (0) | 2025.12.11 |
|---|---|
| 4단계: Java 함수형 프로그래밍 (Java 8+) (0) | 2025.12.11 |
| 2단계: Java 객체지향 프로그래밍 (0) | 2025.12.11 |
| 1단계: Java 기초 다지기 (0) | 2025.12.11 |
| [Java] Java프로그램이 실행되기까지: JDK, JRE, JVM (2) | 2025.04.07 |