Chapter 15 - 컬렉션 자료구조

김태원·2023년 1월 10일
0
post-custom-banner

컬렉션 프레임워크

자바는 널리 알려져 있는 자료구조를 바탕으로 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 관련된 인터페이스와 클래스들을 java.util 패키지에 포함시켜놓았다. 이들을 총칭해서 컬렉션 프레임워크라고 부른다.

컬렉션 프레임워크에는 몇 가지 인터페이스르 통해서 다양한 컬렉션 클래스를 이용할 수 있도록 셜계 되어 있다.
주요 인터페이스로는 List, Set, Map이 있는데, 이 인터페이스로 사용 가능한 컬렉션 객체의 종류는 다음과 같다.

다음은 각 인터페이스별로 사용할 수 있는 컬렉션의 특징을 정리한 것이다.

ArrayList는 파이썬의 List, HashSet은 파이썬의 Set, HashMap은 파이썬의 Dictionary의 역할을 한다.

List 컬렉션

List 컬렉션은 객체를 인덱스로 관리하기 때문에 객체를 저장하면 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.

List 컬렉션에는 ArrayList, Vector, LinkedList 등의 있는데 List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스 메소드는 다음과 같다.

인덱스로 객체들이 관리되기 때문에 인덱스를 매개 값으로 갖는 메소드들이 많다.

ArrayList

ArrayList는 List 컬렉션에서 가장 많이 사용하는 컬렉션이다. 일반 배열과의 차이점은 ArrayList는 제한 없이 객체를 추가할 수 있다는 점이다.


List 컬렉션은 객체 자체를 저장하는 것이 아니라 객체의 번지를 저장한다.
또한 동일한 객체를 중복 저장할 수 있는데, 이경우에는 동일한 번지가 저장된다.
null 또한 저장이 가능하다.

ArrayList 컬렉션은 다음과 같이 생성할 수 있다.

List<E> list = new ArrayList<E>(); // E에 지정된 타입의 객체만 저장
List<E> list = new ArrayList<>(); // E에 지정된 타입의 객체만 저장
List list = new ArrayList(); // 모든 타입의 객체를 저장

ArrayList 컬렉션에 객체를 추가하면 인덱스 0번부터 차례대로 저장된다.
특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨진다.
마찬가지로 특정 인덱스에 객체를 삽입하면 해당 인덱스부터 마지막 인덱스까지 모두 1씩 밀려난다.

이는 파이썬의 List와 동일하며 따라서 객체의 추가, 삭제 작업의 시간복잡도는 O(n)이다.

다음 그림은 4번 인덱스가 제거되었을 때 뒤 인덱스가 모두 앞으로 1씩 당겨지는 모습을 나타낸 것이다.

따라서 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList를 사용하는 것은 바람직하지 않다.
대신 이런 경우라면 LinkedList를 사용하는 것이 좋다.

LinkedList

LinkedList는 ArrayList와 사용 방법은 동일하지만 내부 구조는 완전히 다르다. ArrayList는 내부 배열에 객체의 주소를 저장하지만, LinkedList는 인접 객체를 체인처럼 연결해서 관리한다.

LinkedList의 구조는 다음과 같다.

LinkedList는 특정 위치에서 객체를 삽입하거나 삭제하면 바로 앞뒤 링크만 변경하면 되므로 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList보다 좋은 성능을 발휘한다.

다음은 중간에 객체를 제거할 경우 앞뒤 링크의 수정이 일어나는 모습을 보여준다.

LinkedList의 생성 및 사용 방법은 ArrayList와 동일하다.

하지만 LinkedList는 인덱스에 접근 하는 속도가 느리다는 단점이 존재한다.

ArrayList의 경우 n번째 인덱스에 존재하는 값을 조회하는 작업을 수행할 경우 시간 복잡도는 O(1)이다.
하지만 LinkedList의 경우 다음 노드, 다음 노드, 다음 노드를 거쳐서 가야 하므로 시간 복잡도는 O(n)이다.

아래는 ArrayList와 LinkedList의 시간복잡도를 비교한 표이다.

인덱스 접근삽입, 삭제
ArrayListO(1)O(n)
LinkedListO(n)O(1)

따라서 일반적인 경우에는 ArrayList를 사용하는 것이 효율적일 것이다.

Set

List 컬렉션은 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다.

Set 컬렉션의 구조는 다음과 같다.

Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있는데, Set 컬렉션에서 공통적으로 사용 가능한 Set 인터페이스의 메소드는 다음과 같다.

인덱스로 관리하지 않기 때문에 인덱스를 매개값으로 갖는 메소드가 없다.

HashSet

Set 컬렉션 중에서 가장 많이 사용되는 것이 HashSet이다.

다음은 HashSet 컬렉션을 생성하는 방법이다.

Set<E> set = new HashSet<E>(); // E에 지정된 타입의 객체만 저장
Set<E> set = new HashSet<>(); // E에 지정된 타입의 객체만 저장
Set set = new HashSet(); // 모든 타입의 객체를 저장

HashSet은 동일한 객체는 중복 저장하지 않는다. 여기서 동일한 객체란 동등 객체를 말한다.
HashSet은 다른 객체라도 hashCode() 메소드의 리턴값이 같고, equals() 메소드가 true를 리턴하면 동일한 객체라고 판단하고 중복 저장하지 않는다.

Set 컬렉션은 인덱스로 객체를 검색해서 가져오는 메소드가 없다.
대신 객체를 한 개씩 반복해서 가져와야 하는데, 여기에는 두 가지 방법이 있다.

하나는 다음과 같이 for 문을 이용하는 것이다.

Set<E> se = new HashSet<>();

for (E e: set) {
    System.out.println(e);
}

다른 방법은 다음과 같이 Set 컬렉션의 iterator() 메소드로 반복자를 얻어 객체를 하나씩 가져오는 것이다.
타입 파라미터 E는 Set 컬렉션에 저장되어 있는 객체의 타입이다.

Set<E> set = new HashSet<>();
Iterator<E> = iterator = set.iterator()

String 객체를 저장하는 Set 컬렉션의 사용 예제는 다음과 같다.

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class SwitchValueExample {
	public static void main(String[] args) {
		Set<String> set = new HashSet<>();

		set.add("apple");
		set.add("banana");
		set.add("grape");

		Iterator<String> iterator = set.iterator();

		while (iterator.hasNext()) {
			String e = iterator.next();
			if (e.equals("banana")) iterator.remove();
		}

		for (String e: set) {
			System.out.println(e);
		}
	}
}

출력 결과는 다음과 같다.

apple
grape

Map 컬렉션

Map 컬렉션은 키(Key)와 값(Value)으로 구성된 엔트리(Entry) 객체를 저장한다.

여기서 키와 값은 모두 객체이다. 키는 중복 저장할 수 없지만 값은 중복 저장할 수 있다.
기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값은 없어지고 새로운 값으로 대치된다.

다음은 Map 컬렉션의 구조이다.

Map 컬렉션에는 HashMap, HashTable, LinkedHashMap, Properties, TreeMap 등이 있다.
Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스 메소드는 다음과 같다.

키로 객체들을 관리하기 때문에 키를 매개값으로 갖는 메소드가 많다.

HashMap

Map 컬렉션에서 가장 많이 사용되는 클래스이다.

HashMap은 키로 사용할 객체가 hashCode() 메소드의 리턴값이 같고 equals() 메소드가 true를 리턴할 경우, 동일 키로 보고 중복 저장을 허용하지 않는다.

다음은 HashMap 컬렉션을 생성하는 방법이다.

Map<K, V> map = new HashMap<K, V>();
// Map 에 지정된 키와 값의 타입이 HashMap과 동일할 경우 new HashMap<>(); 사용 가능
Map<K, V> map = new HashMap<>();

K와 V는 각각 키와 값의 타입을 지정할 수 있는 타입 파라미터이다.

다음 예제는 이름을 키로, 점수를 값으로 저장하는 HashMap 사용 예제이다.

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class HashMapExample {
    public static void main(String[] args) {
        // Map 컬렉션 생성
        Map<String, Integer> map = new HashMap<>();

        // 객체 저장
        map.put("신용권", 85);
        map.put("홍길동", 90);
        map.put("동장군", 80);
        map.put("홍길동", 95);
        System.out.println("총 Entry 수: " + map.size());
        System.out.println();

        // 키로 값 얻기
        String key = "홍길동";
        int value = map.get(key);
        System.out.println(key + ": " + value);
        System.out.println();

        // 키 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
        Set<String> keySet = map.keySet();
        Iterator<String> keyIterator = keySet.iterator();
        while (keyIterator.hasNext()) {
            String k = keyIterator.next();
            Integer v = map.get(k);
            System.out.println(k + " : " + v);
        }
        System.out.println();

        // 엔트리 Set 컬렉션을 얻고, 반복해서 키와 값을 얻기
        Set<Entry<String, Integer>> entrySet = map.entrySet();
        Iterator<Entry<String, Integer>> entryIterator = entrySet.iterator();
        while (entryIterator.hasNext()) {
            Entry<String, Integer> entry = entryIterator.next();
            String k = entry.getKey();
            Integer v = entry.getValue();
            System.out.println(k + " : " + v);
        }
        System.out.println();

        // 키로 엔트리 삭제
        map.remove("홍길동");
        System.out.println("총 Entry 수: " + map.size());
        System.out.println();
    }
}

출력 결과는 다음과 같다.

총 Entry 수: 3

홍길동: 95

홍길동 : 95
신용권 : 85
동장군 : 80

홍길동 : 95
신용권 : 85
동장군 : 80

총 Entry 수: 2

TreeMap

TreeMap은 이진 트리를 기반으로 한 Map 컬렉션이다. TreeMap에 엔트리를 저장하면 키를 기준으로 자동 정렬되는데, 부모 키 값과 비교해서 낮은 것은 왼쪽, 높은 것은 오른쪽 자식 노드에 Entry 객체를 저장한다.

다음은 TreeMap의 구조이다.

TreeMap 컬렉션의 생성 방법은 Map, HashMap과 동일하다.
하지만 검색 관련 메소드는 TreeMap에만 정의되어 있기 때문에 Map 타입 변수 대신에 TreeMap 타입으로 대입해야 한다.

TreeMap<K, V> treemap = new TreeMap<K, V>();
TreeMap<K, V> treemap = new TreeMap<>();

다음은 TreeMap이 가지고 있는 검색 관련 메소드들이다.

다음 예제는 영어 단어와 페이지 번호를 무작위로 저장하고 검색하는 예제이다.

import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        // TreeMap 컬렉션 생성
        TreeMap<String, Integer> treeMap = new TreeMap<>();

        // 엔트리 저장
        treeMap.put("apple", 10);
        treeMap.put("forever", 60);
        treeMap.put("description", 40);
        treeMap.put("ever", 50);
        treeMap.put("zoo", 80);
        treeMap.put("base", 20);
        treeMap.put("guess", 70);
        treeMap.put("cherry", 30);

        // 정렬된 엔트리를 하나씩 가져오기
        Set<Entry<String, Integer>> entrySet = treeMap.entrySet();
        for (Entry<String, Integer> entry : entrySet) {
            System.out.println(entry.getKey() + "-" + entry.getValue());
        }
        System.out.println();

        // 특정 키에 대한 값 가져오기
        Entry<String, Integer> entry = null;
        entry = treeMap.firstEntry();
        System.out.println("제일 앞 단어: " + entry.getKey() + "-" + entry.getValue());
        entry = treeMap.lastEntry();
        System.out.println("제일 뒤 단어: " + entry.getKey() + "-" + entry.getValue());
        entry = treeMap.lowerEntry("ever");
        System.out.println("ever 앞 단어: " + entry.getKey() + "-" + entry.getValue() + "\n");

        // 내림차순으로 정렬하기
        NavigableMap<String, Integer> descendingMap = treeMap.descendingMap();
        Set<Entry<String, Integer>> descendingEntrySet = descendingMap.entrySet();
        for (Entry<String, Integer> e : descendingEntrySet) {
            System.out.println(e.getKey() + "-" + e.getValue());
        }
        System.out.println();

        // 범위 검색
        System.out.println("[c~h 사이의 단어 검색]");
        NavigableMap<String, Integer> rangeMap = treeMap.subMap("c", true, "h", false);
        for (Entry<String, Integer> e : rangeMap.entrySet()) {
            System.out.println(e.getKey() + "-" + e.getValue());
        }
    }
}

출력 결과는 다음과 같다.

apple-10
base-20
cherry-30
description-40
ever-50
forever-60
guess-70
zoo-80

제일 앞 단어: apple-10
제일 뒤 단어: zoo-80
ever 앞 단어: description-40

zoo-80
guess-70
forever-60
ever-50
description-40
cherry-30
base-20
apple-10

[c~h 사이의 단어 검색]
cherry-30
description-40
ever-50
forever-60
guess-70

Comparable과 Comparator

TreeMap에 저장되는 키 객체는 저장과 동시에 오름차순으로 정렬되는데, 어떤 객체든 정렬될 수 있는 것은 아니고 객체가 Comparable 인터페이스를 구현하고 있어야 한다.

Integer, Double, String 타입은 모두 Comparable을 구현하고 있기 때문에 상관 없지만, 사용자 정의 객체를 저장할 때에는 반드시 Comparable을 구현하고 있어야 한다.

Comparable 인터페이스에는 compareTo() 메소드가 정의되어 있다.
따라서 사용자 정의 클래스에서 이 메소드를 재정의(Overriding)해서 비교 결과를 정수 값으로 리턴해야 한다.

리턴 타입메소드설명
intcompareTo(T o)주어진 객체보다 작으면 음수를 리턴
주어진 객체와 같으면 0을 리턴
주어진 객체보다 크면 양수를 리턴

일반적으로 -1, 0, 1을 리턴한다.

Stack

Stack 클래스는 LIFO 자료구조를 구현한 클래스이다.

다음은 Stack 객체를 생성하는 방법이다.

Stack<E> stack = new Stack<E>();
Stack<E> stack = new Stack<>();

다음은 Stack 클래스의 주요 메소드이다.

리턴 타입메소드설명
Epush(E item)주어진 객체를 스택에 넣는다.
Epop()스택의 맨 위 객체를 빼낸다.

Queue

Queue 인터페이스는 FIFO 자료구조에서 사용되는 메소드를 정의하고 있다.

다음은 Queue 인터페이스에 정의되어 있는 메소드이다.

리턴 타입메소드설명
booleanoffer(E e)주어진 객체를 큐에 넣는다.
Epoll()큐에서 객체를 빼낸다.

Queue 인터페이스를 구현한 대표적인 클래스는 LinkedList이다. 그렇기 때문에 LinkedList 객체를 Queue 인터페이스 변수에 다음과 같이 대입할 수 있다.

Queue<E> queue = new LinkedList<E>();
Queue<E> queue = new LinkedList<>();

수정할 수 없는 컬렉션

수정할 수 없는(unmodifiable) 컬렉션이란 요소를 추가, 삭제할 수 없는 컬렉션을 말한다.

컬렉션 생성 시 저장된 요소를 변경하고 싶지 않을 때 유용하다.
여러 가지 방법으로 만들 수 있는데, 먼저 첫 번쨰 방법으로는 List, Set, Map 인터페이스의 정적 메소인 of()로 생성할 수 있다.

List<E> immutableList = List.of(E... elements);
Set<E> immutableSet = Set.of(E... elements);
Map<K, V> immutableList = Map.of(K k1, V v1, K k2, V v2, ...);

두 번째 방법은 List, Set, Map 인터페이스의 정적 메소드인 copyOf()를 이용해 기존 컬렉션을 복사하여 수정할 수 없는 컬렉션을 만드는 것이다.

List<E> immutableList = List.copyOf(Collections<E> coll);
Set<E> immutableSet = Set.copyOf(Collections<E> coll);
Map<K, V> immutableList = Map.copyOf(Map<K, V> map);

세 번째 방법은 배열로부터 수정할 수 없는 List 컬렉션을 만들 수 있다.

String[] arr = {"A", "B", "C"};
List<String> immutableList = Arrays.asList(arr);
profile
개발이 재밌어서 하는 Junior Backend Developer
post-custom-banner

0개의 댓글