자바는 널리 알려져 있는 자료구조를 사용해서 객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 인터페이스와 구현 클래스를 java.util 패키지에서 제공한다. 이들을 총칭해서 컬렉션 프레임워크 라고 부른다.
컬렉션은 객체의 저장을 뜻하고, 프레임워크란 사용 방법을 정해놓은 라이브러리를 말한다. 실제로 컬렉션 프레임워크는 사용 방법을 정의한 인터페이스와 실제 객체를 저장하는 다양한 컬렉션 클래스(구현 클래스)를 제공한다.
컬렉션 프레임워크의 주요 인터페이스로는 List, Set, Map이 있다. 이 인터페이스들은 컬렉션 클래스를 사용하는 방법을 정의한 것이다. 그리고 아래는 이들 인터페이스로 사용 가능한 컬렉션 클래스(구현 클래스)를 보여준다.
-----------> Collection <-----------
| |
| |
List Set Map
| | |
|--- ArrayList |--- HashSet |--- HashMap
|--- Vector |--- TreeSet |--- Hashtable
|--- LinkedList |--- TreeMap
|--- Properties
List 컬렉션
List 컬렉션은 배열과 비슷하게 객체를 인덱스로 관리한다. 배열과의 차이점은 저장 용양이 자동으로 증가하여, 객체를 저장할 때 자동 인덱스가 부여된다는 것이다. 그리고 추가, 삭제, 검색을 위한 다양한 메소드들이 제공된다.
List 컬렉션은 객체 자체를 저장하는 것이 아니라 아래와 같이 객체의 번지를 참조한다. 그렇기 때문에 동일한 객체를 중복 저장할 수 있는데, 이 경우 동일한 번지가 참조된다. null도 저장이 가능하며, 이 경우 해당 인덱스는 객체를 참조하지 않는다.
List 컬렉션
| 0 | 1 | 2 | ... | n-1 |
| 번지 | 번지 | 번지 | ... | 번지 |
| 객체1 | 객체2 | 객체2 | ... | 객체3 |
List 컬렉션에는 ArrayList, Vector, LinkedList 등이 있는데, 아래는 List 컬렉션에서 공통적으로 사용 가능한 List 인터페이스의 메소드이다. 인덱스로 객체를 관리하기 때문에 인덱스를 매개값으로 갖는 메소드가 많다.
| 기능 | 메소드 | 설명 |
| 객체 추가 | boolean add(E e) | 주어진 객체를 맨 끝에 추가한다. |
| void add(int index, E element) | 주어진 인덱스에 객체를 추가한다. | |
| E 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) | 주어진 객체를 삭제한다. |
위의 표에서 메소드의 매개 변수 타입과 리턴 타입에 E라는 타입 파라미터가 있는데, 이것은 저장되는 객체의 타입을 List 컬렉션을 생성할 때 결정하라는 뜻이다. 예를 들어 List 컬렉션에 객체를 추가할 때에는 add() 메소드를 사용하고, 객체를 찾아올 때에는 get() 메소드를 사용한다. 그리고 객체 삭제는 remove() 메소드를 사용한다. 아래는 List 컬렉션에 String 객체를 추가, 삽입, 검색, 삭제 하는 방법을 보여준다.
List<String> list = ...;
list.add("홍길동"); // 맨 끝에 객체 추가
list.add(1, "이순신"); // 지정된 인덱스에 객체 삽입
String str = list.get(1); // 인덱스로 객체 검색
list.remove(0); // 인덱스로 객체 삭제
list.remove("이순신"); // 객체 삭제
List<String>으로 list 변수를 선언하였다. 이것은 List 컬렉션에 저장되는 객체를 String 타입으로 하겠다는 뜻이다. 따라서 E 타입 파라미터는 String 타입이 되는 것이다. 그래서 add() 메소드의 매개값은 문자열이 되고 get() 메소드의 리턴값은 문자열이 된다.
List 컬렉션에 저장된 모든 객체를 대상으로 하나씩 가져와 처리하고 싶다면 인덱스를 이용하는 방법과 향상된 for문(forEach문) 을 이용하는 방법이 있다. 아래는 인덱스를 이용한 방법이다. List 컬렉션의 size() 메소드는 현재 저장되어 있는 객체 수를 리턴한다.
List<String> list = ...;
for(int i = 0; i < list.size(); i++){ <-- 저장된 총 객체 수만큼 루핑
String str = list.get(i); <-- i 인덱스에 저장된 String 객체를 가져옴
}
다음은 향상된 for문(forEach문)을 이용하는 방법이다. List 컬렉션에 저장된 객체 수만큼 반복하면서 객체를 하나씩 str 변수에 대입한다.
|------| <-- String 객체를 하나씩 가져옴
for(String str : list){ <-- 저장된 총 객체 수만큼 루핑
...
}
ArrayList
ArrayList는 List 인터페이스의 대표적인 구현 클래스이다. 아래는 ArrayList 객체를 생성하는 방법이다.
List<E> list = new ArrayList<E>();
| |
-------------------------
|
타입 파라미터
ArrayList를 생성하기 위해서는 저장할 객체 타입을 E 타입 파라미터 자리에 표기하고 기본 생성자를 호출하면 된다. 예를 들어 String을 저장하는 ArrayList는 아래와 같이 생성할 수 있다.
List<String> list = new ArrayList<String>();
List<String> list = new ArrayList<>();
두 번째 코드와 같이 ArrayList의 E 타입 파라미터를 생략하면 왼쪽 List 에 지정된 타입을 따라 간다. 따라서 위의 두 코드는 동일하게 String을 저장하는 ArrayList 객체를 생성한다.
기본 생성자로 ArrayList 객체를 생성하면 내부에 10개의 객체를 저장할 수 있는 초기 용량을 가지게 된다. 저장되는 객체 수가 늘어나면 용량이 자동으로 증가한다.
ArrayList에 객체를 추가하면 0번 인덱스부터 차례대로 저장된다. ArrayList에서 특정 인덱스의 객체를 제거하면 바로 뒤 인덱스부터 마지막 인덱스까지 모두 앞으로 1씩 당겨진다. 마찬가지로 특정 인덱스에 객체를 삽입하면 해당 인덱스부터 마지막 인덱스까지 모두 1씩 밀려난다.
이러한 동작 때문에 저장된 객체 수가 많고, 특정 인덱스에 객체를 추가하거나 제거하는 일이 빈번하다면 ArrayList 보다는 LinkedList를 사용하는 것이 좋다. 하지만 인덱스를 이용해서 객체를 찾거나 객체를 추가하는 경우에는 ArrayList가 더 좋은 성능을 발휘한다.
Vector
Vector는 ArrayList와 동일한 내부 구조를 가지고 있다. Vector를 생성하기 위해서는 저장할 객체 타입을 타입 파라미터로 표기하고 기본 생성자를 호출하면 된다.
List<E> list = new Vector<E>();
List<E> list = new Vector<>();
ArrayList와 다른점은 Vector는 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Vector의 메소드들을 실행할 수 없고, 하나의 스레드가 메소드를 실행을 완료해야만 다른 스레드가 메소드를 실행할 수 있다는 것이다. 그래서 멀티 스레드 환경에서 안전하게 객체를 추가, 삭제할 수 있다. 이것을 스레드에 안전하다고 한다.
LinkedList
LinkedList는 List 구현 클래스이므로 ArrayList와 사용 방법은 똑같은데, 내부 구조는 완전 다르다. ArrayList는 내부 배열에 객체를 저장해서 관리하지만, LinkedList는 인접 참조를 링크해서 체인처럼 관리한다.
LinkedList에서 특정 인덱스의 객체를 제거하면 앞뒤 링크만 변경되고 나머지 링크는 변경되지 않는다. 특정 인덱스에 객체를 삽입할 때에도 마찬가지이다.
LinkedList를 생성하기 위해서는 저장할 객체 타입을 타입 파라미터(E)에 표시하고 기본 생성자를 호출하면 된다. LinkedList가 처음 생성될 때에는 어떠한 링크도 만들어지지 않기 때문에 내부는 비어있다고 보면 된다.
List<E> list = new LinkedList<E>();
List<E> list = new LinkedList<>();
ArrayList 와 LinkedList 의 실행 성능 비교
| 구분 | 순차적으로 추가/삭제 | 중간에 추가/삭제 | 검색 |
| ArrayList | 빠르다 | 느리다 | 빠르다 |
| LinkedList | 느리다 | 빠르다 | 느리다 |
Set 컬렉션
List 컬렉션은 객체의 저장 순서를 유지하지만, Set 컬렉션은 저장 순서가 유지되지 않는다. 또한 객체를 중복해서 저장할 수 없고, 하나의 null만 저장할 수 있다.
Set 컬렉션은 수학의 집합과 비슷하다. 집합은 순서와 상관없고 중복이 허용되지 않기 때문이다. 그리고 구슬 주머니와도 같다. 동일한 구슬을 2개 넣을 수 없고, 들어갈(저장할) 때의 순서나 나올(찾을) 때의 순서가 다를 수도 있기 때문이다.
Set 컬렉션에는 HashSet, LinkedHashSet, TreeSet 등이 있는데, 아래는 Set 컬렉션에서 공통적으로 사용 가능한 Set 인터페이스의 메소드이다. 인덱스로 관리하지 않기 때문에 인덱스를 매개값으로 갖는 메소드가 없다.
| 기능 | 메소드 | 설명 |
| 객체 추가 | boolean add(E e) | 주어진 객체를 저장한다. 객체가 성공적으로 저장되면 true를 리턴하고 중복 객체면 false를 리턴한다. |
| 객체 검색 | boolean contains(Object o) | 주어진 객체가 저장되어 있는지 조사한다. |
| boolean isEmpty() | 컬렉션이 비어있는지 조사한다. | |
| Iterator<E> iterator() | 저장된 객체를 한 번씩 가져오는 반복자를 리턴한다. | |
| int size() | 저장되어 있는 전체 객체 수를 리턴한다. | |
| 객체 삭제 | void clear() | 저장된 모든 객체를 삭제한다. |
| boolean remove(Object o) | 주어진 객체를 삭제한다. |
List 컬렉션에서 봤던 것처럼 위 표에도 메소드의 매개 변수 타입과 리턴 타입에 E라는 타입 파라미터가 있는데, 이것은 저장되는 객체의 타입을 Set 컬렉션을 생성할 때 결정하라는 뜻이다. 예를 들어 아래는 Set 컬렉션에 String 객체를 저장하고 삭제하는 방법이다.
Set<String> set = ...;
set.add("홍길동"); // 객체 추가
set.add("이순신");
set.remove("홍길동"); // 객체 삭제
Set<String> 으로 set 변수를 선언하였다. 이것은 Set 컬렉션에 저장되는 객체를 String 타입으로 하겠다는 뜻이다. 따라서 E 타입 파라미터는 String 타입이 되는 것이다. 그래서 add() 메소드와 remove() 메소드의 매개값은 문자열이 된다.
Set 컬렉션은 인덱스로 객체를 검색해서 가져오는 메소드가 없다. 대신, 전체 객체를 대상으로 한 번씩 반복해서 가져오는 반복자(Iterator)를 제공한다. 반복자는 Iterator 인터페이스를 구현한 객체를 말하는데, iterator() 메소드를 호출하면 얻을 수 있다.
Set<String> set = ...;
Iterator<String> iterator = set.iterator();
Iterator<String> 타입의 iterator 변수에 대입한 이유는 반복해서 가져올 객체가 String 타입이기 때문이다. 아래는 Iterator 인터페이스에 선언된 메소드들이다.
| 리턴 타입 | 메소드 | 설명 |
| boolean | hasNext() | 가져올 객체가 있으면 true를 리턴하고 없으면 false를 리턴한다. |
| E | next() | 컬렉션에서 하나의 객체를 가져온다. |
| void | remove() | Set 컬렉션에서 객체를 제거한다. |
Iterator에서 하나의 객체를 가져올 때는 next() 메소드를 사용한다. next() 메소드를 사용하기전에 먼저 가져올 객체가 있는지 확인하는 것이 좋다. hasNext() 메소드는 가져올 객체가 있으면 true를 리턴하고 더 이상 가져올 객체가 없으면 false를 리턴한다. 따라서 true 가 리턴될 때 next() 메소드를 사용해야 한다.
아래는 Set 컬렉션에서 String 객체들을 반복해서 하나씩 가져오는 코드이다.
Set<String> set = ...;
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){ <-- 저장된 객체 수만큼 루핑
//String 객체를 하나씩 가져옴
String str = iterator.next();
}
Iterator를 사용하지 않더라도 향상된 for(forEach)문을 이용해서 전체 객체를 대상으로 반복할 수 있다.
Set<String> set = ...;
for(String str : set){ <-- 저장된 객체 수만큼 루핑
}
Set 컬렉션에서 Iterator의 next() 메소드로 가져온 객체를 제거하고 싶다면 remove() 메소드를 호출하면 된다. Iterator의 메소드이지만, 실제 Set 컬렉션에서 객체가 제거됨을 알아야 한다. 아래는 Set 컬렉션에서 "홍길동"을 제거한다.
while(iterator.hasNext()){
Stirng str = iterator.next();
if(str.equals("홍길동"){
iterator.remove();
}
}
HashSet
HashSet은 Set 인터페이스의 구현 클래스이다. HashSet을 생성하기 위해서는 아래와 같이 기본 생성자를 호출하면 된다.
Set<E> set = new HashSet<E>();
타입 파라미터 E에는 컬렉션에 저장할 객체 타입을 지정하면 된다. 예를 들어 String 객체를 저장하는 HashSet은 아래와 같이 생성할 수 있다.
Set<String> set = new HashSet<String>();
Set<String> set = new HashSet<>();
HashSet은 객체들을 순서 없이 저장하고 동일한 객체는 중복 저장하지 않는다. HashSet이 판단하는 동일한 객체란 꼭 같은 인스턴스를 뜻하지는 않는다. HashSet은 객체를 저장하기 전에 먼저 객체의 hashCode() 메소드를 호출해서 해시코들르 얻어내고, 이미 저장되어 있는 객체들의 해시코드와 비교한다. 만약 동일한 해시코드가 있다면 다시 equals() 메소드로 두 객체를 비교해서 true가 나오면 동일한 객체로 판단하고 중복 저장을 하지 않는다.
문자열을 HashSet에 저장할 경우에 같은 문자열을 갖는 String 객체는 동등한 객체로 간주되고 다른 문자열을 갖는 String 객체는 다른 객체로 간주되는데, 그 이유는 String 클래스가 hashCode() 와 equals() 메소드를 재정의해서 같은 문자열일 경우 hashCode() 의 리턴값은 같게, equals()의 리턴값은 true 가 나오도록 했기 때문이다.
Map 컬렉션
Map 컬렉션은 키와 값으로 구성된 Map.Entry 객체를 저장하는 구조를 가지고 있다. Enrty는 Map 인터페이스 내부에 선언된 중첩 인터페이스이다. 여기서 키와 값은 모두 객체이다. 키는 중복 저장할 수 없지만, 값은 중복 저장될 수 있다. 만약 기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값은 없어지고 새로운 값으로 대체된다.
Map 컬렉션에는 HashMap, Hashtable, LinkedHashMap, Properties, Treemap 등이 있다. 아래는 Map 컬렉션에서 공통적으로 사용 가능한 Map 인터페이스의 메소드들이다. 키로 객체들을 관리하기 때문에 키를 매개값으로 갖는 메소드가 많다.
| 기능 | 메소드 | 설명 |
| 객체 추가 | V put(K key, V value) | 주어진 키로 값을 저장한다. 새로운 키일 경우 null을 리턴하고 동일한 키가 있을 경우 값을 대체하고 이전 값을 리턴한다. |
| 객체 검색 | boolean containesKey(Object key) | 주어진 키가 있는지 여부를 확인한다. |
| booelan containesValue(Object value) | 주어진 값이 있는지 여부를 확인한다. | |
| Set<Map.Enrty<K, V>> entrySet() | 키와 값의 쌍으로 구성된 모든 Map.Entry 객체를 Set에 담아서 리턴한다. | |
| V get(Object key) | 주어진 키가 있는 값을 리턴한다. | |
| boolean isEmpty() | 컬렉션이 비어 있는지 여부를 확인한다. | |
| Set<K> keySet() | 모든 키를 Set 객체에 담아서 리턴한다. | |
| int size() | 저장된 키의 총 수를 리턴한다. | |
| Collection<V> values() | 저장된 모든 값을 Collection에 담아서 리턴한다. | |
| 객체 삭제 | void clear() | 모든 Map.Entry(키와 값)를 삭제한다. |
| V remove(Object key) | 주어진 키와 일치하는 Map.Entry를 삭제하고 값을 리턴한다. |
위 표에서 메소드의 매개 변수 타입과 리턴 타입에 K와 V라는 타입 파라미터가 있다. 이것으 저장되는 키와 객체의 타입을 Map 컬렉션을 생성할 때 결정하라는 뜻이다. 예를 들어 키 타입이 String, 값 타입이 Integer인 Map 컬렉션을 생성하고, put() 메소드로 키와 값을 저장한다. 그리고 키로 값을 얻거나 제거하기 위해 get() 과 remove() 메소드를 사용했다.
Map<String, Integer> map = ...;
map.put("홍길동", 30); // 객체 추가
int score = map.get("홍길동"); // 객체 찾기
map.remove("홍길동"); // 객체 삭제
Map<String, Integer>로 map 변수를 선언하였다. 이것은 Map 컬렉션에 저장되는 키 객체는 String 타입으로, 값 객체는 Integer 타입으로 하겠다는 뜻이다. 따라서 K 타입 파라미터는 String이 되고, V 타입 파라미터는 Integer가 된다. 그래서 put() 메소드의 첫 번째 매개값은 문자열이고, 두 번째 매개값은 30이 포장된 Integer 객체(자동 박싱)가 된다.
저장된 전체 객체를 대상으로 하나씩 얻고 싶을 경우에는 두 가지 방법을 사용할 수 있다. 첫 번쨰는 keySet() 메소드로 모든 키를 Set 컬렉션으로 얻은 다음, 반복자를 통해 키를 하나씩 얻고 get() 메소드를 통해 값을 얻는 방법이다.
Map<K, V> map = ...;
Set<k> keySet = map.keySet();
Iterator<K> keyIterator = keySet.iterator();
while(keyIterator.hasNext()){
K key = keyIterator.next();
V value = map.get(key);
}
두 번째는 enrtySet() 메소드로 모든 Map.Entry 를 Set 컬렉션으로 얻은 다음, 반복자를 통해 Map.Entry를 하나씩 얻고 getKey() 와 getValue() 메소드를 이용해 키와 값을 얻는 방법이다.
Set<Map.Entry<K, V>> entrySet = map.entrySet();
Iterator<Map.Entry<K, V>> entryIterator = entrySet.iterator();
while(entryIterator.hasNext()){
Map.Entry<K, V> entry = entryIterator.next();
K key = entry.getKey();
V value = entry.getValue();
}
HashMap
HashMap은 Map 인터페이스를 구현한 대표적인 Map 컬렉션이다. HashMap의 키로 사용할 객체는 hashCode() 와 equals() 메소드를 재정의해서 동등 객체가 될 조건을 정해야 한다. 객체가 달라도 동등 객체라면 같은 키로 간주하고 중복 저장되지 않도록 하기 위함이다. 동등 깨체의 조건은 hashCode() 의 리턴값이 같아야하고, equals() 메소드가 true를 리턴해야 한다.
주로 키 타입은 String을 많이 사용하는데, String은 문자열이 같을 경우 동등 객체가 될 수 있도록 hashCode()와 equals() 메소드가 재정의되어 있다. HashMap을 생성하기 위해서는 키 타입과 값 타입을 타입 파라미터로 주고 기본 생성자를 호출하면 된다.
키와 값의 타입은 기본 타입을 사용할 수 없고, 클래스 및 인터페이스 타입만 사용이 가능하다. 키로 String 타입으 사용하고 값으로 Integer 타입을 사용하는 hashMap은 아래와 같이 생성할 수 있다.
Map<String, Integer> map = new HashMap<Stirng, Integer>();
Map<String, Integer> map = new HashMap<>();
Hashtable
Hashtable은 HashMap과 동일한 내부 구조를 가지고 있다. Hashtable도 키로 사용할 객체는 hashCode()와 equals() 메소드를 재정의해서 동등 객체가 될 조건을 정해야 한다.
HashMap과의 차이점은 Hashtable은 동기화된 메소드로 구성되어 있기 때문에 멀티 스레드가 동시에 Hashtable의 메소드들을 실행할 수 없고, 하나의 스레드가 실행을 완료해야지만 다른 스레드에 실행할 수 있다는 것이다. 그래서 멀티 스레드 환경에서 안전하게 객체를 추가, 삭제할 수 있기 때문에 hashtable은 스레드에 안전하다.
Hashtable의 생성 방법은 HashMap과 크게 다르지 않다. 키 타입과 값 타입을 지정하고 기본 생성자를 호출하면 된다.
Map<K, V> map = new Hashtable<K, V>();
키로 String 타입을 사용하고, 값으로 Integer 타입을 사용하는 Hashtable은 아래와 같이 생성할 수 있다.
Map<String, Integer> map = new Hashtable<String, Integer>();
Map<String, Integer> map = new Hashtable<>();'Java' 카테고리의 다른 글
| 자바 입출력 스트림 (0) | 2023.06.29 |
|---|---|
| 자바 LIFO와 FIFO 컬렉션 (0) | 2023.06.29 |
| 자바 스레드 제어 (0) | 2023.06.29 |
| 자바 멀티 스레드 (0) | 2023.06.28 |
| 자바 java.util 패키지 (1) | 2023.06.28 |