본문 바로가기

Java/자바 기초

11. 컬렉션 프레임웍

컬렉션 프레임웍

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

- 컬렉션(collection)은 다수의 데이터, 즉 데이터 그룹을 의미한다.

- 프레임웍은 표준화된 프로그래밍 방식을 의미한다.

 

컬렉션 프레임웍은 컬렉션(다수의 데이터)을 다루는데 필요한 다양하고 풍부한 클래스들을 제공한다.

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

컬렉션 프레임웍에서는 컬렉션데이터 그룹을 크게 3가지 타입이 존재한다고 인식하고 각 컬렉션을 다루는데 필요한 기능을 가진 3개의 인터페이스(List, Set, Map)을 정의하였다.

 

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

인터페이스 인터페이스
List 순서가 있는 데이터의 집합. 중복 허용 O
구현 클래스 : ArrayList, LinkedList, Stack, Vector 등
Set 순서를 유지하지 않는 데이터의 집합. 중복 허용 X
구현 클래스 : HashSet, TreeSet 등
Map 키와 값의 쌍으로 이루어진 데이터의 조합, 순서 유지 X
키 : 중복 허용 X
값 : 중복 허용 O
구현 클래스 : HashMap, TreeMap, Hashtable, Properties 등

Collection인터페이스

List와 Set의 조상이다.

컬렉션 클래스에 저장된 데이터를 읽기, 추가, 삭제 등 컬렉션 관리를 위한 기본 메서드 정의되어 있다.

List인터페이스

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

Set인터페이스

중복을 허용하지 않고 저장순서가 유지되지 않는 컬렉션 클래스를 구현하는데 사용된다.

Map인터페이스

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

키는 중복될 수 없지만 값은 중복을 허용한다.

기존에 저장된 데이터와 중복된 키와 값을 저장하면 기존의 값은 없어지고 마지막에 저장된 값이 남게 된다.

 

Map.Entry Interface

- Map Interface의 내부 인터페이스

- Map에 저장되는 key-value쌍을 다루기 위해 내부적으로 Entry Interface를 정의

- Entry는 키와 값의 한 쌍을 의미한다.

ArrayList

List 인터페이스를 구현하기 때문에 데이터의 저장순서가 유지되고 중복을 허용한다.

Object 배열을 이용해 데이터를 순차적 저장하고, 모든 종류의 객체를 담을 수 있다.

ArrayList는 기존의 Vector를 개선한 것으로, 가능하면 Vector보다 ArrayList를 사용하자.

ArrayList 예제

ArrayList의 기본적인 메서드를 이용해서 객체를 다루는 방법을 보여주는 예제

 

Collection은 인터페이스이고, Collections.sort의 Collections는 클래스임에 주의하자.

import java.util.*;

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

//List subList(int fromIndex, int toIndex): fromIndex부터 toIndex사이에 저장된 객체 반환
        ArrayList list2 = new ArrayList(list1.subList(1,4)); 
        print(list1, list2);
// list1:[5, 4, 2, 0, 1, 3]
// list2:[4, 2, 0]

        Collections.sort(list1);    // list1과 list2를 정렬한다.
        Collections.sort(list2);    // Collections.sort(List l)
        print(list1, list2);
// list1:[0, 1, 2, 3, 4, 5]
// list2:[0, 2, 4]

//containsAll : 모든 요소를 포함하고 있을 때만 true
        System.out.println("list1.containsAll(list2):"
                                               + list1.containsAll(list2));
// list1.containsAll(list2):true

        list2.add("B");
        list2.add("C");
// add(int index, Object element): 주어진 객체(element)를 지정된 위치(index)에 추가한다.
        list2.add(3, "A");  
        print(list1, list2);
// list1:[0, 1, 2, 3, 4, 5]
// list2:[0, 2, 4, A, B, C]

// set(int index, Object element): 주어진 객체(element)를 지정된 위치(index)에 저장한다.
        list2.set(3, "AA");
        print(list1, list2);
// list1:[0, 1, 2, 3, 4, 5]
// list2:[0, 2, 4, AA, B, C]

//retainAll: list1에서 list2와 겹치는 부분만 남기고 나머지는 삭제한다.
        System.out.println("list1.retainAll(list2):" + list1.retainAll(list2));
// list1.retainAll(list2):true
        print(list1, list2);
// list1:[0, 2, 4]
// list2:[0, 2, 4, AA, B, C]

        //  list2에서 list1에 포함된 객체들을 삭제한다.
        for(int i= list2.size()-1; i >= 0; i--) {
            if(list1.contains(list2.get(i)))
                list2.remove(i);
        }
        print(list1, list2);
// list1:[0, 2, 4]
// list2:[AA, B, C]

    } // main의 끝

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

ArrayList의 추가와 삭제

ArrayList의 요소를 삭제하는 경우, 삭제할 객체의 바로 아래에 있는 데이터를 한 칸씩 위로 복사해서 삭제할 객체를 덮어쓰는 방식으로 처리한다.

 

새로운 요소를 추가할 때도, 먼저 추가할 위치 이후의 요소들을 모두 한 칸씩 이동시킨 후에 저장해야 한다.

배열의 중간에 위치한 객체를 추가하거나 삭제하는 경우 다른 데이터의 위치를 이동시켜줘야 하기 때문에 다루는 데이터의 개수가 많을수록 작업하는 시간이 오래 걸린다.

LinkedList

배열은 데이터를 읽어오는데 걸리는 시간(접근시간, access time)이 가장 빠르다는 장점을 가지고 있지만 다음과 같은 단점도 가지고 있다.

 

크기를 변경할 수 없다.

- 크기를 변경할 수 없으므로 새로운 배열을 생성해서 데이터를 복사해야 한다.

- 실행속도를 향상시키기 위해서는 충분히 큰 크기의 배열을 생성해야 하므로 메모리가 낭비된다.

 

비순차적인 데이터의 추가 또는 삭제에 시간이 많이 걸린다.

- 차례대로 데이터를 추가하고 마지막에서부터 데이터를 삭제하는 것은 빠르지만,

- 배열의 중간에 데이터를 추가하려면, 빈자리를 만들기 위해 다른 데이터들을 복사해서 이동해야 한다.

 

 

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

배열은 모든 데이터가 연속적으로 존재하지만, 링크드 리스트는 불연속적으로 존재하는 데이터를 서로 연결(link)한 형태로 구성되어 있다.

 

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

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

LinkedList의 추가와 삭제

데이터 삭제

데이터 추가

ArrayList와 LinkedList의 비교

배열은 각 요소들이 연속적으로 메모리상에 존재하기 때문에 간단한 계산만으로 원하는 요소의 주소를 얻어서 저장된 데이터를 곧바로 읽어올 수 있다.

 

LinkedList는 불연속적으로 위치한 각 요소들이 서로 연결된 것이라 처음부터 n번째 데이터까지 차례대로 따라가야만 원하는 값을 얻을 수 있다.

 

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

 

 

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

Stack과 Queue

Stack은 마지막에 저장한 데이터를 가장 먼저 꺼내게 되는 LIFO(Last In Frist Out)구조로 되어 있다.

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

 

순차적으로 데이터를 추가하고 삭제하는 스택에는 ArraList와 같은 배열 기반의 컬렉션 클래스가 적합하다.

 

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

Stack과 Queue 예제

자바에서는 스택은 Stack클래스로 구현하여 제공하고 있지만 큐는 Queue 인터페이스로만 정의해놓았을 뿐 별도의 클래스를 제공하고 있지 않다. 대신 Queue인터페이스를 구현한 클래스들이 있어서 이 들 중의 하나를 선택해서 사용하면 된다.

import java.util.*;

class Ex11_2 {
    public static void main(String[] args) {
        Stack st = new Stack();
        Queue q = new LinkedList();    // Queue인터페이스의 구현체인 LinkedList를 사용

        st.push("0");  //push: stack에서 저장
        st.push("1");
        st.push("2");

        q.offer("0");  //offer: queue에서 저장
        q.offer("1");
        q.offer("2");

        System.out.println("= Stack =");
        while(!st.empty()) {
            System.out.println(st.pop()); // 스택에서 요소 하나를 꺼내서 출력
        }

        System.out.println("= Queue =");
        while(!q.isEmpty()) {
            System.out.println(q.poll()); // 큐에서 요소 하나를 꺼내서 출력
        }
    }
}

/*
출력 결과

= Stack =
2
1
0
= Queue =
0
1
2

*/

인터페이스를 구현한 클래스 찾기

JAVA API문서에서 ‘All Known Implementing Classes’라는 항목이 있는데 여기에 나열된 클래스들이 인터페이스를 구현한 클래스들이다. 정의된 클래스들 중 하나를 선택해서 사용하면 된다.

Stack과 Queue의 활용

스택의 활용 예

- 수식계산, 수식괄호검사 ex) ((3+2)*8)/2

- 워드프로세서의 undo/redo

- 웹브라우저의 뒤로/앞으로

 

큐의 활용 예

- 최근사용문서

- 인쇄작업 대기목록

- 버퍼

Iterator, ListIterator, Enumeration

Iterator, ListIterator, Enumeration은 모두 컬렉션에 저장된 요소를 접근하는 데 사용되는 인터페이스이다.

- Iterator: 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스

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

- Enumeration: Iterator의 구버전

 

컬렉션 프레임웍에서는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화하였다.

Collection에 저장된 각 요소에 접근하는 기능을 가진 Iterator 인터페이스를 정의하고, Collection 인터페이스에는 ‘Iterator를 구현한 클래스의 인스턴스’를 반환하는 iterator()를 정의하고 있다.

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

  public interface Collection{
      ...
      public Iterator iterator();
      ...
  }

 

iterator()는 Collection인터페이스에 정의된 메서드이므로, Collection 인터페이스의 자손인 List와 Set에도 포함되어 있다. 컬렉션 클래스에 대해 iterator()를 호출하여 Iterator를 얻은 다음 반복문, 주로 while문을 사용해서 컬렉션 클래스의 요소들을 읽어올 수 있다.

List list = new ArrayList(); //다른 컬렉션으로 변경할 때는 이 부분만 고치면 된다.
Iterator it = list.iterator();

while(it.hasNext()){ //boolean hasNext() 읽어올 요소가 있는지 확인
	System.out.println(it.next()); // Object next() 다음 요소를 읽어옴
}

Iterator, ListIterator, Enumeration 예제

import java.util.*;

class Ex11_5 {
    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");

        Iterator it = list.iterator();

        while(it.hasNext()) {
            Object obj = it.next();
            System.out.println(obj);
        }
    } // main
}

/*
출력 결과

1
2
3
4
5

*/

 

List클래스들은 저장순서를 유지하기 때문에 Iterator를 이용해서 읽어온 결과 역시 저장순서와 동일하지만, Set클래스들은 각 요소간의 순서가 유지 되지 않기 때문에 Iterator()를 이용해서 저장된 요소들을 읽어 와도 처음에 저장된 순서와 같지 않다.

Map과 Iterator

Map인터페이스를 구현한 컬렉션 클래스는 키(key)와 값(value)을 쌍(pair)으로 저장하고 있기 때문에 iterator()를 직접 호출할 수 없고, 그 대신 keySet()이나 entrySet()과 같은 메서드를 통해서 키와 값을 각각 따로 Set의 형태로 얻어온 후에 다시 iterator()를 호출해야 Iterator를 얻을 수 있다.

Map map = new HashMap();
    ...
Iterator it = map.entrySet().iterator();

Arrays의 메서드(1) - 복사

Arrays클래스에는 배열을 다루는데 유용한 메서드가 정의되어 있다.

 

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

- copyOf(): 배열의 전체를 복사해서 새로운 배열을 만들어 반환한다.

- copyOfRante(): 배열의 일부를 복사해서 새로운 배열을 만들어 반환한다.

int[] arr = {0, 1, 2, 3, 4};
int[] arr2 = Arrays.copyOf(arr, arr.length);  //[0, 1, 2, 3, 4]
int[] arr3 = Arrays.copyOf(arr, 3);           //[0, 1, 2]
int[] arr4 = Arrays.copyOf(arr, 7);           //[0, 1, 2, 3, 4, 0, 0]
int[] arr5 = Arrays.copyOfRange(arr, 2, 4);   //[2,3]  <- 4는 불포함
int[] arr6 = Arrays.copyOfRange(arr, 0, 7);   //[0, 1, 2, 3, 4, 0, 0]

Arrays의 메서드(2) - 채우기, 정렬, 검색

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

- fill(): 배열의 모든 요소를 지정된 값으로 채운다.

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

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

 

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

- sort(): 배열을 정렬한다.

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

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

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

Arrays의 메서드(3) - 비교와 출력

문자열의 출력 - toString(), deepToString()

- toString(): 배열의 모든 요소를 문자열로 출력할 수 있다. 일차원 배열에만 사용 가능하다.

- deepToString(): 배열의 모든 요소를 재귀적으로 접근해서 다차원 배열을 문자열로 출력한다.

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

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

 

문자열의 비교 - equals(), deepEquals()

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

- deepEquals(): 다차원 배열의 비교에 사용된다.

String [][] arr = new String[][]{ {"A", "B"}, {"a", "b"} };
String [][] arr2 = new String[][]{ {"A", "B"}, {"a", "b"} };

Arrays.equals(arr, arr2); //false
Arrays.deepEquals(arr, arr2); //true

 

위와 같이 2차원 String 배열을 equals()로 비교하면 배열에 저장된 내용이 같은데도 false를 결과로 얻는다. 다차원 배열은 ‘배열의 배열’의 형태로 구성되기 때문에 equals()로 비교하면, 문자열을 비교하는 것이 아니라 ‘배열에 저장된 배열의 주소’를 비교하게 된다. 서로 다른 배열은 항상 주소가 다르므로 false를 결과로 얻는다.

Arrays의 메서드(4) - 변환

배열을 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); // list = [1,2,3,4,5]
list add(6); //UnsupportedOperatioException 발생

 

asList()가 반환한 List의 크기를 변경할 수 없다. 즉, 추가 또는 삭제가 불가능하다. 저장된 내용 변경은 가능하다.

만일 크기를 변경할 수 있는 List가 필요하다면 다음과 같이 하면 된다.

List list = new ArrayList(Arrays.asList(1,2,3,4,5));

 

parallelXXX(), spliterator(), stream()

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

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

- stream(): 컬렉션을 스트림으로 변환한다.

Comparator와 Comparable

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

- Comparable: 기본 정렬기준을 구현하는데 사용

- Comparator: 기본 정렬기준 외에 다른 기준으로 정렬하고자 할 때 사용

public interface Comparable {
    int compareTo(Object o);  //객체 자신(this)과 o를 비교
}

public interface Comparator {
    int compare(Object o1, Objec o2); //o1과 o2를 비교
    boolean equals(Object obj);
}

 

Comparable을 구현하고 있는 클래스들(ex., Integer)은 같은 타입의 인스턴스끼리 서로 비교할 수 있고, 기본적으로 오름차순으로 정렬되도록 구현되어 있다. Comparable을 구현한 클래스는 정렬이 가능하다는 것을 의미한다.

 

compare()과 compareTo()의 반환값은 int인데, 비교하는 두 객체가 같으면 0, 비교하는 값보다 작으면 음수, 크면 양수를 반환하도록 구현해야 한다.

Comparator와 Comparable 예제

import java.util.*;

class Ex11_7 {
    public static void main(String[] args) {
        String[] strArr = {"cat", "Dog", "lion", "tiger"};

        Arrays.sort(strArr); // String의 Comparable구현에 의한 정렬
        System.out.println("strArr=" + Arrays.toString(strArr));
        //strArr=[Dog, cat, lion, tiger]

        Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); // 대소문자 구분안함
        System.out.println("strArr=" + Arrays.toString(strArr));
        //strArr=[cat, Dog, lion, tiger]

        Arrays.sort(strArr, new Descending()); // 역순 정렬 (Comprator 구현에 의한 정렬)
        System.out.println("strArr=" + Arrays.toString(strArr));
        //strArr=[tiger, lion, cat, Dog]

    }
}

class Descending implements Comparator { 
    public int compare(Object o1, Object o2){
        if( o1 instanceof Comparable && o2 instanceof Comparable) {
            Comparable c1 = (Comparable)o1;
            Comparable c2 = (Comparable)o2;
            return c1.compareTo(c2) * -1 ; // -1을 곱해서 기본 정렬방식의 역으로 변경한다.
                                        // 또는 c2.compareTo(c1)와 같이 순서를 바꿔도 된다.
        }
        return -1;
    } 
}

 

Arrays.sort()는 배열을 정렬할 때, Comparator를 지정해주지 않으면 저장하는 객체(Comparable을 구현한 클래스의 객체)에 구현된 내용에 따라 정렬된다.

static void sort(Object[] a) //객체 배열에 저장된 객체가 구현한 Comparable에 의한 정렬
static void sort(Object[] a, Comparator c) //지정한 Comparator에 의한 정렬

 

정렬할 때는 정렬 기준(Comparator)을 매개 변수로 제공하던가, 아니면 정렬 대상에 저장된 객체가 정렬 기준(Comparable)을 가지고 있어야 한다. 그렇지 않으면 예외가 발생한다.

 

String의 Comparable구현은 문자열이 사전 순으로 정렬되도록 작성되어 있다. 문자열의 오름차순 정렬은 공백, 숫자, 대문자, 소문자의 순으로 정렬되는 것을 의미한다. 정확히 얘기하면 문자의 유니코드의 순서가 작은 값에서부터 큰 값으로 정렬되는 것이다.

 

String은 대소문자를 구분하지 않고 비교하는 Comparator를 상수의 형태로 제공한다.

public satic final Comparator CASE_INSENSITIVE_ORDER

HashSet

HashSet은 Set인터페이스를 구현한 가장 대표적인 컬렉션이이다.

Set인터페이스의 특징대로 HashSet은 중복된 요소를 저장하지 않는다.

저장 순서를 유지하지 않으므로, 저장순서를 유지하고자 한다면 LinkedHashSet을 사용해야한다.

HashSet 예제1

import java.util.*;

class Ex11_9 {
    public static void main(String[] args) {
        Object[] objArr = {"1",new Integer(1),"2","2","3","3","4","4","4"};
        Set set = new HashSet();

        for(int i=0; i < objArr.length; i++) {
            set.add(objArr[i]);    // HashSet에 objArr의 요소들을 저장한다.
        }
                // HashSet에 저장된 요소들을 출력한다.
        System.out.println(set);    

        // HashSet에 저장된 요소들을 출력한다.(Iterator이용)
        Iterator it = set.iterator();

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

/*
출력 결과

[1, 1, 2, 3, 4]
1
1
2
3
4

*/

‘1’이 두 번 출력되었는 데, 하나는 String 인스턴스이고 다른 하나는 Integer인스턴스로 서로 다른 객체이므로 중복으로 간주하지 않는다.

HashSet 예제2

Set에 있는 요소를 List로 정렬해서 출력하는 예제이다.

import java.util.*;

class Ex11_10 {
    public static void main(String[] args) {
        Set set = new HashSet();

        for (int i = 0; set.size() < 6 ; i++) {
            int num = (int)(Math.random()*45) + 1;
            set.add(new Integer(num));
        }

        List list = new LinkedList(set); // LinkedList(Collection c)
        Collections.sort(list);          // Collections.sort(List list)
        System.out.println(list);
    }
}

/*
출력 결과
[7, 1, 17, 18, 24, 28]
*/

HashSet 예제3

참조 변수를 Set에 담는 예제이다.

import java.util.*;

class Ex11_11 {
    public static void main(String[] args) {
        HashSet set = new HashSet();

        set.add("abc");
        set.add("abc");
        set.add(new Person("David",10));
        set.add(new Person("David",10));

        System.out.println(set);
    }
}

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return name +":"+ age;
    }
}

/*
출력 결과
[abc, David:10, David:10]
*/

 

Person 클래스는 name과 age를 멤버변수로 갖는다. name과 age가 같으면 같은 사람으로 인식하도록 하려는 의도로 작성하였다. 하지만 출력 결과를 보면 두 인스턴스의 name과 age의 값이 같음에도 불구하고 서로 다른 것으로 인식하여 ‘David:10’이 두 번 출력되었다.

 

두 인스턴스를 같은 것으로 인식하게 하려면 Person클래스 아래 equals()와 hashCode()를 추가(오버라이딩)해야 한다. HashSet의 add메서드는 새로운 요소를 추가하기 전에 기존에 저장된 요소와 같은 것인지 판별하기 위해 추가하려는 요소의 equals()와 hashCode()를 호출하기 때문이다.

public boolean equals(Object obj){
    if(!(obj instanceof Person)) return false;
    Person p = (Person)obj;
    return name.equals(p.name) && age==p.age;
}

public int hashCode(){
    return Objects.hash(name,age); //int hash(Object... values)
}

TreeSet

TreeSet은 이진 탐색 트리(bineary search tree)라는 자료구조의 형태로 데이터를 저장하는 컬렉션 클래스이다.

 

정렬, 검색, 범위검색(range search)에 높은 성능을 보이는 자료구조이다.

 

Set인터페이스를 구현했으므로 중복된 데이터의 저장을 허용하지 않으며, 정렬된 위치에 저장하므로 저장 순서를 유지하지도 않는다.

 

이진 트리(binary tree)는 링크드 리스트처럼 여러 개의 노드(node)가 서로 연결된 구조로, 각 노드에 최대 2개의 자식 노드를 가진다. 이진 탐색 트리는 이진 트리의 한 종류이다.

이진 탐색 트리(binary search tree)

  • 모든 노드는 최대 두 개의 자식 노드를 가질 수 있다.
  • 왼쪽 자식노드의 값은 부모노드의 값보다 작고 오른쪽 자식노드의 값은 부모노드의 값보다 커야한다.
  • 노드의 추가 삭제에 시간이 걸린다. (반복 비교로 자리를 차장 저장)
  • 검색(범위검색)정렬에 유리하다.
  • 중복된 값을 저장하지 못한다.

이진 탐색 트리의 저장과정

왼쪽 마지막 레벨이 제일 작은 값이 되고 오른쪽 마지막 레벨의 값이 제일 큰 값이 된다.

 

컴퓨터는 알아서 값을 비교하지 못한다. TreeSet에 저장되는 객체가 Comparable을 구현하던가 아니면, Comparator를 제공해서 두 객체를 비교할 방법을 알려줘야 한다. 그렇지 않으면, TreeSet에 객체를 저장할 때 예외가 발생한다.

 

참고로, HashSet은 equals(), hashCode()로 비교하고, TreeSet은 comapre()를 호출해서 비교한다.

TreeSet 예제1

import java.util.*;

class Ex11_13 {
    public static void main(String[] args) {
        Set set = new TreeSet();

        for (int i = 0; set.size() < 6 ; i++) {
            int num = (int)(Math.random()*45) + 1;
            set.add(num);  // set.add(new Integer(num));
        }

        System.out.println(set);
    }
}

/*
출력 결과
[5, 12, 24, 26, 33, 45]
*/

 

‘HashSet 예제2’에서 HashSet과 다르게, TreeSet은 저장할 때 이미 정렬하기 때문에 읽어올 때 따로 정렬할 필요가 없다.

HashMap과 Hashtable

Hashtable은 HashMap의 구버전으로 HashMap을 사용할 것을 권한다.

 

해싱(hashing)을 사용하기 때문에 많은 양의 데이터를 검색할 때 뛰어난 성능을 보인다.

 

Map 인터페이스를 구현해서, 데이터를 키(key)와 값(value)의 쌍(entry)으로 저장한다.

- 키(key): 컬렉션 내의 키(key) 중에서 유일해야 한다.

- 값(value): 키(key)와 달리 데이터의 중복을 허용한다.

 

키와 값은 각각 Object 타입으로 저장한다.

public class HashMap extends AbstractMap 
                    implements Map, Cloneable, Serializable  {

  transient Entry[] table;
    ...

  static class Entry implements Map.Entry {
    final Object key;
    Object value;
      ...
  }
}

해싱(Hashing)

해시함수(hash function)로 해시테이블(hash table)에 데이터를 저장, 검색한다.

 

해시테이블은 배열과 링크드 리스트가 조합된 형태이다.

 

해시테이블에 저장된 데이터를 가져오는 과정

1) 키로 해시함수를 호출해서 해시코드를 얻는다.

2) 해시코드(해시함수의 반환값)에 대응하는 링크드리스트를 배열에서 찾는다.

3) 링크드리스트에서 키와 일치하는 데이터를 찾는다.

 

해시함수는 같은 키에 대해 항상 같은 해시코드를 반환해야 한다.

해시함수는 서로 다른 키일지라도 같은 값의 해시코드를 반환할 수도 있다.

HashMap 예제1

import java.util.*;

class Ex11_16 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("myId", "1234");
        map.put("asdf", "1111");
        map.put("asdf", "1234"); // 이미 존재하는 키 추가 가능. 기존 값은 없어짐

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

        while(true) {
            System.out.println("id와 password를 입력해주세요.");
            System.out.print("id :");
            String id = s.nextLine().trim();

            System.out.print("password :");
            String password = s.nextLine().trim();
            System.out.println();

            if(!map.containsKey(id)) {
                System.out.println("입력하신 id는 존재하지 않습니다. 다시 입력해주세요.");
                continue;
            } 

            if(!(map.get(id)).equals(password)) {
                System.out.println("비밀번호가 일치하지 않습니다. 다시 입력해주세요.");
            } else {
                System.out.println("id와 비밀번호가 일치합니다.");
                break;
            }
        } // while
    } // main의 끝
}

/*
출력 결과

c:\jdk1.8\work\ch11>java Ex11_16
id와 password를 입력해주세요.
id : asdf
password : 1111

비밀번호가 일치하지 않습니다. 다시 입력해주세요.
id와 password를 입력해주세요.
id : asdf
password : 1234

id와 비밀번호가 일치합니다.

c:\jdk1.8\work\ch11>
*/

Collections의 메서드 - 동기화

Arrays가 배열과 관련된 메서드를 제공하는 것처럼, Collections는 컬렉션과 관련된 메서드를 제공한다.

 

컬렉션의 동기화

- 멀티 쓰레드(multi-thread) 프로그래밍에서는 하나의 객체를 여러 쓰레드가 동시에 접근할 수 있기 때문에 데이터의 무결성(integrity)을 유지하기 위해서는 공유되는 객체에 동기화(synchronization)가 필요하다.

 

- Vector와 Hashtable과 같은 구 버전은 자체적으로 동기화가 처리되어 있는데, 멀티쓰레드 프로그래밍이 아닌 경우에는 불필요한 기능이 되어 성능을 떨어뜨리는 요인이 된다.

 

- ArrayList와 HashMap과 같은 컬렉션은 동기화를 자체적으로 처리하지 않고, 필요한 경우에만 java.util.Collections클래스의 동기화 메서드를 이용해서 동기화 처리가 가능하도록 변경하였다.

 

- Collections클래스에는 다음과 같은 동기화 메서드를 제공한다

// synchronizedXXX()
static Collection synchronizedCollection(Collection c)
static List synchronizedList(List list)
static Set synchronizedSet(Set s)
static Map synchronizedMap(Map m)
static SortedSet synchronizedSortedSet(SortedSet s)
static SortedMap synchronizedSortedMap(SortedMap m)

Collections의 메서드 - 변경불가, 싱글톤

변경불가 컬렉션 만들기

- 컬렉션에 저장된 데이터를 보호하기 위해서 컬렉션을 변경할 수 없게, 즉 읽기 전용으로 만들어야할 때가 있다.

- 주로 멀티 쓰레드 프로그래밍에서 여러 쓰레드가 하나의 컬렉션을 공유하다보면 데이터가 손상될 수 있는데, 이를 방지하려면 아래의 메서드들을 이용하면 된다.

// unmodifiableXXX()
static Collection unmodifiableCollection(Collection c)
static List unmodifiableList(List list)
static Set unmodifiableSet(Set c)
static Map unmodifiableMap(Map c)
static NavigableSet unmodifiableNavigableSet(NavigableSet c)
static SortedSet unmodifiableSortedSet(SortedSet c)
static NavigableMap unmodifiableNavigableMap(NavigableMap c)
static SortedMap unmodifiableSortedMap(SortedMap c)

 

싱글톤 컬렉션 만들기

- 단 하나의 객체만을 저장하는 컬렉션을 만들어야 하는 경우가 있다.

- 이럴 때 아래 메서드를 사용하면 된다.

// singletonXXX()
static List singletonList(Object o)
static Set singleton(Object o)
static Map singletonMap(Object o)

- 매개변수로 저장할 요소를 지정하면, 해당 요소를 저장하는 컬렉션을 반환한다. 그리고 반환된 컬렉션은 변경할 수 없다.

Collections의 메서드 - 단일 컬렉션

한 종류의 객체만 저장하는 컬렉션 만들기

- 컬렉션에 지정된 종류의 객체만 저장할 수 있도록 제한하고 싶을 때 아래의 메서드를 사용한다.

- 컬렉션에 저장할 요소의 타입을 제한하는 것은 지네릭스(generics)로 간단히 처리할 수 있는데도 이 메서드를 제공하는 이유는 호환성 때문이다. 지네릭스는 JDK1.5부터 도입된 기능으로 JDK1.5이전에 작성된 코드를 사용할 때는 이 메서드들이 필요할 수 있다.

// checkedXXX()
static Collection checkedCollection(Collection c, Class type)
static List checkedList(List c, Class type)
static Set checkedSet(Set c, Class type)
static Map checkedMap(Map c, Class type)
static Queue checkedQueue(Queue c, Class type)
static NavigableSet checkedNavigableSet(NavigableSet c, Class type)
static SortedSet checkedSortedSet(SortedSet c, Class type)
static NavigableMap checkedNavigableMap(NavigableMap c, Class type)
static SortedMap checkedSortedMap(SortedMap c, Class type)

- 예시

List list = ArrayList();
List checkedList = checkedList(list, String.class); //String만 저장 가능
checkedList.add("abc");
checkedList.add(new Integer(3)); // 에러. ClassCastException발생

Collections 예제

import java.util.*;
import static java.util.Collections.*;

class Ex11_19 {
    public static void main(String[] args) {
        List list = new ArrayList();
        System.out.println(list);
    // []

        addAll(list, 1,2,3,4,5); 
        System.out.println(list);
    // [1, 2, 3, 4, 5]

        rotate(list, 2);  // 오른쪽으로 두 칸씩 이동 
        System.out.println(list);
    // [4, 5, 1, 2, 3]

        swap(list, 0, 2); // 첫 번째와 세 번째를 교환(swap)
        System.out.println(list);
    // [1, 5, 4, 2, 3]

        shuffle(list);    // 저장된 요소의 위치를 임의로 변경 
        System.out.println(list);
    // [4, 1, 2, 3, 5]

        sort(list, reverseOrder()); // 역순 정렬 reverse(list);와 동일 
        System.out.println(list);
    // [5, 4, 3, 2, 1]    

        sort(list);       // 정렬 
        System.out.println(list);
    // [1, 2, 3, 4, 5]

        int idx = binarySearch(list, 3);  // 3이 저장된 위치(index)를 반환
        System.out.println("index of 3 = " + idx);
    // index of 3 = 2

        System.out.println("max="+max(list)); //ma=5
        System.out.println("min="+min(list));  //min=1
        System.out.println("min="+max(list, reverseOrder())); //min=1

        fill(list, 9); // list를 9로 채운다.
        System.out.println("list="+list);
    // list=[9, 9, 9, 9, 9]

        // list와 같은 크기의 새로운 list를 생성하고 2로 채운다. 단, 결과는 변경불가
        List newList = nCopies(list.size(), 2); 
        System.out.println("newList="+newList);
    // newList=[2, 2, 2, 2, 2]

        System.out.println(disjoint(list, newList)); // 공통요소가 없으면 true
    // true

        copy(list, newList); 
        System.out.println("newList="+newList);
        System.out.println("list="+list);
    // newList=[2, 2, 2, 2, 2]
    // list=[2, 2, 2, 2, 2]

        replaceAll(list, 2, 1); 
        System.out.println("list="+list);
    // list=[1, 1, 1, 1, 1]

        Enumeration e = enumeration(list);
        ArrayList list2 = list(e); 

        System.out.println("list2="+list2);
    // list2=[1, 1, 1, 1, 1]
    }
}

컬렉션 클래스 정리 & 요약

 

'Java > 자바 기초' 카테고리의 다른 글

10. 날짜와 시간 & 형식화  (0) 2022.03.14
9. java.lang 패키지와 유용한 클래스  (0) 2022.03.14
8. 예외처리  (0) 2022.03.14
7. 객체지향 프로그래밍 II  (0) 2022.03.11
6. 객체지향 프로그래밍 I  (0) 2022.03.11