[Java] 컬렉션 프레임워크

조민서·2022년 8월 29일
2

JAVA

목록 보기
8/16
post-thumbnail

컬렉션 프레임워크란?

컬렉션 프레임워크(Collections Framework)란, 데이터 군(群)을 저장하는 클래스들을 표준화한 설계를 뜻한다. 컬렉션은 다수의 데이터, 즉 데이터 그룹을, 프레임워크는 표준화된 프로그래밍 방식을 의미한다.


컬렉션 프레임워크의 핵심 인터페이스

컬렉션 데이터 그룹을 크게 3가지 타입의 존재를 인식하여, 각 컬렉션을 다루는데 필요한 기능을 가진 3개의 인터페이스를 정의하였다. 그리고 인터페이스 ListSet의 공통된 부분을 다시 뽑아서 새로운 인터페이스인 Collection을 추가로 정의 하였다.

반면 Map인터페이스는 이들과는 전혀 다른 형태로 컬렉션을 다루기 때문에 같은 상속계층도에 포함되지 못했다.


컬렉션 프레임워크의 핵심 인터페이스 특징

컬렉션 프레임워크의 모든 컬렉션 클래스들은 List, Set, Map 중의 하나를 구현하고 있다. 그러나 Vector, Stack, Hashtable, Properties와 같은 클래스들은 컬렉션 프레임워크가 만들어지기 이전부터 존재하던 것이기 때문에 컬렉션 프레임워크 명명법을 따르지 않는다.

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

Vector클래스의 상속계층도 변화 - 왼쪽이 JDK1.2 이전, 오른쪽이 이후


Collection인터페이스

Collection인터페이스는 컬렉션 클래스에 저장된 데이터를 읽고, 추가하고 삭제하는 등 컬렉션을 다루는데 가장 기본적인 메서드들을 정의하고 있다.
이 외에도 JDK1.8부터 추가된 람다(Lambda)와 스트림(Stream)에 관련된 메서드들이 더 있는데, 이 메서드들은 나중에 따로 다뤄보자.

그리고 자바 API문서 문서를 보면, 표에서 사용된 Object가 아닌 E로 표기되어있는데, E는 특정 타입을 의미하는 것으로 JDK1.5부터 추가된 지네릭스(Generics)에 의한 표기다.


List인터페이스

List인터페이스는 중복을 허용하면서 저장순서가 유지되는 컬렉션을 구현하는데 사용된다.


List인터페이스의 메서드

Collection인터페이스로부터 상속받은 것들은 제외한 List인터페이스에 정의된 메서드는 다음과 같다.


Set인터페이스

Set인터페이스는 중복을 허용하지 않고 저장순서가 유지되지 않는 컬렉션 클래스를 구현하는데 사용된다. Set인터페이스를 구현한 클래스로는 HashSet, TreeSet등이 있다.


Set인터페이스의 메서드 - Collection인터페이스와 동일

Set인터페이스의 집합과 관련된 메서드(Collection에 변화가 있으면 true, 아니면 false를 반환)


Map인터페이스

Map인터페이스는 키(key)와 값(value)을 하나의 쌍으로 묶어서 저장하는 컬렉션 클래스를 구현하는 데 사용된다. 키는 중복될 수 없지만 값은 중복을 허용한다. 기존에 저장된 데이터와 중복된 키와 값을 저장하면 기존의 값은 없어지고 마지막에 저장된 값이 남게 된다. Map인터페이스를 구현한 클래스로는 Hashtable, HashMap, LinkedHashMap, SortedMap, TreeMap 등이 있다.


Map 인터페이스의 메서드

values()에서는 반환타입이 Collection이고, KeySet()에서는 반환타입이 Set인 것에 주목하자.
Map인터페이스에서 값(value)은 중복을 허용하기 때문에 Collection타입으로 반환하고, 키(Key)는 중복을 허용하지 않기 때문에 Set타입으로 반환한다.


Map.Entry인터페이스

Map.Entry인터페이스는 Map인터페이스의 내부 인터페이스다. 내부 클래스와 같이 인터페이스도 인터페이스 안에 인터페이스를 정의하는 내부 인터페이스(inner interface)를 정의하는 것이 가능하다.
Map에 저장되는 key-value쌍을 다루기 위해 내부적으로 Entry인터페이스를 정의해 놓았다. 이것은 보다 객체지향적으로 설계하도록 유도하기 위한 것으로 Map인터페이스를 구현하는 클래스에서는 Map.Entry인터페이스도 함께 구현해야한다.

Map인터페이스의 소스코드 일부

public interface Map { 
	...
	public static interface Entry {
		Object getKey(); //  Entry의 Key객체를 반환한다.
		Object getValue(); // Entry의 value객체를 반환한다.
		Object setValue(Object value); //Entry의 value객체를 지정한 객체로 바꾼다.
		boolean equals(Object o); // 동일한 Entry인지 비교한다.
		int hashCode(); // Entry의 해시코드를 반환한다.
		... // JDK8.0부터 추가된 메서드는 생략
	} 
}

List


ArrayList

  • ArrayList는 기존의 Vector를 개선한 것으로 구현원리와 기능적으로 동일하다. Vector는 자체적으로 동기화처리가 되어 있으나 ArrayList는 그렇지 않다.

  • List인터페이스를 구현하므로, 저장순서가 유지되고 중복을 허용한다.

  • 데이터의 저장공간으로 배열을 사용한다. (배열기반)

위의 코드는 ArrayList의 소스코드 일부이다.

  • 기본 Capacity가 10이다.
  • ArrayList는 elemetnData라는 이름의 Object배열을 멤버변수로 선언하고 있다. 선언된 배열의 타입이 모든 객체의 최고조상인 Object이기 때문에 모든 종류의 객체를 담을 수 있다.

transient는 직렬화(serialization)와 관련된 제어자이다.

ArrayList 생성 시 용량을 초기화한 경우와 안한 경우.


ArrayList의 add() 메서드

void add(E e, Object[] elementData, int s)메서드를 한번 더 실행해서 grow() 라는 메소드로 크기를 확장해 준 뒤 새로운 값을 추가하고 있다.


ArrayList의 grow() 메서드

if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

  1. 먼저 ArrayList가 비어 있는지 확인한다.
  2. 비어 있을 경우, else로 기본 용량인 10과 size + 1 중 큰 값으로 새 Object 배열을 생성한다.

그렇지 않은 경우, 새로운 capacity로 크기를 확장해 준다.

int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);

  • oldCapacity : 기존 용량
  • minCapacity - oldCapacity : (size + 1) - (elementData.length) = 일반적으로 최소 증가값은 1이 된다.
  • oldCapacity >> 1 : 기존 용량 / 2 (우측 쉬프트 연산)

ArraysSupport의 소스코드 일부

MAX_ARRAY_LENGTH의 값이 inteher.MAX_VALUE - 8인것으로 보아 inteher.MAX_VALUE - 8이 가장 안전한 숫자인거 같다.


if (newLength값이 - MAX_ARRAY_LENGTH?<= 0)
newLength의 값은 일반적으로 Integer.MAX_VALUE-8인 MAX_ARRAY_LENGTH보다 작을것이다. 그런 일반적인 경우에는 기존용량의 1.5배만큼 증가시킨 값을 리턴한다.

하지만 이런 일반적인 경우가 아닌 newLength값이 0보다 작거나 최대 용량 값을 넘는 경우는 어떻게 될까? 우선 return hugeLength(oldLength, minGrowth)을 실행한다.


private static int hugeLength(int oldLength, int minGrowth)
minLength = 기존 용량+최소 증가값
1. minLength가 0보다 작은 경우 OutOfMemoryError()가 발생한다.
2. minLength가 MAX_ARRAY_LENGTH보다 이하인 경우는 가장 안전한 숫자인 MAX_ARRAY_LENGTH를 리턴한다.
3. minLength가 MAX_ARRAY_LENGTH보다 큰 경우는 int의 최대치인 integer.MAX_VALUE를 리턴한다.

  • 여기서 ArrayList의 최대크기는 integer.MAX_VALUE라는 사실을 알 수 있다.

ArrayList의 remove() 메서드

  1. 먼저 remove()메서드에서 삭제하려는 데이터가 있는지 없는지 여부를 파악한다. 만약 삭제하려는 데이터가 없는경우 false를 리턴한다.

  2. 만약 삭제하려는 데이터가 있는경우 원본을 복제한 배열과 삭제하는 데이터 인덱스를 파라미터로한 fastRemove(es, i)를 실행한다.

  3. 만약 배열의 마지막데이터가 아닌 중간데이터를 삭제하는 경우 중간데이터를 바로 삭제하는게 아니라, 삭제하려는 중간데이터 다음 원소부터 마지막 원소까지 복사한다.

System.arraycopy(data, 3, data, 2, 2)
순차적으로 data[3]부터 data[2]로 2개의 데이터를 복사하라는 의미.
ex)
data[] = {0, 1, 2, 3, 4}
System.arraycopy(data, 3, data, 2, 2)
data[] = {0, 1, 3, 4, 4}

즉 System.arraycopy(es, i+1, es, i, newSize - i)는
es[i+1]에서 es[i]부터 newSize-i개의 데이터를 복사하라는 의미


결론

  1. ArrayList는 새 요소를 추가하고자 할 때, 요소를 바로 추가하지 못하고 기존 배열 크기 + 추가한 요소의 크기capacity와 같아지면 기존 용량의 1.5배만큼 증가시킨 배열을 만든다.

  2. 그 크기가 늘어난 배열에 기존 elementData를 copy한다. 실제로는 정적 배열을 사용하지만, 요소가 추가될 때마다 최소 증가량(일반적으로 1.5배)을 계산하여 크기를 늘리는 동적 할당 방식을 이용한다.

  3. ArrayList나 Vector와 같이 배열을 이용한 자료구조는 데이터를 읽어오고 저장하는 데는 효율이 좋지만, 용량을 변경해야할 때(중간에 데이터 삽입 또는 중간에 있는 데이터 삭제)는 새로운 배열을 생성한 후 기존의 배열로부터 새로 생성된 배열로 데이터를 복사해야 하기 때문에 상당히 효율이 떨어진다는 단점을 가지고 있다. 그래서 처음에 인스턴스를 생성할 때, 저장할 데이터의 개수를 잘 고려하여 충분한 용량의 인스턴스를 생성하는 것이 좋다.


ArrayList 활용

import java.util.ArrayList;
import java.util.Collections;


class Main {
    public static void main(String[] args) {
        ArrayList list1 = new ArrayList();
        list1.add(new Integer(5));
        list1.add(1);
        list1.add(-3);
        list1.add(2);

        ArrayList list2 = new ArrayList(list1.subList(1, 4));
        print(list1, list2);

        Collections.sort(list1); //list1과 list2를 정렬한다.
        Collections.sort(list2);
        System.out.println("list1: " + list1);
        System.out.println("list2: " + list2);
        // list1이 list2를 모두 포함하는지 확인
        System.out.println("list1.containsAll(list2): " + list1.containsAll(list2) + "\n");

        list2.add("Z");
        list2.add(0, "A");
        print(list1, list2);

        // list1의 1번째 인덱스를 'A'로 변경
        list1.set(1, "A");
        list1.set(2, "B");
        list2.set(1, "B");
        print(list1, list2);

        // list1에서 list2와 겹치는 부분만 남기고 나머지는 삭제한다.
        System.out.println("list1.retainAll(list2): " + list1.retainAll(list2));
        print(list1, list2);

        // list2에서 list1에 포함된 객체들을 삭제한다.
        //  for문 변수 i를 뒤에서 부터 감소시킨 경우
        for(int i = list2.size()-1; i>=0; i--) {
            if(list1.contains(list2.get(i))) {
                list2.remove(i);

            }
        }

        print(list1, list2);

        /*
        list2에서 list1에 포함된 객체들을 삭제한다.
        for문 변수 i를 0부터 증가 시킨 경우
        int pos = 0;
        for(int i = 0; i<list2.size(); i++) {
            if(list1.contains(list2.get(pos))) {
                list2.remove(pos);

            } else pos += 1;
        }*/


    }

    static void print(ArrayList list1, ArrayList list2) {
        System.out.println("list1: " + list1);
        System.out.println("list2: " + list2);
        System.out.println();
    }
}

코드 결과
list1: [5, 1, -3, 2]
list2: [1, -3, 2]

list1: [-3, 1, 2, 5] //list1과 list2를 정렬한다.
list2: [-3, 1, 2]
list1.containsAll(list2): true // list1이 list2를 모두 포함하는지 확인

list1: [-3, 1, 2, 5]
list2: [A, -3, 1, 2, Z] // list2에 'Z'추가, list2 0번째 인덱스에 'A'추가

list1: [-3, A, B, 5] // list1의 1번째 인덱스를 'A'로 변경
list2: [A, B, 1, 2, Z]

list1.retainAll(list2): true
list1: [A, B] // list1에서 list2와 겹치는 부분만 남기고 나머지는 삭제한다.
list2: [A, B, 1, 2, Z]

list1: [A, B]
list2: [1, 2, Z] // list2에서 list1에 포함된 객체들을 삭제한다.


for(int i = list2.size()-1; i>=0; i--) {
	if(list1.contains(list2.get(i))) {
		list2.remove(i);
	}
}

여기서 list2의 각 요소를 접근 하기위해 get(int index)메서드와 for문을 사용했는데, for문 변수 i를 0부터 증가시킨 것이 아니라, list2.size()-1부터 감소시키면서 거꾸로 반복했다.

만일 변수 i를 증가시켜가면서 삭제하면, 한 요소가 삭제될 때마다 빈 공간을 채우기 위해 나머지 요소들이 자리이동을 하기 떄문에 올바른 결과를 얻을 수 없다. 그렇다고 방법이 없는건 아니다.

int pos = 0;
for(int i = 0; i<list2.size(); i++) {
	if(list1.contains(list2.get(pos))) {
		list2.remove(pos);

	} else pos += 1;
}

위와 같이 새로운 변수(pos)를 만들어 구현할 수 있다.


LinkedList

배열은 가장 기본적인 형태의 자료구조로 구조가 간단하며 사용하기 쉽고 데이터를 읽어 오는데 걸리는 시간(접근시간, access time)이 가장 빠르다는 장점을 가지고 있지만 다음과 같은 단점도 있다.

배열의 단점

  1. 크기를 변경할 수 없다.
    • 크기를 변경할 수 없으므로 새로운 배열을 생성해서 데이터를 복사해야한다.
    • 실행속도를 향상시키기 위해서는 충분히 큰 크기의 배열을 생성해야 하므로 메모리가 낭비된다.
  1. 비순차적인 데이터의 추가 또는 삭제에 시간이 많이 걸린다.
    • 차례대로 데이터를 추가하고 마지막에서부터 데이터를 삭제하는 것은 빠르지만,
    • 배열의 중간에 데이터를 추가하려면, 빈자리를 만들기 위해 다른 데이터들을 복사해서 이동해야 한다.

이러한 배열의 단점을 보완하기 위해서 링크드 리스트(Linked List)라는 자료구조가 고안되었다. 배열은 모든 데이터가 연속적으로 존재하지만 링크드 리스트는 불연속적으로 존재하는 데이터를 서로 연결(link)한 형태로 구성되어 있다.

위의 그림에서 알 수 있듯이 링크드 리스트의 각 요소(node)들은 자신과 연결된 다음 요소에 대한 참조(주소값)데이터로 구성되어 있다.

class Node {
	Node next; // 다음 요소의 주소를 저장
    Object data; // 데이터를 저장
}

LinkedList의 add() 메서드

새로운 데이터를 추가할 때는 새로운 요소를 생성한 다음 추가하고자 하는 위치의 이전요소의 참조를 새로운 요소에 대한 참조로 변경해주고, 새로운 요소가 그다음 요소를 참조하도록 변경하기만 하면 되므로 처리속도가 매우 빠르다.

연결리스트 여기서 연결리스트에 대한 코드를 간단히 볼 수 있다.


LinkedList의 remove() 메서드

링크드 리스트에서 데이터 삭제는 간단하다. 삭제하고자 하는 요소의 이전요소가 삭제하고자 하는 요소의 다음 요소를 참조하도록 변경하기만 하면 된다. 단 하나의 참조만 변경하면 삭제가 이루어진다. 배열처럼 데이터를 이동하기 위해 복사하는 과정이 없기때문에 처리속도가 매우 빠르다.

  1. 삭제하고자 하는 데이터를 찾은 후 unlink(x)메서드를 실행한다.

  2. unlink()메서드를 통해 삭제하고자 하는 요소의 이전요소가 삭제하고자 하는 요소의 다음 요소를 참조하도록 변경한다.

unlink 소스코드

ArrayList vs LinkedList 성능 비교

소스코드

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

class Main {
    public static void main(String[] args) {
        // 추가할 데이터의 개수를 고려하여 충분히 잡아야 한다.
        ArrayList al = new ArrayList(2000000);
        LinkedList ll = new LinkedList();

        System.out.println("순차적으로 추가하기");
        System.out.println("ArrayList : " + add1(al));
        System.out.println("LinkedList : " + add1(ll));
        System.out.println();

        System.out.println("중간에 추가하기");
        System.out.println("ArrayList : " + add2(al));
        System.out.println("LinkedList : " + add2(ll));
        System.out.println();


        System.out.println("접근시간 테스트");
        System.out.println("ArrayList : " + access(al));
        System.out.println("LinkedList : " + access(ll));
        System.out.println();

        System.out.println("중간에서 삭제하기");
        System.out.println("ArrayList : " + remove1(al));
        System.out.println("LinkedList : " + remove1(ll));
        System.out.println();

        System.out.println("순차적으로 삭제하기");
        System.out.println("ArrayList : " + remove2(al));
        System.out.println("LinkedList : " + remove2(ll));
    }

    public static long add1(List list) {
        long start = System.currentTimeMillis();
        for(int i=0; i<1000000; i++) list.add(i+"");
        long end = System.currentTimeMillis();
        return end - start;
    }

    public static long add2(List list) {
        long start = System.currentTimeMillis();
        for(int i=0; i<10000; i++) list.add(500,i+"");
        long end = System.currentTimeMillis();
        return end - start;
    }

    public static long remove1(List list) {
        long start = System.currentTimeMillis();
        for(int i=0; i<10000; i++) list.remove(i);
        long end = System.currentTimeMillis();
        return end - start;
    }

    public static long remove2(List list) {
        long start = System.currentTimeMillis();
        for(int i=list.size()-1; i>=0; i--) list.remove(i);
        long end = System.currentTimeMillis();
        return end - start;
    }

    public static long access(List list) {
        long start = System.currentTimeMillis();
        for(int i=0; i<10000; i++) list.get(i);
        long end = System.currentTimeMillis();
        return end - start;
    }
}

코드 결과
순차적으로 추가하기
ArrayList : 79
LinkedList : 179

중간에 추가하기
ArrayList : 4130
LinkedList : 13

접근시간 테스트
ArrayList : 1
LinkedList : 111

중간에서 삭제하기
ArrayList : 1700
LinkedList : 207

순차적으로 삭제하기
ArrayList : 7
LinkedList : 24

결론

  • 순차적으로 추가/삭제하는 경우에는 ArrayList가 LinkedList보다 빠르다.

    단순히 저장하는 시간만을 비교할 수 있도록 하기 위해서 ArrayList를 생성할 때는 저장할 데이터의 개수만큼 충분한 초기용량을 확보해서, 저장곤이 부족해서 새로운 ArrayList를 생성해야하는 상황이 일어나지 않도록 했다. 만일 ArrayList의 크기가 충분하지 않으면, 새로운 크기의 ArrayList를 생성하고 데이터를 복사하는 일이 발생하게 되므로 순차적으로 데이터를 추가해도 ArrayList보다 LinkedList가 더 빠를 수 있다.
    순차적으로 삭제한다는 것은 마지막 데이터부터 역순으로 삭제해나간다는 것을 의미하며, ArrayList는 마지막 데이터부터 삭제할 경우 각 요소들의 재배치가 필요하지 않기 때문에 상당히 빠르다. 단지 마지막 요소의 값을 null로만 바꾸면 되기 때문이다.

  • 중간 데이터를 추가/삭제 하는 경우에는 LinkedList가 ArrayList보다 빠르다.

    중간 요소를 추가 또는 삭제하는 경우, LinkedList는 각 요소간의 연결만 변경해주면 되기 때문에 처리속도가 상당히 빠르다. 반면에 ArrayList는 각 요소들을 재배치하여 추가할 공간을 확보하거나 빈 공간을 채워야하기 때문에 처리속도가 늦다.
    위의 소스코드에서 ArrayList와 LinkedList의 차이를 비교하기 위해 데이터의 개수를 크게 잡았는데, 사실 데이터의 개수가 그리 크지 않다면 어느 것을 사용해도 큰 차이가 나지는 않는다.

  • 배열의 경우 각 요소들이 연속적으로 메모리상에 존재하기 때문에 인덱스가 n인 요소의 값을 얻어 오려면 단순히 이와 같은 수식을 계산함으로써 해결된다. 인덱스가 n인 데이터 주소 = 배열의 주소 + (n * 데이터 타입의 크기)

  • 반면에 LinekdList는 불연속적으로 위치한 각 요소들이 서로 연결된 것이라 처음부터 n번째 데이터까지 차례대로 따라가야만 원하는 값을 얻을 수 있다. 그래서 LinkedList는 저장해야하는 데이터의 개수가 많아질수록 데이터를 읽어 오는 시간, 즉 접근시간(access time)이 길어진다는 단점이 있다.

-


ArrayList vs LinkedList 고르는 Tip !

다루고자 하는 데이터의 개수가 변하지 않는 경우라면, ArrayList가 최상의 선택이 되겠지만, 데이터 개수의 변경이 잦다면 LinkedList를 사용하는 것이 더 나은 선택이 될 것이다.

두 클래스의 장점을 이용해서 두 클래스를 조합해서 사용하는 방법도 생각해 볼 수 있다. 처음에 작업하기 전에 데이터를 저장할 때는 ArrayList를 사용한 다음, 작업할 떄는 LinkedList로 데이터를 옮겨서 작업하면 좋은 효율을 얻을수 있을 것이다.


Stack과 Queue

스택은 마지막에 저장한 데이터를 가장 먼저 꺼내는 LIFO(Last In Frist Out) 구조로 되어 있고,
큐는 처음에 저장한 데이터를 가장 먼저 꺼내는 FIFO(Fist In First Out)구조로 되어 있다.

그렇다면 스택과 큐를 구현하기 위해서는 어떤 컬렉션 클래스를 사용하는 것이 좋을까?

순차적으로 데이터를 추가하고 삭제하는 스택에는 ArrayList와같은 배열기반의 컬렉션 클래스가 적합하지만, 큐는 데이터를 꺼낼 때 항상 첫번째 저장된 데이터를 삭제하므로, ArrayList와 같은 배열기반의 컬렉션 클래스를 사용한다면 데이터를 꺼낼 떄마다 빈 공간을 채우기 위해 데이터의 복사가 발생하므로 비효율적이다. 그래서 큐는 ArrayList보다 데이터의 추가/삭제가 쉬운 LinkedList로 구현하는 것이 더 적합하다.

스택과 큐의 조상

스택과 큐의 활용 예시

  • 스택 : 수식계산, 수식괄호검사, 워드프로세서의 undo/redo, 웹브라우저의 뒤로/앞으로
  • 큐 : 최근사용문서, 인새좍업 대기목록, 버퍼(Buffer)

Deque(Double - Ended Queue)

큐의 변형으로, 한 쪽 끝으로만 추가/삭제할 수 있는 큐와 달리, Deque(덱, 또는 디큐)는 양쪽 끝에 추가/삭제가 가능하다. Deque의 조상은 Queue이며, 구현체로는 ArrayDeque와 LinkedList 등이 있다.

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

덱의 메서드에 대응하는 큐와 스택의 메서드


Priority Queue

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

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

  • 힙은 이진 트리의 한 종류로 가장 큰 값이나 가장 작은 값을 빠르게 찾을 수 있다는 특징이 있다.

소스 코드

import java.util.*;

class Main {
    public static void main(String[] args) {
        PriorityQueue pq = new PriorityQueue();
        pq.offer(3); // pq.offer(new Integer(3)); 오토박싱
        pq.offer(1);
        pq.offer(2);
        pq.offer(4);
        pq.offer(0);
        System.out.println(pq);

        Object obj = null;
        while((obj = pq.poll()) != null) { // !pq.isEmpty()
            System.out.println(obj);
        }
    }
}

코드 결과
[0, 1, 2, 4, 3]
0
1
2
3
4

저장순서가 3,1,2,4,0인데도 출력하면 0,1,2,3,4이다. 우선순위는 숫자가 작을수록 높은 것이므로 1이 가장 먼저 출력된 것이다. 물론 숫자뿐만 아니라 객체를 저장할 수도 있는데 그럴 경우 각 객체의 크기를 비교할 수 있는 방법을 제공해야 한다. 위의 코드에서는 정수를 사용했는데, 컴파일러가 Integer로 오토박싱 해준다. Integer와 같은 Number의 자손들은 자체적으로 숫자를 비교하는 방법을 정의하고 있기 때문에 비교 방법을 지정해주지 않아도 된다. 비교방법은 Comparator와 Comparable을 통해 지정해줄 수 있다.

참조변수 pq를 출력하면, PriorityQueue가 내부적으로 가지고 있는 배열의 내용이 출력되는데, 저장한 순서와 다르게 저장되었다. 앞서 설명한 것과 같이 힙이라는 자료구조의 형태로 저장된 것이라서 그렇다. 힙에 대해서는 나중에 제대로 다뤄보자.


Arrays

Arrays클래스에는 배열을 다루는데 유용한 메서드가 정의되어 있다. 같은 기능의 메서드가 배열의 타입만 다르게 오버로딩되어 있어서 많아 보인다. 아래는 Arrays에 정의된 toString()인데, 모든 기본형 배열과 참조형 배열 별로 하나씩 정의 되어 있다.

매개변수 타입이 int배열인 메서드에 대한 사용법만 알아보자.

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

copyOf()는 배열 전체를, copyOfRange()는 배열의 일부를 복사해서 새로운 배열을 만들어 반환한다. 늘 그렇듯이 copyOfRange()에 지정된 범위의 끝은 포함되지 않는다.

import java.util.*;

class Main {
    public static void main(String[] args) {
        int[] arr = {0, 1, 2, 3, 4};
        int[] arr2 = Arrays.copyOf(arr, arr.length);
        int[] arr3 = Arrays.copyOf(arr, 3);
        int[] arr4 = Arrays.copyOf(arr, 7);
        int[] arr5 = Arrays.copyOfRange(arr, 0, 4);
        int[] arr6 = Arrays.copyOfRange(arr, 2, 7);

        System.out.println(Arrays.toString(arr));
        System.out.println(Arrays.toString(arr2));
        System.out.println(Arrays.toString(arr3));
        System.out.println(Arrays.toString(arr4));
        System.out.println(Arrays.toString(arr5));
        System.out.println(Arrays.toString(arr6));

        /* 코드 결과
        [0, 1, 2, 3, 4]
        [0, 1, 2, 3, 4]
        [0, 1, 2]
        [0, 1, 2, 3, 4, 0, 0]
        [0, 1, 2, 3]
        [2, 3, 4, 0, 0]
        */
    }
}

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

fill()은 배열의 모든 요소를 지정된 값으로 채운다. setAll()은 배열을 채우는데 사용할 함수형 인터페이스를 매개변수로 받는다. 이 메서드를 호출할 때는 함수형 인터페이스를 구현한 객체를 매개변수로 지정하던가 아니면 람다식을 지정해야한다.

import java.util.*;

class Main {
    public static void main(String[] args) {
        int[] arr = new int[5];
        Arrays.fill(arr, 9);
        System.out.println(Arrays.toString(arr));
        Arrays.setAll(arr, () -> (int) (Math.random()*5+1));
        System.out.println(Arrays.toString(arr));
        
        /* 코드 결과
        [9, 9, 9, 9, 9]
        [1, 2, 1, 5, 1]
        */
    }
}

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

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

import java.util.*;

class Main {
    public static void main(String[] args) {
        int[] arr = {3, 2, 0, 1, 4};
        int idx = Arrays.binarySearch(arr, 2);
        System.out.println(idx);

        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));

        idx = Arrays.binarySearch(arr, 2);
        System.out.println(idx);
        
        /* 코드 결과
        -5 // 잘못된 결과
        [0, 1, 2, 3, 4] // 정렬된 배열
        2 // 올바른 결과
        */
    }
}

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

toString(), deepToString()

toString()배열의 모든 요소를 문자열로 편하게 출력할 수 있다. 이미 많이 사용해서 익숙할 것이다.
toString()은 일차원 배열에만 사용할 수 있으므로, 다차원 배열에는 deepToString()을 사용해야 한다. deepToString()은 배열의 모든 요소를 재귀적으로 접근해서 문자열을 구성하므로 2차원뿐만 아니라 3차원 이상의 배열에도 동작한다.

import java.util.*;

class Main {
    public static void main(String[] args) {
        int[] arr = {3, 2, 0, 1, 4};
        int[][] arr2D = {{11, 12}, {21, 22}};

        System.out.println(Arrays.toString(arr));
        System.out.println(Arrays.deepToString(arr2D));

        /* 코드 결과
        [3, 2, 0, 1, 4]
        [[11, 12], [21, 22]]
        */
    }
}

equals(), deepEquals()

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

import java.util.*;

class Main {
    public static void main(String[] args) {
        int[][] arr2D = {{11, 12}, {21, 22}};
        int[][] arr2D2 = {{11, 12}, {21, 22}};

        System.out.println(Arrays.equals(arr2D, arr2D2));
        System.out.println(Arrays.deepEquals(arr2D, arr2D2));

        /* 코드 결과
        false
        true
        */
    }
}

위와 같이 2차원 String배열을 equals()로 비교하면 배열에 저장된 내용이 같은데도 false를 결과로 얻는다.

다차원 배열은 배열의 배열형태로 구성하기 때문에 equals()로 비교하면, 문자열을 비교하는 것이 아니라 배열에 저장된 배열의 주소를 비교하게 된다. 서로 다른 배열은 항상 주소가 다르모로 false를 결과로 받는다.


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

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

import java.util.*;

class Main {
    public static void main(String[] args) {
        List list = Arrays.asList(new Integer[]{1,2,3,4,5});
        List list2 = Arrays.asList(1,2,3,4,5);
        List list3 = new ArrayList(Arrays.asList(1,2,3,4,5));

        System.out.println(list);
        System.out.println(list2);
        
        list3.add(6);
        System.out.println(list3);

        list.add(6); // 예외 발생


        /* 코드 결과
        [1, 2, 3, 4, 5]
        [1, 2, 3, 4, 5]
        [1, 2, 3, 4, 5, 6]
        Exception in thread "main" java.lang.UnsupportedOperationException
            at java.base/java.util.AbstractList.add(AbstractList.java:153)
            at java.base/java.util.AbstractList.add(AbstractList.java:111)
            at Main.main(Main.java:10)
        */
    }
}

한 가지 주의할 점은 asList()가 반환한 List의 크기를 변경할 수 없다는 것이다. 즉, 추가또는 삭제가 불가능하다. 저장된 내용은 변경가능하다. 만일 크기를 변경할 수 있는 List가 필요하다면
List list3 = new ArrayList(Arrays.asList(1,2,3,4,5));처럼 하면 된다.


parallelXXX(), spliterator(), stream()

  • 이 외에도 parallel로 시작하는 이름의 메서드들이 있는데, 이 메서드들은 보다 빠른 결과를 얻기 위해 여러 쓰레드가 작업을 나누어 처리하도록 한다.
  • spliterator()는 여러 쓰레드가 처리할 수 있게 하나의 작업을 여러 작업으로 나누는 Spliterator를 반환하며,
  • stream은 컬렉션을 스트림으로 변환한다.
    이 메서드들은 나중에 모던 자바 인 액션이라는 책을 읽으며 다시 다뤄보자.

Set


HashSet


TreeSet


Map


해싱과 해시함수


HashMap과 Hashtable


TreeMap


Iterator, ListIterator, Enumeration


Comparator와 Comparable


Properties


Collections


컬렉션 클래스 정리 & 요약

profile
내 두뇌는 휘발성 메모리다. 😪

0개의 댓글