리스트는 배열처럼 인덱스를 통해 접근할 수 있으며, 중복된 요소를 허용합니다. 크기가 동적으로 조정된다는 점에서 배열보다 유연한 특징을 가지고 있습니다.
List 인터페이스의 구현 클래스
ArrayList: 배열 기반으로 동작하는 List 구현체입니다. 요소에 빠르게 접근할 수 있으나, 요소를 삽입하거나 삭제하는 경우 배열을 재조정해야 하므로 성능에 영향을 미칠 수 있습니다. 많은 양의 읽기 작업이 있을 때 효율적입니다.
LinkedList: 이중 연결 리스트로 구현된 List입니다. 요소를 삽입하거나 삭제할 때 빠른 성능을 보장하지만, 인덱스를 기반으로 접근할 때는 ArrayList에 비해 성능이 떨어질 수 있습니다. 양쪽 끝에서 삽입/삭제 작업이 자주 발생하는 경우에 유리합니다.
Vector: Vector는 ArrayList와 비슷하지만, 모든 메서드가 동기화되어 있어 다중 스레드 환경에서 안전합니다. 그러나 이로 인해 일반적인 상황에서는 성능이 떨어질 수 있습니다. 과거에 많이 사용되었으나, 현재는 동기화가 필요한 경우 ArrayList를 구현으로 List를 감싸는 래퍼인 Collections.synchronizedList()을 사용합니다.
Stack: Vector를 상속받은 클래스로, LIFO(Last In First Out) 방식으로 동작하는 스택 구조를 구현합니다. push(), pop(), peek() 등의 메서드를 제공하여 스택 자료 구조를 쉽게 구현할 수 있습니다. Stack은 Vector를 상속받았기 때문에 기본적으로 동기화된 메서드를 사용하며, 이로 인해 성능이 저하될 수 있습니다. 또한, 구조적으로 다른 자료구조 클래스들과 일관성이 부족하고 설계가 다소 구식입니다. Stack 대신 Deque(double-ended queue)를 사용하는 것이 더 권장됩니다.
List의 기본 메서드
add(E e): 리스트의 끝에 요소를 추가.
add(int index, E e): 리스트의 특정 위치에 요소를 추가.
get(int index): 리스트의 특정 인덱스에 있는 요소를 반환.
remove(int index): 리스트에서 특정 인덱스의 요소를 제거.
set(int index, E e): 리스트의 특정 위치의 요소를 새로운 값으로 갱신.
size(): 리스트에 있는 요소의 개수를 반환.
contains(Object o): 리스트에 특정 요소가 있는지 확인.
clear(): 리스트의 모든 요소를 제거.
isEmpty(): 리스트가 비어 있는지 확인.
List의 조건부 삽입 및 갱신 메서드
replaceAll(UnaryOperator<E> operator): 리스트의 모든 요소를 주어진 조건에 맞게 갱신.
removeIf(Predicate<? super E> filter): 주어진 조건에 맞는 요소를 리스트에서 제거.
Map
맵은 키와 값 쌍으로 데이터를 저장하며, 각 키는 중복되지 않지만 값은 중복될 수 있습니다. 맵은 특정 키로 빠르게 값을 찾을 수 있는 자료구조입니다.
Map 인터페이스의 구현 클래스
HashMap: 키와 값의 쌍을 해시 테이블 기반으로 저장하며, 삽입 순서를 보장하지 않습니다. 가장 많이 사용되는 Map 구현 클래스입니다.
TreeMap: 키에 대해 정렬된 순서로 키-값 쌍을 저장합니다. 내부적으로 레드-블랙 트리를 사용하여 키를 정렬합니다.
LinkedHashMap: 삽입 순서(또는 액세스 순서)에 따라 키-값 쌍을 저장합니다. HashMap의 기능을 기반으로 하면서도 순서를 유지합니다.
Map의 기본 메서드
put(K key, V value): 키-값 쌍을 삽입 또는 업데이트.
get(Object key): 특정 키에 해당하는 값 반환.
remove(Object key): 특정 키에 해당하는 항목 삭제.
containsKey(Object key): 특정 키가 맵에 존재하는지 확인.
containsValue(Object value): 특정 값이 맵에 존재하는지 확인.
size(): 맵에 저장된 항목의 개수 반환.
isEmpty(): 맵이 비어있는지 여부 확인.
clear(): 맵에 저장된 모든 항목 삭제.
keySet(): 맵의 모든 키를 반환.
values(): 맵의 모든 값을 반환.
entrySet(): 맵의 모든 키-값 쌍을 반환.
Map의 조건부 삽입 및 갱신 메서드
putIfAbsent(K key, V value): 이미 해당 키가 존재하면, 기존 값을 유지하고 아무 것도 하지 않음.
getOrDefault(Object key, V defaultValue): 키가 존재하지 않으면 기본값을 반환.
remove(Object key, Object value): 키와 값이 모두 일치할 때만 안전하게 삭제.
replace(K key, V oldValue, V newValue): 키와 기존 값이 일치하는 경우에만 값을 새로운 값으로 교체.
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction): 키가 없을 때 동적으로 값을 생성하여 삽입.
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction): 키가 있을 때만 값을 계산하여 갱신.
LinkedHashMap
LinkedHashMap은 요소들이 삽입된 순서를 유지하며, 필요에 따라 액세스된 순서를 유지할 수도 있습니다. 이는 순차적으로 데이터를 처리하거나, 순서가 중요한 데이터의 경우 유용합니다.
// 요소 추가 linkedHashMap.put("banana", 2); linkedHashMap.put("apple", 3); linkedHashMap.put("orange", 1);
// LinkedHashMap 출력 (삽입된 순서 유지) System.out.println("LinkedHashMap 출력 (삽입 순서):"); for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
// 특정 키에 접근 (액세스 순서로 변경) linkedHashMap.get("banana");
// 액세스 순서로 정렬되도록 새로운 LinkedHashMap 생성 LinkedHashMap<String, Integer> accessOrderMap = newLinkedHashMap<>(16, 0.75f, true); accessOrderMap.putAll(linkedHashMap);
// 액세스 순서로 재정렬된 LinkedHashMap 출력 System.out.println("\nLinkedHashMap 출력 (액세스 순서):"); for (Map.Entry<String, Integer> entry : accessOrderMap.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } } }
1 2 3 4 5 6 7 8 9
LinkedHashMap 출력 (삽입 순서): banana: 2 apple: 3 orange: 1
LinkedHashMap 출력 (액세스 순서): apple: 3 orange: 1 banana: 2
new LinkedHashMap<>(16, 0.75f, true);
initialCapacity (16): 맵의 초기 용량을 지정합니다. inkedHashMap이 처음 생성될 때 내부적으로 몇 개의 버킷을 할당할지를 결정합니다.
loadFactor (0.75f): 해시 테이블의 “적재율”을 나타내는 값입니다. 0.75f는 기본 값으로, 해시 테이블이 75% 차게 되면 버킷의 수가 두 배로 증가하게 됩니다. 이는 해시 충돌을 줄이기 위해 사용됩니다.
accessOrder (true): 이 매개변수는 LinkedHashMap이 요소를 유지하는 순서를 결정합니다. false로 설정하면 삽입 순서에 따라 요소를 유지합니다. 즉, 요소들이 삽입된 순서대로 유지됩니다. true로 설정하면 접근 순서에 따라 요소를 유지합니다. 즉, 요소에 접근(읽기 또는 쓰기)할 때마다 해당 요소가 맵의 끝으로 이동하여 가장 최근에 접근한 요소가 마지막에 위치하게 됩니다. accessOrder가 true로 설정되면, LinkedHashMap은 LRU (Least Recently Used) 캐시 처럼 동작할 수 있습니다. 이는 최근에 접근된 요소를 맵의 끝으로 이동시키며, 가장 오래된 요소가 맨 앞에 위치하게 됩니다.
Set
Java에서 Set은 중복을 허용하지 않는 데이터 컬렉션을 관리하는 자료구조입니다. Set은 수학에서의 집합과 유사한 개념으로, 각 요소가 고유해야 하며 순서는 중요하지 않습니다.
Java의 Set 인터페이스는 다음과 같은 주요 구현 클래스로 제공됩니다:
a. HashSet
특징: HashSet은 가장 일반적으로 사용되는 Set 구현체로, 요소들을 해시 테이블을 사용하여 저장합니다. 저장된 순서가 유지되지 않으며, 요소의 삽입, 삭제, 검색이 빠릅니다.
용도: 빠른 조회가 필요하고, 순서가 중요하지 않으며 중복을 허용하지 않아야 할 때 사용됩니다.
b. LinkedHashSet
특징: LinkedHashSet은 HashSet과 비슷하지만, 요소의 삽입 순서를 유지합니다. 내부적으로 이중 연결 리스트(doubly-linked list)를 사용하여 요소를 저장하며, 삽입된 순서대로 요소를 순회할 수 있습니다.
용도: 요소가 삽입된 순서대로 순회해야 할 때 사용됩니다.
c. TreeSet
특징: TreeSet은 요소들을 정렬된 순서로 저장합니다. 내부적으로 레드-블랙 트리(Red-Black Tree)를 사용하여 요소를 관리하며, 요소들이 자연스러운 순서(또는 지정된 Comparator에 따른 순서)로 정렬됩니다.
// 요소 추가 (스택의 push와 동일) stack.add("apple"); stack.add("banana"); stack.add("cherry");
// 스택의 맨 위 요소 확인 (peek) System.out.println("Top element: " + stack.peek());
// 스택에서 요소 제거 (pop) System.out.println("Popped element: " + stack.pop());
// 스택 출력 System.out.println("Stack after pop: " + stack);
// 요소 추가 (push 동작을 명확히 하기 위해) stack.push("date"); System.out.println("Stack after push: " + stack); } }
출력 결과
1 2 3 4
Top element: cherry Popped element: cherry Stack after pop: [apple, banana] Stack after push: [date, apple, banana]
3. 요약 및 비교
ArrayDeque:
빠르고 효율적인 스택 구현이 가능합니다.
ArrayDeque는 동기화되지 않은 환경에서 성능이 뛰어나고, ArrayList보다 스택 연산에 더 적합합니다.
스택 연산은 add, push, pop, peek 메서드로 구현할 수 있습니다.
LinkedList:
요소 추가 및 제거가 빈번히 발생하는 경우에 적합합니다.
LinkedList는 메모리 오버헤드가 있지만, 요소의 삽입과 삭제가 빠릅니다.
스택 연산은 add, push, pop, peek 메서드로 구현할 수 있습니다.
두 컬렉션 모두 스택 기능을 쉽게 구현할 수 있으며, 상황에 맞게 선택할 수 있습니다. 일반적으로, 스택으로 사용하기 위해서는 ArrayDeque를 사용하는 것이 성능 면에서 더 유리합니다.
4. 메모리 오버헤드
메모리 오버헤드는 데이터를 저장하는 데 필요한 추가적인 메모리 소비를 의미합니다. LinkedList에서 메모리 오버헤드가 발생하는 이유는 LinkedList의 내부 구조가 **이중 연결 리스트(Doubly Linked List)**로 구현되어 있기 때문입니다.
LinkedList의 구조:
노드(Node): LinkedList는 각 요소를 노드로 저장합니다. 각 노드는 다음과 같은 세 가지 필드를 가집니다:
데이터 필드: 실제 데이터(요소)가 저장되는 부분입니다.
다음 노드에 대한 참조(Next): 리스트에서 다음 요소를 가리키는 참조(포인터)입니다.
이전 노드에 대한 참조(Previous): 리스트에서 이전 요소를 가리키는 참조(포인터)입니다.
이렇게 각 노드가 데이터 외에도 두 개의 추가적인 참조(이전 노드와 다음 노드에 대한 참조)를 가지고 있기 때문에, LinkedList는 단순히 데이터만 저장하는 배열(Array)이나 ArrayDeque와 비교했을 때 더 많은 메모리를 사용하게 됩니다. 이 추가적인 참조가 바로 메모리 오버헤드의 원인입니다.
5. add()와 push()의 차이점
add():
add(E e) 메서드는 List 인터페이스와 Deque 인터페이스에 공통으로 정의되어 있으며, 요소를 리스트의 끝에 추가하는 역할을 합니다.
Deque 인터페이스에서 addFirst(E e)와 addLast(E e) 메서드를 사용하여 요소를 앞이나 뒤에 추가할 수 있습니다. 기본적으로 add(E e)는 addLast(E e)와 동일하게 작동합니다.
push():
push(E e) 메서드는 주로 스택의 개념에서 사용됩니다. Deque 인터페이스에서 push(E e)는 요소를 스택의 맨 위(즉, 리스트의 첫 번째 위치)에 추가합니다.
publicclassMaxHeapExample { publicstaticvoidmain(String[] args) { // 최대힙을 구현하기 위해 PriorityQueue를 사용, Collections.reverseOrder()를 사용하여 역순 정렬 PriorityQueue<Integer> maxHeap = newPriorityQueue<>(Collections.reverseOrder());
// 힙에 값 추가 maxHeap.add(10); maxHeap.add(4); maxHeap.add(15); maxHeap.add(20); maxHeap.add(0);
// 힙에서 값을 하나씩 제거하며 출력 (내림차순) while (!maxHeap.isEmpty()) { System.out.println(maxHeap.poll()); // poll() 메서드는 힙에서 최대값을 제거하고 반환 } } }
1 2 3 4 5
20 15 10 4 0
메서드
PriorityQueue 메서드
add(E e): 우선순위 큐에 요소를 추가합니다. 만약 힙의 용량이 초과되면 IllegalStateException이 발생할 수 있습니다.
offer(E e): 우선순위 큐에 요소를 추가합니다. 힙의 용량이 초과되어도 add()와 달리 false를 반환합니다.
peek(): 힙의 루트 요소(최소 또는 최대값)를 반환합니다. 힙에서 제거하지는 않습니다.
poll(): 힙의 루트 요소를 반환하고 제거합니다. 큐가 비어 있으면 null을 반환합니다。
remove(Object o): 특정 요소를 제거합니다.
contains(Object o): 큐에 특정 요소가 포함되어 있는지 확인합니다。
clear(): 큐의 모든 요소를 제거합니다。
size(): 큐의 크기를 반환합니다。
isEmpty(): 큐가 비어 있는지 확인합니다。
각종 변환 방법
String -> Array
1 2 3 4 5 6 7 8
// String을 char 배열로 변환 char[] charArray = str.toCharArray();
// 쉼표를 기준으로 문자열을 배열로 변환 String[] strArray = str.split(",");
// String을 한 글자씩 잘라서 배열로 변환 String[] strArray = str.split("");
// 첫 번째 요소를 기준으로 2차원 배열을 정렬 Arrays.sort(array, newComparator<int[]>() { @Override publicintcompare(int[] a, int[] b) { return Integer.compare(a[0], b[0]); // 첫 번째 요소(a[0], b[0])를 기준으로 비교 } });
// 정렬된 배열 출력 for (int[] row : array) { System.out.println(Arrays.toString(row)); } } }