# JCF 자료구조의 초기 용량을 지정하면 좋은 점
# JCF란?
- Java Collection Framework
- 다수의 데이터를 쉽고 효과적으로 처리하기 위한 표준화된 방법을 제공하는 클래스의 집합
- JCF 이전에는 사용 목적은 동일해도 각 Collection 마다 사용하는 메서드, 문법, 생성자가 달라 혼동하기 쉬웠음
![]()
- 이러한 문제를 해결하기 위해 공통의 인터페이스를 설계하였고, 이를 JCF(Java Collections Framework)라 함
# JCF 도입 전
// 배열 생성 - 크기 고정
String[] names = new String[3];
names[0] = "김철수";
names[1] = "이영희";
names[2] = "박민수";
// 요소 접근
String firstName = names[0];
// 크기 확인
int size = names.length;
// 새 요소 추가 불가 - 배열 재생성 필요
String[] newNames = new String[4];
System.arraycopy(names, 0, newNames, 0, 3);
newNames[3] = "최지훈";
// 요소 삭제 불가 - 직접 구현 필요
// ----------
import java.util.Vector;
// Vector 생성
Vector names = new Vector(); // 제네릭 없음
// 요소 추가 - addElement() 메서드
names.addElement("김철수");
names.addElement("이영희");
names.addElement("박민수");
// 요소 접근 - elementAt() 메서드
String firstName = (String) names.elementAt(0); // 타입 캐스팅 필요
// 크기 확인
int size = names.size();
// 요소 삭제 - removeElementAt() 메서드
names.removeElementAt(1);
# JCF 도입 후 (JDK 1.2 이후)
import java.util.List;
import java.util.ArrayList;
// List 생성 - 인터페이스 타입으로 선언
List<String> names = new ArrayList<>(); // 제네릭 사용
// 요소 추가 - add() 메서드 (통일된 인터페이스)
names.add("김철수");
names.add("이영희");
names.add("박민수");
// 요소 접근 - get() 메서드
String firstName = names.get(0); // 타입 캐스팅 불필요
// 크기 확인 - size() 메서드 (통일됨)
int size = names.size();
// 요소 삭제 - remove() 메서드 (통일된 인터페이스)
names.remove(1);
// 다른 Collection으로 쉽게 전환 가능
List<String> linkedNames = new LinkedList<>(names);
- 도입 후 List 인터페이스를 통해 통일된 메서드 사용 가능 및 제네릭으로 타입 안정성도 확보됨
# JCF에서 초기 용량을 지정하면 좋은 점
- JCF에서 가변 크기의 자료 구조를 사용하는 경우, 초기 용량을 설정하면 리사이징을 줄이고 메모리와 연산 비용을 절약할 수 있음
# 실험 코드
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.ArrayList;
import java.util.List;
public class Main {
private static final int MAX = 5_000_000;
public static void main(String[] args) {
testWithDefaultCapacity();
System.gc();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
testWithInitialCapacity();
}
private static void testWithDefaultCapacity() {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
long beforeUsed = getUsedHeap(memoryMXBean);
System.out.println("=== Default Capacity Test ===");
System.out.println("Before: " + beforeUsed + " MB");
long startTime = System.currentTimeMillis();
List<String> arr = new ArrayList<>(); // 기본 capacity = 10
for (int i = 0; i < MAX; i++) {
arr.add("a");
}
long endTime = System.currentTimeMillis();
long afterUsed = getUsedHeap(memoryMXBean);
System.out.println("After: " + afterUsed + " MB");
System.out.println("Memory used: " + (afterUsed - beforeUsed) + " MB");
System.out.println("Time: " + (endTime - startTime) + " ms");
System.out.println("Final capacity: ~6,153,400 (33 resizes)\n");
}
private static void testWithInitialCapacity() {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
long beforeUsed = getUsedHeap(memoryMXBean);
System.out.println("=== Initial Capacity Test ===");
System.out.println("Before: " + beforeUsed + " MB");
long startTime = System.currentTimeMillis();
List<String> arr = new ArrayList<>(MAX); // 초기 capacity = 5,000,000
for (int i = 0; i < MAX; i++) {
arr.add("a");
}
long endTime = System.currentTimeMillis();
long afterUsed = getUsedHeap(memoryMXBean);
System.out.println("After: " + afterUsed + " MB");
System.out.println("Memory used: " + (afterUsed - beforeUsed) + " MB");
System.out.println("Time: " + (endTime - startTime) + " ms");
System.out.println("Final capacity: 5,000,000 (0 resizes)");
}
private static long getUsedHeap(MemoryMXBean memoryMXBean) {
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
return heapUsage.getUsed() / 1024 / 1024;
}
}
# 결과
=== Default Capacity Test ===
Before: 3 MB
After: 81 MB
Memory used: 78 MB
Time: 29 ms
Final capacity: ~6,153,400 (33 resizes)
=== Initial Capacity Test ===
Before: 1 MB
After: 21 MB
Memory used: 20 MB
Time: 19 ms
Final capacity: 5,000,000 (0 resizes)
# 기본 용량 사용
- 기본 용량(10)으로 시작한 경우, 용량이 가득 차면 기존 크기의 1.5배로 증가함
// ArrayList.java
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
- 위와 같이 여러 리사이징이 발생해 최종적으로 약 80MB 사용
# 초기 용량 설정 사용
- 불필요한 리사이징 없이 처음 설정한 크기로 유지되며 약 20MB만 사용함
- 즉 JCF에서 가변 크기의 자료 구조를 사용하는 경우, 초기 용량을 설정하면 리사이징을 줄이고 메모리와 연산 비용 절약 가능
# 로드 팩터와 임계점이란?
- 로드 팩터(load factor)란 특정 크기의 자료 구조에 데이터가 얼마나 적재되었는지를 나타내는 비율
- 임계점(threshold)란 가변적인 크기를 가진 자료구조에서 얼마나 크기를 증가시켜야 하는지를 나타내는 수치
- 로드 팩터와 임계점을 사용하는 이유는 꽉 차기 전에 미리 확장하여 성능 저하를 방지하기 위함
# 예시
- JCF에서 HashMap의 경우에는 내부적으로 배열을 사용하며, 초기 사이즈는 16
- 이때, HashMap의 기준 로드 팩터는 0.75이므로 임계점은 12(capacity * load factor = threshold)이고
- 만약, HashMap 내부 배열의 사이즈가 12를 넘는 경우 내부 배열의 크기를 2배 늘리고, 재해싱을 수행