[TIL #15-2][JAVA] Collection Framework, List, Set, Map
코드스테이츠 백엔드 부트캠프 39기 23~24일차, 05/17~18
Collection Framework
자바에서 Collection Framework는, 널리 알려져 있는 자료 구조를 바탕으로 데이터들을 효율적으로 추가, 삭제, 검색할 수 있도록 컬렉션을 만들고 관련된 인터페이스와 클래스들을 포함시켜둔 것이다. Collection Framework가 제공하는 다양한 인터페이스와 구현 클래스를 활용하면 객체 지향적이고 재사용성이 높은 코드를 작성할 수 있다.
주요 인터페이스로 List , Set, Map 이 있다. List 와 Set 은 객체를 추가, 삭제, 검색하는 방법에 공통점이 많아 둘의 공통된 메소드만 모아 Collection 이라는 인터페이스로 정의해두었다. Map 은 Key-Value 쌍으로 묶어 관리하는 구조로, List 와 Set 과는 사용법이 달라 따로 독립된 인터페이스로 정의해두었다.
추가적으로, 컬렉션 인터페이스 상위에는 Iterable 인터페이스가 있다.
다음은 각 인터페이스의 특징과 이를 구현한 클래스이다.
인터페이스 | 특징 | 구현 클래스 |
List | 순서를 유지하고 저장, 중복저장 가능 <순서O,중복O> | ArrayList, Vector, Stack, LinkedList 등 |
Set | 순서를 유지하지 않고 저장, 중복저장 불가능 <순서X,중복X> | HashSet, TreeSet 등 |
Map | 키(key)와 값(value)의 쌍으로 저장, 순서 유지하지 않음, 키는 중복저장 불가 <순서X,Key중복X> | HashMap, Hashtable, TreeMap, Properties 등 |
List 와 Set 이 포함된 Collection 인터페이스에서 제공하는 주요 메서드는 다음과 같다.
기능 | 리턴타입 | 메소드 | 설명 |
객체 추가 | boolean | add(Object o), addAll(Collection c) | 주어진 객체 / 컬렉션의 객체들을 컬렉션에 추가 |
객체 검색 | boolean | contains(Object o), containsAll(Collection c) | 주어진 객체 / 컬렉션이 저장되어 있는지 여부 |
Iterator | iterator() | 컬렉션의 iterator를 반환 | |
boolean | equals(Object o) | 컬렉션이 동일한지 여부 | |
boolean | isEmpty() | 컬렉션이 비어있는지 여부 | |
int | size() | 저장되어 있는 전체 객체 수를 반환 | |
객체 삭제 | void | clear() | 컬렉션에 저장된 모든 객체를 삭제 |
boolean | remove(Object o), removeAll(Collection c) | 주어진 객체 / 컬렉션을 삭제하고 성공 여부를 반환 | |
boolean | retainAll(Collection c) | 주어진 컬렉션을 제외한 모든 객체를 컬렉션에서 삭제하고 컬렉션에 변화가 있는지 여부를 반환 | |
객체 변환 | Object[] | toArray() | 컬렉션에 저장된 객체를 객체배열(Object [])로 반환 |
Object[] | toArray(Object[] a) | 주어진 배열에 컬렉션의 객체를 저장해서 반환 |
Iterable
자바의 컬렉션 프레임워크에서는 컬렉션에 저장된 요소를 읽어오는 방법을 Iterable 인터페이스로 표준화하고 있다. Iterable 인터페이스를 구현한 클래스의 인스턴스를 반환하는 iterator() 메소드를 정의하여 각 요소를 접근할 수 있도록 하고 있다. 따라서 Iterable 인터페이스를 상속받는 Collection 인터페이스를 또다시 상속받는 List와 Set 인터페이스에서도 iterator() 메소드를 사용할 수 있다.
Iterator
Iterable 인터페이스, Collection 인터페이스와는 별개로 존재하는 인터페이스이다. 용도는 컬렉션 클래스의 데이터를 하나씩 읽어올 때 사용한다. 다음은 Iterator 인터페이스의 메소드이다.
메소드 | 설명 |
hasNext() | 가져올 객체가 있으면 true를 리턴하고, 없으면 false를 리턴한다 |
next() | 컬렉션에서 하나의 객체를 가져온다 |
remove() | 컬렉션에서 객체를 제거한다 |
현재는 하나씩 요소를 불러올 경우, '향상된 for문'이 권장되고 있기는 하다.
List
<순서O,중복O>
객체를 일렬로 늘어놓은 구조이다. 따라서 객체를 저장하면 자동으로 인덱스가 부여되고, 인덱스를 이용하는 것이 중요하다.
List 인터페이스를 구현(implements)한 클래스로는 ArrayList, LinkedList, Vector, Stack 등이 있다. List 인터페이스의 메소드들은 다음과 같다.
기능 | 리턴 타입 | 메소드 | 설명 |
객체 추가 | void | add(int index, Object element) | 주어진 인덱스에 객체를 추가 |
boolean | addAll(int index, Collection c) | 주어진 인덱스에 컬렉션을 추가 | |
Object | set(int index, Object element) | 주어진 위치에 객체를 저장 | |
객체 검색 | Object | get(int index) | 주어진 인덱스에 저장된 객체를 반환 |
int | indexOf(Object o) / lastIndexOf(Object o) | 순방향 / 역방향으로 탐색하여 주어진 객체의 위치를 반환 | |
ListIterator | listIterator() / listIterator(int index) | List의 객체를 탐색할 수 있는ListIterator 반환 / 주어진 index부터 탐색할 수 있는 ListIterator 반환 | |
List | subList(int fromIndex, int toIndex) | fromIndex부터 toIndex에 있는 객체를 반환 | |
객체 삭제 | Object | remove(int index) | 주어진 인덱스에 저장된 객체를 삭제하고 삭제된 객체를 반환 |
boolean | remove(Object o) | 주어진 객체를 삭제 | |
객체 정렬 | void | sort(Comparator c) | 주어진 비교자(comparator)로 List를 정렬 |
📌 ArrayList
컬렉션 프레임워크에서 가장 빈번하게 사용되는 클래스로, 기존의 Vector와 기능적으로 동일하지만 개선한 것이다. ArrayList는 인덱스로 관리한다는 점에서 배열과 거의 동일하지만, 한 가지 차이점이 크기가 가변적이라는 것이다. 배열은 생성될 때 크기가 고정되고 사용 중 크기를 변경할 수 없지만, ArrayList는 가능하다. 최초 설정한 크기를 넘을 경우 자동으로 크기가 증가된다. (👉크기변화가능한 배열)
ArrayList의 생성은 저장할 객체 타입을 타입 파라미터, 즉 제네릭으로 표기하고 기본 생성자를 호출한다. 아래와 같다.
List<타입 파라미터> 객체명 = new ArrayList<타입 파라미터>(초기 저장용량);
List<String> container1 = new ArrayList<String>();
//String타입의 객체를 저장하는 ArrayList, 이때 초기 용량은 10개 객체 저장가능.
List<String> container2 = new ArrayList<String>(30);
//용량의 크기를 매개값으로 받아 ArrayList 객체 생성
다음은 ArrayList에 String 객체를 추가, 검색, 삭제하는 예제이다.
public class ArrayListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("Java"); // String 객체 저장/추가
list.add("egg");
list.add("tree");
int size = list.size(); // 저장된 총 객체 수 얻기
String skill = list.get(0); // 0번 인덱스의 객체 얻기
for(int i = 0; i < list.size(); i++){// 저장된 총 객체 수 만큼 조회
String str = list.get(i);
System.out.println(i + ":" + str);
}
list.remove(0); // 0번 인덱스 객체 삭제
}
}
📌 LinkedList
LinkedList는 데이터가 불연속적으로 존재하지만 주소값을 참조하는 방법으로 서로 연결(link)되어 있다. 아래 그림을 보면 바로 이해가 된다.
LinkedList 에 담겨있는 요소들을 노드(node)라고 하는데 노드는 [이전 노드 주소값 + 객체 + 다음 노드 주소값] 으로 구성되어 있다.
📌 ArrayList 와 LinkedList 의 비교
- LinkedList 는 추가, 삭제에 유리하다.
- ArrayList 는 검색에 유리하다.
LinkedList는 추가, 삭제 시 참조하는 주소값만 변경해주면 된다. 하지만 ArrayList 는 추가, 삭제 시 해당 인덱스의 자리를 만들어주거나(=추가) 메꾸기 위해(=삭제) 많은 요소들을 복사하여 이동시켜줘야 한다. 따라서 ArrayList가 추가, 삭제 시 유리하다.
하지만 검색 시, ArrayList는 인덱스로 관리할 수 있기 때문에 배열의 주소 + n * 데이터 타입의 크기 로 빠르게 접근이 가능하지만, LinkedList 는 하나씩 주소를 따라 순회해야 하므로 느리다.
Set
<순서X,중복X>
Set은 '집합'의 의미이다. 대표적인 클래스로 HashSet, TreeSet 이 있다.
📌 HashSet
Set 의 가장 기본형태의 클래스이다. <순서X, 중복X>인 집합이다.
import java.util.*;
public class Main {
public static void main(String[] args) {
HashSet<String > languages = new HashSet<String>();
languages.add("Java");
languages.add("Python");
languages.add("Javascript");
languages.add("C++");
languages.add("Kotlin");
languages.add("Ruby");
languages.add("Java");
Iterator it = languages.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}
📌 TreeSet
이진 탐색 트리(Binary Search Tree)이다. 이진 탐색 트리는 하나의 부모 노드가 최대 두 개의 자식 노드와 연결되는 이진 트리(Binary Tree)의 일종으로, 정렬과 검색에 특화된 자료구조이다. 최상위 노드를 '루트'라고 한다. 모든 왼쪽 자식의 값이 부모보다 작고, 모든 오른쪽 자식의 값이 루트나 부모보다 큰 값을 가지는 특성이 있다.
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<String> workers = new TreeSet<>();
workers.add("Lee Java");
workers.add("Park Hacker");
workers.add("Kim Coding");
System.out.println(workers);
System.out.println(workers.first());
System.out.println(workers.last());
System.out.println(workers.higher("Lee"));
System.out.println(workers.subSet("Kim", "Park"));
}
}
출력값을 보면 오름차순으로 정렬된 것을 확인할 수 있다. TreeSet의 기본 정렬이 오름차순이기 때문이다.
Map
<순서X,Key중복X>
<Key, Value>쌍으로 구성된 Entry 객체를 저장하는 구조이다. map 이라는 단어의 의미는 '매핑(mapping)'의 의미로 Key 와 Value를 매핑한다는 뜻이다. 기존의 key와 동일한 값의 key로 add하면 해당 key의 value값만 업데이트 된다.
Map 인터페이스를 구현한 클래스에는 HashMap, HashTable, TreeMap, SortedMap 등이 있다.
다음은 Map 인터페이스의 메소드들이다.
기능 | 리턴 타입 | 메소드 | 설명 |
객체 추가 | Object | put(Object key, Object value) | 주어진 키로 값을 저장, 새로운 키일 경우 null을 리턴하고 동일한 키가 있을 경우 값을 대체하고 이전값을 리턴 |
객체 검색 | boolean | containsKey(Object key) | 주어진 키가 있으면 true, 없으면 false를 리턴 |
boolean | containsValue(Object value) | 주어진 값이 있으면 true, 없으면 false를 리턴 | |
Set | entrySet() | 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴 | |
Object | get(Object key) | 주어진 키에 해당하는 값을 리턴 | |
boolean | isEmpty() | 컬렉션이 비어 있는지 확인 | |
Set | keySet() | 모든 키를 Set 객체에 담아서 리턴 | |
int | size() | 저장된 키-값 쌍의 총 갯수를 리턴 | |
Collection | values() | 저장된 모든 값을 Collection에 담아서 리턴 | |
객체 삭제 | void | clear() | 모든 Map.Entry(키와 값)을 삭제 |
Object | remove(Object key) | 주어진 키와 일치하는 Map.Entry를 삭제하고 값을 리턴 |
📌 HashMap
Map의 가장 기본적인 형태의 클래스이다. 아래 그림과 같이 Key 와 Value 로 구성된 Entry 객체를 저장한다.
HashMap은 해시 함수를 통해 Key와 Value가 저장되는 위치를 결정하므로, 사용자는 그 위치를 알 수 없고 삽입되는 순서와 위치 또한 관계가 없다.
HashMap의 키로 사용할 객체는 hashCode()와 equals() 메서드를 재정의해서 동등 객체가 될 조건을 정해야 한다. 동등 객체, 즉 동일한 키가 될 조건은 HashSet과 동일하다. hashCode()의 리턴값이 같아야 하고, equals()메서드가 true를 리턴해야 한다. 이렇듯 HashMap은 이름 그대로 해싱(Hashing)을 사용하기 때문에 많은 양의 데이터를 검색하는 데 있어서 뛰어난 성능을 보인다. 주로 Key 타입은 String을 많이 사용하는데, String은 문자열이 같을 경우 동등 객체가 될 수 있도록, hashcode()와 equals() 메서드가 재정의 되어 있다.
HashMap을 생성하기 위해서는 Key 타입과 Value 타입을 파라미터로 주고 기본 생성자를 호출하면 된다. 코드로 표현하면 다음과 같다.
Map<Key, Value> map = new HashMap<Key, Value>();
Map<Key, Value> map = new HashMap<Key, Value>(capacity, loadFactor);
Key와 Value의 타입은 기본 타입(byte, short, int, float, double, boolean, char)을 사용할 수 없고, 클래스 및 인터페이스 타입만 가능하다.
다음은 HashMap의 사용 예제이다.
package com.company;
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
//객체 저장
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)로 점수(value)검색
System.out.println("///");
//객체를 하나씩 처리
Set<String> keySet = map.keySet(); //Set 컬렉션으로 이름(key) 얻기
//반복해서 이름(key)를 얻고, 값을 Map에서 얻어냄
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("///");
System.out.println("총 entry 수: " + map.size());
//객체를 하나씩 처리
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
//Set 컬렉션으로 Map.Entry 얻기
Iterator<Map.Entry<String, Integer>> entryIterator = entrySet.iterator();
//반복해서 Map.Entry를 얻고 키와 값을 얻어냄
while(entryIterator.hasNext()) {
Map.Entry<String, Integer> entry = entryIterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " : " + value);
}
//객체 전체 삭제
map.clear();
}
}
/*
총 entry 수: 5
파이리 : 65
///
야도란 : 75
꼬부기 : 95
파이리 : 65
피카츄 : 85
피존투 : 15
///
총 entry 수: 4
야도란 : 75
꼬부기 : 95
파이리 : 65
피카츄 : 85
*/
위 코드에서 주의할 점은, Map은 Key와 Value를 쌍으로 저장하기 때문에 iterator() 를 직접 호출할 수 없다는 점이다. 그 대신 keySet() 이나 entrySet() 메서드를 이용해 Set 형태로 반환된 컬렉션에 iterator() 를 호출해야 한다.
[참고1] HashMap은 키(key)와 값(value)에 null 값을 허용하지만, Hashtable은 null 값을 허용하지 않는다.
[참고2] Hashtable은 과거부터 쓰던 클래스이나 호환성을 위해 아직 남아있는 것.(Vector 클래스처럼..)
Comparator 와 Comparable
Comparator와 Comparable은 자바에서 컬렉션을 정렬하기 위해 제공하는 인터페이스들이다. 객체들 간 크기를 비교하여 오름차순 등으로 정렬할 수 있다. 두 인터페이스의 주요 차이점은, Comparable은 비교 대상(매개 변수)과 자기 자신을 비교하고, Comparator는 매개 변수인 두 객체를 비교한다는 것이다.
📌 Comparable
Comparable 인터페이스는 compareTo() 메소드를 사용해 객체를 정렬한다. Integer, String, File, Date와 같은 클래스에서는 자체적으로 Comparable 인터페이스를 구현하여 인스턴스 간 크기를 비교하고 있다. 기본 정렬은 오름차순이다. compareTo() 메소드에서는 비교하는 두 객체가 같으면 0, 작으면 음수, 크면 양수를 반환한다.