[Java] 컬렉션 프레임워크

Nakjoo·2023년 1월 5일
0

[SEB_BE_43]

목록 보기
14/29

컬렉션 프레임워크는 특정 자료구에 데이터를 추가하고, 삭제하고, 수정하고, 검색하는 등의 동작을 수행하는 편리한 메서드들을 제공해준다.

컬렉션 프레임워크의 구조


List

  • List는 데이터의 순서가 유지되며, 중복 저장이 가능한 컬렉션을 구현하는 데에 사용된다.
  • ArrayList, Vector, Stack, LinkedList 등이 List 인터페이스를 구현한다.

Set

  • Set은 데이터의 순서가 유지되지 않으며, 중복 저장이 불가능한 컬렉션을 구현하는 데에 사용된다.
  • HashSet, TreeSet 등이 Set 인터페이스를 구현한다.

Map

  • Map은 키(Key)와 값(Value)의 쌍으로 데이터를 저장하는 컬렉션을 구현하는 데에 사용된다.
  • 데이터의 순서가 유지되지 않으며, 키는 값을 식별하기 위해 사용되므로 중복 저장이 불가능하지만, 값은 중복 저장이 가능하다.
  • HashMap, HashTable, TreeMap, Properties 등

이 셋 중에서 List와 Set은 서로 공통점이 많아 위 그림과 같이 Collection이라는 인터페이스로 묶인다. 즉, 이 둘의 공통점이 추출되어 추상화한 것이 Collection이라는 인터페이스이다.

Collection 인터페이스


다음은 Collection 인터페이스의 메서드들이다.

기능리턴 타입메소드설명
객체 추가booleanadd(Object o) / addAll(Collection c)주어진 객체 및 컬렉션의 객체들을 컬렉션에 추가한다.
객체 검색booleancontains(Object o) / containsAll(Collection c)주어진 객체 및 컬렉션이 저장되어 있는지 여부를 리턴한다.
Iteratoriterator()컬렉션의 iterator를 리턴한다.
booleanequals(Object o)컬렉션이 동일한지 여부를 확인한다.
booleanisEmpty()컬렉션이 비어있는지 여부를 확인한다.
intsize()저장되어 있는 전체 객체 수를 리턴한다.
객체 삭제voidclear()컬렉션에 저장된 모든 객체 수를 리턴한다.
booleanremove(Object o) / removeAll(Collection c)주어진 객체 및 컬렉션을 삭제하고 성공 여부를 리턴한다.
booleanretainAll(Collection c)주어진 컬렉션을 제외한 모든 객체를 컬렉션에서 삭제하고, 컬렉션에 변화가 있는지의 여부를 리턴한다.
객체 변환Object[]toArray()컬렉션에 저장된 객체를 객체배열(Object[])로 반환한다.
Object[]toArray(Object[] a)주어진 배열에 컬렉션의 객체를 저장해서 반환한다.

List


List 인터페이스는 배열과 같이 객체를 일렬로 늘어놓은 구조를 가지고 있다. 객체를 인덱스로 관리하기 때문에 객체를 저장하면 자동으로 인덱스가 부여되고, 인덱스로 객체를 검색, 추가, 삭제할 수 있는 등의 여러 기능을 제공한다.

List 인터페이스에서 공통적으로 사용 가능한 메서드는 다음과 같다. 위에 있는 컬렉션 인터페이스의 메서드 또한 상속받아 사용이 가능하다.

기능리턴 타입메서드설명
기능 추가voidadd(int index, Object element)주어진 인덱스에 객체를 추가
booleanaddAll(int index, Collection c)주어진 인덱스에 컬렉션을 추가
Objectset(int index, Object element)주어진 위치에 객체를 저장
객체 검색Objectget(int index)주어진 인덱스에 저장된 객체를 반환
intindexOf(Object o) / lastIndexOf(Object o)순방향 / 역방향으로 탐색하여 주어진 객체의 위치를 반환
ListIteratorlistIterator() / listIterator(int index)List의 객체를 탐색할 수 있는ListIterator 반환 / 주어진 index부터 탐색할 수 있는 ListIterator 반환
ListsubList(int fromIndex, int toIndex)fromIndex부터 toIndex에 있는 객체를 반환
객체 삭제Objectremove(int index)주어진 인덱스에 저장된 객체를 삭제하고 삭제된 객체를 반환
booleanremove(Object o)주어진 객체를 삭제
객체 정렬voidsort(Comparator c)주어진 비교자(comparator)로 List를 정렬

List 인터페이스를 구현한 클래스로는 ArrayList, Vector, LinkedList, Stack 등이 있다. 이 중에서 가장 중요하며 많이 사용되는 ArrayList와 LinkedList에 대해서 알아보자.

ArrayList


ArrayList는 List 인터페이스를 구현한 클래스로, 컬렉션 프레임워크에서 가장 많이 사용된다. 기능적으로는 Vector와 동일하지만 기존의 Vector를 개선한 것이므로, Vector보다는 주로 ArrayList를 사용한다.

ArrayList에 객체를 추가하면 객체가 인덱스로 관리된다는 점에서 배열과 유사하다. 그러나 배열은 생성될 때 크기가 고정되며, 크기를 변경할 수 없는 반면, ArrayList는 저장 용량을 초과하여 객체들이 추가되면, 자동으로 저장용량이 늘어나게 된다. 또한, 리스트 계열 자료구조의 특성을 이어받아 데이터가 연속적으로 존재한다. 즉 데이터의 순서를 유지한다.

ArrayList를 생성하기 위해서는 저장할 객체 타입을 타입 매개변수, 즉 제네릭으로 표기하고 기본 생성자를 호출한다.

ArrayList<타입 매개변수> 객체명 = new ArrayList<타입 매개변수>(초기 저장 용량);

ArrayList<String> container1 = new ArrayList<String>();
// String 타입의 객체를 저장하는 ArrayList 생성
// 초기 용량이 인자로 전달되지 않으면 기본적으로 10으로 지정된다. 

ArrayList<String> container2 = new ArrayList<String>(30);
// String 타입의 객체를 저장하는 ArrayList 생성
// 초기 용량을 30으로 지정했다. 

ArrayList에 객체를 추가하면 인덱스 0부터 차례대로 저장된다. 그리고 특정 인덱스의 객체를 제거하면, 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨진다.

따라서 빈번한 객체 삭제와 삽입이 일어나는 곳에서는 ArrayList보다는 이후에 배우게 되는 LinkedList를 사용하는 것이 좋다.

다음은 ArrayList에 String 객체를 추가, 검색, 삭제하는 예제다.

public class ArrayListExample {
	public static void main(String[] args) {

		// ArrayList를 생성하여 list에 할당
		ArrayList<String> list = new ArrayList<String>();

		// String 타입의 데이터를 ArrayList에 추가
		list.add("Java");
		list.add("egg");
		list.add("tree");

		// 저장된 총 객체 수 얻기
		int size = list.size(); 

		// 0번 인덱스의 객체 얻기
		String skill = list.get(0);

		// 저장된 총 객체 수 만큼 조회
		for(int i = 0; i < list.size(); i++){
			String str = list.get(i);
			System.out.println(i + ":" + str);
		}

		// for-each문으로 순회 
		for (String str: list) {
			System.out.println(str);
		}		

		// 0번 인덱스 객체 삭제
		list.remove(0);
	}
}

LinkedList


LinkedList 컬렉션은 데이터를 효율적으로 추가, 삭제, 변경하기 위해 사용한다. 배열에는 모든 데이터가 연속적으로 존재하지만, LinkedList에는 불연속적으로 존재하며, 이 데이터는 서로 연결(link)되어 있다.

LinkedList의 각 요소(node)들은 자신과 연결된 이전 요소 및 다음 요소의 주소값과 데이터로 구성되어 있다.

LinkedList에서 데이터를 삭제하려면, 삭제하고자 하는 요소의 이전 요소가 삭제하고자 하는 요소의 다음 요소를 참조하도록 변경하면 된다. 링크를 끊어주는 방식이라고 생각하면 된다. 배열처럼 데이터를 이동하기 위해 복사할 필요가 없기 때문에 처리 속도가 훨씬 빠르다.

데이터를 추가할 때에도 마찬가지로, 새로운 요소를 추가하고자 하는 위치의 이전 요소와 다음 요소 사이에 연결해주면 된다. 즉, 이전 요소가 새로운 요소를 참조하고, 새로운 요소가 다음 요소를 참조하게 만드는 것.

Iterator


Iterator는 컬렉션에 저장된 요소들을 순차적으로 읽어오는 역할을 한다.

이러한 Iterator의 컬렉션 순회 기능은 Iterator 인터페이스에 정의되어져 있으며, Collection 인터페이스에는 Iterator 인터페이스를 구현한 클래스의 인스턴스를 반환하는 메서드인 iterator()가 정의되어져 있다.

즉, Collection 인터페이스에 정의된 iterator()를 호출하면, Iterator 타입의 인스턴스가 반환된다.

따라서 Collection 인터페이스를 상속받는 List와 Set 인터페이스를 구현한 클래스들은 iterator() 메서드를 사용할 수 있다.

다음은 Iterator 인터페이스에 정의된 메서드로, iterator()를 통해 만들어진 인스턴스는 아래의 메서드를 사용할 수 있다.

메서드설명
hasNext()읽어올 객체가 남아 있으면 true를 리턴하고, 없으면 false를 리턴한다.
next()컬렉션에서 하나의 객체를 읽어옵니다. 이 때, next()를 호출하기 전에 hasNext()를 통해 읽어올 다음 요소가 있는지 먼저 확인해야 합니다.
remove()next()를 통해 읽어온 객체를 삭제합니다. next()를 호출한 다음에 remove()를 호출해야 합니다.

다음은 Iterator를 활용한 예제이다.

ArrayList<String> list = ...;
Iterator<String> iterator = list.iterator();

while(iterator.hasNext()) { // 다음 객체가 있다면
	String str = iterator.next(); // 객체를 읽어오고,
    if (str.equals("str과 같은 단어")) { // 조건에 부합한다면
    	iterator.remove() // 해당 객체를 컬렉션에서 제거
    }
}

Set


Set은 요소의 중복을 허용하지 않고, 저장 순서를 유지하지 않는 컬렉션이다. 대표적인 Set을 구현한 클래스에서는 HashSet, TreeSet이 있다.

Set 인터페이스에 정의된 메서드들은 다음과 같습니다.

기능리턴 타입메서드설명
객체 추가booleanadd(Object o)주어진 객체를 추가하고, 성공하면 true, 중복 객체면 false를 반환한다.
객체 검색booleancontains(Object o)주어진 객체가 Set에 존재하는지 확인한다.
booleanisEmpty()Set이 비어있는지 확인한다.
IteratorIterator()저장된 객체를 하나씩 읽어오는 반복자를 리턴한다.
intsize()Set에 저장되어져 있는 모든 객체를 삭제한다.
객체 삭제voidclear()
booleanremove(Object o)주어진 객체를 삭제한다.

HashSet


HashSet은 Set 인터페이스를 구현한 가장 대표적인 컬렉션 클래스이다. 따라서, Set 인터페이스의 특성을 그대로 물려받으므로 중복된 값을 허용하지 않으며, 저장 순서를 유지하지 않는다.

HashSet에 값을 추가할 때, 해당 값이 중복된 값인지 어떻게 판단할까? 그 과정을 간단하게 설명하면 아래와 같다.

  1. add(Object o)를 통해 객체를 저장하고자 한다.

  2. 이 때, 저장하고자 하는 객체의 해시코드를 hashCode() 메서드를 통해 얻어낸다.

  3. Set이 저장하고 있는 모든 객체들의 해시코드를 hashCode() 메서드로 얻어낸다.

  4. 저장하고자 하는 객체의 해시코드와, Set에 이미 저장되어져 있던 객체들의 해시코드를 비교하여, 같은 해시코드가 있는지 검사한다.

    a. 이 때, 만약 같은 해시코드를 가진 객체가 존재한다면 아래의 5번으로 넘어간다.

    b. 같은 해시코드를 가진 객체가 존재하지 않는다면, Set에 객체가 추가되며 add(Object o) 메서드가 true를 리턴한다.

  5. equals() 메서드를 통해 객체를 비교한다.

    a. true가 리턴된다면 중복 객체로 간주되어 Set에 추가되지 않으며, add(Object o)가 false를 리턴한다.

    b. false가 리턴된다면 Set에 객체가 추가되며, add(Object o) 메서드가 true를 리턴한다.

아래 예제는 Set을 활용한 간단한 예제이다.

import java.util.*;

public class Main {
    public static void main(String[] args) {

				// HashSet 생성
        HashSet<String > languages = new HashSet<String>();

				// HashSet에 객체 추가
        languages.add("Java"); 
        languages.add("Python");
        languages.add("Javascript");
        languages.add("C++");
        languages.add("Kotlin");
        languages.add("Ruby");
        languages.add("Java"); // 중복

				// 반복자 생성하여 it에 할당
        Iterator it = languages.iterator();

				// 반복자를 통해 HashSet을 순회하며 각 요소들을 출력
        while(it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
// 출력값
Java
Python
Javascript
C++
Kotlin
Ruby

TreeSet


TreeSet은 이진 탐색 트리 형태로 데이터를 저장한다. 데이터의 중복 저장을 허용하지 않고 저장 순서를 유지하니 않는 Set 인터페이스의 특징은 그대로 유지된다.

이진 탐색 트리(Binary Search Tree)란 하나의 부모 노드가 최대 두 개의 자식 노드와 연결되는 이진 트리(Binary Tree)의 일종으로, 정렬과 검색에 특화된 자료 구조이다.

이 때 최상위 노드를 '루트'라고 한다.

이진 탐색 트리는 모든 왼쪽 자식의 값이 루트나 부모보다 작고, 모든 오른쪽 자식의 값이 루트나 부모보다 큰 값을 가지는 특징이 있다.

Map


Map 인터페이스는 키(key)와 값(value)으로 구성된 객체를 저장하는 구조를 가지고 있다. 여기서 이 객체를 Entry 객체라고 하는데, 이 Entry 객체는 키와 값을 각각 Key 객체와 Value 객체로 저장한다.

Map은 키를 중복 저장할 수 없지만, 값은 중복 저장 가능하다. 이는 키의 역할이 값을 식별하는 것이기 때문이다.

만약 기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값이 새로운 값으로 대치된다.

Map 인터페이스를 구현한 클래스에는 HashMap, HashTable, TreeMap, SortedMap 등이 있다.

다음은 Map 인터페이스를 구현한 클래스에서 공통적으로 사용 가능한 메서드다. List가 인덱스를 기준으로 관리되는 반면에, Map은 키(key)로 객체들을 관리하기 때문에 키를 매개값으로 갖는 메서드가 많다.

기능리턴 타입메서드설명
객체 추가Objectput(Object key, Object value)주어진 키로 값을 저장합니다. 해당 키가 새로운 키일 경우 null을 리턴하지만, 동일한 키가 있을 경우에는 기존의 값을 대체하고 대체되기 이전의 값을 리턴합니다.
객체 검색booleancontainsKey(Object key)주어진 키가 있으면 true, 없으면 false를 리턴합니다.
booleancontainsValue(Object value)주어진 값이 있으면 true, 없으면 false를 리턴합니다.
SetentrySet()주어진 값이 있으면 true, 없으면 false를 리턴합니다.
Objectget(Object key)주어진 키에 해당하는 값을 리턴합니다.
booleanisEmpty()컬렉션이 비어 있는지 확인합니다.
SetkeySet()모든 키를 Set 객체에 담아서 리턴합니다.
intsize()저장된 Entry 객체의 총 갯수를 리턴합니다.
Collectionvalues()저장된 Entry 객체의 총 갯수를 리턴합니다.
객체 삭제voidclear()모든 Map.Entry(키와 값)을 삭제합니다.
Objectremove(Object key)주어진 키와 일치하는 Map.Entry를 삭제하고 값을 리턴합니다.

HashMap


HashMap은 Map 인터페이스를 구현한 대표적인 클래스다. HashMap은 아래 그림과 같이 키와 값으로 구성된 객체를 저장하는데, 이 객체를 Entry 객체라고 한다.

HashMap은 해시 함수를 통해 '키'와 '값'이 저장되는 위치를 결정하므로, 사용자는 그 위치를 알 수 없고, 삽입되는 순서와 위치 또한 관계가 없다.

이렇게, HashMap은 이름 그대로 해싱(Hashing)을 사용하기 때문에 많은 양의 데이터를 검색하는 데 있어서 뛰어난 성능을 보인다.

또한, HashMap의 개별 요소가 되는 Entry 객체는 Map 인터페이스의 내부 인터페이스인 Entry 인터페이스를 구현하며, Map.Entry 인터페이스에는 다음과 같은 메서드가 정의되어져 있다.

리턴 타입메서드설명
booleanequals(Object o)동일한 Entry 객체인지 비교한다.
ObjectgetKey()Entry 객체의 Key 객체를 반환한다.
ObjectgetValue()Entry 객체의 Value 객체를 반환한다.
inthashCode()Entry 객체의 해시코드를 반환한다.
ObjectsetValue(Object value)Entry 객체의 Value 객체를 인자로 전달한 value 객체로 바꾼다.

HashMap을 생성할 때에는 아래와 같이 키와 값의 타입을 따로 지정해주어야 한다.

HashMap<String, Integer> hashmap = new HashMap<>();

아래의 예제를 통해 메서드 사용 방법을 알 수 있다.

import java.util.*;

public class HashMapExample {
    public static void main(String[] args) {

	    // HashMap 생성
        HashMap<String, Integer> map = new HashMap<>();

        // Entry 객체 저장
        map.put("피카츄", 85);
        map.put("꼬부기", 95);
        map.put("야도란", 75);
        map.put("파이리", 65);
        map.put("피존투", 15);

        // 저장된 총 Entry 수 얻기
        System.out.println("총 entry 수: " + map.size());

        // 객체 찾기
        System.out.println("파이리 : " + map.get("파이리"));
				
        // key를 요소로 가지는 Set을 생성 -> 아래에서 순회하기 위해 필요합니다. 
        Set<String> keySet = map.keySet();

        // keySet을 순회하면서 value를 읽어옵니다. 
        Iterator<String> keyIterator = keySet.iterator();
        while(keyIterator.hasNext()) {
            String key = keyIterator.next();
            Integer value = map.get(key);
            System.out.println(key + " : " + value);
        }

        // 객체 삭제
        map.remove("피존투");

        System.out.println("총 entry 수: " + map.size());

        // Entry 객체를 요소로 가지는 Set을 생성 -> 아래에서 순회하기 위해 필요합니다. 
        Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

        // entrySet을 순회하면서 value를 읽어옵니다. 
        Iterator<Map.Entry<String, Integer>> entryIterator = entrySet.iterator();
        while(entryIterator.hasNext()) {
            Map.Entry<String, Integer> entry = entryIterator.next();
            String key = entry.getKey(); // Map.Entry 인터페이스의 메서드
            Integer value = entry.getValue(); // Map.Entry 인터페이스의 메서드
            System.out.println(key + " : " + value);
        }

        // 객체 전체 삭제
        map.clear();
    }
}

Map은 키와 값을 쌍으로 저장하기 때문에 iterator()를 직접 호출할 수 없다. 그 대신 keySet() 이나 entrySet() 메서드를 이용해 Set 형태로 반환된 컬렉션에 iterator()를 호출하여 반복자를 만든 후, 반복자를 통해 순회할 수 있다.

0개의 댓글