wallnut's 2020. 4. 28. 19:31

컬렉션 프레임워크(Java Collection Framework(JCF))

컬렉션 이란 사전적 의미로 요소를 수집해서 저장하는 것을 의미합니다.

자바 컬렉션은 객체를 수집해서 저장하는 역할을 하며 자바 컬렉션 프레임워크는 몇 가지 인터페이스를 통해서 다양한 컬렉션 클래스를 이용할 수 있도록 할 수 있으며. 컬렉션 프레임워크의 주요 인터페이스로는 List, Set, Map이 있습니다. 이 인터페이스들은 컬렉션을 사용하는 방법을 정의하는 것인데 다음은 인터페이스로 사용 가능한 컬렉션 클래스를 보여줍니다.

위의 사진을 보면 List와 Set은 객체 추가,삭제 검색하는 방법에 많은 공통점이 있기 때문에 인터베이스들의 공통된 메소들만 모아 Collection인터 페이스로 정의해 두고 있습니다 Map은 키와 같은 하나의 쌍으로 묶어서 관리하는 구조로 되어있어 List 및 Set과 사용방법이 완전히 다릅니다.

List 컬렉션

List 컬렉션은 객체를 일렬로 늘어놓은 구조를 가지고 있습니다. 객체를 인덱스로 관리하기 때문에 객체를 저장하면 자동 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공합니다. List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있는데 다음은 List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스의 메서드들입니다. 인덱스로 객체를 관리하기 때문에 인덱스를 매게 값으로 갖눈 메서드가 많습니다.

객체 추가

객체 검색

객체 삭제

ArrayList

ArrayList는 List 인터페이스의 구현 클래스로 ArrayList에 객체를 추가하면 객체가 인덱스로 관리됩니다. 일반배열과 ArrayList는 인덱스로 객체를 관리한다는 점에서는 유사하지만 큰 차이점을 가지고 있습니다. 배열은 생성할 때 크기가 고정되고 사용 중에 크기를 변경할 수 없지만 ArrayList는 저장 용량을 초과한 객체들이 들어오면 자동적으로 저장용량이 늘어난다. 다음은 ArrayList 객체의 내부 구조를 보여줍니다.

검사 버튼 삭제 버튼 초기에 Array List를 선언하면 10개의 객체를 저장할 수 있는 List가 선언이 된다 만약 초기부터 용량을 크게 선언하고 싶다면 아래와 같이 선언하면 됩니다.

List<String> list = new ArrayList<String>(30);

ArrayList에 객체를 저장하면 인덱스 0 부터 차례대로 저장됩니다. ArrayList에서 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨집니다. 마찬가지로 특정 인덱스에 객체를 삽입하면 해당 인덱스부터 마지막 인덱스까지 모두 1씩 밀려나게 됩니다. 다음은 4번 인덱스가 제거되었을 때 5번 인덱스부터 모두 앞으로 1씩 당겨지는 모습을 보여줍니다.

 

위와 같이 List안에서 노드 가 삭제가 되면 앞으로 하나하나 다 밀려나야 합니다. 따라서 빈번한 객체 삭제와 삽입이 일어나 나는 곳에서는 ArrayList를 사용하지 않고 삽입 삭제에 용이한 LinkedList를 사용하는 것이 좋습니다. 그러나 인덱스 검색이나 마지막 노드 추가에서는 ArrayList가 더 좋은 성능을 발휘합니다.

 

Vector

Vector는 ArrayList와 동일한 내부 구조를 가지고 있습니다 Vector를 생성하기 위해서는 지정할 객체 타입을 타입 파라미터로 표기하고 기본 생성자를 호출하면 됩니다.

List<String> list = new ArrayList<String>(30);

ArrayList와 다른점은 Vector는 동기화된 메서드로 구성되어 있기 때문에 멀티 스레드가 동시에 이 메서드들을 실행할 수 없으며 하나의 스레드가 실행을 완료해야만 다른 스레드가 실행할 수 있습니다. 그래서 멀티 스레드 환경에서 안전하게 객체를 추가하거나 삭제할 수 있습니다.

LinkedList

LinkedList는 List 구현 클래스이므로 ArrayList와 사용방법은 같지만 내부 구조는 완전 다릅니다. ArrayList는 내부 배열 객체를 저장해서 인덱스로 관리하지만, LinkedList는 인접 참조를 링크해서 체인처럼 관리합니다.

LinkedList에서 특정 인덱스의 객체를 제거하면 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않으며 특정 인덱스에 객체를 삽입할 때에도 마찬가지다. 그렇기 때문에 빈번한 객체를 삭제할 때는 LinkedList가 성능이 좋습니다. 왜냐하면 중간에서 하나 삭제를 하더라도 앞뒤의 노드만 연결해주면 되기 때문입니다.
LinkedList는 생성하기 위해서는 저장할 객체 타입 파라미터(E)에 표기하고 기본 생성자로 호출하면 됩니다. 기본적으로 LinkedList는 처음 생성될 때에는 어떠한 링크도 만들어지지 않기 때문에 내부적으로 비어있습니다.

List<E> list = new LinkedList<E>();

Set 컬렉션

List 컬렉션은 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않습니다. 또한 객체를 중복해서 저장할 수 없고 하나의 null만 저장할 수 있습니다. Set 컬렉션은 수학의 집합에 비유될 수 있는데 집합은 순서와 상관없고 중복이 허용되지 않기 때문입니다.

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

Set 컬렉션은 인덱스로 객체를 검색해서 가져오는 메서드가 없습니다. 대신 전체 객체를 대상으로 한 번씩 반복해서 가져오는 반복자(Iterator)를 제공합니다. 반복자는 Iterator 인터페이스를 구현한 객체를 말하는데, itorator() 메서드를 호출하면 얻을 수 있습니다.

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

다음은 Iterator 인터페이스에 선언된 메서드들입니다.

Iterator에서 하나의 객체를 가져올 때는 next() 메서드를 사용합니다. next() 메소드를 사용하기전에 먼저 가져올 객체가 있는지 확인하는것이 좋으며 hasNext() 메소드는 가져올 객체가 있으면 true를 리턴하고 더이상 가져올 객체가 없으면 false를 리턴합니다. 따라서 true가 리턴될때 next() 메소드를 사용해야 합니다. 다음은 Set을 이용한 루핑 방법이다.

여기서 루핑이란 요소 전체를 반복하는 것을 의미합니다

방법 1. Iterator를 사용한 방법

Set<String> set = ...;
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){ // has가 다음 객체가있는지 체크해주니까 있는 만큼 루핑된다.
  String str = iterator.next();

방법 2. For문 사용

for(String str : set){ // 저장된 객체수만큼 루핑한다.
}

 

HashSet

HashSet은 Set 인터페이스의 구현 클래스입니다. HashSet을 생성하기 위해서는 다음과 같이 기본 생성자를 호출하면 됩니다.

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

HashSet은 Set의 성격대로 순서 상관없이 저장되고, 중복 저장되지 않습니다. HashSet이 판단하는 동일한 객체란 꼭 같은 인스턴스를 뜻하지 않습니다. HashSet은 객체를 저장하기 전에 먼저 객체의 HashCode()메서드를 호출해서 해시코드를 얻습니다. 그리고 이미 저장되어 있는 객체의 hashCode() 메소드를 호출해서 해시코드를 얻어냅니다. 그리고 이미저장되어 있는 객체들의 해시코드와 비교 하고. 만약 equals()메소드로 두 객체를 비교해서 true가 나오면 동일한 객체로 판단하고 중복 저장을 하지 않습니다.

문자열을 HashSet에 저장할 경우 깊은 문자열을 갖는 String 객체는 동등한 객체로 간주되고 다른 문자열을 갖는 String 객체는 다른 객체로 간주됩니다 그 이유는 String 클래스가 hashCode()와 equals() 메소드를 재정의해서 같은 문자열일 경우 hashCode()의 리턴 값을 같게 equals()의 리턴 값은 true가 나오도록 오버 라이딩이 돼있기 때문입니다. 물론 자신이 정의 한 클래스에도 똑같이 적용할 수 있습니다

 

예를 들어 Member 클래스를 정의했다고 가정했을 때 이 MemberClass에는 인스턴스가 달라도 이름과 나이가 동일합니다 면 동등 객체라고 equals()와 hashCode를 정의했다고 치자. 그리고 아래와 같이 Set에 객체를 넣는다고 해보겠습니다.

Set<Member> set = new HashSet<Member>();
set.add(new Member("홍길동", 30));
set.add(new Member("홍길동", 30));
System.out.println("총 객체수 : " + set.size()); 저장된 객체 수 얻기 // 사이즈 1

위와 같이 Set에 이름과 나이가 같은 객체를 두 개 넣더라도 실제적으로 set에는 1개의 객체만 들어가게 됩니다.

Map 컬렉션

Map 컬렉션은 키(Key)와 값(value으로 구성된 Entry 객체를 저장하는 구조를 가지고 있습니다. 여기서 키와 값은 모두 객체입니다. 키는 중복 저장될 수 없지만 값은 중복 저장될 수 있습니다. 키값이 중복되어 저장된다면 먼저 저장된 값은 저장되지 않습니다.

Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, TreeMap 등이 있습니다. 다음은 Map컬렉션에서 공통적으로 사용 가능한 Map 인터페이스의 메서드들입니다. 키로 객체들을 관리하기 때문에 키를 매개 값으로 갖는 메서드가 많습니다.

Map에서는 키를 알고 싶다면 get() 메소 도로 객체를 얻어낼 수 있습니다. 하지만 맵 안에 데이터를 하나하나 얻어 내고 싶다면 두 가지 방법이 있습니다. 먼저 아래는 Key를 Set 타입으로 뽑아서 하나하나 Iterator 시켜 그 key 값으로 value를 얻는 방법이 있을 수 있습니다.

Map<String, String> map = new HashMap<>();

final Set<String> strings = map.keySet();
final Iterator<String> iterator = strings.iterator();
while(iterator.hasNext()){
    String key = iterator.next();
    String value = map.get(key);
}

두 번째 방법은 entrySet을 통해 Entry객체를 Set타입으로 뽑아서 key와 value를 동시에 얻는 방법이 있을 수 있습니다.

Map<String, String> map = new HashMap<>();

final Set<Map.Entry<String, String>> entries = map.entrySet();
final Iterator<Map.Entry<String, String>> iterator = entries.iterator();
while(iterator.hasNext()){
    final Map.Entry<String, String> mapEntry = iterator.next();
    mapEntry.getKey();
    mapEntry.getValue()
}

HashMap

HashMap의 키로 사용할 객체는 HashCode와 equals() 메서드를 재정의해서 동등 객체가 될 조건을 정해야 합니다. 동등 객체, 즉 동일한 키가 될 조건은 hashCode()의 리턴 값이 같아야 하고, equals() 메서드가 true를 리턴해야합니다. 동등 객체, 즉 동일한 키가 될 조건은 hashCoDE()의 리턴값이 같아야하고 equals()메소드가 true를 리턴해야 합니다.

주로 키 타입으로 String을 많이 사용하는데 String은 글자가 다르면 hashCode, equals 메서드가 다르도록 재정의가 돼 있기 때문에 키로 적합합니다. 해쉬 맵은 아래와 같이 기타 입과 value 타입을 파라미터로 주고 기본 생성자로 선언할 수 있습니다.

Map<K, V> map = new HashMap<K, V>();

Hashtable

Hashtable은 HashMap과 동일한 내부구조를 가집니다. HashMap과 차이점은 Hashtable은 동기화된(synchronized) 메서드로 구성되어 있기 때문에 멀티스레드가 동시에 이 메서드들을 실행할 수는 없고, 하나의 스레드가 실행을 완료해야만 다른 스레드를 실행할 수 있습니다.

Properties

Properties는 Hashtable의 하위 클래스이기 때문에 HashTable의 모든 특징을 그대로 가지고 있습니다. Hashtable은 키와 값을 다양한 타입으로 지정이 가능한데 비해 Properties는 키와 값을 String 타입으로 제한한 컬렉션입니다. Properties는 애플리케이션의 옵션 정보, 데이터베이스 연결 정보 그리고 국제화(다국어) 정보가 저장된 프로퍼티(~. properties) 파일을 읽을 때 주로 사용합니다.

프로퍼티 파일은 키와 값이 = 기호로 연결되어 있는 텍스트 파일로 ISO 8859-1 문자셋으로 저장이 됩니다. 이 문자셋으로 직접 표현할 수 없는 한글은 유니코드로 변환되어 저장됩니다.

검색 기능을 강화시킨 컬렉션

컬렉션 프레임워크는 검색 기능을 강화시킨 TreeSet과 TreeMap을 제공하고 있습니다. 이름에서 알 수 있듯이 TreeSet은 Set 컬렉션이고, TreeMap은 Map 컬렉션입니다. 이 컬렉션들은 이진트리를 이용해서 계층적(Tree 구조)를 가지면서 객체를 저장합니다.

이진 트린 구조

이진트리는 여러 개의 노드가 트리 형태로 이루어진 연결된 구조로, 루트 노드라고 불리는 하나의 노드에서부터 시작해서 각 노드에 최대 2개의 노드를 연결할 수 있는 구조를 가지고 있습니다.

위아래로 연결된 두 노드를 부모 - 자식 관계에 있다고 하며 위의 노드를 부모 노드, 아래 노드를 자식 노드라고 합니다. 하나의 노드는 최대 2개의 자식 노드와 연결될 수 있습니다. 이진트리는 부모 노드의 값보다 작은 노드는 왼쪽에 위치시키고, 부모 노드의 값보다 큰 노드는 오른쪽에 위치시킵니다. 예를 들어 6,3,9,2,5의 순서로 값을 저장하면 다음과 같은 순서로 진행됩니다.

작은 값은 왼쪽에, 큰 값은 오른쪽에 저장합니다. 숫자가 아닌 문자가 저장할경우네느 문자의 유니코드 값으로 비교합니다. 이진트리가 범위 검색을 쉽게 할 수 있는 이유는 아래와 같이 값들이 정렬되어 있어 그룹핑이 쉽기 때문입니다.

TreeSet

TreeSet은 이진트리를 기반으로 한 Set 컬렉션입니다. 하나의 노드는 노드 값인 value와 왼쪽과 자식 노드를 참조하기 위한 두 개의 변수로 구성됩니다. TreeSet에 객체를 저장하면 자동으로 정렬되는데 부모 값과 비교해서 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장합니다.

 

 

TreeSet을 생성하기 위해서는 저장할 객체 타입을 파라미터로 표기하고 기본 생성자를 호출하면 됩니다.

TreeSet<E> treeSet = new TreeSet<E>();

Set 인터페이스 타입 변수에 대입해도 되지만 TreeSet클래스 타입으로 대입한 이유는 객체를 찾거나 범위 검색과 관련된 메서드를 사용하기 위해서입니다. 다음은 TreeSet이 가지고 있는 검색 관련된 메서드들입니다.

TreeMap

TreeMap은 이진트리를 기반으로 한 Map 컬렉션입니다. TreeSet 과의 차이점은 키와 값이 저장된 MapEntry를 저장한다는 점입니다. TreeMap에 객체를 저장하면 자동으로 정렬되는데 기본적으로 부모 키값과 비교해서 키값이 낮은 것은 왼쪽 자식 노드에, 키값이 높은 것은 오른쪽 자식 노드에 Map.Entry 객체를 저장합니다.

TreeMap을 생성하기 위해서는 키로 저장할 객체 타입과 값으로 저장할 객체 타입을 타입 파라미터로 주고 기본 생성자를 호출하면 됩니다.

TreeMap<String, Integer> treeMap = new TreeMap<String, Integer>();

다음은 TreeMap 가지고 있는 정렬과 관련된 메서드들입니다.

descendingMap 메서드를 호출하면 내림차순 NavigableMap타입의 객체를 리턴해주는데 만약 한번 더호출하게 되면 오름차순 객체를 리턴합니다.

NavigableMap<K, V> descendingMap = treeMap.descendingMap();
NavigableMap<K, V> ascendingMap = descendingMap.descendingMap();

여기에서 간단하게 SubMap() 메서드의 사용방법을 간단히 보면. subMap() 메서드는 네 개의 매개 변수가 있는데, 시작 키와 끝키, 그리고 이 키들의 Map.Entry를 포함할지 여부의 boolean값을 받습니다. 이것을 기반으로 그 해당 범위 내의 Map을 리턴해줍니다.

 

Comparable과 Compartor

TreeSet의 객체와 TreeMap의 키는 저장과 동시에 자동 오름차순으로 정렬되는데, 숫자일 경우 값으로 정렬하고, 문자열일 경우에는 유니코드로 정렬합니다. TreeSet과 TreeMap은 이진트리로 크기를 비교해 트리구조를 구성해야 하기 때문에 java.lang.Comparable 인터페이스를 구현해야 쓸 수 있습니다. 기본적으로 Wrraper Class들은 Comparable 인터페이스가 구현되어 있어 TreeSet과 TreeMap을 사용할 수 있습니다.

또한 ArrayList 등의 자료구조를 Collection.sort() 함수를 사용할 때도 Comparable을 이용해 compareTo() 메서드만 구현하게되면 쉽게 Collection프레임워크의 자료구조들을 정렬하여 사용할 수 있습니다. 쉽게 Coparable을 implements를 하여 compareTo()메소드만 정의해주면 됩니다. 아래의 설명을 참조하여 compareTo()를 재정의 해주면 됩니다.

다음은 나이를 기준으로 Person객체를 오름차순으로 정렬하기 위해 Comparable 인터페이스를 구현한 것입니다. 나이가 적을 경우는 -1을 동일한 경우는 0을 클경우는 1을 리턴하도록 하여 compareTo() 메서드를 재정의하여 treeSet을 이용하여 유저를 출력하였습니다.

 

위에서 말했듯이 TreeSet, TreeMap을 사용할 때의 키가 Comparable을 구현하고 있지 않을 경우에는 저장하는 순간 ClassCastException이 발생합니다. 이진트리를 구성하기 위해서는 값 비교는 필 수입니다. 그렇다면 객체에 Comparable 구현체를 구현하는 방법 말고 정렬하는 방법은 없을까… 그 방법은 Comparator 인터페이스입니다. 그런데 결국에는 둘 다 비슷한 방법이라고 생각합니다.

TreeSet<E> treeSet = new TreeSet<E>(new AscendingComparator());

TreeMap<K,V> treeMap = new TreeMap<K, V> (new DescendingComparator());

정렬자는 Comparator 인터페이스를 구현한 객체를 말하는데, Comparator 인터페이스는 다음과 같이 메서드가 정의되어 있습니다.

LIFO와 FIFO 컬렉션

후입 선출(LIFO : Last In First Out)은 나중에 넣은 객체가 먼저 빠져나가는 자료구조를 말합니다. 반대로 선입 선출(FIFO : First In First Out)은 먼저 넣은 객체가 먼저 빠져나가는 구조를 말합니다. 컬렉션 프레임워크에는 LIFO 자료구조를 제공하는 스택(Stack) 클래스와 FIFO 자료구조를 제공하는 (Queue) 인터페이스를 제공하고 있습니다. 다음은 스택과 큐의 구조를 보여줍니다.

스택을 응용한 대표적인 예가 프로그램들의 함수를 저장하는 공간인 스택 공간일 수가 있습니다. 스택 메모리에 저장된 변수는 나중에 저장된 것부터 저장이 됩니다. 큐를 응용한 대표적인 예가 스레드 풀의 작업입니다. 작업 큐는 먼저 들어온 작업부터 처리합니다.

Stack

Stack 클래스는 LIFO 자료구조를 구현한 클래스입니다. 다음은 Stack 클래스의 주요 메서드들입니다.

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

 

Queue

Queue인터페이스는 FIFO자료구조에서 사용되는 메서드를 정의하고 있습니다. 다음은 Queue 인터페이스에 정의되어 있는 메소드를 보여줍니다

Queue 인터페이스를 구현한 대표적인 클래스는 LinkedList입니다. LinkedList는 List인터페이스를 구현했기 때문에 List 컬렉션이기도 합니다. 다음 코드는 LinkedList 객체를 Queue 인터페이스 타입으로 변환한 것입니다.

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