elevne's Study Note

Java - Collection 본문

Backend/Java

Java - Collection

elevne 2023. 5. 17. 17:15

애플리케이션을 개발하다 보면 다수의 객체를 저장해두고 필요할 때마다 꺼내서 사용하는 경우가 많다. 이 때 배열을 사용할 수도 있지만, 배열은 저장할 수 있는 객체의 수가 배열을 생성할 때 결정되기 때문에 불특정 다수의 객체를 저장하기에는 문제가 있다. 또, 배열을 사용했을 때의 문제점으로 객체를 삭제했을 때 해당 인덱스가 비게 된다. Java 에서는 이러한 배열 대신 Collection 프레임워크의 다양한 자료구조들을 사용해볼 수 있다.

 

 

 

 

 

 

ArrayList

 

ArrayList 는 List 의 구현클래스이다. 기본 생성자로 ArrayList 객체를 생성하면 내부에 10 개를 저장할 수 있는 초기 용량을 가지게 된다. 저장되는 객체 수가 늘어나면 용량이 자동으로 증가하지만, 처음부터 크게 잡고 생성할 수도 있다. ArrayList 에서 특정 인덱스에 객체를 삽입하거나 삭제하게 되었을 때 매번 다른 객체들의 인덱스들이 업데이트된다. 따라서 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList 를 사용하지 않는 편이 좋다. 

 

 

또 아래와 같이 고정된 객체들로 구성된 List 를 생성해볼 수도 있다.

 

 

List<String> list = Arrays.asList(new String[] {"HI", "HELLO"});

 

 

 

Vector

 

VectorArrayList 와 같은 내부구조를 가지고 있다. 다만 다른 점은 Vector 은 동기화된 (synchronized) 메서드로 구성되어 있기 때문에 멀티스레드가 동시에 Vector 의 메서드들을 실행할 수 없고, 하나의 스레드가 실행을 완료해야만 다른 스레드를 실행할 수 있다. Thread Safe 한 객체인 것이다. 

 

 

 

LinkedList

 

LinkedList 도 List 의 구현 객체이지만 ArrayList 와 내부구조는 완전히 다르다. ArrayList 는 내부 배열에 객체를 저장해서 인덱스로 관리하지만, LinkedList 는 인접 참조를 링크해서 체인처럼 관리한다. LinkedList 에서 특정 인덱스의 객체를 제거하면 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않는다. (삽입할 때에도 마찬가지) 빈번한 객체의 삽입/삭제가 이루어지는 곳에서는 ArrayList 보다는 LinkedList 를 사용하는 편이 좋다. 

 

 

끝쪽에서부터 추가/삭제하는 경우에는 ArrayList 가 빠르고, 중간에 있는 데이터를 건드릴 경우에는 LinkedList 가 빠르다.

 

 

 

 

그 다음으로는 Set 컬렉션에 대해 알아보았다. Set 은 인덱스로 요소들을 관리하지 않기 때문에 인덱스를 매개값으로 갖는 메서드가 없다. 또, 이는 인덱스로 객체를 가져오는 메서드는 없지만 전체 객체를 대상으로 한 번씩 반복해서 가져오는 Iterator 을 제공한다. 아래와 같이 사용해볼 수 있다.

 

 

public static void main(String[] args) {
    Set<String> setTest = new HashSet<>();
    setTest.add("TEST1");
    setTest.add("TEST2");

    Iterator<String> iterator = setTest.iterator();
    while(iterator.hasNext()){
        String str = iterator.next();
        System.out.println(str);
    }
}

 

 

result

 

 

위와 같이 Iterator 을 사용하지 않더라도 for 문으로 Set 을 돌 수도 있다. 

 

 

 

HashSet

 

HashSet 은 객체들을 순서 없이 저장하고 동일한 객체는 중복저장하지 않는다. HashSet 은 객체를 저장하기 전에 먼저 객체의 hashCode() 메서드를 호출하여 해시코드를 얻어내고, 이미 저장되어 있는 객체들의 해시코드와 비교한다. 만약 동일한 해시코드가 있으면 다시 equals() 메서드로 두 객체를 비교해서 true 가 나오면 동일 객체로 판단, 저장하지 않는다.

 

 

 

TreeSet

 

TreeSet은 이진 검색 트리라는 자료구조의 형태로 데이터를 저장하는 컬렉션 클래스다. 이진 검색 트리는 정렬, 검색, 범위 검색에 높은 성능을 보인다.

 

 

하나의 노드는 노드값인 Value 와 왼쪽과 오른쪽 자식 노드를 참조하기 위한 두 개의 변수로 구성된다. TreeSet 에 객체를 저장하면 자동으로 정렬되는데, 부모값과 비교해서 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장한다.

 

 

 

그 다음으로는 Map 에 대해서 알아보았다. Map 컬렉션은 Key 와 Value 로 구성된 Entry 객체를 저장하는 구조를 가지고 있다. (Key, Value 는 모두 객체) Key 는 중복될 수 없다. (만약 기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 키 값은 없어지고 새로운 값으로 저장된다) 

 

 

 

HashMap

 

HashMap 은 Map 인터페이스를 구현한 객체로, 키로 사용할 객체는 hashCode()equals() 메서드를 재정의하여 동등 객체가 될 조건을 정해야 한다. 

 

 

 

HashTable 

 

HashMap동일한 내부 구조를 가진 HashTable 은, 동기화가 되어있다는 차이점이 있다. Vector 처럼 Thread-Safe 한 것이다.

 

 

 

Properties

 

PropertiesHashTable 의 하위 클래스로, HashTable 의 모든 특징을 그대로 가지고 있다. 차이점은 HashTable 은 키와 값을 다양한 타이으로 지정이 가능하지만, Properties 는 키와 값 모두 String 타입으로 제한되어 있다. 주로 애플리케이션의 옵션 정보, DB 연결 정보 등을 읽어올 때 많이 사용된다.

 

 

또 Properties 객체를 활용하여 .properties 파일을 쉽게 읽어들일 수 있다. 

 

 

Properties properties = new Properties();
properties.load(new FileReader("..."));
String value = properties.getProperty("key");

 

 

 

TreeMap

 

TreeMap 은 이진 트리를 기반으로 한 Map 컬렉션으로, TreeSet 과의 차이점은 키와 값이 저장된 Entry 를 저장한다는 점이다. 부모 키값과 비교하여 키 값이 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장하는 방식을 사용한다. 

 

 

 

Stack

 

StackLIFO 를 구현한 클래스로 push(), pop(), peek() 메서드가 있다. 

 

 

 

Queue

 

QueueFIFO 를 구현한 클래스로 offer() (데이터 저장), peek() (객체 가져오기, 삭제 x), poll() (객체 가져오기, 삭제 o) 메서드를 사용할 수 있다. Queue 인터페이스를 구현한 대표적인 클래스는 LinkedList 이다. (List 컬렉션) 

 

 

static class Msg {
    public String command;
    public String to;
    public Msg(String command, String to) { this.command = command; this.to = to; }
}

public static void main(String[] args) {
    Queue<Msg> queue = new LinkedList<Msg>();
    queue.offer(new Msg("sendEmail", "H"));
    queue.offer(new Msg("sendSMS", "J"));
    queue.offer(new Msg("sendKatalk", "K"));

    while (!queue.isEmpty()) {
        Msg msg = queue.poll();
        switch (msg.command) {
            case "sendEmail":
                System.out.println("SENDING EMAIL");
                break;
            case "sendSMS":
                System.out.println("SENDING SMS");
                break;
            case "sendKatalk":
                System.out.println("SENDING KATALK");
                break;
        }
    }
}

 

 

result

 

 

 

컬렉션 프레임워크의 대부분의 클래스들은 싱글 스레드 환경에서 사용하도록 설정되어서, VectorHashTable 같은 객체 외에는 멀티 스레드 환경에서 안전하지 않다. 그 외의 클래스들을 멀티 스레드 환경에서 사용하기 위해서는, 비동기화된 메서드를 동기화된 메서드로 래핑해주는 CollectionssynchronizedXXX() 메서드를 사용해야 한다. 해당 메서드에 비동기화된 컬렉션을 대입하면 동기화된 컬렉션을 리턴한다. (synchronizedList(), synchronizedMap(), synchronizedSet())

 

 

또, 이러한 동기화된 컬렉션은 멀티스레드 환경에서 하나의 스레드가 요소를 안전하게 처리하도록 도와주지만, 전체 요소를 멀티 스레드로 빠르게 처리하지는 못한다. 하나의 스레드가 요소를 처리할 때 전체 잠금이 발생하여 다른 스레드는 대기 상태가 된다. 병렬적으로 처리하기 위해서는 java.util.concurrent 패키지의 ConcurrentHashMap, ConcurrentLinkedQueue 를 사용해야 한다. 각각 Map, Queue 구현클래스이다.

 

ConcurrentHashMap 을 사용하면 스레드에 안전하면서도 멀티스레드가 요소를 병렬적으로 처리할 수 있다. 이는 부분잠금을 통해 구현된다. (예를 들어 컬렉션에 10 개의 요소가 있을 때 1 개를 처리할 동안 10 개 전체를 다른 스레드가 처리하지 못하도록 하는게 전체 잠금이고, 처리하는 요소가 포함된 부분만 잠금하고 다른 부분은 스레드가 변경할 수 있게끔 하는 것이 부분 잠금) 

 

 

ConcurrentLinkedQueueLock-Free 알고리즘을 구현한 컬렉션이다. 이는 여러 개의 스레드가 동시 접근할 경우, 잠금을 사용하지 않고도 최소한 하나의 스레드가 안전하게 요소를 저장하거나 얻도록 해준다고 한다. 

 

 

 

 

 

Reference:

이것이 자바다

'Backend > Java' 카테고리의 다른 글

Java - IO 입출력  (0) 2023.05.20
Java - Stream  (0) 2023.05.19
Java - Multi Thread (4)  (0) 2023.05.14
Java - Multi Thread (3)  (0) 2023.05.13
Java - Map Compute, To JSON  (0) 2023.05.10