안드로이드 컬렉션 프레임워크: List, Set, Map 활용법

안드로이드 개발 시 자주 사용되는 데이터 구조로는 List, Set, Map이 있습니다. 이들은 객체들을 효율적으로 관리하고 접근하는 데 필수적입니다. ListSet은 '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에서 요소를 삭제할 때는 Iteratorremove() 메서드를 사용하는 것이 안전하고 올바른 방법입니다.

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;
    }
}

LinkedListList 인터페이스 외에도 Queue 인터페이스를 구현하여 큐(Queue) 자료구조로도 활용될 수 있습니다.

3. Vector

VectorArrayList와 유사한 기능을 제공하지만, 모든 메서드가 동기화(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. TreeSetLinkedHashSet

  • 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);

태그: 안드로이드 컬렉션 list set map

6월 10일 19:37에 게시됨