|
| 1 | +# JCF 자료구조의 초기 용량을 지정하면 좋은 점 |
| 2 | +## JCF란? |
| 3 | +* Java Collection Framework |
| 4 | +* 다수의 데이터를 쉽고 효과적으로 처리하기 위한 표준화된 방법을 제공하는 클래스의 집합 |
| 5 | +* JCF 이전에는 사용 목적은 동일해도 각 Collection 마다 사용하는 메서드, 문법, 생성자가 달라 혼동하기 쉬웠음 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +* 이러한 문제를 해결하기 위해 공통의 인터페이스를 설계하였고, 이를 JCF(Java Collections Framework)라 함 |
| 10 | +### JCF 도입 전 |
| 11 | +```java |
| 12 | +// 배열 생성 - 크기 고정 |
| 13 | +String[] names = new String[3]; |
| 14 | +names[0] = "김철수"; |
| 15 | +names[1] = "이영희"; |
| 16 | +names[2] = "박민수"; |
| 17 | + |
| 18 | +// 요소 접근 |
| 19 | +String firstName = names[0]; |
| 20 | + |
| 21 | +// 크기 확인 |
| 22 | +int size = names.length; |
| 23 | + |
| 24 | +// 새 요소 추가 불가 - 배열 재생성 필요 |
| 25 | +String[] newNames = new String[4]; |
| 26 | +System.arraycopy(names, 0, newNames, 0, 3); |
| 27 | +newNames[3] = "최지훈"; |
| 28 | + |
| 29 | +// 요소 삭제 불가 - 직접 구현 필요 |
| 30 | + |
| 31 | +// ---------- |
| 32 | + |
| 33 | +import java.util.Vector; |
| 34 | + |
| 35 | +// Vector 생성 |
| 36 | +Vector names = new Vector(); // 제네릭 없음 |
| 37 | + |
| 38 | +// 요소 추가 - addElement() 메서드 |
| 39 | +names.addElement("김철수"); |
| 40 | +names.addElement("이영희"); |
| 41 | +names.addElement("박민수"); |
| 42 | + |
| 43 | +// 요소 접근 - elementAt() 메서드 |
| 44 | +String firstName = (String) names.elementAt(0); // 타입 캐스팅 필요 |
| 45 | + |
| 46 | +// 크기 확인 |
| 47 | +int size = names.size(); |
| 48 | + |
| 49 | +// 요소 삭제 - removeElementAt() 메서드 |
| 50 | +names.removeElementAt(1); |
| 51 | +``` |
| 52 | +### JCF 도입 후 (JDK 1.2 이후) |
| 53 | +```java |
| 54 | +import java.util.List; |
| 55 | +import java.util.ArrayList; |
| 56 | + |
| 57 | +// List 생성 - 인터페이스 타입으로 선언 |
| 58 | +List<String> names = new ArrayList<>(); // 제네릭 사용 |
| 59 | + |
| 60 | +// 요소 추가 - add() 메서드 (통일된 인터페이스) |
| 61 | +names.add("김철수"); |
| 62 | +names.add("이영희"); |
| 63 | +names.add("박민수"); |
| 64 | + |
| 65 | +// 요소 접근 - get() 메서드 |
| 66 | +String firstName = names.get(0); // 타입 캐스팅 불필요 |
| 67 | + |
| 68 | +// 크기 확인 - size() 메서드 (통일됨) |
| 69 | +int size = names.size(); |
| 70 | + |
| 71 | +// 요소 삭제 - remove() 메서드 (통일된 인터페이스) |
| 72 | +names.remove(1); |
| 73 | + |
| 74 | +// 다른 Collection으로 쉽게 전환 가능 |
| 75 | +List<String> linkedNames = new LinkedList<>(names); |
| 76 | +``` |
| 77 | + |
| 78 | +* 도입 후 List 인터페이스를 통해 통일된 메서드 사용 가능 및 제네릭으로 타입 안정성도 확보됨 |
| 79 | + |
| 80 | +## JCF에서 초기 용량을 지정하면 좋은 점 |
| 81 | +* JCF에서 가변 크기의 자료 구조를 사용하는 경우, 초기 용량을 설정하면 리사이징을 줄이고 메모리와 연산 비용을 절약할 수 있음 |
| 82 | + |
| 83 | +### 실험 코드 |
| 84 | +```java |
| 85 | +import java.lang.management.ManagementFactory; |
| 86 | +import java.lang.management.MemoryMXBean; |
| 87 | +import java.lang.management.MemoryUsage; |
| 88 | +import java.util.ArrayList; |
| 89 | +import java.util.List; |
| 90 | + |
| 91 | +public class Main { |
| 92 | + private static final int MAX = 5_000_000; |
| 93 | + |
| 94 | + public static void main(String[] args) { |
| 95 | + testWithDefaultCapacity(); |
| 96 | + System.gc(); |
| 97 | + try { Thread.sleep(1000); } catch (InterruptedException e) {} |
| 98 | + |
| 99 | + testWithInitialCapacity(); |
| 100 | + } |
| 101 | + |
| 102 | + private static void testWithDefaultCapacity() { |
| 103 | + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); |
| 104 | + |
| 105 | + long beforeUsed = getUsedHeap(memoryMXBean); |
| 106 | + System.out.println("=== Default Capacity Test ==="); |
| 107 | + System.out.println("Before: " + beforeUsed + " MB"); |
| 108 | + |
| 109 | + long startTime = System.currentTimeMillis(); |
| 110 | + List<String> arr = new ArrayList<>(); // 기본 capacity = 10 |
| 111 | + for (int i = 0; i < MAX; i++) { |
| 112 | + arr.add("a"); |
| 113 | + } |
| 114 | + long endTime = System.currentTimeMillis(); |
| 115 | + |
| 116 | + long afterUsed = getUsedHeap(memoryMXBean); |
| 117 | + System.out.println("After: " + afterUsed + " MB"); |
| 118 | + System.out.println("Memory used: " + (afterUsed - beforeUsed) + " MB"); |
| 119 | + System.out.println("Time: " + (endTime - startTime) + " ms"); |
| 120 | + System.out.println("Final capacity: ~6,153,400 (33 resizes)\n"); |
| 121 | + } |
| 122 | + |
| 123 | + private static void testWithInitialCapacity() { |
| 124 | + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); |
| 125 | + |
| 126 | + long beforeUsed = getUsedHeap(memoryMXBean); |
| 127 | + System.out.println("=== Initial Capacity Test ==="); |
| 128 | + System.out.println("Before: " + beforeUsed + " MB"); |
| 129 | + |
| 130 | + long startTime = System.currentTimeMillis(); |
| 131 | + List<String> arr = new ArrayList<>(MAX); // 초기 capacity = 5,000,000 |
| 132 | + for (int i = 0; i < MAX; i++) { |
| 133 | + arr.add("a"); |
| 134 | + } |
| 135 | + long endTime = System.currentTimeMillis(); |
| 136 | + |
| 137 | + long afterUsed = getUsedHeap(memoryMXBean); |
| 138 | + System.out.println("After: " + afterUsed + " MB"); |
| 139 | + System.out.println("Memory used: " + (afterUsed - beforeUsed) + " MB"); |
| 140 | + System.out.println("Time: " + (endTime - startTime) + " ms"); |
| 141 | + System.out.println("Final capacity: 5,000,000 (0 resizes)"); |
| 142 | + } |
| 143 | + |
| 144 | + private static long getUsedHeap(MemoryMXBean memoryMXBean) { |
| 145 | + MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); |
| 146 | + return heapUsage.getUsed() / 1024 / 1024; |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +### 결과 |
| 152 | +```bash |
| 153 | +=== Default Capacity Test === |
| 154 | +Before: 3 MB |
| 155 | +After: 81 MB |
| 156 | +Memory used: 78 MB |
| 157 | +Time: 29 ms |
| 158 | +Final capacity: ~6,153,400 (33 resizes) |
| 159 | + |
| 160 | +=== Initial Capacity Test === |
| 161 | +Before: 1 MB |
| 162 | +After: 21 MB |
| 163 | +Memory used: 20 MB |
| 164 | +Time: 19 ms |
| 165 | +Final capacity: 5,000,000 (0 resizes) |
| 166 | +``` |
| 167 | + |
| 168 | +### 기본 용량 사용 |
| 169 | +* 기본 용량(10)으로 시작한 경우, 용량이 가득 차면 기존 크기의 1.5배로 증가함 |
| 170 | +```java |
| 171 | +// ArrayList.java |
| 172 | + |
| 173 | +private Object[] grow(int minCapacity) { |
| 174 | + int oldCapacity = elementData.length; |
| 175 | + if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { |
| 176 | + int newCapacity = ArraysSupport.newLength(oldCapacity, |
| 177 | + minCapacity - oldCapacity, /* minimum growth */ |
| 178 | + oldCapacity >> 1 /* preferred growth */); |
| 179 | + return elementData = Arrays.copyOf(elementData, newCapacity); |
| 180 | + } else { |
| 181 | + return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | +* 위와 같이 여러 리사이징이 발생해 최종적으로 약 80MB 사용 |
| 186 | + |
| 187 | +### 초기 용량 설정 사용 |
| 188 | +* 불필요한 리사이징 없이 처음 설정한 크기로 유지되며 약 20MB만 사용함 |
| 189 | +* 즉 JCF에서 가변 크기의 자료 구조를 사용하는 경우, 초기 용량을 설정하면 리사이징을 줄이고 메모리와 연산 비용 절약 가능 |
| 190 | + |
| 191 | +## 로드 팩터와 임계점이란? |
| 192 | +* 로드 팩터(load factor)란 특정 크기의 자료 구조에 데이터가 얼마나 적재되었는지를 나타내는 비율 |
| 193 | +* 임계점(threshold)란 가변적인 크기를 가진 자료구조에서 얼마나 크기를 증가시켜야 하는지를 나타내는 수치 |
| 194 | +* 로드 팩터와 임계점을 사용하는 이유는 꽉 차기 전에 미리 확장하여 성능 저하를 방지하기 위함 |
| 195 | + |
| 196 | +### 예시 |
| 197 | +* JCF에서 HashMap의 경우에는 내부적으로 배열을 사용하며, 초기 사이즈는 16 |
| 198 | +* 이때, HashMap의 기준 로드 팩터는 0.75이므로 임계점은 12(capacity * load factor = threshold)이고 |
| 199 | +* 만약, HashMap 내부 배열의 사이즈가 12를 넘는 경우 내부 배열의 크기를 2배 늘리고, 재해싱을 수행 |
0 commit comments