[Java] Java의 정석 | Chapter 11 컬렉션 프레임웍 Collections Framework (1)

숙취엔 꿀물.·2024년 1월 24일

Java

목록 보기
11/13
post-thumbnail

👉 1. 컬렉션 프레임웍(Collections Framework)

컬렉션 프레임웍 : '데이터 군(群)을 저장하는 클래스들을 표준화한 설계'

  • 컬렉션 프레임웍은 컬렉션, 다수의 데이터 등을 다루는 데 필요한 다양하고 풍부한 클래스들을 제공하기 때문에 프로그래머의 짐을 상당히 덜어 주고 있으며,

  • 또한 인터페이스와 다형성을 이용한 객체지향적 설계를 통해 표준화되어 있기 때문에 사용법을 익히기에도 편리하고

  • 재사용성이 높은 코드를 작성할 수 있다는 장점이 있다.


1.1 컬렉션 프레임웍의 핵심 인터페이스

각 컬렉션을 다루는데 필요한 기능을 가진 List, Set, Map 3개의 인터페이스를 정의하였다.

그리고 인터페이스 List와 Set의 공통된 부분을 다시 뽑아서 새로운 인터페이스인 Collection 을 추가로 정의하였다.

컬렉션 프레임웍의 핵심 인터페이스간의 상속계층도

  • List와 Set은 공통부분이 있으나 Map은 전혀 다른 형태로 컬렉션을 다룸

<표11-1, 컬렉션 프레임웍의 핵심 인터페이스와 특징>

인터페이스특징
List순서가 있는 데이터의 집합. 데이터의 중복을 허용한다.
예) 대기자 명단
구현클래스 : ArrayList, LinkedList, Stack, Vector 등
Set순서를 유지하지 않는 데이터의 집합. 데이터의 중복을 허용하지 않는다.
예) 양의 정수집합, 소수의 집합
구현클래스: HashSet, TreeSet 등
Map키(key)와 값(value)의 쌍(pair)으로 이루어진 데이터의 집합
순서는 유지되지 않으며, 키는 중복을 허용하지 않고, 값은 중복을 허용한다.
예)우편번호, 지역번호(전화번호)
구현클래스: HashMap, TreeMap, Hashtable, Properties 등

구현한 인터페이스의 이름이 클래스의 이름에 포함되어 있어서 이름만으로도 클래스의 특징을 쉽게 알 수 있도록 되어있다.

Vector, Hashtable등과 같이 기존의 컬렉션 클래스들은 호환을 위해, 설계를 변경해서 남겨두었지만 가능하면 사용하지 않는 것이 좋다.
-> 대신에 ArrayList와 HashMap을 사용하자.


Collection 인터페이스

List와 Set의 조상인 Collection 인터페이스에는 다음과 같은 메서드들이 정의되어 있다.

<표11-2, Collection인터페이스에 정의된 메서드>

메서드설명
boolean add(Object o)
boolean addAll(Collection c)
지정된 객체(o) 또는 Collection(c) 의 객체들을 Collection에 추가한다.
void clear()Collection의 모든 객체를 삭제한다.
boolean contains(Object o)
boolean containsAll(Collection c)
지정된 객체(o) 또는 Collection의 객체들이 Collection에 포함되어 있는지 확인한다.
boolean equals(Object o)동일한 Collection인지 비교한다.
int hashCode()Collection의 hash code를 반환한다.
boolean isEmpty()Collection이 비어있는지 확인한다.
Iterator iterator()Collection의 Iterator를 얻어서 반환한다.
boolean remove(Object o)지정된 객체를 삭제한다.
boolean removeAll(Collection c)지정된 Collection에 포함된 객체들을 삭제한다.
boolean retainAll(Collection c)지정된 Collection에 포함된 객체만을 남기고 다른 객체들은 Collection에서 삭제한
다. 이 작업으로 인해 Collection에 변화가 있으면 true를 그렇지 않으면 false를 반
환한다.
int size()Collection에 저장된 객체의 개수를 반환한다.
Object[] toArray()Collection에 저장된 객체를 객체배열(Object[])로 반환한다.
Object[] toArray(Object[] a)지정된 배열에 Collection의 객체를 저장해서 반환한다.

Collection 인터페이스는 컬렉션 클래스에 저장된 데이터를 읽고, 추가하고 삭제하는 등 컬렉션을 다루는데 가장 기본적인 메서드들을 정의하고 있다.

원래 위의 표에서 Object가 아닌 E로 표기되어있는데, 지네릭스(Generics)를 아직 배우지 않았기 때문에 임의로 표기를 변경했다고 한다.


List 인터페이스

List 인터페이스는 중복을 허용하면서 저장순서가 유지되는 컬렉션을 구현하는데 사용된다.
그림11-3 List의 상속계층도

<List 인터페이스에 정의된 메서드 (Collection 인터페이스로부터 상속받은 것들은 제외)>

메서드설명
void add(int index, Object element)
boolean addAll(int index, Collection c)
지정된 위치(index)에 객체(element) 또는 컬렉션에 포함된 객체들을 추가한다.
Object get(int index)지정된 위치(index)에 있는 객체를 반환한다.
int indexOf(Object o)지정된 객체의 위치(index)를 반환한다.
(List의 첫 번쨰 요소부터 순방향으로 찾는다.
int lastIndexOf(Object o)지정된 객체의 위치(index)를 반환한다.
(List의 마지막 요소부터 역방향으로 찾는다.)
ListIterator listIterator()
ListIterator listIterator(int index)
List의 객체에 접근할 수 있는 ListIterator를 반환한다.
Object remove(int index)지정된 위치(index)에 있는 객체를 삭제하고 삭제된 객체를 반환한다.
Object set(int index, Object element)지정된 위치(index)에 객체(element)를 저장한다.
void sort(Comparator c)지정된 비교자(comparator)로 List를 정렬한다.
List subList(int fromIndex, int toIndex)지정된 범위(fromIndex부터 toIndex)에 있는 객체를 반환한다.

Set 인터페이스

Set 인터페이스는 중복 허용 X, 저장순서 유지 X의 컬렉션 클래스를 구현하는데 사용된다.


Map 인터페이스

Map 인터페이스는 키(key)와 값(value)을 하나의 쌍으로 묶어서 저장하는 컬렉션 클래스를 구현하는 데 사용된다.

  • 키는 중복될 수 없지만 값은 중복을 허용한다.
  • 기존에 저장된 데이터와 중복된 키와 값을 저장하면 기존의 값은 없어지고 마지막에 저장된 값이 남게된다.
메서드설명
void clear()Map의 모든 객체를 삭제한다.
boolean containsKey(Object key)지정된 key객체와 일치하는 Map의 key객체가 있는지 확인한다.
boolean containsValue(Object value)지정된 value객체와 일치하는 map의 value객체가 있는지 확인한다.
Set entrySet()Map에 저장되어 있는 key-value쌍을 Map.Entry타입의 객체로 저장한 Set으로 반환한다.
boolean equals(Object o)동일한 Map인지 비교한다.
Object get(Object key)지정한 key객체에 대응하는 value객체를 찾아서 반환한다.
int hashCode()해시코드를 반환한다.
boolean isEmpty()Map이 비어있는지 확인한다.
Set keySet()Map에 저장된 모든 key객체를 반환한다.
Object put(Object key, Object value)Map에 value객체를 key객체에 연결(mapping)하여 저장한다.
void putAll(Map t)지정된 Map의 모든 key-value쌍을 추가한다.
Object remove(Object key)지정한 key객체와 일치하는 key-value객체를 삭제한다.
int size()Map에 저장된 key-value쌍의 개수를 반환한다.
Collection values()Map에 저장된 모든 value객체를 반환한다.

values() 와 keySet()를 보면, Map 인터페이스에서는

  • 값(value)은 중복을 허용하기 때문에 Collection타입으로 반환하고,
  • 키(key)는 중복을 허용하지 않기 때문에 Set타입으로 반환한다.

Map.Entry 인터페이스

Map인터페이스의 내부 인터페이스이다. Map에 저장되는 key-value쌍을 다루기 위해 내부적으로 Entry인터페이스를 정의해 놓았다.

보다 객체지향적으로 설계하도록 유도하기 위한 것으로 Map인터페이스를 구현하는 클래스에서는 Map.Entry인터페이스도 함께 구현해야 한다.

public interface Map {
	...
    public static interface Entry {
    	Object getKey();
        Object getValue();
        Object setValue(Object value);
        boolean equals(Object o);
        int hashCode();
        ...
	}
}
메서드설명
boolean equals(Object o)동일한 Entry인지 비교한다.
Object getKey()Entry의 key객체를 반환한다.
Object getValue()Entry의 value객체를 반환한다.
int hashCode()Entry의 해시코드를 반환한다.
Object setValue(Object value)Entry의 value객체를 지정된 객체로 바꾼다.

1.2 ArrayList

ArrayList 는 컬렉션 프레임웍에서 가장 많이 사용되는 컬렉션 클래스일 것이다. List 인터페이스를 구현하기 때문에 데이터의 저장순서가 유지 되고 중복을 허용 한다는 특징을 갖는다.

Object 배열을 이용해서 데이터를 순차적으로 저장한다.

public class ArrayList extends AbstractList
	implements List, RandomAccess, Cloneable, java.io.Serializable {
    	...
        transient Object[] elementData;	// Object배열
        ...
}

<표11-6 ArrayList의 생성자와 메서드>

메서드설명
ArrayList()크기가 10인 ArrayList를 생성
ArrayList(Collection c)주어진 컬렉션이 저장된 ArrayList를 생성
ArrayList(int initialCapacity)지정된 초기용량을 갖는 ArrayList를 생성
boolean add(Object o)ArrayList의 마지막에 객체를 추가. 성공하면 true
void add(int index, Object element)지정된 위치(index)에 객체를 저장
boolean addAll(Collection c)주어진 컬렉션의 모든 객체를 저장한다.
boolean addAll(int index, Collection c)지정된 위치부터 주어진 컬렉션의 모든 객체를 저장한다.
void clear()ArrayList를 완전히 비운다.
Object clone()ArrayList를 복제한다.
boolean contains(Object o)지정된 객체(o)가 ArrayList에 포함되어 있는지 확인
void ensureCapacity(int minCapacity)ArrayList의 용량이 최소한 minCapacity가 되도록 한다.
Object get(int index)지정된 위치(index)에 지정된 객체를 반환한다.
int indexOf(Object o)지정된 객체가 저장된 위치를 찾아 반환한다.
boolean isEmpty()ArrayList가 비어있는지 확인한다.
Iterator iterator()ArrayList의 Iterator객체를 반환
int lastIndexOf(Object o)객체(o)가 저장된 위치를 끝부터 역방향으로 검색해서 반환
ListIterator listIterator()ArrayList의 ListIterator를 반환
ListIterator listIterator(int index)ArrayList의 지정된 위치부터 시작하는 ListIterator를 반환
Object remove(int index)지정된 위치(index)에 있는 객체를 제거한다.
boolean remove(Object o)지정한 객체를 제거한다.(성공하면 true, 실패하면 false)
boolean removeAll(Collection c)지정한 컬렉션에 저장된 것과 동일한 객체들을 ArrayList에서 제거한다.
boolean retainAll(Collection c)ArrayList에 저장된 객체 중에서 주어진 컬렉션과 공통된 것들만을 남기고 나머지는 삭제한다.
Object set(int index, Object element)주어진 객체(element)를 지정된 위치(index)에 저장한다.
int size()ArrayList에 저장된 객체의 개수를 반환한다.
void sort(Comparator c)지정된 정렬기준(c)으로 ArrayList를 정렬
List subList(int fromIndex, int toIndex)fromIndex부터 toIndex 사이에 저장된 객체를 반환한다.
Object[] toArray()ArrayList에 저장된 모든 객체들을 객체배열로 반환한다.
Object[] toArray(Object[] a)ArrayList에 저장된 모든 객체들을 객체배열 a에 담아 반환한다.
void trimToSize()용량을 크기에 맞게 줄인다.(빈 공간을 없앤다.)

테스트 코드는 패스..

Iterator 가 뭐지 싶었는데 <1.5> 파트에서 나온다. Collections 클래스에 대한 내용과 정렬(sort)하는 방법도 마찬가지 !

ArrayList를 생성할 때, 저장할 요소의 개수를 고려해서 실제 저장할 개수보다 약간 여유있는 크기로 하는 것이 좋다.

생성할 때 지정한 크기보다 더 많은 객체를 저장하면 자동적으로 크기가 늘어나기는 하지만 이 과정에서 처리시간이 많이 소요되기 때문이다.

심화 내용은 생략하도록 하겠다.

  • ArrayList나 Vector 클래스와 같이 Java API에서 제공하는 기본 클래스의 실제 소스를 보고 싶다면, JDK를 설치한 디렉토리의 src.zip 파일에서 볼 수 있다.

  • 혹은 Intellij IDE에서 아래와 같이 있을 때, Vector 글씨에 Ctrl + 마우스 좌클릭 을 하면 해당 API의 실제 코드를 편하게 볼 수 있으니 참고 바란다.


1.3 LinkedList

일반 배열은 가장 기본적인 형태의 자료구조로

  • 구조가 간단하며 사용하기 쉽고 데이터를 읽어 오는데 걸리는 시간(접근시간, access time)이 가장 빠르다는 장점을 가지고 있지만
  • 다음과 같은 단점도 가지고 있다.

1. 크기를 변경할 수 없다.
- 크기를 변경할 수 없으므로 새로운 배열을 생성해서 데이터를 복사해야한다.
- 실행속도를 향상시키기 위해서는 충분히 큰 크기의 배열을 생성해야 하므로 메모리가 낭비된다.

2. 비순차적인 데이터의 추가 또는 삭제에 시간이 많이 걸린다.
- 차례대로 데이터를 추가하고 마지막에서부터 데이터를 삭제하는 것은 빠르지만,
- 배열의 중간에 데이터를 추가하려면, 빈자리를 만들기 위해 다른 데이터들을 복사해서 이동해야 한다.

이러한 배열의 단점을 보완하기 위해서 링크드 리스트(linked list) 라는 자료구조가 고안되었다.

/* 대략적인 구조 */

// 연결리스트
class Node {
	Node next;	// 다음 요소의 주소를 저장
    Object obj;	// 데이터를 저장
}

// 이중 연결리스트
class Node {
	Node next;		// 다음 요소의 주소를 저장
    Node previous;	// 이전 요소의 주소를 저장
    Object obj;		// 데이터를 저장
}

좀 많긴한데 그냥 다 적으려고 한다...

<표11-7 LinkedList의 생성자와 메서드>

생성자 또는 메서드설명
LinkedList()LinkedList객체를 생성
LinkedList(Collection c)주어진 컬렉션을 포함하는 LinkedList객체를 생성
boolean add(Object o)지정된 객체(o)를 LinkedList의 끝에 추가, 저장에 성공하면 true, 실패하면 false
void add(int index, Object element)지정된 위치(index)에 객체(element)를 추가
boolean addAll(Collection c)주어진 컬렉션에 포함된 모든 요소를 LinkedList의 끝에 추가한다. 성공하면 ture,
실패하면 false
boolean addAll(int index, Collection c)지정된 위치(index)에 주어진 컬렉션에 포함된 모든 요소를 추가. 성공하면 true,
실패하면 false
void clear()LinkedList의 모든 요소를 삭제
boolean contains(Object o)지정된 객체가 LinkedList에 포함되었는지 알려줌
boolean containsAll(Collection c)지정된 컬렉션의 모든 요소가 포함되었는지 알려줌
Object get(int index)지정된 위치(index)의 객체를 반환
int indexOf(Object o)지정된 객체가 저장된 위치(앞에서 몇 번째)를 반환
boolean isEmpty()LinkedList가 비어있는지 알려준다. 비어있으면 true
Iterator iterator()Iterator를 반환한다.
int lastIndexOf(Object o)지정된 객체의 위치(index)를 반환(끝부터 역순검색)
ListIterator listIterator()ListIterator를 반환한다.
ListIterator listIterator(int index)지정된 위치에서부터 시작하는 ListIterator를 반환
Object remove(int index)지정된 위치(index)의 객체를 LinkedList에서 제거
boolean remove(Object o)지정된 객체를 LinkedList에서 제거, 성공하면 true, 실패하면 false
boolean removeAll(Collection c)지정된 컬렉션의 요소와 일치하는 요소를 모두 삭제
boolean retainAll(Collection c)지정된 컬렉션의 모든 요소가 포함되어 있는지 확인
Object set(int index, Object element)지정된 위치(index)의 객체를 주어진 객체로 바꿈
int size()LinkedList에 저장된 객체의 수를 반환
List subList(int fromIndex, int toIndex)LinkedList의 일부를 List로 반환
Object[] toArray()LinkedList에 저장된 객체를 배열로 반환
Object[] toArray(Object[] a)LinkedList에 저장된 객체를 주어진 배열에 저장하여 반환

아래는 Queue 인터페이스 를 구현하면서 추가된 것이다.

생성자 또는 메서드설명
Object element()LinkedList의 첫 번째 요소를 반환
boolean offer(Object o)지정된 객체(o)를 LinkedList의 끝에 추가. 성공하면 true, 실패하면 false
Object peek()LinkedList의 첫 번째 요소를 반환
Object poll()LinkedList의 첫 번째 요소를 반환. LinkedList에서는 제거된다.

또, 아래는 Deque 인터페이스 를 구현하면서 추가된 것이다.

생성자 또는 메서드설명
Object remove()LinkedList의 첫 번째 요소를 제거
void addFirst(Object o)LinkedList의 맨 앞에 객체(o)를 추가
void addLast(Object o)LinkedList의 맨 끝에 객체(o)를 추가
Iterator descending Iterator()역순으로 조회하기 위한 DescendingIterator를 반환
Object getFirst()LinkedList의 첫번째 요소를 반환
Object getLast()LinkedList의 마지막 요소를 반환
boolean offerFirst(Object o)LinkedList의 맨 앞에 객체(o)를 추가. 성공하면 true
boolean offerLast(Object o)LinkedList의 맨 끝에 객체(o)를 추가. 성공하면 true
Object peekFirst()LinkedList의 첫번째 요소를 반환
Object peekLast()LinkedList의 마지막 요소를 반환
Object pollFirst()LinkedList의 첫번째 요소를 반환하면서 제거
Object PollLast()LinkedList의 마지막 요소를 반환하면서 제거
Object pop()removeFirst()와 동일
void push(Object o)addFirst()와 동일
Object removeFirst()LinkedList의 첫번째 요소를 제거
Object removeLast()LinkedList의 마짐가 요소를 제거
boolean removeFirstOccurrence(Object o)LinkedList에서 첫번째로 일치하는 객체를 제거
boolean removeLastOccurrence(Object o)LinkedList에서 마지막으로 일치하는 객체를 제거

코드를 쓰진 않겠지만 알고리즘을 풀면서 ArrayListLinkedList 를 어떤 경우에 사용하냐에 따라 성능차이가 나는 것을 본 적이 있을 것이다. 결론은 다음과 같다.

1. 순차적으로 추가/삭제하는 경우에는 ArrayList > LinkedList
순차적 삭제는 마지막 데이터부터 역순으로 삭제해나간다는 것을 의미한다.

2. 중간 데이터를 추가/삭제하는 경우에는 LinkedList > ArrayList

LinkedList 는 저장해야하는 데이터의 개수가 많아질수록 데이터를 읽어 오는 시간, 즉 접근시간(access time)이 길어진다는 단점이 있다.

컬렉션읽기(접근시간)추가/삭제비고
ArrayList빠르다느리다순차적인 추가삭제는 더 빠름.
비효율적인 메모리사용
LinkedList느리다빠르다데이터가 많을수록 접근성이 떨어짐
  • 즉, 데이터의 개수가 변하지 않는 경우라면 ArrayList가 최상
  • 데이터 개수의 변경이 잦다면 LinkedList 를 사용하는 것이 더 나은 선택

1.4 Stack 과 Queue

스택(stack)과 큐(queue)의 기본 개념

스택 : 마지막에 저장한 데이터를 가장 먼저 꺼내게 되는 LIFO(Last In First Out)구조 로 되어 있음

: 처음에 저장한 데이터를 가장 먼저 꺼내게 되는 FIFO(First In First Out)구조 로 되어 있음

  • 스택 은 ArrayList와 같은 배열기반의 컬렉션 클래스가 적합
  • 는 데이터의 추가/삭제가 쉬운 LinkedList로 구현하는 것이 더 적합

<표11-9 Stack의 메서드>

메서드설명
boolean empty()Stack이 비어있는지 알려준다.
Object peek()Stack의 맨 위에 저장된 객체를 반환. pop()과 달리 Stack에서 객체를 꺼내지는 않음.
(비었을 때는 EmptyStackException 발생)
Object pop()Stack의 맨 위에 저장된 객체를 꺼낸다. (비었을 때는 EmptyStackException 발생)
Object push(Object them)Stack에 객체(item)를 저장한다.
int search(Object o)Stack에서 주어진 객체(o)를 찾아서 그 위치를 반환. 못찾으면 -1을 반환.
(배열과 달리 위치는 0이 아닌 1부터 시작)
boolean add(Object o)지정된 객체를 Queue에 추가한다. 성공하면 true를 반환. 저장공간이 부족하면
IllegalStateException 발생
Object remove()Queue에서 객체를 꺼내 반환. 비어있으면 NoSuchElementException 발생
Object element()삭제없이 요소를 읽어온다. peek와 달리 Queue가 비었을 때 NoSuchElementException 발생
boolean offer(Object o)Queue에 객체를 저장. 성공하면 true, 실패하면 false를 반환
Object poll()Queue에서 객체를 꺼내서 반환. 비어있으면 null을 반환
Object peek()삭제없이 요소를 읽어 온다. Queue가 비어있으며 null을 반환

자바에서는 스택을 Stack 클래스로 구현하여 제공하고 있지만 큐는 Queue 인터페이스로만 정의해 놓았을 뿐 별도의 클래스를 제공하고 있지 않다.

대신 Queue 인터페이스 를 구현한 클래스들이 있어서 이 들 중의 하나를 선택해서 사용하면 된다.

// 이런식으로
Queue q = new LinkedList();		// Queue 인터페이스의 구현체인 LinkedList를 사용

Java API 문서의 All Known Implementing Classes 에 적혀있는 클래스들 중에서 골라서 사용하면 된다.


스택과 큐의 활용

스택의 활용 예 - 수식계산, 수식괄호검사, 워드프로세서의 undo/redo, 웹브라우저의 뒤로/앞으로

큐의 활용 예 - 최근사용문서, 인쇄작업 대기목록, 버퍼(buffer)

🤔 스택의 활용은 생략하고 큐의 활용만 예시를 들자면 아래의 코드는 유닉스의 history 명령어 를 Queue를 이용해서 구현한 것이다.

대부분의 프로그램이 최근에 열어 본 문서들의 목록을 보여 주는 기능을 제공하는데, 그러한 기능도 아래의 예제를 응용하면 쉽게 구현할 수 있을 것이다.

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Queue;
import java.util.Scanner;

public class QueueEx1 {
    static Queue q = new LinkedList();
    static final int MAX_SIZE = 5;      // Queue에 최대 5개까지만 저장되도록 한다.

    public static void main(String[] args) {
        System.out.println("help를 입력하면 도움말을 볼 수 있습니다.");

        while (true) {
            System.out.print(">>");

            try {
                // 화면으로부터 라인단위로 입력받는다.
                Scanner s = new Scanner(System.in);
                String input = s.nextLine().trim();

                if ("".equals(input)) continue;

                if (input.equalsIgnoreCase("q")) {
                    System.exit(0);
                } else if (input.equalsIgnoreCase("help")) {
                    System.out.println(" help - 도움말을 보여줍니다.");
                    System.out.println(" q 또는 Q - 프로그램을 종료합니다.");
                    System.out.println(" history - 최근에 입력한 명령어를 " + MAX_SIZE + "개 보여줍니다.");
                } else if (input.equalsIgnoreCase("history")) {
                    int i = 0;
                    // 입력받은 명령어를 저장하고,
                    save(input);

                    // LinkedList의 내용을 보여준다.
                    LinkedList tmp = (LinkedList) q;
                    ListIterator it = tmp.listIterator();

                    while (it.hasNext())
                        System.out.println(++i + "." + it.next());
                } else {
                    save(input);
                    System.out.println(input);
                }
            } catch (Exception e) {
                System.out.println("입력오류입니다.");
            }
        }
    }

    public static void save(String input) {
        // queue에 저장한다.
        if (!"".equals(input))
            q.offer(input);

        // queue의 최대크기를 넘으면 제일 처음 입력된 것을 삭제한다.
        if (q.size() > MAX_SIZE)     // size()는 Collection 인터페이스에 정의
            q.remove();
    }
}
  • 실행결과

PriorityQueue

Queue 인터페이스의 구현체 중의 하나로, 저장한 순서에 관계없이 우선순위(priority)가 높은 것부터 꺼내게 된다는 특징이 있다.

  • null은 저장할 수 없다 !

PriorityQueue 는 저장공간으로 배열을 사용하며, 각 요소를 힙(heap) 이라는 자료구조의 형태로 저장한다.

class PriorityQueueEx {
	public static void main(String[] args) {
    	Queue pq = new PriorityQueue();
        pq.offer(3);	// pq.offer(new Integer(3)); 오토박싱
        pq.offer(1);
        pq.offer(5);
        pq.offer(2);
        pq.offer(4);
        System.out.println(pq);	// pq의 내부 배열을 출력
        
        Object obj = null;
        
        // PriorityQueue에 저장된 요소를 하나씩 꺼낸다.
        while((obj = pq.poll()) != null)
        	System.out.println(obj);
	}
}
/* 실행결과
[1, 2, 5, 3, 4]
1
2
3
4
5
*/

참고로 Collections.reverseOrder() 를 사용하면 반대의 순서로도 데이터를 저장할 수 있다.


Deque(Double-Ended Queue)

Queue의 변형으로, 한 쪽 끝으로만 추가/삭제할 수 있는 Queue와 달리, Deque (덱, 또는 디큐라고 읽음)은 양쪽 끝에 추가/삭제가 가능하다.

Deque의 조상은 Queue이며, 구현체로는 ArrayDeque와 LinkedList 등이 있다.

덱은 스택과 큐를 하나로 합쳐놓은 것과 같으며 스택으로 사용할 수도 있고, 큐로 사용할 수도 있다.

<표11-11, 덱(Deque)의 메서드에 대응하는 큐와 스택의 메서드>

DequeQueueStack
offerLast()offer()push()
pollLast()-pop()
pollFirst()poll()-
peekFirst()peek()-
peekLast()-peek()

1.5 Iterator, ListIterator, Enumeration

앞서 말했던 Iterator 가 이 부분에서 나온다.

셋 모두 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다. Enumeration 은 Iterator의 구버젼이고, ListIterator 는 Iterator의 기능을 향상 시킨 것이다.


Iterator

컬렉션 프레임웍에서는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화하였다. 컬렉션에 저장된 각 요소에 접근하는 기능을 가진 Iterator인터페이스 를 정의하고, Collection인터페이스에는 'Iterator(Iterator를 구현한 클래스의 인스턴스)'를 반환하는 iterator()를 정의하고 있다.

public interface Iterator {
	boolean hasNext();
    Object next();
    void remove();
}
public interface Collection {
	...
    public Iterator iterator();
    ...
}

<표11-12, Iterator인터페이스의 메서드>

메서드설명
boolean hasNext()읽어 올 요소가 남아있는지 확인한다. 있으면 true, 없으면 false를 반환한다ㅏ.
Object next()다음 요소를 읽어 온다. next()를 호출하기 전에 haxNext()를 호출해서 읽어 올 요소가 있는지 확인하는 것이 안전하다.
void remove()next()로 읽어 온 요소를 삭제한다. next()를 호출한 다음에 remove()를 호출해야한다.(선택적 기능)

ArrayList에 저장된 요소들을 출력하기 위한 코드는 다음과 같이 작성할 수 있다.

Collection c = new ArrayList();	// 다른 컬렉션으로 변경시 이 부분만 고치면 된다.
Iterator it = c.iterator();

while(it.hasNext())	{
	System.out.println(it.next());
}

Q. 여기서 참조변수의 타입을 ArrayList 타입이 아니라 Collection 타입 으로 한 이유는 무엇일까?

A. Collection에 없고 ArrayList에만 있는 메서드를 사용하는게 아니라면, Collection타입의 참조변수로 선언하는 것이 좋습니다. 만일 Collection인터페이스를 구현한 다른 클래스, 예를 들어 LinkedList로 바꿔야 한다면 선언문 하나만 변경하면 나머지 코드는 검토하지 않아도 됩니다. 참조변수 타입이 Collection이므로 Collection에 정의되지 않은 메서드는 사용되지 않았을 것이 확실하니까요. 그러나 참조변수 타입을 ArrayList로 했다면, 선언문 이후의 문장들을 검토해야 합니다. Collection에 정의되지 않은 메서드를 호출했을 수 있기 때문입니다.

-> 이렇게 공통 인터페이스를 정의해서 표준을 정의하고 구현하여 표준을 따르도록 함으로써 코드의 일관성을 유지하여 재사용성을 극대화하는 것 이 객체지향 프로그래밍의 중요한 목적 중의 하나인 것이다.


Map인터페이스를 구현한 컬렉션 클래스는 iterator()를 직접 호출할 수 없고,

그 대신 keySet()이나 entrySet()과 같은 메서드를 통해서 키와 값을 각각 따로 Set의 형태로 얻어 온 후에 다시 iterator()를 호출해야 Iterator를 얻을 수 있다.

Map map = new HashMap();
...
Iterator it = map.entrySet().iterator(); // 이 문장은 아래의 두 문장을 합친 것이다.

Set eSet = map.entrySet();
Iterator it = eSet.iterator();

StringBuffer를 사용할 때 이와 유사한 코드를 많이 봤을 것이다.
-> append메서드가 수행결과로 StringBuffer를 리턴하기 때문에 가능한 것 !


ListIterator 와 Enumeration

Enumeration - Iterator의 구버전
ListIterator - Iterator에 양방향 조회기능추가 (List를 구현한 경우만 사용가능)

<표 11-13, Enumeration 인터페이스의 메서드>

메서드설명
boolean hasMoreElements()읽어 올 요소가 남아있는지 확인한다. 있으면 true, 없으면 false를 반환한다.
Iterator의 hasNext()와 같다.
Object nextElement()다음 요소를 읽어 온다. nextElement()를 호출하기 전에 hasMoreElements()를
호출해서 읽어올 요소가 남아있는지 확인하는 것이 안전하다.
Iterator의 next()와 같다.

<표 11-14, ListIterator의 메서드>

메서드설명
void add(Object o)컬렉션에 새로운 객체(o)를 추가한다. (선택적 기능)
boolean hasNext()읽어 올 다음 요소가 남아있는지 확인한다. 있으면 true, 없으면 false를 반환
boolean hasPrevious()읽어 올 이전 요소가 남아있는지 확인한다. 있으면 true, 없으면 false를 반환
Object next()다음 요소를 읽어 온다. next()를 호출하기 전에 hasNext()를 호출해서 읽어 올 요소가 있는지
확인하는 것이 안전하다.
Object previous()이전 요소를 읽어 온다. previous()를 호출하기 전에 hasPrevious()를 호출해서 읽어 올 요소가
있는지 확인하는 것이 안전하다.
int nextIndex()다음 요소의 index를 반환한다.
int previousIndex()이전 요소의 index를 반환한다.
void remove()next() 또는 previous()로 읽어 온 요소를 삭제한다. 반드시 next()나 previous()를 먼저 호출한
다음에 이 메서드를 호출해야한다. (선택적 기능)
void set(Object o)next() 또는 previous()로 읽어 온 요소를 지정된 객체(o)로 변경한다. 반드시 next()나 previous()
를 먼저 호출한 다음에 이 메서드를 호출해야한다. (선택적 기능)

public class ListIteratorEx1 {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        ListIterator it = list.listIterator();

        while(it.hasNext()){
            System.out.println(it.next());  // 순방향으로 진행하면서 읽어온다.
        }
        System.out.println();

        while(it.hasPrevious()){
            System.out.println(it.previous());  // 역방향으로 진행하면서 읽어온다.
        }
        System.out.println();
    }
}

/* 실행결과
12345
54321
*/

ListIterator의 사용방법을 보여주는 간단한 예제이다. Iterator 는 단방향으로만 이동하기 때문에 컬렉션의 마지막 요소에 다다르면 더 이상 사용할 수 없지만, ListIterator 는 양방향으로 이동하기 때문에 각 요소간의 이동이 자유롭다.

다만 이동하기 전에 반드시 hasNext()나 hasPrevious()를 호출해서 이동할 수 있는지 확인해야 한다.


1.6 Arrays

Arrays 클래스에는 배열을 다루는데 유용한 메서드가 정의되어 있다. 같은 기능의 메서드가 배열의 타입만 다르게 오버로딩되어 있어서 많아 보이지만, 실제로는 그리 많지 않다.

배열의 복사 - copyOf(), copyOfRange()

copyOf() 는 배열 전체를, copyOfRange() 는 배열의 일부를 복사해서 새로운 배열을 만들어 반환한다.

int[] arr = {0, 1, 2, 3, 4};
int[] arr2 = Arrays.copyOf(arr, arr.length);	// arr2 = [0, 1, 2, 3, 4]
int[] arr3 = Arrays.copyOf(arr, 3);				// arr3 = [0, 1, 2]
int[] arr4 = Arrays.copyOf(arr, 7);				// arr4 = [0, 1, 2, 3, 4, 0, 0]

int[] arr5 = Arrays.copyOfRange(arr, 2, 4);		// arr5 = [2, 3]	<- 4는 불포함
int[] arr6 = Arrays.copyOfRange(arr, 0, 7);		// arr6 = [0, 1, 2, 3, 4, 0, 0]

배열 채우기 - fill(), setAll()

fill() 은 배열의 모든 요소를 지정된 값으로 채운다. setAll() 은 배열을 채우는데 사용할 함수형 인터페이스를 매개변수로 받는다.

이 메서드를 호출할 때는 함수형 인터페이스를 구현한 객체를 매개변수로 지정하던가 아니면 람다식을 지정해야한다.

int[] arr = new int[5];
Arrays.fill(arr, 9);	// arr = [9, 9, 9, 9, 9]
Arrays.setAll(arr, () -> (int)(Math.random() * 5) + 1);	// arr = [1, 5, 2, 1, 1]

-> 람다식은 나중에 나오니까 대충 이런게 있구나 하고 넘어가자.


배열의 정렬과 검색 - sort(), binarySearch()

sort() 는 배열을 정렬할 때, 그리고 배열에 저장된 요소를 검색할 때는 binarySearch()를 사용한다.

binarySearch()는 배열에서 지정된 값이 저장된 위치(index)를 찾아서 반환하는데, 반드시 배열이 정렬된 상태이어야 올바른 결과를 얻는다. ,, 만일 검색한 값과 일치하는 요소들이 여러 개 있다면, 그 중 어떤 것의 위치가 반환되는지는 알 수 없다.

int[] arr = {3, 2, 0, 1, 4};
int idx = Arrays.binarySearch(arr, 2);		// idx = -5 <- 잘못된 결과

Arrays.sort(arr);	// 배열을 정렬
System.out.println(Arrays.toString(arr));	// [0, 1, 2, 3, 4]
int idx = Arrays.binarySearch(arr, 2);		// idx = 2 <- 올바른 결과

요소가 하나씩 있고, 배열이 정렬되어 있다면, 속도가 빠른 binarySearch()를 쓰는 것이 좋겠다.


배열의 비교와 출력 - equals(), toString()

toString() 배열의 모든 요소를 문자열로 편하게 출력할 수 있다. 다차원 배열에는 deepToString()을 사용해야 한다.

int[] arr = {0, 1, 2, 3, 4};
int[] arr2D = { {11, 12}, {21, 22} };

System.out.println(Arrays.toString(arr));	// [0, 1, 2, 3, 4]
System.out.println(Arrays.deepToString(arr2D));	// [[11, 12], [21, 22]]

equals()는 두 배열에 저장된 모든 요소를 비교해서 같으면 true, 다르면 false를 반환한다. 마찬가지로 다차원 배열의 비교에는 deepEquals()를 사용해야 한다.

String[][] str2D = new String[][] {{"aaa", "bbb"}, {"AAA", "BBB"}};
String[][] str2D2 = new String[][] {{"aaa", "bbb"}, {"AAA", "BBB"}};

System.out.println(Arrays.equals(str2D, str2D2));		// false
System.out.println(Arrays.deepEquals(str2D, str2D2));	// true

배열을 List로 변환 - asList(Object... a)

asList()는 배열을 List에 담아서 반환한다. 매개변수의 타입이 가변인수라서 배열 생성없이 저장할 요소들만 나열하는 것도 가능하다.

List list = Arrays.asList(new Integer[] {1,2,3,4,5});	// list = [1,2,3,4,5]
List list = Arrays.asList(1,2,3,4,5);					// lsit = [1,2,3,4,5]

list.add(6);	// UnsupportedOperationException 예외 발생
  • asList()가 반환한 List의 크기를 변경할 수는 없다..!
  • 추가 또는 삭제가 불가능하다는 뜻

parallelXXX(), spliterator(), stream()

parallel로 시작하는 이름의 메서드들이 있는데, 이 메서드들은 보다 빠른 결과를 얻기 위해 여러 쓰레드가 작업을 나누어 처리하도록 한다.

spliterator()는 여러 쓰레드가 처리할 수 있게 하나의 작업을 여러 작업으로 나누는 Spliterator를 반환하며,

stream()은 컬렉션을 스트림으로 반환한다.

-> 이 메서드들은 14장과 관련된 내용이므로 참고로만 알아두자..!


1.7 Comparator와 Comparable

이전 예제에서 보듯 Arrays.sort()를 호출만 하면 컴퓨터가 알아서 배열을 정렬하는 것처럼 보이지만, 사실은 Character클래스의 Comparable의 구현에 의해 정렬되었던 것이다.

Comparable - 기본 정렬기준을 구현하는데 사용.
Comparator - 기본 정렬기준 외에 다른 기준으로 정렬하고자할 때 사용

Comparator와 Comparable은 모두 인터페이스로 컬렉션을 정렬하는데 필요한 메서드를 정의하고 있으며,

Comparable을 구현하고 있는 클래스들은 같은 타입의 인스턴스끼리 서로 비교할 수 있는 클래스들, 주로 Integer와 같은 wrapper클래스와 String, Date, File과 같은 것들이며 기본적으로 오름차순, 즉 작은 값에서부터 큰 값의 순으로 정렬되도록 구현되어 있다.

그래서 Comparable을 구현한 클래스는 정렬이 가능하다는 것을 의미한다.

public interface Comparator {
	int compare (Object o1, Object o2);
    boolean equals (Object obj);
}
public interface Comparable {
	public int compareTo (Object o);
}
Arrays.sort()는 배열을 정렬할 때, Comparator를 지정해주지 않으면 저장하는 객체에 구현된 내요에 따라 정렬된다.
profile
단단하게 오래가고자 하는 백엔드 개발자

0개의 댓글