컬렉션 프레임워크를 알아보자.
목차
1. 컬렉션 프레임워크 개요
2. Iterator
3. Set
4. Map
5. 기타 컬렉션
5.1 Properties
5.2 BitSet
5.3 EnumSet, EnumMap
5.4 Stack, Queue, Deque, ProirityQueue
5.5 WeakHashMap
6. 뷰
1. 컬렉션 프레임워크 개요
컬렉션 프레임워크에는 자료 구조의 구현체가 있다. 선택한 자료구조에 독립적인 코드를 쉽게 작성할 수 있게 하기 위해 컬렉션 프레임워크는 공통 인터페이스를 제공한다. 기본 인터페이스는 Collection이다.
List는 순차 컬렉션이다. (요소의 위치카 0, 1, 2등이다.)
ArraysList 클래스와 LinkedList 클래스는 둘 다 List 인터페이스를 구현한다. 자료구조의 연결리스트가 기억날 것이다. 연결 리스트 중간에 요소를 삽입하는 일은 빠르게 수행된다. 그저 삽입할 노드를 이어주기만 하면 된다.
Set은 요소를 특정 위치에 삽입하지 않으며, 중복 요소를 허용하지 않는다. SortedSet에는 정렬 순서로 요소를 순회하는 기능이 있으며, NavigableSet에는 이웃 요소를 찾는 메서드가 있다.
Queue는 삽입 순소를 유지하지만, 사람들이 줄이어 있는 것처럼 요소를 뒤(tail)에서만 삽입하고, 앞(head)에서만 제거할 수 있다. Deque은 더블 엔디드 큐로, 양쪽 끝에서 삽입과 제거를 할 수 있다.
컬렉션 인터페이스는 모두 제네릭이며 요소 타입에 대응하는 타입 파라미터를 받는다. 코드를 작성할 때 가능하면 인터페이스를 사용하는 게 좋다. 예를 들어 ArrayList를 생성한 후에는 해당 참조를 List 타입 변수에 저장한다.
List<String> words = new ArrayList<>();
컬렉션은 처리하는 메서드를 구현할 때는 가장 덜 제한적인 인터페이스를 파라미터 타입을 사용하면 좋다. 보통은 Collection, List, Map이면 충분하다.
컬렉션 프레임워크의 장점은 몇몇 알고리즘 메서드를 이미 만들어 놓았다는 것이다. Collection 인터페이스에 addAll, removeIf와 같은 메서드가 이미 존재한다. 그 밖에도 다양한 종류의 컬렉션에 동작하는 여러 알고리즘이 있다. 정렬하고, 뒤섞고, 순환시키고, 뒤집을 수 있다. 컬렉션에서 최댓값 또는 최솟값이나 임의의 요소의 위치를 찾을 수 있다. 요소가 없거나, 한 개만 있거나, 같은 요소의 사본 n개를 포함하는 컬렉션을 만들어낼 수도 있다.
copy, disjoint, addAll, replaceAll, fill, nConpies…
2. Iterator
각 컬렉션에는 어떤 순서대로 요소를 순회하는 메서드가 있다. Collection의 슈퍼인터페이스 Iterable<T>는 다음과 같은 메서드를 정의한다.
Iterator<T> iterator()
이 메서드는 모든 요소를 방문하는 데 사용할 수 있는 반복자를 돌려준다.
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("show");
coll.add("my");
coll.add("words");
Iterator<String> iter = coll.iterator();
while (iter.hasNext()) {
String element = iter.next();
System.out.println(element);
}
// enhanced for를 쓰는 게 낫다.
for(String word : coll) {
System.out.println(word);
}
}
Iterator 인터페이스에는 remove 메서드도 있다. remove 메서드는 방금 방문한 요소를 제거한다.
while(iter.hasNext()) {
String element = iter.next();
if(element.equals("show")) {
iter.remove();
}
}
removeIf 메서드를 사용하면 더 쉽다.
coll.removeIf(s -> s.equals("show"));
ListIterator 인터페이스는 Iterator의 서브인터페이스로, 반복자 앞쪽에 요소를 추가하고 방문한 요소를 다른 값으로 설정하며, 거꾸로 순회하는 메서드가 포함되어 있다. ListIterator 인터페이스는 주로 연결 리스트를 이용할 때 유용하다.
List<String> friends = new LinkedList<>();
friends.add("Jwa");
friends.add("Go");
friends.add("Bu");
ListIterator<String> listIterator = friends.listIterator();
listIterator.add("Baker");
listIterator.add("Bong");
listIterator.add("Dong");
for(String friend : friends) {
System.out.println(friend);
}
/*
Baker
Bong
Dong
Jwa
Go
Bu
*/
3. Set
집합(set)은 어떤 값이 요소인지 효율적으로 테스트할 수 있자만 요소를 추가한 순서는 기억하지 않는다. 그래서 집합(set)은 순서가 중요하지 않을 때 유용하다. 예를 들어 나쁜 단어를 사용자명으로 쓸 수 없게 한다고 해보자. 이때 순서는 중요하지 않다. 사용자명이 나쁜 단어 집합에 있는지 없는지만 알면 그만이다.
HashSet과 TreeSet 클래스는 Set 인터페이스를 구현한다. 자료구조의 해시 테이블(hash table)과 이진 트리(binary tree)를 생각하면 된다.
예를 들어 나쁜 단어 집합을 만들수 있다.
public static void main(String[] args) {
Set<String> badWords = new HashSet<>();
badWords.add("fuck");
badWords.add("c++");
badWords.add("si");
String username = "fuck";
if(badWords.contains(username.toLowerCase()))
System.out.println("Please choose a different user name");
}
정렬된 순서로 집합을 순회하려면 TreeSet을 사용한다. TreeSet은 사용자에게 정렬된 목록을 보여준다.
TreeSet을 사용하는 집합의 요소 타입은 반드시 Comparable 인터페이스를 구현해야 한다. Comparable 인터페이스를 구현하지 않을 때는 생성자에 Comparator를 전달해 주어야 한다.
Set<String> contries = new TreeSet<>();
contries = new TreeSet<>((u, v) ->
u.equals(v) ? 0
: u.equals("USA") ? -1
: v.equals("USA") ? 1
: u.compareTo(v));
// USA가 항상 맨 앞에 온다.
contries.add("Bahrain");
contries.add("Australia");
contries.add("USA");
contries.add("Canada");
System.out.println(contries);
// [USA, Australia, Bahrain, Canada]
4. Map
Map은 연관된 키와 값을 저장한다. 연관된 키와 값을 새로 추가하거나 기존 키 값을 변경하려면 put을 호출한다.
키를 정렬 순서로 방문해야 하는 경우가 아니라면 보통은 HashMap을 사용하는 게 좋다. 정렬 순서로 방문하려면 TreeMap을 사용하면 된다.
Map<String, Integer> counts = new HashMap<>();
counts.put("James", 1);
counts.put("Fred", 2);
counts.put("Sanchez", 3);
System.out.println(bocountsoks.get(1));
// James
getOrDefault
// 'Owen'라는 키가 없으면 0이 반환된다.
int count = counts.getOrDefault("Owen", 0);
System.out.println(count);
Map에 들어있는 카운터를 업데이트 하려면 먼저 해당 카운터가 있는지 확인하고 기존 값에 1을 더한다.
// 아직 키가 없으면 word를 1로 설정하고 있으면 Integer::sum 함수로 기존 값에 1을 더한다.
String word = "Fred";
counts.merge(word, 1, Integer::sum);
counts.merge(word, 1, Integer::sum);
System.out.println(counts.get(word));
// 4
맵의 키, 값, 엔트리의 뷰
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("key : " + key + ", value : " + value);
//process(key, value);
}
/*
key : James, value : 1
key : Fred, value : 4
key : Sanchez, value : 3
*/
간단하게 forEach 메서드 사용
psvm {
counts.forEach((k, v) -> process(k, v));
}
public static void process(String key, Integer value) {
System.out.printf("Processing key %s and value %d\n", key, value);
}
/*
Processing key James and value 1
Processing key Fred and value 4
Processing key Sanchez and value 3
*/
5. 기타 컬렉션
5.1 Properties
Properties 클래스는 텍스트 형식으로 쉽게 저장하고 불러올 수 있는 Map을 구현한다. 주로 프로그램의 설정 옵션을 저장하는 용도로 사용한다.
public static void main(String[] args) throws IOException {
Properties settings = new Properties();
settings.put("width", "200");
settings.put("title", "Hello, World!");
Path path = Paths.get("demo.properties");
try (OutputStream out = Files.newOutputStream(path)) {
settings.store(out, "Program Properties");
}
String title = settings.getProperty("title", "New Document");
String height = settings.getProperty("height", "100");
System.out.println(title);
System.out.println(height);
System.out.println();
System.out.println("System properties");
Properties sysprops = System.getProperties();
sysprops.forEach((k, v) -> System.out.printf("%s=%s\n", k, v));
}
Properties 클래스는 값이 항상 문자열인데도 Map<Object, Object>를 구현한다. 그러므로 get 메서드를 사용하면 값을 Object로 반환하므로 사용하며 안 된다.
5.2 BitSet
BitSet 클래스는 비트 시퀀스를 저장한다. bit set은 비트를 long 값의 배열로 패킹하므로 boolean 값의 배열을 이용할 때보다 효율적이다. 또한, 비트 집합은 flag bit 시퀀스나 양의 정수 집합을 표현하는 데 유용하다. (이때 i번째 비트 1이면 i가 집합에 들어 있음을 나타낸다)
public static void main(String[] args) {
// This program demonstrates the Sieve of Erathostenes for finding primes
int n = 100000;
BitSet primes = new BitSet(n + 1);
for (int i = 2; i <= n; i++)
primes.set(i);
for (int i = 2; i * i <= n; i++) {
if (primes.get(i)) {
for (int k = 2 * i; k <= n; k += i)
primes.clear(k);
}
}
System.out.println(primes);
}
5.3 EnumSet, EnumMap
열거 값의 집합을 모으려면 BitSet 대신 EnumSet 클래스를 사용한다. public 생성자가 없기때문에 static factory method로 집합을 생성한다.
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
public static void main(String[] args) {
Set<Weekday> always = EnumSet.allOf(Weekday.class);
Set<Weekday> never = EnumSet.noneOf(Weekday.class);
Set<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
Set<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
System.out.println(always);
System.out.println(never);
System.out.println(workday);
System.out.println(mwf);
EnumMap<Weekday, String> personInCharge = new EnumMap<>(Weekday.class);
personInCharge.put(Weekday.MONDAY, "Fred");
System.out.println(personInCharge);
}
5.4 Stack, Queue, Deque, ProirityQueue
자바 켈력션 프레임워크에 Stack 인터페이스는 없고 레거시 Stack 클래스만 있다.(이 클래슨느 사용하면 안 된다.) 스택, 큐, 덱이 필요하지만 스레드 안정성을 신경 쓰지 않는다면 ArrayDeque을 사용하면 된다.
스택을 이용할 때는 push, pop 메서드를 사용한다.
큐를 이용할 때는 add, remove 메서드를 시용한다.
병행 프로그래밍에서는 thread-safe-queue를 사용한다.
public static void main(String[] args) {
ArrayDeque<String> stack = new ArrayDeque<>();
stack.push("Peter");
stack.push("Paul");
stack.push("Mary");
while (!stack.isEmpty())
System.out.println(stack.pop());
System.out.println();
Queue<String> queue = new ArrayDeque<>();
queue.add("Peter");
queue.add("Paul");
queue.add("Mary");
while (!queue.isEmpty())
System.out.println(queue.remove());
}
우선순위 큐(priority-queue)는 요소를 무작위로 삽입해도 정렬된 순서로 꺼낸다. 즉 remove 메서드를 호출할 때마다 우선순위 큐에서 현재 가장 작은 요소를 얻는다. 보통 작업 스케줄링에 사용된다. 각 작업에는 우선순위가 있다. 그리고 작업은 무작위로 추가된다.
public static void main(String[] args) {
PriorityQueue<Job> jobs = new PriorityQueue<>();
jobs.add(new Job(4, "Collect garbage"));
jobs.add(new Job(9, "Match braces"));
jobs.add(new Job(1, "Fix memory leak"));
while (jobs.size() > 0) {
Job job = jobs.remove(); // The most urgent jobs are removed first
execute(job);
}
}
public static void execute(Job job) {
System.out.println(job.getDescription());
}
TreeSet과 마찬가지로 우선순위 큐는 Comparable 인터페이스를 구현하는 클래스의 요소를 저장할 수 있다. 또는 생성자에 Comparator를 전달할 수 있다. TreeSet과 달리 요소를 순회할 때 꼭 정렬된 순서로 돌려주지는 않는다.
5.5 WeakHashMap
WeakHashMap 자료 구조는 가비 컬렉터와 협동해서 키의 유일한 참조가 해시 테이블의 엔트리라면 키/값 쌍을 제거한다.
6. 뷰
컬렉션 뷰는 컬렉션 인터페이스를 구현하는 경량 객체다. 하지만 요소를 저장하지는 않는다. 예를 들어 keySet과 values 메서드는 해당 맵을 들여다보는 뷰를 돌려준다.
다른 예로는 Arrays.adList 메서드다. a가 E입의 배열이면 Arrays.asList(a)는 배열요소에 기반을 둔 List를 반환한다.
보통 뷰는 자신이 구현한 인터페이스의 모든 연산을 지원하지는 않는다. 예를 들어 Arrays.asList가 반환한 리스트에 add를 호출하는 건 의미없는 일이다.
public class RangeDemo {
public static void main(String[] args) {
List<String> sentence = Arrays.asList("A man, a plan, a cat, a ham, a yak, a yam, a hat, a canal, Panama!".split("[ ,]+"));
List<String> nextFive = sentence.subList(5, 10);
System.out.println(nextFive);
TreeSet<String> words = new TreeSet<>(sentence);
words.add("zebra");
SortedSet<String> ysOnly = words.subSet("y", "z");
System.out.println(ysOnly);
SortedSet<String> nAndBeyond = words.tailSet("p");
System.out.println(nAndBeyond);
}
}
/*
[cat, a, ham, a, yak]
[yak, yam]
[plan, yak, yam, zebra]
*/