Java Map Interface 반복시키는 가장 효율적인 방법
Map Interface
Map 인터페이스는 Key와 Value를 하나의 쌍으로 저장하는 방식의 자료형이며, 리스트나 배열과 같이 순차적으로(sequential) 해당 요소 값을 구하는 것이 아니라 'key를 통해 value를 가져오는 방식'이 가장 큰 특징입니다.
구현체 중 가장 많이 사용되는 클래스는 HashMap<K, V>이며, 순서가 필요한 경우 입력된 순서대로 데이터가 출력되는 LinkedHashMap, 입력된 key의 sort 순서로 데이터가 정렬되는 TreeMap 등이 있습니다.
아래는 java에서 map을 반복시키는 몇 가지 방법들에 대해 어떤 방법이 더 효율적인지 궁금하여 비교하며 정리해 본 내용입니다.
Map 반복 방법
Map<String, String> map = new HashMap<String, String>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
//1. Iterator를 사용하는 방식
Iterator<String> keys = map.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
System.out.println(String.format("key: %s, value: %s", key, map.get(key)));
}
//2. EntrySet을 사용하는 방식
for (Map.Entry<String, String> elem : map.entrySet()) {
System.out.println(String.format("key: %s, value: %s", elem.getKey(), elem.getValue()));
}
//3. KeySet을 사용하는 방식
for (String key : map.keySet()) {
System.out.println(String.format("key: %s, value: %s", key, map.get(key)));
}
//4. forEach문에 Lambda를 사용한 방식
map.forEach((key, value) -> {
System.out.println(String.format("key: %s, value: %s", key, value));
});
java에서 map 인터페이스에 대한 반복은 다음과 같은 4가지 방식으로 처리할 수 있는데요.
(Iterator, EntrySet, KeySet, forEach Lambda)
***
그중에서 Iterator를 사용하는 첫 번째 방식의 경우, key를 Iterator로 반환하여 while문에서 hasNext() 메서드를 통해 남은 키가 있는지 확인하고 next() 메서드를 통해 key를 가져와서 map에서 해당 key를 통해 value를 가져오는 조금 번거로운 과정으로 동작하는데요.
해당 방식의 경우 다른 방식들에 비해 속도도 느리게 나왔으며, 위 이미지와 같이 IntelliJ에서도 for 문으로 사용하는 것이 추천되는 것을 확인할 수 있습니다.
반복 테스트
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
for (int i = 1; i <= 1000000; i++) {
map.put("key" + i, "value" + i);
}
long startTime = System.currentTimeMillis();
entrySetLoop(map);
long endTime = System.currentTimeMillis();
System.out.println("elapsed " + (endTime - startTime) + "(ms)");
}
public static void iteratorLoop(Map<String, String> map) {
Iterator<String> keys = map.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
String value = map.get(key);
}
}
public static void entrySetLoop(Map<String, String> map) {
for (Map.Entry<String, String> elem : map.entrySet()) {
String key = elem.getKey();
String value = elem.getValue();
}
}
public static void keySetLoop(Map<String, String> map) {
for (String key : map.keySet()) {
String value = map.get(key);
}
}
public static void forEachLambdaLoop(Map<String, String> map) {
map.forEach((key, value) -> {
map.get(key);
});
}
대략적인 속도 비교를 위해 1,000,000개 / 10,000,000개의 데이터(key - value 쌍)를 가진 map을 반복시켰을 때 아래와 같은 결과를 확인할 수 있었는데요.
Iterator | EntrySet | KeySet | forEach Lambda | |
1,000,000개 | 78ms | 52ms | 76ms | 71ms |
10,000,000개 | 550ms | 450ms | 528ms | 483ms |
(20회 반복에 대한 평균입니다.)
이 정도의 간단한 테스트로 정확한 성능 비교는 할 수 없겠지만, 위에서 잠시 봤던 것처럼 Iterator를 사용하는 방법의 속도가 가장 느렸으며, EntrySet을 사용한 방법의 속도가 가장 빠른 것을 확인할 수 있었습니다.
***
forEach Lambda 방법의 경우, 코드 라인이 줄어들고 가독성이 올라간다는 장점이 있는 반면, for문에 비해 속도가 느리다는 이야기를 들었었는데요. 테스트 결과로 봤을 때, 다른 방법에 비해 생각보다 느리지 않다는 것을 확인할 수 있었습니다.
해당 테스트 환경이 java 17을 사용하였는데, 아마 java 8에서 처음 사용된 stream과 lambda가 java 버전이 올라가며 성능적인 측면에서 개선이 있지 않았을까 하는 생각입니다.
하지만 forEach lambda 방법의 경우 외부 요소에 대한 값 처리가 어렵기 때문에 사용 환경에 따라 다를 수는 있겠지만 일반적으로 map을 반복시킬 때는 entrySet을 사용하는 것이 가장 효율적일 것 같습니다.
< 참고 자료 >
'Programming > Java' 카테고리의 다른 글
(java) transient volatile 키워드는 무엇인가 (1) | 2023.08.02 |
---|---|
java 임시 비밀번호 생성(SecureRandom 사용하는 이유) (0) | 2023.07.30 |
java stream BigDecimal add 합계 구하는 방법 (0) | 2023.07.19 |
java image resize library 이미지 리사이즈 라이브러리 비교해보기 (0) | 2023.07.15 |
Java 8, 11, 17 버전별 추가된 기능 (+ 무슨 버전을 써야할까?) (0) | 2023.07.05 |