Java 애플리케이션에서는 일반적으로 다양한 유형의 객체를 처리합니다. 그리고 이러한 객체에 대해 정렬, 검색 및 반복 등의 작업을 수행하고 싶을 수 있습니다.

JDK 1.2에서 Collections 프레임워크가 도입되기 전에는 배열과 벡터를 사용하여 객체 그룹을 저장하고 관리했을 것입니다. 그러나 이러한 방법에는 각각의 단점이 있었습니다.

Java Collections Framework는 이러한 문제를 극복하기 위해 일반적인 데이터 구조의 고성능 구현을 제공함으로써 낮은 수준의 작업에 집중하기보다 애플리케이션 로직 작성에 집중할 수 있도록 합니다.

그리고 JDK 1.5에서 제네릭을 도입함으로써 Java Collections Framework가 크게 개선되었습니다. 제네릭을 사용하면 컬렉션에 저장된 객체의 유형 안전성을 강제할 수 있어 애플리케이션의 견고성을 향상시킬 수 있습니다. Java 제네릭에 대해 더 읽어보려면 여기를 참조하세요.

본 문서에서는 Java Collections Framework의 사용 방법을 안내합니다. List, Set, Queue, Map과 같은 다양한 유형의 컬렉션에 대해 논의할 것입니다. 또한 다음과 같은 핵심 특성에 대한 간략한 설명을 제공할 것입니다:

  • 내부 메커니즘

  • 중복 처리

  • null 값 지원

  • 순서

  • 동기화

  • 성능

  • 주요 방법

  • 일반적인 구현

더 나은 이해를 위해 몇 가지 코드 예제를 살펴보고 Collections 유틸리티 클래스와 그 사용법에 대해 다룰 것입니다.

목차:

  1. Java Collections Framework 이해

  2. 자바 컬렉션 인터페이스

  3. 컬렉션 유틸리티 클래스

  4. 결론

Java 컬렉션 프레임워크 이해

자바 문서에 따르면, “컬렉션은 객체 그룹을 나타내는 객체입니다. 컬렉션 프레임워크는 컬렉션을 표현하고 조작하기 위한 통합된 아키텍처입니다.”

간단히 말해, Java 컬렉션 프레임워크는 객체 그룹을 효율적으로 조작하고 조직적으로 관리할 수 있도록 도와줍니다. 이를 통해 다양한 메서드를 제공하여 객체 그룹을 처리하는 것이 더 쉬워집니다. Java 컬렉션 프레임워크를 사용하면 객체를 효율적으로 추가, 제거, 검색 및 정렬할 수 있습니다.

컬렉션 인터페이스

자바에서 인터페이스는 구현하는 클래스가 반드시 준수해야 하는 계약을 지정합니다. 이는 구현 클래스가 인터페이스에 선언된 모든 메서드에 대한 구체적인 구현을 제공해야 함을 의미합니다.

Java 컬렉션 프레임워크에서 Set, List, Queue와 같은 다양한 컬렉션 인터페이스는 Collection 인터페이스를 확장하며, Collection 인터페이스에서 정의한 계약을 준수해야 합니다.

Java 컬렉션 프레임워크 계층 구조 해독하기

Java 컬렉션 계층 구조를 설명하는 이 기사의 깔끔한 다이어그램을 확인해 보세요:

위에서 아래로 내려가면서 이 다이어그램이 무엇을 보여주는지 이해해 보겠습니다:

  1. Java 컬렉션 프레임워크의 루트는 Iterable 인터페이스로, 컬렉션의 요소를 반복할 수 있게 해줍니다.

  2. Collection 인터페이스는 Iterable 인터페이스를 확장합니다. 이는 Iterable 인터페이스의 속성과 동작을 상속받으며, 요소를 추가하고, 제거하고, 검색하는 자체 동작을 추가함을 의미합니다.

  3. 특정 인터페이스인 List, Set, QueueCollection 인터페이스를 더 확장합니다. 이러한 인터페이스 각각은 메서드를 구현하는 다른 클래스를 가지고 있습니다. 예를 들어, ArrayListList 인터페이스의 인기 있는 구현체이며, HashSetSet 인터페이스를 구현합니다.

  4. Map 인터페이스는 Java Collections Framework의 일부이지만, 앞에서 언급한 다른 인터페이스와는 달리 Collection 인터페이스를 확장하지 않습니다.

  5. 이 프레임워크의 모든 인터페이스와 클래스는 java.util 패키지의 일부입니다.

참고: Java Collections Framework에서 혼란의 일반적인 원인은 CollectionCollections 사이의 차이에 있습니다. Collection은 프레임워크의 인터페이스이며, Collections는 유틸리티 클래스입니다. Collections 클래스는 컬렉션의 요소에 대한 작업을 수행하는 정적 메서드를 제공합니다.

Java 컬렉션 인터페이스

지금쯤이면, 컬렉션 프레임워크의 기초를 이루는 다양한 유형의 컬렉션에 익숙해졌을 것입니다. 이제 List, Set, Queue, Map 인터페이스를 자세히 살펴보겠습니다.

이 섹션에서는 이러한 인터페이스 각각에 대해 논의하면서 내부 메커니즘을 탐색할 것입니다. 중복 요소를 처리하는 방법과 null 값 삽입을 지원하는지 여부를 살펴볼 것입니다. 또한 삽입 중 요소의 순서 및 동기화에 대한 지원을 이해할 것이며, 이는 스레드 안전성 개념을 다룹니다. 그런 다음 이러한 인터페이스의 몇 가지 주요 메서드를 살펴보고 각각의 일반적인 구현과 다양한 작업에 대한 성능을 검토하여 마무리하겠습니다.

시작하기 전에, 동기화와 성능에 대해 간단히 이야기해 봅시다.

  • 동기화는 여러 스레드에 의해 공유 객체에 대한 액세스를 제어하여 그들의 무결성을 보장하고 충돌을 방지합니다. 이는 스레드 안전성을 유지하는 데 중요합니다.

  • 컬렉션 유형을 선택할 때 한 가지 중요한 요소는 삽입, 삭제 및 검색과 같은 일반 작업 중의 성능입니다. 성능은 일반적으로 빅 오 표기법을 사용하여 표현됩니다. 여기

리스트

List는 요소의 순서가 있는 또는 순차적인 컬렉션입니다. 요소는 0부터 시작하는 인덱싱을 따르며, 요소를 삽입, 제거 또는 인덱스 위치를 사용하여 액세스할 수 있습니다.

  1. 내부 메커니즘: List는 구현 유형에 따라 배열 또는 연결 리스트로 내부적으로 지원됩니다. 예를 들어, ArrayList는 배열을 사용하고, LinkedList는 내부적으로 연결 리스트를 사용합니다. LinkedList에 대해 더 읽어볼 수 있습니다여기서. List는 요소의 추가 또는 제거에 따라 자동으로 크기를 조정합니다. 인덱스 기반 검색은 매우 효율적인 유형의 컬렉션으로 만듭니다.

  2. 중복: List에서는 중복 요소가 허용되므로, 동일한 값을 가진 여러 요소가 List에 존재할 수 있습니다. 저장된 인덱스를 기준으로 어떤 값이든 검색할 수 있습니다.
  3. : List에서도 널 값이 허용됩니다. 중복이 허용되므로, 여러 개의 널 요소도 가질 수 있습니다.
  4. 순서: List는 삽입 순서를 유지하므로, 요소는 추가된 순서대로 저장됩니다. 이는 요소를 삽입된 정확한 순서로 검색하고 싶을 때 유용합니다.
  5. 동기화: List는 기본적으로 동기화되지 않으므로, 여러 스레드에 의한 동시 접근을 처리할 내장 방식이 없습니다.

  6. 주요 메서드: List 인터페이스의 몇 가지 주요 메서드는 다음과 같습니다: add(E element), get(int index), set(int index, E element), remove(int index), 그리고 size(). 이러한 메서드를 예제 프로그램과 함께 사용하는 방법을 알아봅시다.

     import java.util.ArrayList;
     import java.util.List;
    
     public class ListExample {
         public static void main(String[] args) {
             // 리스트 생성
             List<String> list = new ArrayList<>();
    
             // add(E element)
             list.add("Apple");
             list.add("Banana");
             list.add("Cherry");
    
             // get(int index)
             String secondElement = list.get(1); // "Banana"
    
             // set(int index, E element)
             list.set(1, "Blueberry");
    
             // remove(int index)
             list.remove(0); // "Apple" 제거
    
             // size()
             int size = list.size(); // 2
    
             // 리스트 출력
             System.out.println(list); // 결과: [Blueberry, Cherry]
    
             // 리스트 크기 출력
             System.out.println(size); // 결과: 2
         }
     }
    
  7. 일반적인 구현: ArrayList, LinkedList, Vector, Stack

  8. 성능: 일반적으로 ArrayListLinkedList 모두에서 삽입 및 삭제 작업은 빠릅니다. 하지만 요소를 가져오는 것은 노드를 순회해야 하므로 느릴 수 있습니다.

작업 ArrayList LinkedList
삽입 끝에서 빠름 – O(1) 평균, 시작 또는 중간에서 느림 – O(n) 시작 또는 중간에서 빠름 – O(1), 끝에서 느림 – O(n)
삭제 끝에서 빠름 – O(1) 평균, 시작 또는 중간에서 느림 – O(n) 위치가 알려져 있다면 빠름 – O(1)
검색 무작위 접근 시 빠름 – O(1) 무작위 접근 시 느림 – O(n), 순회가 필요함

집합

Set은 중복 요소를 허용하지 않는 컬렉션의 일종으로, 수학적 집합의 개념을 나타냅니다.

  1. 내부 메커니즘: Set은 내부적으로 HashMap에 의해 지원됩니다. 구현 유형에 따라 HashMap, LinkedHashMap, 또는 TreeMap 중 하나에 의해 지원됩니다. HashMap의 내부 작동 방법에 대해 자세히 설명한 기사를 여기에 작성했습니다. 꼭 확인해보세요.

  2. 중복: Set은 수학적 집합의 개념을 나타내기 때문에 중복 요소가 허용되지 않습니다. 이를 통해 모든 요소가 고유하게 유지되어 컬렉션의 무결성을 유지합니다.

  3. Null: Set에서는 하나의 널 값만 허용되며, 중복은 허용되지 않습니다. 그러나 이는 TreeSet 구현에는 적용되지 않으며, 여기서는 널 값이 전혀 허용되지 않습니다.

  4. Ordering: Set의 요소 정렬은 구현 유형에 따라 다릅니다.

    • HashSet: 순서가 보장되지 않으며, 요소는 어떠한 위치에도 배치될 수 있습니다.

    • LinkedHashSet: 이 구현은 삽입 순서를 유지하므로 삽입된 순서대로 요소를 검색할 수 있습니다.

    • TreeSet: 요소는 자연 순서에 따라 삽입됩니다. 또는 사용자 지정 비교자를 지정하여 삽입 순서를 제어할 수 있습니다.

  5. Synchronization: Set은 동기화되지 않으므로 동시에 Set 객체에 액세스하려는 두 개 이상의 스레드가 있을 때 경합 조건과 같은 동시성 문제가 발생할 수 있으며 데이터 무결성에 영향을 줄 수 있습니다.

  6. 핵심 메서드: Set 인터페이스의 몇 가지 핵심 메서드는 다음과 같습니다: add(E element), remove(Object o), contains(Object o), 그리고 size()입니다. 이러한 메서드를 예제 프로그램과 함께 사용하는 방법을 살펴봅시다.

     import java.util.HashSet;
     import java.util.Set;
    
     public class SetExample {
         public static void main(String[] args) {
             // 집합 생성
             Set<String> set = new HashSet<>();
    
             // 집합에 요소 추가
             set.add("Apple");
             set.add("Banana");
             set.add("Cherry");
    
             // 집합에서 요소 제거
             set.remove("Banana");
    
             // 집합에 요소가 포함되어 있는지 확인
             boolean containsApple = set.contains("Apple");
             System.out.println("Apple이 포함되어 있음: " + containsApple);
    
             // 집합의 크기 가져오기
             int size = set.size();
             System.out.println("집합의 크기: " + size);
         }
     }
    
  7. 일반적인 구현: HashSet, LinkedHashSet, TreeSet

  8. 성능: Set 구현은 기본 작업에 대해 빠른 성능을 제공하지만, TreeSet의 경우 내부 데이터 구조가 이러한 작업 중에 요소를 정렬하기 때문에 상대적으로 성능이 느릴 수 있습니다.

작업 HashSet LinkedHashSet TreeSet
삽입 빠름 – O(1) 빠름 – O(1) 더 느림 – O(log n)
삭제 빠름 – O(1) 빠름 – O(1) 더 느림 – O(log n)
검색 빠름 – O(1) 빠름 – O(1) 더 느림 – O(log n)

대기열

Queue는 여러 항목을 처리하기 전에 여러 항목을 보유하는 데 사용되는 선형 컬렉션으로, 일반적으로 FIFO(먼저들어온것이먼저나간다) 순서를 따릅니다. 이는 요소가 한쪽 끝에 추가되고 다른 쪽에서 제거되므로 대기열에 추가된 첫 번째 요소가 제일 먼저 제거됨을 의미합니다.

  1. 내부 메커니즘: Queue의 내부 작업은 해당 구현에 따라 다를 수 있습니다.

    • LinkedList – 요소를 저장하는 데 이중 연결 목록을 사용하여 앞뒤로 모두 이동할 수 있어 유연한 작업이 가능합니다.

    • PriorityQueue – 이진 힙으로 내부적으로 지원되며 검색 작업에 대해 매우 효율적입니다.

    • ArrayDeque – 추가되거나 제거되는 요소에 따라 확장되거나 축소되는 배열을 사용하여 구현됩니다. 여기서 요소는 대기열의 양쪽 끝에서 추가 또는 제거할 수 있습니다.

  2. 중복: Queue에서는 중복 요소가 허용되어 동일한 값의 여러 인스턴스를 삽입할 수 있습니다.

  3. : Queue에 널 값을 삽입할 수 없습니다. 설계상 Queue의 일부 메서드는 비어 있음을 나타내기 위해 널을 반환하기 때문입니다. 혼동을 피하기 위해 널 값은 허용되지 않습니다.

  4. 순서: 요소는 자연 순서에 따라 삽입됩니다. 또는 사용자 지정 비교기를 지정하여 삽입 순서를 제어할 수 있습니다.

  5. 동기화: Queue는 기본적으로 동기화되지 않습니다. 그러나 스레드 안전성을 달성하기 위해 ConcurrentLinkedQueue 또는 BlockingQueue 구현을 사용할 수 있습니다.

  6. 주요 메서드: Queue 인터페이스의 주요 메서드 몇 가지는 다음과 같습니다: add(E element), offer(E element), poll(), peek(). 이러한 메서드를 예제 프로그램과 함께 사용하는 방법을 알아봅시다.

     import java.util.LinkedList;
     import java.util.Queue;
    
     public class QueueExample {
         public static void main(String[] args) {
             // LinkedList를 사용하여 큐 생성
             Queue<String> queue = new LinkedList<>();
    
             // 요소 삽입에 add 메서드 사용, 삽입 실패시 예외 발생
             queue.add("Element1");
             queue.add("Element2");
             queue.add("Element3");
    
             // 요소 삽입에 offer 메서드 사용, 삽입 실패시 false 반환
             queue.offer("Element4");
    
             // 큐 출력
             System.out.println("Queue: " + queue);
    
             // 첫 번째 요소 확인 (삭제하지 않음)
             String firstElement = queue.peek();
             System.out.println("Peek: " + firstElement); // "Element1" 출력
    
             // 첫 번째 요소 폴링 (가져오고 제거)
             String polledElement = queue.poll();
             System.out.println("Poll: " + polledElement); // "Element1" 출력
    
             // 폴링 후 큐 출력
             System.out.println("Queue after poll: " + queue);
         }
     }
    
  7. 일반적인 구현: LinkedList, PriorityQueue, ArrayDeque

  8. 성능: LinkedListArrayDeque와 같은 구현체는 일반적으로 항목 추가 및 제거에 빠릅니다. PriorityQueue는 항목을 설정된 우선 순위 순서에 따라 삽입하기 때문에 약간 느릴 수 있습니다.

작업 LinkedList PriorityQueue ArrayDeque
삽입 시작이나 중간에서 빠름 – O(1), 끝에서 느림 – O(n) 느림 – O(log n) 빠름 – O(1), 느림 – O(n), 내부 배열의 크기 조정이 필요한 경우
삭제 빠름 – 위치가 알려진 경우 O(1) 느림 – O(log n) 빠름 – O(1), 느림 – O(n), 내부 배열의 크기 조정이 필요한 경우
검색 랜덤 액세스의 경우 느림 – O(n), 탐색이 필요함 빠름 – O(1) 빠름 – O(1)

Map은 각 키가 단일 값에 매핑되는 키-값 쌍의 컬렉션을 나타냅니다. Map은 Java Collection 프레임워크의 일부이지만 java.util.Collection 인터페이스를 확장하지는 않습니다.

  1. 내부 메커니즘: Map은 해싱 개념을 기반으로 한 HashTable을 사용하여 내부적으로 작동합니다. 이 주제에 대해 자세한 기사를 작성했으니 더 깊이 이해하고 싶다면 읽어보세요.

  2. 중복: Map은 데이터를 키-값 쌍으로 저장합니다. 여기서 각 키는 고유하므로 중복 키는 허용되지 않습니다. 그러나 중복 값은 허용됩니다.

  3. : 중복 키가 허용되지 않으므로 Map은 하나의 널 키만 가질 수 있습니다. 중복 값이 허용되므로 여러 개의 널 값을 가질 수 있습니다. TreeMap 구현에서는 키를 기준으로 요소를 정렬하기 때문에 키는 널일 수 없습니다. 그러나 널 값은 허용됩니다.

  4. 순서: Map의 삽입 순서는 구현에 따라 다릅니다:

    • HashMap – 삽입 순서는 해싱 개념에 기반하여 결정되므로 보장되지 않습니다.

    • LinkedHashMap – 삽입 순서가 보존되며, 요소를 추가된 순서대로 다시 검색할 수 있습니다.

    • TreeMap – 요소는 자연 순서에 따라 삽입되며, 사용자 정의 비교자를 지정하여 삽입 순서를 제어할 수도 있습니다.

  5. 동기화: Map은 기본적으로 동기화되지 않습니다. 그러나 Collections.synchronizedMap() 또는 ConcurrentHashMap 구현을 사용하여 스레드 안전성을 달성할 수 있습니다.

  6. 주요 메서드: Map 인터페이스의 주요 메서드는 다음과 같습니다: put(K key, V value), get(Object key), remove(Object key), containsKey(Object key), 그리고 keySet(). 이러한 메서드를 예제 프로그램과 함께 사용하는 방법을 살펴보겠습니다.

     import java.util.HashMap;
     import java.util.Map;
     import java.util.Set;
    
     public class MapMethodsExample {
         public static void main(String[] args) {
             // 새 HashMap 생성
             Map<String, Integer> map = new HashMap<>();
    
             // put(K key, V value) - 키-값 쌍을 맵에 삽입
             map.put("Apple", 1);
             map.put("Banana", 2);
             map.put("Orange", 3);
    
             // get(Object key) - 키에 연관된 값 반환
             Integer value = map.get("Banana");
             System.out.println("'Banana'에 대한 값: " + value);
    
             // remove(Object key) - 지정된 키에 대한 키-값 쌍 제거
             map.remove("Orange");
    
             // containsKey(Object key) - 맵이 지정된 키를 포함하는지 확인
             boolean hasApple = map.containsKey("Apple");
             System.out.println('Apple' 포함 여부: " + hasApple);
    
             // keySet() - 맵에 포함된 키의 집합 뷰 반환
             Set<String> keys = map.keySet();
             System.out.println("맵의 키들: " + keys);
         }
     }
    
  7. 일반적인 구현: HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap

  8. 성능: HashMap 구현은 주로 효율적인 성능 특성으로 널리 사용되며 아래 표에 나타납니다.

작업 HashMap LinkedHashMap TreeMap
삽입 빠름 – O(1) 빠름 – O(1) 느림 – O(log n)
삭제 빠름 – O(1) 빠름 – O(1) 느림 – O(log n)
검색 빠름 – O(1) 빠름 – O(1) 느림 – O(log n)

컬렉션 유틸리티 클래스

이 글의 초반에 강조된 대로, Collections 유틸리티 클래스에는 컬렉션의 요소에 대해 일반적으로 사용되는 작업을 수행할 수 있는 유용한 정적 메서드가 여러 개 있습니다. 이러한 메서드를 사용하면 응용 프로그램의 보일러플레이트 코드를 줄이고 비즈니스 로직에 집중할 수 있습니다.

다음은 주요 기능 및 메서드와 간단한 설명이 나열되어 있습니다:

  1. 정렬: Collections.sort(List<T>) – 이 메서드는 목록의 요소를 오름차순으로 정렬하는 데 사용됩니다.

  2. 검색: Collections.binarySearch(List<T>, key) – 이 메서드는 정렬된 목록에서 특정 요소를 검색하고 해당 인덱스를 반환하는 데 사용됩니다.

  3. 역순: Collections.reverse(List<T>) – 이 메서드는 목록의 요소 순서를 뒤집는 데 사용됩니다.

  4. 최소/최대 연산: Collections.min(Collection<T>)Collections.max(Collection<T>) – 이러한 메서드는 각각 컬렉션에서 최소 및 최대 요소를 찾는 데 사용됩니다.

  5. 동기화: Collections.synchronizedList(List<T>) – 이 메서드는 목록을 동기화하여 스레드로부터 안전하게 만드는 데 사용됩니다.

  6. 수정할 수 없는 컬렉션: Collections.unmodifiableList(List<T>) – 이 메서드는 수정을 방지하기 위해 리스트의 읽기 전용 뷰를 생성하는 데 사용됩니다.

다음은 Collections 유틸리티 클래스의 다양한 기능을 보여주는 샘플 Java 프로그램입니다:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(8);
        numbers.add(1);

        // 정렬
        Collections.sort(numbers);
        System.out.println("Sorted List: " + numbers);

        // 검색
        int index = Collections.binarySearch(numbers, 3);
        System.out.println("Index of 3: " + index);

        // 역순
        Collections.reverse(numbers);
        System.out.println("Reversed List: " + numbers);

        // 최소/최대 연산
        int min = Collections.min(numbers);
        int max = Collections.max(numbers);
        System.out.println("Min: " + min + ", Max: " + max);

        // 동기화
        List<Integer> synchronizedList = Collections.synchronizedList(numbers);
        System.out.println("Synchronized List: " + synchronizedList);

        // 수정할 수 없는 컬렉션
        List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
        System.out.println("Unmodifiable List: " + unmodifiableList);
    }
}

이 프로그램은 정렬, 검색, 역순, 최소값 및 최대값 찾기, 동기화 및 Collections 유틸리티 클래스를 사용하여 수정할 수 없는 리스트를 생성하는 기능을 보여줍니다.

결론

이 기사에서는 Java Collections Framework에 대해 알아보고 Java 애플리케이션에서 객체 그룹을 관리하는 데 도움이 되는 방법을 배웠습니다. 리스트, 세트, 큐 및 맵과 같은 다양한 컬렉션 유형을 탐색하고 각 유형이 지원하는 주요 특성과 그 특성을 지원하는 방법에 대해 통찰력을 얻었습니다.

성능, 동기화 및 주요 메서드에 대해 알아보고 필요에 맞는 올바른 데이터 구조를 선택하는 데 유용한 통찰력을 얻었습니다.

이러한 개념을 이해함으로써 Java Collections Framework를 완전히 활용할 수 있으며, 더 효율적인 코드를 작성하고 견고한 애플리케이션을 구축할 수 있습니다.

이 기사가 흥미로웠다면, freeCodeCamp에서 다른 기사를 확인하고 LinkedIn에서 저와 연락하세요.