collection framework는 몇 가지 인터페이스를 통해서 다양한 collection class를 이용할 수 있도록 설계되어 있다. 주요 인터페이스로 List
, Set
, Map
이 있는데, 이 인터페이스로 사용 가능한 collection 객체의 종류는 다음과 같다.
collection
^
|
------------ Map
^ ^ ^
| | |
List Set |
| | HashMap
ArrayList HashSet Hashtable
Vector TreeSet TreeMap
LinikedList Properties
List
와 Set
interface는 사용 방법이 같다. 둘 다 추가, 삭제, 변경에 대해서 인터페이스가 같다. 단지, Set
은 집합의 개념으로 중복을 허용하지 않고 List
는 중복을 허용한다.
List
collection은 저장하는 객체를 index로 관리하기 때문에 객체를 저장하면 인덱스가 부여되고, 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.
기능 | 메서드 | 설명 |
---|---|---|
객체 추가 | boolean add(E e) | 주어진 객체를 맨 끝에 추가 |
객체 추가 | void add(int index, E element) | 주어진 객체를 index에 추가 |
객체 추가 | set(int index, E element) | 주어진 인덱스의 객체를 새로운 객체로 바꿈 |
객체 검색 | boolean contains(Object o) | 주어진 객체가 저장되어 있는 지 확인 |
객체 검색 | E get(int index) | 주어진 인덱스에 저장된 객체를 리턴 |
객체 검색 | boolean isEmpty() | 컬렉션이 비어있는 지 조사 |
객체 검색 | int size() | 저장되어 있는 전체 객체 수를 리턴 |
객체 삭제 | void clear() | 저장된 모든 객체를 삭제 |
객체 삭제 | E remove(int index) | 주어진 인덱스에 저장된 객체를 삭제 |
객체 삭제 | boolean remove(Object o) | 주어진 객체를 삭제 |
한 가지 조심해야할 것은 객체를 저장할 때 List
는 객체를 복사해서 저장하는 것이 아니라, 객체의 주소를 저장하는 것이다. 따라서, 동일한 객체를 중복 저장하면 동일한 주소가 저장되는 것이다. null
또한 저장이 가능하다.
ArrayList
는 객체를 추가하면 내부 배열에 객체가 저장된다. 일반 배열과의 차이점은 제한 없이 객체를 추가할 수 있다는 것이다. ArrayList
의 생성은 다음과 같다.
List<E> list = new ArrayList<E>();
List list = new ArrayList();
type parameter E
에는 ArrayList
에 저장하고 싶은 객체 타입을 지정하면 된다.
ArrayList
는 인덱스 0번부터 차례대로 저장한다. 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 당겨지므로 O(n)
시간이 걸린다.
따라서, 빈번한 객체의 삽입, 삭제가 있다면 LinkedList
를 쓰는 것이 좋다.
아래는 ArrayList
의 사용 예제이다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// ArrayList collection
List<Board> list = new ArrayList<Board>();
// add object
list.add(new Board("0"));
list.add(new Board("1"));
list.add(new Board("2"));
list.add(new Board("3"));
list.add(new Board("4"));
int size = list.size();
System.out.println("Total Object: " + size); // 5
System.out.println();
Board board = list.get(2);
System.out.println(board.content); // 2
System.out.println();
list.remove(2); // 2
list.remove(2); // 3
for(Board b: list) {
System.out.println(b.content); // 0 1 4
}
}
}
Vector
는 ArrayList
와 동일한 내부 구조를 가지지만 Vector
는 동기화된 메서드로 구성되어 있기 때문에 멀티 thread가 동시에 Vector
메서드를 실행할 수 없다. 반대로 이야기하자면 ArrayList
는 동기화가 안되기 때문에 멀티 thread에서 동시에 접근 시에 문제가 발생할 수 있다.
List<E> list = new Vector<E>();
List list = new Vector();
다음은 ThreadA
와 ThreadB
에서 동시에 Board
객체를 접근해서 1000개씩 추가한 후, 전체 저장된 수를 출력하는 예제이다.
public class Main {
public static void main(String[] args) {
// vector collection
List<Board> list = new Vector<>();
Thread threadA = new Thread() {
@Override
public void run() {
for(int i = 1; i <= 1000; i++) {
list.add(new Board("content" + i));
}
}
};
Thread threadB = new Thread() {
@Override
public void run() {
for(int i = 1001; i <= 2000; i++) {
list.add(new Board("content" + i));
}
}
};
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (Exception e) {
e.printStackTrace();
}
int size = list.size();
System.out.println("total object: " + size); // total object: 2000
}
}
Vector
에서 ArrayList
로 바꿔서 실험해보면 total object값이 이상하게 나올 것이다.
LinkedList
는 ArrayList
와 사용 방법은 동이하지만 내부 구조는 완전히 다르다. ArrayList
는 내부 배열에 객체를 저장하는 동적 배열이지만, LinkedList
는 인접 객체를 체인처럼 연결해서 관리한다.
LinkedList
특성 상 객체를 삽입하거나, 삭제하면 바로 앞 뒤 링크만 변경하면 되므로 빈번한 객체 삽입, 삭제에 있어서 O(1)
시간이 걸린다.
public class Main {
public static void main(String[] args) {
List<String> list1 = new ArrayList<String>();
List<String> list2 = new LinkedList<String>();
long startTime, endTime;
startTime = System.nanoTime();
for(int i = 0; i < 100000; i++) {
list1.add(0, String.valueOf(i)); // insert
}
endTime = System.nanoTime();
System.out.printf("%-17s %8d ns \n", "ArrayList time: ", (endTime - startTime));
startTime = System.nanoTime();
for(int i = 0; i < 100000; i++) {
list2.add(0, String.valueOf(i)); // insert
}
endTime = System.nanoTime();
System.out.printf("%-17s %8d ns \n", "LinkedList time: ", (endTime - startTime));
}
}
위 코드는 ArrayList
와 LinkedList
둘의 차이를 확연히 보여주는 예제로, 성능 테스트 결과를 보여준다. ArrayList
로 삽입을 실행 시에 매번 O(N)
시간이 걸리는 반면에 LinkedList
는 O(1)
시간이 걸려서 더 빠르다.
ArrayList time: 416353140 ns
LinkedList time: 5442651 ns
약 8배는 더 빠른 걸로 나온다.
List
collection은 저장 순서를 유지하지만, Set
은 저장 순서를 유지하지않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null
만 저장할 수 있다. Set
은 집합 개념이기 때문이다. 따라서, 정리하자면 순서도 상관없고, 중복도 허용하지 않는다. 순서도 없기 때문에 index로 관리하지 않는다.
Set
에는 HashSet
, LinkedHashSet
, TreeSet
등이 있는데, Set
collection에서 공통으로 사용하는 인터페이스는 다음과 같다.
기능 | 메서드 | 설명 |
---|---|---|
객체 추가 | boolean add(E e) | 주어진 객체를 성공적으로 저장하면 true 를 반환하고, 중복 객체면 false 를 반환 |
객체 검색 | boolean contains(Object o) | 주어진 객체가 저장되어 있는 지 여부 |
객체 검색 | isEmpty() | collection이 비어있는 지 조사 |
객체 검색 | iterator iterator() | 저장된 객체를 한 번씩 가져오는 iterator 반환 |
객체 검색 | int size() | 저장되어 있는 전체 객체 수 반환 |
객체 삭제 | void clear() | 저장된 모든 객체를 삭제 |
객체 삭제 | boolean remvoe(Object o) | 주어진 객체를 삭제 |
Set
collection 중에서 가장 많이 사용되는 것이 HashSet
이다. 다음은 HashSet
collection을 생성하는 방법이다.
Set<E> set = new HashSet<E>();
Set set = new HashSet();
HashSet
은 동일한 객체는 중복 저장하지 않는다. 동일하다는 의미의 '동등'하다는 말은 HashSet
은 다른 객체라도 hashCode
메서드의 반환값이 같고, equals
메서드가 true를 반환하면 동일한 객체라고 판단하고 중복 저장하지 않는다.
hashCode()
반환값이 동일equals()
반환값이 동일문자열을 HashSet
에 저장할 경우, 같은 문자열을 갖는 String
객체는 동등한 객체로 간주한다. 같은 문자열이면 hashCode
의 반환값이 같고, equals
의 반환값이 true
로 나온다.
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("Java");
set.add("JDBC");
set.add("JSP");
set.add("Java"); // duplicated
set.add("Spring");
System.out.println("total object: " + set.size()); // 4
}
}
아래의 예제는 이름과 나이가 동일한 경우 Member
객체를 HashSet
에 중복 저장하지 않는다. Member
class를 선언할 때 이름과 나이가 동일한 hashCode
가 반환되도록 hashCode
를 오버라이드하고, equals
메서드가 true
를 반환하도록 오버라이드했다.
public class Member {
public String name;
public int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (o instanceof Member target) {
return target.name.equals(name) && (target.age == age);
} else {
return false;
}
}
@Override
public int hashCode() {
return name.hashCode() + age;
}
}
equals
와 hashCore
를 오버라이드하여 name
과 age
가 같은 member
에 대해서는 같은 객체로 판단한다.
public class Main {
public static void main(String[] args) {
Set<Member> set = new HashSet<Member>();
set.add(new Member("Hong", 30));
set.add(new Member("Park", 30));
set.add(new Member("Hong", 30));
System.out.println("total object: " + set.size()); // total object: 2
}
}
총 저장 객체 수로 2개가 나온다.
Set
collection은 index로 객체를 가져오지 못하므로 검색을 위해서는 for문으로 순회를 돌아야한다.
Set<E> set = new HashSet<>();
for(E e: set) {
...
}
다른 방법으로는 Set
collection의 iterator()
메서드로 iterator
를 얻어 객체를 하나씩 가져오는 것이다. type parameter E는 Set
collection에 저장되어 있는 개체 타입이다.
Set<E> set = new HashSet<>();
Iterator<E> iterator = set.iterator();
iterator
는 Set
collection의 객체를 가져오거나 제거하기 위해 다음 메서드를 제공한다.
반환 타입 | 메서드 명 | 설명 |
---|---|---|
boolean | hasNext() | 가져올 객체가 있으면 true, 없으면 false |
E | next() | 객체를 하나 가져온다. |
void | remove() | 객체를 Set에서 삭제한다. |
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("Java");
set.add("JDBC");
set.add("JSP");
set.add("Spring");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()) {
String elem = iterator.next();
System.out.println(elem);
if(elem.equals("JSP")) {
iterator.remove();
}
}
System.out.println();
set.remove("JDBC");
for(String elem: set) {
System.out.println(elem);
}
}
}
HashSet
을 반복하여 JSQP
와 JDBC
값을 가진 객체를 삭제한다. 결과는 다음과 같다.
Java
JSP
JDBC
Spring
Java
Spring
Map
collection은 key-value로 이루어진 entry에 객체를 저장한다. key는 중복 저장할 수 없지만, 값은 중복 저장할 수 있다. 즉, 기존의 저장된 key와 동일한 key로 값으 저장하면 기존의 값은 없어지고 새로운 값으로 대치된다.
Map
collection은 HashMap
, Hashtable
, LinkedHashMap
, Properties
, TreeMap
등이 있다. Map
collection들은 공통적으로 사용 가능한 Map
interface method로 다음을 가진다.
기능 | 메서드 | 설명 |
---|---|---|
객체 추가 | V put(K key, V value) | 주어진 키와 값을 추가, 저장이 되면 값을 리턴 |
객체 검색 | boolean containsKey(Object key) | 주어진 키가 있는 지 여부 |
객체 검색 | boolean containsValue(Object value) | 주어진 값이 있는 지 여부 |
객체 검색 | Set<Map.Entry<K,V>> entrySet() | 키와 값의 쌍으로된 Map.Entry 객체를 Set 에 담아서 반환 |
객체 검색 | V get(Object key) | 주어진 키의 값을 반환 |
객체 검색 | boolean isEmpty() | collection이 비어있는 지 여부 |
객체 검색 | Set keySet() | 모든 key를 Set객체에 담아 반환 |
객체 검색 | int size() | 저장된 키의 총 수 반환 |
객체 검색 | Collection values() | 저장된 모든 값을 Collection에 담아서 반환 |
객체 삭제 | void clear() | 모든 Map.Entry(키와 값) 을 삭제 |
객체 삭제 | V remove(Object key) | 주어진 Key와 일치하는 entry 삭제, 삭제 시 값을 반환 |
HashMap은 key로 사용할 객체가 hashCode
메서드의 반환값이 같고, equals
메서드가 true
를 반환할 경우, 동일 key로 보고 중복 저장을 허용하지 않는다.
다음은 HashMap
collection을 생성하는 방법이다.
Map<K,V> map = new HashMap<K,V>();
아래는 key가 String이고, 값은 Integer 타입으로 갖는 HashMap
이다.
Map<String, Integer> map = new HashMap<String, Integer>();
Map<String, Integer> map = new HashMap<>();
아래는 이름을 key로 점수를 value로 저장하는 HashMap
사용 방법이다.
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Hong", 85);
map.put("Park", 90);
map.put("Hong", 85); // duplicated
map.put("Lee", 95);
String key = "Hong";
System.out.println(key + ": " + map.get(key));
Set<String> keySet = map.keySet();
for(String k : keySet) {
System.out.println("Key: " + k + " Value: " + map.get(k));
}
}
}
결과는 아래와 같다.
Hong: 85
Key: Hong Value: 85
Key: Lee Value: 95
Key: Park Value: 90
참고로 HashMap
은 key-value entry를 입력한 순서를 지키지 않는 특성을 가지는데, 순서를 유지하고 싶다면 LinkedHashMap
을 사용하면 된다. 나머지 사용 방법은 같지만, LinkedHashMap
은 linked list로 입력된 entry들의 앞 뒤를 기억한다.
Hashtable
은 HashMap
과 동일한 내부 구조를 가진다. 차이점은 Hashtable
은 동기화된 메서드로 구성되어있다는 것으로 멀티 thread가 동시에 Hashtable
메서드을 실행할 수 없다. 따라서, 멀티 thread 환경에서도 안전하게 객체를 추가, 삭제할 수 있다.
Map<String, Integer> map = new Hashtable<String, Integer>();
Map<String, Integer> map = new Hashtable<>();
아래는 두 개의 thread들이 Hashtable
에 접근하는 코드이다.
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new Hashtable<>();
Thread threadA = new Thread() {
@Override
public void run() {
for(int i = 1; i <= 1000; i++) {
map.put(String.valueOf(i), i);
}
}
};
Thread threadB = new Thread() {
@Override
public void run() {
for(int i = 1001; i <= 2000; i++) {
map.put(String.valueOf(i), i);
}
}
};
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("total entries: " + map.size());
}
}
실행결과를 보면 딱 2000개가 나온다. 만약 HashMap
을 사용했다면 thread들끼리의 경합 때문에 제대로 결과가 나오지 않는다.
total entries: 2000
Properties
는 Hashtable
의 자식 클래스이기 때문에 Hashtable
의 특징을 그대로 가지고 있다. Properties
는 key와 value을 String
으로 고정한 collection이다. Properties
는 주로 .properties
인 프로퍼티 파일을 읽을 때 사용한다.
properties는 일반 파일과 다르게 ISO 8859-1
문자셋으로 저장된다.
driver=oracle.jdbc.OracleDriver
url=jdbc:oracle:thin:@localhost:1521:orcl
username=scott
password=tiger
Properties
를 사용하면 위와 같은 properties 파일의 내용을 쉽게 읽을 수 있다. 먼저 Properties 객체를 생성하고, load
메서드로 properties 파일의 내용을 메모리로 로드한다.
Properties properties = new Properties();
properties.load(Xxx.class.getResourceAsStream("database.properties"));
일반적으로 properties 파일은 class 파일과 함께 저장된다. 따라서, class파일을 기준으로 상대 경로를 이용해서 읽는 것이 편리하다. class 객체의 getResourceAsStream
메서드는 주어진 상대 경로의 resource 파일을 읽는 InputStream
을 반환한다.
public class Main {
public static void main(String[] args) {
try {
Properties properties = new Properties();
properties.load(Main.class.getResourceAsStream("database.properties"));
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String username = properties.getProperty("username");
String password = properties.getProperty("password");
System.out.println("driver: " + driver); // driver: oracle.jdbc.OracleDriver
System.out.println("url: " + url); // url: jdbc:oracle:thin:@localhost:1521:orcl
System.out.println("username: " + username); // username: scott
System.out.println("password: " + password); // password: tiger
} catch (IOException ie) {
ie.printStackTrace();
}
}
}
Main
class를 기준으로 경로를 탐색하기 때문에 database.properties
가 Main
class와 같은 path에 있으면 properties를 읽어온다.
검색 기능이 매우 효율적인 collection으로 TreeSet
과 TreeMap
이 존재한다. TreeSet
은 Set
collection의 하위이고, TreeMap
은 Map
collection의 하위이다.
TreeSet
은 이진 트리를 기반으로 한 Set
collection이다. 여러 개의 node가 tree형태로 연결된 구조인 것이다.
root node
/ \
left child right child
/ \ /
child1 child2 child3
TreeSet
에 객체를 저장하면 다음과 같이 자동으로 정렬된다. 부모 노드의 객체와 비교해서 낮은 것은 왼쪽 자식 노드에, 높은 것은 오른쪽 자식 노드에 저장한다.
TreeSet<E> treeSet = new TreeSet<E>();
TreeSet
은 Set
이 없는 메서드들도 많이 가지고 있는데, 다음은 TreeSet
이 가지고 있는 검색 관련 메서드들이다.
메서드 | 설명 |
---|---|
E first() | 제일 낮은 객체를 반환 |
E last() | 제일 높은 객체를 반환 |
E lower(E e) | 주어진 객체보다 바로 아래 객체를 반환 |
E higher(E e) | 주어진 객체보다 바로 위 객체를 반환 |
E floor(E e) | 주어진 객체와 동등한 객체가 있으면 반환, 만약 없다면 주어진 객체의 바로 아래의 객체를 반환 |
E ceiling() | 주어진 객체와 동등한 객체가 있으면 반환, 만약 없다면 주어진 객체의 바로 위의 객체를 반환 |
E pollFirst() | 제일 낮은 객체를 꺼내오고 collection에서 제거 |
E pollLast() | 제일 높은 객체를 꺼ㅐ오고 collection에서 제거 |
Iterator descendingIterator() | 내림차순으로 정렬된 Iterator를 반환 |
NavigableSet descendingSet() | 내림차순으로 정렬된 NavigableSet 반환 |
NavigableSet headSet(E to, boolean inclusive) | 주어진 객체보다 낮은 객체들을 NavigableSet으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐 |
NavigableSet tailSet(E from, boolean inclusive) | 주어진 객체보다 높은 객체들을 NavigableSet으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐 |
NavigableSet subSet(E from, boolean frominclusive, E to, boolean toinclusive) | 시작과 끝으로 주어진 key 사이의 Map.Entry 들을 NavigableSet으로 반환, 시작과 끝 객체의 포함 여부는 두번째, 네번째 값에 따라 달라진다. |
다음은 무작위로 저장한 점수를 검색하는 방법을 보여준다.
public class Main {
public static void main(String[] args) {
TreeSet<Integer> scores = new TreeSet<>();
scores.add(87);
scores.add(98);
scores.add(75);
scores.add(95);
scores.add(80);
for (Integer s : scores) {
System.out.print(s + " "); // 75 80 87 95 98
}
System.out.println();
System.out.println("first score: " + scores.first()); // first score: 75
System.out.println("high score: " + scores.last()); // high score: 98
System.out.println("under 95 score " + scores.lower(95)); // under 95 score 87
System.out.println("higher 95 score " + scores.higher(95)); // higher 95 score 98
System.out.println("95거나 95보다 바로 아래 score " + scores.floor(95)); // 95 or under 95 score 95
System.out.println("85거나 85보다 바로 위 score " + scores.ceiling(85)); // higher 85 or upper score 87
NavigableSet<Integer> descendingScores = scores.descendingSet();
for(Integer s : descendingScores) {
System.out.print(s + " "); // 98 95 87 80 75
}
System.out.println();
NavigableSet<Integer> rangeSet = scores.tailSet(80, true);
for(Integer s: rangeSet) {
System.out.print(s + " "); // 80 87 95 98
}
System.out.println();
// 80 <= score < 90
rangeSet = scores.subSet(80, true, 90, false);
for (Integer s: rangeSet) {
System.out.println(s + " "); // 80 87
}
}
}
최대, 최소, 범위 검색 등이 가능한 것을 볼 수 있다.
TreeMap
은 이진 트리를 기반으로 한 Map
collection으로 TreeSet
과의 차이점은 key-value이 저장된 Entry값을 저장한다는 점이다. 이때 key를 기준으로 자동 정렬되며, 부모 키와 비교해서 낮은 것은 왼쪽, 높은 것은 오른쪽으로 정렬된다.
TreeMap<K, V> treeMap = new TreeMap<K,V>();
TreeMap
은 Map
의 child이기 때문에 Map
의 특성을 가지지만, 다음과 같은 TreeMap
메서드들도 있다.
메서드 | 설명 |
---|---|
Map.Entry<K,V> firstEntry() | 제일 낮은 Map.Entry 를 반환 |
Map.Entry<K,V> lastEntry() | 제일 높은 Map.Entry 를 반환 |
Map.Entry<K,V> lowerEntry(K key) | 주어진 key보다 바로 아래 Map.Entry 를 반환 |
Map.Entry<K,V> higherEntry(K key) | 주어진 key보다 바로 위 Map.Entry 를 반환 |
Map.Entry<K,V> floorEntry(K key) | 주어진 key와 동등한 key가 있으면 해당 Map.Entry 를 반환, 없다면 주어진 key 바로 아래의 Map.Entry 를 반환 |
Map.Entry<K,V> ceilingEntry(K key) | 주어진 key와 동등한 key가 있으면 해당 Map.Entry 를 반환, 없다면 주어진 key바로 위의 Map.Entry 를 반환 |
Map.Entry<K,V> pollFirstEntry() | 제일 낮은 Map.Entry 를 반환하고 제거 |
Map.Entry<K,V> pollLastEntry() | 제일 높은 Map.Entry 를 반환하고 제거 |
NavigableSet descendingKeySet() | 내림차순으로 정렬된 key의 NavigableSet을 반환 |
NavigableMap<K,V> descendingMap() | 내림차순으로 정렬된 Map.Entry 의 NavigableMap을 반환 |
NavigableMap<K,V> headMap(K to, boolean inclusive) | 주어진 key보다 낮은 Map.Entry 들을 NavigableMap으로 반환, 주어진 key의 Map.Entry 포함 여부는 두 번째 매개값에 따라 달라짐 |
NavigableMap<K,V> tailMap(K from, boolean inclusive) | 주어진 객체보다 높은 Map.Entry 들을 NavigableMap으로 반환, 주어진 객체 포함 여부는 두 번째 매개값에 따라 달라짐 |
NavigableMap<K,V> subMap(K from, boolean fromInclusive, K to, boolean toInclusive) | 시작과 끝으로 주어진 key 사이의 Map.Entry 들을 NavigableMap 컬렉션으로 반환, 시작과 끝 key의 Map.Entry 포함 여부는 두 번째, 네 번째 매개값에 따라 달라짐 |
아래 예제는 영어 단어와 페이지 번호를 무작위로 저장하고 검색하는 방법을 보여준다.
public class Main {
public static void main(String[] args) {
TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("apple", 10);
treeMap.put("forever", 60);
treeMap.put("description", 40);
treeMap.put("ever", 50);
treeMap.put("zoo", 80);
treeMap.put("base", 20);
treeMap.put("guess", 70);
treeMap.put("cherry", 30);
Set<Map.Entry<String, Integer>> entrySet = treeMap.entrySet();
for(Map.Entry<String,Integer> entry: entrySet) {
System.out.println(entry.getKey() + "-" + entry.getValue());
}
System.out.println();
Map.Entry<String, Integer> entry = null;
entry = treeMap.firstEntry();
System.out.println("First word: " + entry.getKey() + "-" + entry.getValue());
entry = treeMap.lastEntry();
System.out.println("Last word: " + entry.getKey() + "-" + entry.getValue());
entry = treeMap.lowerEntry("ever");
System.out.println("the word before ever: " + entry.getKey() + "-" + entry.getValue());
NavigableMap<String, Integer> descendingMap = treeMap.descendingMap();
Set<Map.Entry<String, Integer>> descendingEntrySet = descendingMap.entrySet();
for(Map.Entry<String, Integer> e: descendingEntrySet) {
System.out.println(e.getKey() + "-" + e.getValue());
}
System.out.println();
System.out.println("[search between c and b]");
NavigableMap<String, Integer> rangeMap = treeMap.subMap("c", true, "h", false);
for(Map.Entry<String, Integer> e: rangeMap.entrySet()) {
System.out.println(e.getKey() + "-" + e.getValue());
}
}
}
결과는 아래와 같다.
apple-10
base-20
cherry-30
description-40
ever-50
forever-60
guess-70
zoo-80
First word: apple-10
Last word: zoo-80
the word before ever: description-40
zoo-80
guess-70
forever-60
ever-50
description-40
cherry-30
base-20
apple-10
[search between c and b]
cherry-30
description-40
ever-50
forever-60
guess-70
TreeSet
에 저장되는 객체와 TreeMap
에 저장되는 key 객체는 저장과 동시에 오름차순으로 정렬되는데, 어떤 객체든 정렬될 수 있는 것은 아니고, 객체가 Comparable
interface를 구현하고 있어야 가능하다. Integer
, Double
, String
타입은 모두 Comparable
을 구현하고 있기 때문에 상관없지만, 사용자 정의 객체를 저장할 때는 반드시 Comparable
을 구현하고 있어야 한다.
Comparable
interface는 compareTo
메서드가 정의되어 있다.
| 메서드 | 설명 |
|----------------------|--------------------|
| int compareTo(T o) | 같으면 0, 작으면 음수, 크면 양수 |
아래는 나이를 기준으로 Person
객체를 오름차순으로 정렬하기 위해 Comparable
인터페이스를 구현하고 있다. 나이가 적으면 -1, 같으면 0, 크면 1을 반환하도록 정의하였다.
public class Person implements Comparable<Person>{
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person person) {
return this.age - person.age;
}
}
Comparable
interface를 받아서 compareTo
를 구현하고 있다.
public class Main {
public static void main(String[] args) {
TreeSet<Person> treeSet = new TreeSet<Person>();
treeSet.add(new Person("Hong", 45));
treeSet.add(new Person("Java", 25));
treeSet.add(new Person("Won", 31));
for(Person person: treeSet) {
System.out.println(person.name + ":" + person.age); // Java:25 Won:31 Hong:45
}
}
}
비교 기능이 있는 Comparable
구현 객체를 TreeSet
에 저장하거나, TreeMap
의 키로 저장하는 것이 원칙이지만, 비교 기능이 없는 Comparable
비구현 객체를 저장하고 싶다면 TreeSet
과 TreeMap
을 생성할 때 비교자(Comparator
)를 다음과 같이 제공하면 된다.
TreeSet<E> treeSet = new TreeSet<E>(new ComparatorImpl());
비교자는 Comparator
인터페이스를 구현한 객체를 말하는데, Comparator
인터페이스에는 compare
메서드가 정의되어있다. 비교자는 이 메서드를 재정의해서 비교 결과를 정수 값으로 리턴하면 된다.
메서드 | 설명 |
---|---|
int compare(T o1, T o2) | 동등하면 0, o1이 o2보다 작으면 음수, o1이 o2보다 크면 양수 |
다음은 Comparable
을 구현하고 있지 않은 Fruit
객체를 TreeSet
에 저장하는 예제이다. TreeSet
을 생성할 때 비교자가 필요한데, FruitComparator
를 비교자로 사용해서 가격 기준으로 Fruit
객체를 정렬시킨다.
public class Fruit {
public String name;
public int price;
public Fruit(String name, int price) {
this.name = name;
this.price = price;
}
}
Comparable
interface를 받고 있지 않다.
public class FruitComparator implements Comparator<Fruit> {
@Override
public int compare(Fruit fruit1, Fruit fruit2) {
return fruit1.price - fruit2.price;
}
}
FruitComparator
객체를 사용하여 TreeSet
이나 TreeMap
에 전달해주면 Fruit
가 compareTo
를 구현하고 있지 않아도 문제없이 비교가 가능하다.
public class Main {
public static void main(String[] args) {
TreeSet<Fruit> treeSet = new TreeSet<Fruit>(new FruitComparator());
treeSet.add(new Fruit("apple", 3000));
treeSet.add(new Fruit("watermelon", 10000));
treeSet.add(new Fruit("orange", 6000));
for(Fruit fruit: treeSet) {
System.out.println(fruit.name +": " + fruit.price);
}
}
}
결과는 아래와 같다.
apple: 3000
orange: 6000
watermelon: 10000
LIFO에 대해서는 Stack
을 제공하고, FIFO에 대해서는 Queue
를 제공한다. 먼저 Stack
에 대해서 알아보자.
메서드 | 설명 |
---|---|
E push(E item) | 주어진 객체를 스택에 넣는다. |
E pop() | 스택의 맨 위의 객체를 빼낸다. |
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.add(10);
stack.add(20);
stack.add(30);
stack.add(40);
System.out.println("stack-pop: " + stack.pop()); // 40
System.out.println("stack-pop: " + stack.pop()); // 30
}
}
Queue
인터페이스는 FIFO 자료구조에서 사용되는 메서드를 정의하고 있다. 다음은 Queue
인터페이스에 정의되어 있는 메서드이다.
메서드 | 설명 |
---|---|
boolean offer(E e) | 주어진 객체를 큐에 넣는다. |
E poll() | 큐에서 객체를 빼낸다. |
Queue
인터페이스를 구현한 대표적인 클래스가 바로 LinkedList
이다. 그렇기 때문에 LinkedList
객체를 Queue
인터페이스 변수에 다음과 같이 대입할 수 있다.
Queue<E> queue1 = new LinkedList<E>();
다음은 Queue를 이용한 예제이다.
public class Main {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<String>();
queue.offer("Mail");
queue.offer("Gmail");
queue.offer("Kakao");
while (!queue.isEmpty()) {
String msg = queue.poll();
System.out.println("Msg: "+ msg);
// Mail
// Gmail
// Kakao
}
}
}
collection들 대부분은 single thread 환경에서 사용할 수 있도록 설꼐되었다. 그렇기 때문에 여러 thread가 동시에 collection에 접근하면 원하는 상태로 동작하지 않는 문제가 있다.
Vector
, Hashable
은 synchronized 메서드로 구성되어 있기 때문에 멀티 thread 환경에서 안전하게 요소를 처리할 수 있지만, ArrayList
와 HashSet
, HashMap
은 동기화된 메서드로 구성되어 있지 않아, 멀티 스레드 환경에 안전하지 않다.
만약 ArrayList
, HashSet
, HashMap
을 멀티 스레드 환경에서 사용하고 싶다면, 비동기화된 메서드를 동기화된 메서드로 wrapping하는 synchronizedList
, synchronizedMap
, synchronizedSet
메서드를 사용하면 된다.
메서드 | 설명 |
---|---|
List synchronizedList(List l) | List를 동기화된 List로 반환 |
Map<K,V> synchronizedMap(Map<K,V> m) | Map을 동기화된 Map으로 반환 |
Set synchronizedSet(Set s) | Set를 동기화된 Set로 반환 |
이 메서드들은 매개값으로 비동기화된 collection을 대입하면 동기화된 collection을 반환한다. 다음은 ArrayList
를 Collections.synchromizedList
메서드를 사용해서 동기화된 List로 변환한다.
List<T> list = Collections.synchronizedList(new ArrayList<T>());
list
는 이제 동기화된 동작으로 실행된다.
다음은 ThreadA
와 ThreadB
에서 동시에 Board
객체를 HashMap
에 각각 1000개씩 추가 후 전체 저장된 수를 출력하는 예제이다.
public class Board {
public String content;
public Board(String content) {
this.content = content;
}
}
public class Main {
public static void main(String[] args) {
Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());
Thread threadA = new Thread() {
@Override
public void run() {
for(int i =1; i <= 1000; i++) {
map.put(i, "content: " + i);
}
}
};
Thread threadB = new Thread() {
@Override
public void run() {
for(int i = 1000; i <= 2000; i++) {
map.put(i, "content: " + i);
}
}
};
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (Exception e) {
e.printStackTrace();
}
int size = map.size();
System.out.println("total object: " + size); // 2000
}
}
만약 Collections.synchronizedMap
로 wrapping하지 않고 HashMap
만 사용했다면 2000이 나오지 않았을 것이다.
수정할 수 없는(unmodified) collection이란 요소를 추가, 삭제할 수 없는 collection을 말한다. collection에 저장된 요소를 변경하고 싶지 않을 때 유용하다. 여러 가지 방법으로 만들 수 있는데, 먼저 첫번째 방법으로 List
, Set
, Map
interface의 정적 메서드인 of()
로 생성할 수 있다.
List<E> immutableList = List.of(E... elements);
Set<E> immutableSet = Set.of(E... elements);
Map<K,V> immutableMap = Map.of(K k1, V v1, K k2, V v2, ...);
두번째 방법으로는 List
, Set
, Map
인터페이스의 정적 메서드인 copyOf()
을 이용해 기존 collection을 복사하여 수정할 수 없는 collection을 만드는 것이다.
List<E> immutableList = List.copyOf(Collection<E> coll);
Set<E> immutableSet = Set.copyOf(Collection<E> coll);
Map<K,V> immutableMap = Map.copyOf(Map<K,V> map);
세 번째 방법은 배열로부터 수정할 수 없는 List
컬렉션을 만들 수 있다.
String[] arr = {"A", "B", "C"};
List<String> immutableList = Arrays.asList(arr);