안드로이드 개발 시 자주 사용되는 데이터 구조로는 List, Set, Map이 있습니다. 이들은 객체들을 효율적으로 관리하고 접근하는 데 필수적입니다. List와 Set은 'Collection' 인터페이스를 상속받으며, List는 데이터 중복을 허용하는 반면 Set은 고유한 데이터만 저장합니다. Map은 키(key)와 값(value)의 쌍으로 데이터를 저장하는 구조이며, HashMap이 대표적인 예입니다.
컬렉션 계층 구조
Collection: 객체의 집합을 나타내는 최상위 인터페이스입니다.List: 순서가 있고 데이터 중복을 허용하는 컬렉션입니다.ArrayList: 내부적으로 배열을 사용하며, 임의 접근이 빠릅니다. 스레드에 안전하지 않습니다.LinkedList: 내부적으로 이중 연결 리스트를 사용하며, 삽입 및 삭제 연산이 효율적입니다. 스레드에 안전하지 않습니다.Vector:ArrayList와 유사하지만, 스레드에 안전합니다.
Set: 고유한 객체만 저장하며, 순서가 보장되지 않을 수 있습니다.HashSet: 해시 테이블을 사용하여 객체를 저장하므로 빠른 검색 속도를 제공합니다.LinkedHashSet:HashSet의 속도와 함께 삽입 순서를 유지합니다.TreeSet: 데이터를 정렬된 순서로 저장합니다.
Queue: 선입선출(FIFO) 순서를 유지하는 컬렉션입니다.PriorityQueue: 우선순위에 따라 요소를 처리합니다.
Map: 키-값 쌍으로 데이터를 저장하는 컬렉션입니다.HashMap: 해시 테이블을 사용하여 키-값 쌍을 저장합니다. 스레드에 안전하지 않습니다.LinkedHashMap:HashMap과 유사하지만, 삽입 순서를 유지합니다.TreeMap: 키를 기준으로 데이터를 정렬하여 저장합니다.HashTable:HashMap과 유사하지만, 스레드에 안전합니다.
주요 컬렉션 클래스 사용 예제
1. ArrayList
ArrayList는 내부적으로 배열을 사용하므로 특정 인덱스의 요소에 접근하는 속도가 빠릅니다. 다만, 요소를 추가하거나 삭제할 때 배열의 재배치가 필요할 수 있어 성능에 영향을 줄 수 있습니다.
초기화 및 요소 추가:
// 방법 1: 일반적인 방식
ArrayList<String> stringList = new ArrayList<>();
stringList.add("첫 번째 아이템");
stringList.add("두 번째 아이템");
// 방법 2: 초기화와 동시에 요소 추가 (익명 클래스 활용)
ArrayList<String> anotherList = new ArrayList<>() {{
add("다른 첫 아이템");
add("다른 두 번째 아이템");
}};
주의: 반복 중 요소 삭제
ArrayList를 순회하면서 요소를 삭제할 때는 주의가 필요합니다. 일반적인 for 반복문 내에서 ArrayList.remove(index)를 사용하면 인덱스가 변경되어 일부 요소가 건너뛰거나 ConcurrentModificationException이 발생할 수 있습니다.
// 잘못된 예: for 루프 내 remove(index) 사용 시
ArrayList<String> numbers = new ArrayList<>(Arrays.asList("1", "2", "3", "3", "4", "3"));
for (int i = 0; i < numbers.size(); i++) {
if (numbers.get(i).equals("3")) {
numbers.remove(i); // 인덱스 문제 발생 가능
}
}
System.out.println("잘못된 삭제 결과: " + numbers); // 예상과 다른 결과
// 올바른 예: Iterator 사용
ArrayList<String> numbersCorrect = new ArrayList<>(Arrays.asList("1", "2", "3", "3", "4", "3"));
Iterator<String> iterator = numbersCorrect.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("3")) {
iterator.remove(); // Iterator를 통한 안전한 삭제
}
}
System.out.println("올바른 삭제 결과: " + numbersCorrect);
권장: ArrayList에서 요소를 삭제할 때는 Iterator의 remove() 메서드를 사용하는 것이 안전하고 올바른 방법입니다.
2. LinkedList
LinkedList는 이중 연결 리스트 구조로 구현되어, 요소의 삽입 및 삭제 연산이 매우 효율적입니다. 각 노드는 데이터와 함께 이전 노드 및 다음 노드에 대한 참조를 가집니다.
// LinkedList 내부 노드 구조 (개념적 표현)
private static class Node<T> {
T data;
Node<T> previous;
Node<T> next;
Node(T data, Node<T> prev, Node<T> next) {
this.data = data;
this.previous = prev;
this.next = next;
}
}
LinkedList는 List 인터페이스 외에도 Queue 인터페이스를 구현하여 큐(Queue) 자료구조로도 활용될 수 있습니다.
3. Vector
Vector는 ArrayList와 유사한 기능을 제공하지만, 모든 메서드가 동기화(synchronized)되어 있어 스레드 환경에서 안전합니다. 하지만 이 동기화 메커니즘으로 인해 ArrayList보다 성능이 저하될 수 있습니다.
4. HashSet
Set은 중복된 요소를 허용하지 않는 컬렉션입니다. HashSet은 해시 코드를 기반으로 요소를 저장하므로 매우 빠른 검색, 추가, 삭제 성능을 제공합니다. List의 중복 요소를 제거하는 데 유용하게 사용됩니다.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
List<String> initialList = new ArrayList<>(Arrays.asList("apple", "banana", "orange", "apple", "grape"));
System.out.println("원본 리스트: " + initialList);
List<String> uniqueList = removeDuplicates(initialList);
System.out.println("중복 제거 후 리스트: " + uniqueList);
}
private static List<String> removeDuplicates(List<String> list) {
// HashSet을 사용하여 중복 제거
Set<String> uniqueSet = new HashSet<>(list);
// Set을 다시 List로 변환
return new ArrayList<>(uniqueSet);
}
}
5. TreeSet 및 LinkedHashSet
TreeSet: 요소를 자동으로 정렬하여 저장하는Set구현체입니다. 내부적으로 레드-블랙 트리 구조를 사용합니다.LinkedHashSet:HashSet의 빠른 검색 속도를 유지하면서, 요소가 추가된 순서를 기억합니다. 따라서Iterator로 순회 시 삽입 순서대로 요소를 얻을 수 있습니다.
6. Queue
Queue는 선입선출(FIFO) 원칙을 따르는 데이터 구조입니다. 주요 연산은 다음과 같습니다.
| 구분 | 메서드 |
|---|---|
| 요소 추가 (Offer) | offer(E e), add(E e) |
| 요소 제거 (Poll) | poll(), remove() |
| 요소 확인 (Peek) | peek(), element() |
Queue의 구현체는 스레드 안전성에 따라 나뉩니다:
- 스레드 비안전:
LinkedList,PriorityQueue등 - 스레드 안전 (비차단):
ConcurrentLinkedQueue - 스레드 안전 (차단):
BlockingQueue인터페이스 구현체 (예:ArrayBlockingQueue,LinkedBlockingQueue)
안드로이드에서는 스레드 풀의 작업 큐 등에 LinkedBlockingQueue와 같은 차단 큐가 자주 사용됩니다.
// 스레드 풀 생성 시 LinkedBlockingQueue 사용 예
public static ExecutorService createFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>() // 작업 큐로 LinkedBlockingQueue 사용
);
}
7. Map
Map은 키(key)와 값(value)을 쌍으로 저장하는 컬렉션입니다. Map의 내용을 순회하는 일반적인 방법은 다음과 같습니다.
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class MapTraversal {
public static void main(String[] args) {
Map<String, String> dataMap = new HashMap<>();
dataMap.put("key1", "value1");
dataMap.put("key2", "value2");
dataMap.put("key3", "value3");
// 1. 값(value)만 순회
System.out.println("--- 값 순회 ---");
for (String value : dataMap.values()) {
System.out.println("Value: " + value);
}
// 2. 키(key)를 통해 값(value) 접근 (keySet 사용)
System.out.println("--- 키셋 순회 ---");
for (String key : dataMap.keySet()) {
System.out.println("Key: " + key + ", Value: " + dataMap.get(key));
}
// 3. Entry(키-값 쌍)를 통해 순회 (Iterator 사용 - 권장)
System.out.println("--- 엔트리셋 순회 (Iterator) ---");
Iterator<Map.Entry<String, String>> iterator = dataMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 4. Entry(키-값 쌍)를 통해 순회 (Enhanced for loop - 권장)
System.out.println("--- 엔트리셋 순회 (Enhanced for) ---");
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
특히 데이터 양이 많을 경우, entrySet()을 사용한 순회가 키를 조회하는 get()을 반복 호출하는 것보다 효율적입니다.
안드로이드 특화 데이터 구조: SparseArray
SparseArray는 안드로이드 프레임워크에서 제공하는 효율적인 데이터 저장 구조로, 특히 키가 정수형(int)일 때 HashMap보다 메모리 사용량을 줄일 수 있습니다.
HashMap을 대체할 수 있으며, 메모리 효율성이 뛰어납니다. (내부적으로 압축된 배열 구조 사용)- 키는 반드시 정수형(
int)이어야 합니다. - 데이터 검색 시 이진 탐색 알고리즘을 사용하여 효율성을 높입니다.
import android.util.SparseArray;
// SparseArray 초기화 및 사용 예
SparseArray<String> sparseData = new SparseArray<>(16); // 초기 용량 지정 가능
sparseData.put(101, "첫 번째 항목");
sparseData.put(205, "두 번째 항목");
sparseData.put(50, "세 번째 항목");
String item = sparseData.get(205); // 특정 키의 값 가져오기
System.out.println("Key 205의 값: " + item);
int itemCount = sparseData.size(); // 저장된 항목 수
System.out.println("총 항목 수: " + itemCount);
// 특정 키 존재 여부 확인
boolean containsKey = sparseData.indexOfKey(101) >= 0;
System.out.println("Key 101 존재 여부: " + containsKey);