본문 바로가기
난 정말 JAVA를 공부한적이 없다구요

22. 컬렉션 프레임워크(Collection Framework)

by moca7 2024. 6. 27.

 

ㅁ 컬렉션 프레임워크의 기본적인 이해

- 프레임워크라는 단어는 여러 분야에서 상이한 개념으로 사용되지만, 공통적으로 "잘 정의된, 약속된 구조나 골격"이라는 의미를 가지고 있다.

- 자바에서는, "잘 정의된, 약속된 구조의 클래스들"이라고 정의할 수 있다.

- 쉽게 말해서 여러 프로그래머들에 의해 사용되로록, 잘 정의된 클래스들의 모임이라 할 수 있다.

그런데 이것이 전부라면 이는 그저 라이브러리에 지나지 않는다.

 

하지만 유독 컬렉션과 관련해서는 '컬렉션 라이브러리'라 하지 않고, '컬렉션 프레임워크'라 하고 있다.

이는 컬렉션과 관련된 클래스들의 정의에 적용되는 설계의 원칙 또는 구조가 존재하기 때문이다.

 

- 컬렉션의 의미와 컬렉션을 프레임워크라 부를 수 있도록 도입이 된 설계의 원칙 또는 구조를 정확히 이해한다면(특히 두번째) 컬렉션 프레임워크의 활용은 아무 문제도 되지 않는다.

 

ㅁ 컬렉션의 의미와 자료구조

- 컴퓨터 공학에는 '자료구조(Data Structures)'라는 학문과 '알고리즘(Algorithms)'이라는 학문이 있다. 

- 자료구조는 데이터의 저장과 관련이 있는 학문으로서, 검색 등 다양한 측면을 고려하여 효율적인 데이터의 저장 방법을 연구하는 학문이다.

- 알고리즘저장된 데이터의 일부 또는 전체를 대상으로 진행하는 각종 연산을 연구하는 학문이다.

- 따라서 이 둘은 서로 다른 학문이면서 매우 긴밀한 연관이 있다.

 

- 자료구조에서 정형화하고 있는 데이터의 저장방식 중에서 대표적인 것 몇 가지를 정리하면 다음과 같다.

 

배열 Array, 리스트 List, 스택 Strack, 큐 Queue, 트리 Tree, 해시 Hash

 

- 쉬운 축에 속하는 알고리즘 몇 가지를 정리하면 다음과 같다.

 

정렬 Sort, 탐색 Search, 최대 Max 최소 Min 검색

 

- 컬렉션 데이터의 저장, 그리고 이와 관련있는 알고리즘구조화해 놓은 프레임워크이다.

쉽게는 바로 위에서 언급한 자료구조와 알고리즘을 클래스로 구현해 놓은 것 정도로 생각해도 좋다.

 

때문에 컬렉션 클래스들을 가리켜 '컨테이너 클래스'라고도 한다.

컬렉션 프레임워크를 구성하는 클래스들은 많은 양의 인스턴스를 다양한 형태로 저장하는 기능을 제공하고 있다.

따라서 자료구조와 알고리즘을 잘 몰라도 자바의 컬렉션 프레임워크를 활용하면 다양하고 효율적으로 인스턴스의 저장이 가능하다.

 

※ 자료구조 학습의 필요성

- 자바의 컬렉션 프레임워크가 자료구조의 학습을 대신하는 것은 아니다. 

자료구조라는 학문은 데이터의 저장으로만 설명할 수 있는 학문이 아니기 때문이다.

따라서 자료구조를 몰라도 되는 것은 아니다.

오히려 자료구조의 학습이 매우 중요하다. 

 

 

ㅁ 컬렉션 프레임워크의 기본 골격

- 컬렉션 프레임워크를 공부한다고 생각하면 부담될 수 있으니, 그냥 지금까지와 마찬가지로 다양한 클래스를 공부한다고 생각하자. 다만 공부의 대상이 데이터의 저장을 위해 정의된 클래스일 뿐이다.

 

< 컬렉션 프레임워크의 인터페이스 구조 >

 

- Java에서 컬렉션 프레임워크는 데이터를 저장, 관리, 처리하기 위한 인터페이스와 클래스들의 집합을 말합니다. 이 프레임워크는 데이터 구조를 표준화하여 다양한 종류의 데이터를 저장하고 다루는 방법을 제공합니다.

 

- <E> 그리고 <K, V>는 모든 인터페이스가 제네릭으로 정의되었음을 표현한 것이다. 

- 어떠한 인터페이스를 구현하느냐에 따라서 데이터를 저장하는 방식에 차이가 있기 때문에, 구현하는 인터페이스의 종류만 알아도 컬렉션 클래스의 데이터 저장방식을 알 수 있다. 

- 그리고 제네릭 관련 인터페이스와 클래스들은 위의 그림에서 보이는 인터페이스를 포함하여 대부분 java.util 패키지에 묶여있다.

 

- 컬렉션이 프레임 워크인 이유 : 매우 단순하게 말하면, 위의 그림에서 보이는 인터페이스의 구조를 기반으로 클래스들이 정의되어 있기 때문에 프레임워크라 하는 것이다.

 

- 위의 그림을 보면 크게는 Collection <E> 인터페이스와 Map <K, V> 인터페이스로 나뉨을 알 수 있다.

이중에서 일반적인(잠시 후에 소개하는 Map의 저장방식에 비해서 일반적이라는 뜻) 데이터의 저장방식을 지원하는 제네릭 클래스들은 모두 Collection <E>를 구현하고 있다.

물론 이 인터페이스를 직접 구현하는 것은 아니다.

위의 그림에서 보이듯이 Collection <E>를 상속하는 하위 인터페이스의 구현을 통해서 간접적으로 구현할 뿐이다.

- 반면 Map <K, V>는 key-value를 기반으로 하는 데이터의 저장방식을 위해 정의된 인터페이스인데, 이에 대해서는 Collection <E> 인터페이스를 구현하는 클래스를 소개하고 난 다음에 설명한다.

- 위의 그림에서 소개하는 인터페이스들의 특성과 이를 구현하는 제네릭 클래스들의 활용 예를 접하면서 컬렉션 프레임워크를 보다 깊이 있게 이해하게 될 것이다.

 

 

ㅁ Collection <E> 인터페이스를 구현하는 제네릭 클래스들

- Collection 인터페이스를 구현하는 제네릭 클래스들은 모두 인스턴스(정확히는 인스턴스의 참조값)를 저장의 대상으로 삼는다. 다만 저장하는 방식에 있어서 중복 저장을 허용하느냐 마느냐, 또는 저장시 정렬을 하느냐 마느냐 등등의 차이가 있을 뿐이다.

 

ㅁ List <E> 인터페이스와 이를 구현하는 제네릭 클래스 ArrayList <E>, LinkedList <E>

- List <E> 인터페이스를 구현하는 제네릭 클래스들은 다음 두 가지 특성을 공통으로 지닌다.

달리 말해서 다음 두 가지 특성을 지니는 제네릭 클래스가 필요하다면, API 문서를 통해서 List <E> 인터페이스를 구현하는 클래스들을 참조하면 된다.

(1) 동일한 인스턴스의 중복 저장을 허용한다.

(2) 인스턴스의 저장 순서가 유지된다.

- List 인터페이스를 구현하는 대표적인 제네릭 클래스는 ArrayList <E>와 LinkedList <E>이다.

이 둘의 사용방법은 거의 동일하다. 

다만 데이터를 저장하는 방식에서 큰 차이를 보이는데, 이와 관련해서는 잠시 후에 이야기하고 일단 ArrayList <E>의 사용방법을 알아본다.

 

ㅁ IntroArrayList.java

-

import java.util.ArrayList;                  //        제네릭 클래스라 하더라도 import문 작성시에는 클래스명만 명시함.

 

class IntroArrayList {

    public static void main(String[] args) {

 

        ArrayList <Integer> list = new ArrayList <Integer> ();              //     list는  Integer 인스턴스의 저장소로 사용된다.

 

        /* 데이터의 저장 */

       list.add(new Integer(11));

       list.add(new Integer(22));

       list.add(new Integer(33));                 //         순차적으로 저장됨.

 

       /* 데이터의 참조 */

       System.out.println("1차 참조");

       for(int i=0; i<list.size(); i++)

             System.out.println( list.get(i) );         //       0이 첫번째

 

        /* 데이터의 삭제 */

        list.remove(0);     //     0이 전달되었으므로 첫번째 데이터 삭제

 

        System.out.println("2차 참조");

        for(int i=0; i<list.size(); i++)

              System.out.println( list.get(i) );  

 

    }

}

[실행 결과]

1차 참조

11

22

33

2차 참조

22

33

- ArrayList는 동적 배열로, 크기를 자동으로 조절하므로 객체를 생성한 후에도 데이터를 추가하고 삭제할 수 있습니다. size() 메서드는 현재 리스트의 요소 개수를 반환합니다.

- add 메소드로 순차 저장

size 메소드로 저장된 인스턴스의 수를 확인

get 메소드로 해당 인스턴스의 참조값을 얻음

remove 메소드로 삭제. 전달되는 값에 해당하는 인스턴스 정보가 list에서 지워짐. 

(주의점은 저장된 인스턴스의 참조값이 지워지는거지, 인스턴스가 소멸하는 것은 아님.)

 

- ArrayList<E> 클래스는 배열과 상당히 유사하다. 

그러나 데이터의 저장을 위해 인덱스 정보를 별도로 관리할 필요가 없고, 데이터의 삭제를 위한 추가적인 코드의 작성이 전혀 필요 없다. 

뿐만 아니라 저장되는 인스턴스의 수에 따라서 그 크기도 자동으로 늘어나기 때문에 배열과 달리 길이를 고민하지 않아도 된다. 

 

ㅁ ArrayList<E> 클래스의 용량(버퍼) 설정

- ArrayList 클래스는 저장되는 데이터의 수가 증가함에 따라서 용량(데이터의 저장 가능 용량)이 자동으로 증가하는 클래스이다. 그런데 용량을 증가시키는 과정에서 수반되는 연산으로 인해 때로는 성능에 부담이 되기도 한다.

때문에 대략 500여개의 데이터가 저장될 것이 예상되는 상황에서는 점진적으로 용량을 500으로 늘리기 보다 처음부터 용량을 500으로 잡음으로 인해서 불필요한 연산을 줄일 필요도 있다.

 

(1) Integer 인스턴스를 저장할 수 있는 ArrayList<E>를 생성하고 저장용량을 500으로 늘린다.

- list.ensureCapacity(500);

 

(2)  ArrayList<E>에 저장되어 있는 인스턴스 수의 두 배로 저장용량을 늘린다.

- list.ensureCapacity(list.size()*2);

 

- java.util.ArrayList 클래스에는 public void ensureCapaciry(int minCapacity)라는 메소드가 있다.

저장용량을 '최소' 인자값만큼으로 증가시키는 코드이다.

 

 

ㅁ LinkedList <E> 클래스

- LinkedList 클래스는 ArrayList<E>의 사용방법과 거의 동일하다. 

 

- IntroLinkedList.java

 

import java.util.LinkedList;

 

class IntroLinkedList {

    public static void main(String[] args) {

        LinkedList<Integer> list = new LinkedList<Integer> ();

 

        /* 데이터의 저장 */

        list.add(new Integer(11));

        list.add(new Integer(22));

        list.add(new Integer(33));

 

       /* 데이터의 참조 */

       System.out.println("1차 참조");

       for(int i=0; i<list.size(); i++)

             System.out.println( list.get(i) );      

 

        /* 데이터의 삭제 */

        list.remove(0);     

 

        System.out.println("2차 참조");

        for(int i=0; i<list.size(); i++)

              System.out.println( list.get(i) );  

 

    }

}

[실행 결과]

1차 참조

11

22

33

2차 참조

22

33

- 이렇듯 ArrayList<E> 클래스와 LinkedList<E> 클래스 사이에서의 선택 기준은 사용방법의 차이가 아닌 내부적으로 인스턴스를 저장하는 방식의 차이에 있다.

 

ㅁ ArrayList<E>와 LinkedList<e>의 차이점

- 데이터의 저장, 참조, 삭제 기능의 활용방법만 놓고 보면 차이는 없다.

그러나 내부적으로 인스턴스를 저장하는 방식에 큰 차이가 있다.

 

- 우선 ArrayList<E>는 그 이름이 의미하듯이 배열을 기반으로 한다. 

즉 내부적으로 배열을 이용해서 인스턴스의 참조값을 저장하기 때문에 다음의 특징이 있다.

 

저장소의 용량을 늘리는 과정에서 많은 시간이 소요된다.(단)

데이터의 삭제에 필요한 연산과정이 매우 길다.(단)

데이터의 참조가 용이해서 빠른 참조가 가능하다.(장)

 

- 배열은 한번 생성되면 그 길이를 변경시킬 수 없는 인스턴스이다. 그렇기 때문에 용량을 늘린다는 것은 새로운 배열 인스턴스의 생성과 기존 데이터의 복사가 필요하다.

- 데이터의 삭제 작업 역시 그리 간단하지 않다.

예를 들어 첫 번째 위치에 저장된 데이터를 지우려는 경우, 그 뒤에 저장된 데이터들을 한 칸씩 앞으로 이동시켜야 한다.

- 하지만 데이터의 참조는 용이하다.

17번째 데이터를 참조하고 싶다면 인덱스 16을 통해 바로 접근이 가능하다.

 

 

- LinkedList<E>는 배열을 사용\하는 대신에 서로서로 연결하는 방식으로 데이터를 저장한다. 

 

- SoSimpleLinkedListImpl.java

 

class Box<T> {

    public Box<T> nextBox;                         //            다른 Box<T>의 인스턴스 참조를 위한 변수

    T item;

 

    public void store(T item) { this.item = item; }

    public T pullOut() { return item; }

}

 

class SoSimpleLinkedListImpl {

    public static void main(String[] args) {

 

        Box<String> boxHead = new Box<String> ();

        boxHead.store("First String");

 

        boxHead.nextBox = new Box<String> ();

        boxHead.nextBox.store("Second String");

 

        boxHead.nextBox.nextBox = new Box<String> ();

        boxHead.nextBox.nextBox.store("Third String");

 

        Box<String> tempRef;

 

        /* 두 번째 박스에 담긴 문자열 출력 과정 */

        tempRef = boxHead.nextBox;

        System.out.println(tempRef.pullOut() );

 

        /* 세 번째 박스에 담긴 문자열 출력 과정 */

        tempRef = boxHead.nextBox;

        tempRef = tempRef.nextBox;

        System.out.println(tempRef.pullOut() );

    }

}

[실행 결과]

Second String

Third String

 

- Box<T> 클래스는 인스턴스를 담을 일종의 상자이다. 

- 변수 nextBox는 상자들을 연결하기 위한 참조변수이다.

- item은 인스턴스의 저장을 위한 참조변수이다. 즉 하나의 상자에는 하나의 인스턴스만 저장이 가능하다.

 

- 두번째, 세번째 박스에 담긴 문자열 출력 과정. 배열에 비해 데이터의 참조 과정이 복잡하다.

- tempRef = boxHead.nextBox.nextBox; 이렇게 두 줄을 한 줄로 표현도 가능.

 

- 상자에 데이터가 저장되어 있고(item), 여러 상자들을 연결하여 데이터를 저장한다.(nextBox)

 

- 지금 설명하는 이것이 '리스트 자료구조'라는 것이다. 

리스트 자료구조의 유형에도 여러 가지가 존재하고 LinkedList<E> 클래스의 실제 구현도 위와는 차이가 있지만 위 예제를 통해 LinkedList<E>의 다음 특성들을 파악할 수 있다.

 

저장소의 용량을 늘리는 과정이 간단하다. 장점

데이터의 삭제가 매우 간단하다. 장점

데이터의 참조가 다소 불편하다. 단점

 

위 예제에서 보이지는 않았지만 중간에 위치한 상자 하나를 삭제하는 것은 어려운 일이 아니다.

그러나 데이터의 참조는 불편하다. 데이터의 참조를 위해서는 연결되어 있는 상자들을 통해서 이동을 해야하기 때문이다.

 

이 둘은 서로 상대적인 장점과 단점을 지니고 있어서, 선택에 있어서 논란의 여지를 전혀 남기지 않는다.

 

 

ㅁ Iterator를 이용한 인스턴스의 순차적 접근 

- Collection <E> 인터페이스에는 iterator라는 이름의 메소드가 다음의 형태로 정의되어 있다.

 

Iterator<E> iterator()

 

이 메소드는 반환형이 Iterator<E>인데, 이는 인터페이스의 이름이다. 

즉 Iterator 메소드가 하는 일은 다음과 같이 정리할 수 있다.

 

"iterator 메소드가 호출되면 인스턴스가 하나 생성되는데, 이 인스턴스는 Iterator<E> 인터페이스를 구현하는 클래스의 인스턴스이다. 그리고 iterator 메소드는 이 인스턴스의 참조값을 반환한다."

 

- 인터페이스를 구현하는 클래스의 이름 등에 관심을 둘 필욕 ㅏ없다.

다만 인터페이스에 정의된 메소드가 제공하는 기능을 파악하고 활용하면 된다.

Iterator 인터페이스에 정의되어 있는 메소드는 아래의 세 메소드가 전부이다.

 

boolean hasNext()           참조할 다음 번 요소(element)가 존재하면 true를 반환

E next()          다음 번 요소를 반환

void remove()         현재 위치의 요소를 삭제

 

- IteratorUsage.java

import java.util.Iterator;

import java.util.LinkedList;

 

class IteratorUsage {

    public static void main(String[] args) {

   

        LinkedList<String> list = new LinkedList<String>();

        list.add("First");

        list.add("Second");

        list.add("Third");

        list.add("Fourth");

 

        Iterator<String> itr = list.iterator();

 

        System.out.println("반복자를 이용한 1차 출력과 \"Third\" 삭제");

        while(itr.hasNext()){

              String curStr = itr.next();

              System.out.println(curStr);

              if(curStr.compareTo("Third") == 0 )

                       itr.remove();

        }

 

        System.out.println("\n \"Third\" 삭제 후 반복자를 이용한 2차 출력 " );

        itr = list.iterator();

        while(itr.hasNext() )

              System.out.println(itr.next() );

   }

}

[실행 결과]

반복자를 이용한 1차 출력과 "Third" 삭제

First

Second

Third

Fourth

"Third" 삭제 후 반복자를 이용한 2차 출력

First

Second

Fourth

 

-  java.util.Iterator;

인터페이스 Iterator는 java.util 패키지에 속해있다.

- iterator 메소드가 호출될 때 생성되는 인스턴스를 가리켜 '반복자(iterator)'라 한다.

컬렉션 인스턴스에 저장되어 있는 데이터들을 순차적으로 참조하는데 사용되기 때문이다.

- 반복자는 생성과 동시에 컬렉션 인스턴스의 첫번째 저장공간을 관찰하게 된다.

그리고 그 곳에 유효한 데이터가 존재하면 메소드 hasNext는 true를 반환한다.

- next 메소드는 반복자가 현재 관찰하고 있

는 저장공간에 저장된 데이터를 반환한다.

뿐만 아니라 관찰 위치를 다음 번 저장공간으로 이동시킨다.

- remove 메소드는 next 메소드 호출로 인해 반환된 데이터를 컬렉션 인스턴스 내에서 삭제한다. 

 

- remove()를 호출하기 전에 next()를 호출하지 않거나, 한 번 이상 호출한 경우는 예외가 발생할 수 있습니다.

remove()를 호출하고 싶다면 next()를 호출한 다음에 remove()를 호출해야 한다.

 

- remove 메소드는 next()로  읽어온 요소를 삭제한다.

현재값(= 마지막으로 가져온값)을 삭제함. 

 

 

- Java에서 Collection<E> 인터페이스는 iterator() 메서드를 선언하고 있습니다.

따라서 Collection<E> 인터페이스를 구현하는 모든 컬렉션 클래스는 iterator() 메서드를 구현해야 합니다.

List<E> 인터페이스는 Collection<E> 인터페이스를 확장(상속)하므로,

List<E> 인터페이스를 구현하는 모든 클래스(예: ArrayList<E>, LinkedList<E>)는 iterator() 메서드를 반드시 구현해야 합니다. ( 가지고있다. )

- List<E> 인터페이스는 Collection<E> 인터페이스의 모든 메서드를 포함하게 됩니다.

 

- itr = list.iterator(); 를 두 번 하는 이유는 다음과 같습니다.

첫 번째 반복문에서 반복자 itr을 사용하여 리스트를 순회하고, 동시에 "Third"을 발견하면 해당 요소를 삭제합니다. 이후 itr이 리스트의 끝에 도달하고 반복이 끝나면, itr의 상태는 리스트의 끝을 가리키게 됩니다. 이 상태에서 다시 반복자를 사용하여 리스트를 순회하려면, 반복자를 다시 초기화해야 합니다. 그래서 itr = list.iterator(); 를 다시 호출하여 반복자를 리스트의 처음으로 되돌립니다.

 

 

- 위 예제에서의 while문은 컬렉션 인스턴스 안에 저장된 데이터를 순차적으로 참조할 때 공식처럼 사용되는 문장이다.

그런데 for-each문(향상된 for문)으로도 구성 가능하다.

 

    for(String str : list)

         System.out.println(str);

 

이렇게 하면 반복자를 생성하지 않아도 되기 때문에 코드를 보다 간결하면서도 명확하게 구성할 수 있다.

그러나 위 예제의 첫번재 while문은 데이터의 삭제가 있어서 이렇게 대체가 불가능 하므로 두 가지 방식 모두에 익숙해져야 한다. 

 

- 첫번째 while문을 향상된 for문으로 대체하면 1차 출력에서 Fourth가 안나옴.

그 이유는,

  • "Third"을 제거한 후, itr.next() 호출은 반복자를 다음 요소인 "Fourth"로 이동시킵니다.
  • 그러나 향상된 for 루프 (for(String str : list))는 "Third"을 제거한 후에도 계속해서 list에서 다음 요소를 가져와야 합니다.
  • 이로 인해 향상된 for 루프는 "Fourth"를 건너뛰게 됨

- "Five"를 추가하고 똑같이 돌리면 ConcurrentModificationException 발생.

 

  • Iterator를 사용하여 리스트를 순회하고 있는 도중에, 리스트의 구조가 변경되었습니다. 즉, 반복자를 통해 요소를 제거한 후에도 향상된 for 루프를 사용하여 list를 순회하려고 했습니다.
  • Java에서는 리스트를 순회하는 도중에 리스트의 구조가 변경되면 ConcurrentModificationException을 발생시킵니다.

 

 

 

ㅁ 반복자를 꼭 이용해야할 필요?

- 예제 IntroLinkedList.java와 IteratorUsage.java의 데이터 참조방식만 놓고 보면 반복자의 내용에 대해 의문이 든다. 

그러나 반복자의 의미는 다른데 있다. 

 

"컬렉션 클래스의 종류에 상관없이 동일한 형태의 데이터 참조방식을 유지한다."

 

- LinkedList<E> 클래스의 데이터 참조를 위해 정의된 메소드는 get이다.

get 메소드를 호출하면 참조하고자 하는 데이터의 위치 정보를 인자로 전달한다.

그러나 이는 어디까지나 LinkedList<E>에 어울리는 데이터 참조방식이다.

(정확히는 List<E> 인터페이스를 구현하는 컬렉션 클래스들에 어울리는 데이터 참조방식이다)

데이터의 저장순서가 유지되기 때문이다.

 

그러나 컬렉션 클래스들 중에는 데이터의 저장순서가 유지되지 않는 것도 있다.

그리고 그러한 클래스들에는 당연히 get 메소드가 정의되어 있지 않다. 그래서 반복자를 이용할 필요가 있는 것이다.

반복자는 저장된 데이터 전부를 참조할 때 매우 유용하다.

그리고 데이터 전부의 참조는 데이터의 저장방식과 상관없이 매우 빈번하게 요규되는 기능이다.

때문에 대부분의 컬렉션 클래스들은 반복자를 반환하는 iterator 메소드를 구현하고 있다.

 

- 그럼 예제 IteratorUsage.java가 완료된 상태에서 LinkedList<E> 보다는 HashSet<E>가 더 잘어울린다고 생각되면? 반복자를 기반으로 코드가 작성되면 컬렉션 클래스의 교체만 필요할 뿐, 추가적인 변경이 필요하지 않다.

 

- UsefullIterator.java

import java.util.Iterator;

import java.util.HashSet;

 

class UsefullIterator {

    public static void main(String[] args) {

   

        HashSet<String> set = new HashSet<String>();

        set.add("First");

        set.add("Second");

        set.add("Third");

        set.add("Fourth");

 

        Iterator<String> itr = list.iterator();

 

        System.out.println("반복자를 이용한 1차 출력과 \"Third\" 삭제");

        while(itr.hasNext()){

              String curStr = itr.next();

              System.out.println(curStr);

              if(curStr.compareTo("Third") == 0 )

                       itr.remove();

        }

 

        System.out.println("\n \"Third\" 삭제 후 반복자를 이용한 2차 출력 " );

        itr = list.iterator();

        while(itr.hasNext() )

              System.out.println(itr.next() );

   }

}

[실행 결과]

반복자를 이용한 1차 출력과 "Third" 삭제

First

Second

Third

Fourth

"Third" 삭제 후 반복자를 이용한 2차 출력

Fourth

Second

First

 

- 참고로 HashSet<E> 클래스에는 get 메소드가 정의되어 있지 않다. 

따라서 반복자를 기반으로 코드를 작성하지 않았다면 코드의 상당부분이 변경되어야 한다.

- 실행결과를 보면 출력 순서가 달라졌다. HashSet<E> 클래스에 대해서는 나중에 소개.

 

 

ㅁ 컬렉션 클래스를 이용해서 int형 정수 열 개를 저장하려면?

- 기본자료형을 기반으로 제네릭 인스턴스를 생성할 수 없기 때문에 쉽지 않다.

다음과 같이 컬렉션 인스턴스를 생성할 수 없다.

 

ArrayList<int> arr = new ArrayList<int>();

LinkedList<int> link = new LinkedList<int>();

 

그렇다면 제네릭 클래스를 이용해서 기본 자료형 데이터의 저장은 어떻게?

Auto Boxing과 Auto Unboxing을 사용.

 

       LinkedList <Integer> list = new LinkedList <Integer> ();

 

        list.add(11);         //         Auto Boxing

        list.add(22);

        list.add(33);

 

        while(itr.hasNext() )

              int num = itr.next();                    //          Auto Unboxing

              System.out.println( num );

 

- int형 데이터를 저장하는 컬렉션 인스턴스의 생성은 불가능하지만, int형의 wrapper 클래스인 Integer의 인스턴스를 저장하는 컬렉션 인스턴스의 생성은 가능하다.

- Integer형 참조변수가 와야 할 위치에 int형 데이터가 오면 atuo boxing이 진행된다.

 

 

ㅁ Set<E> 인터페이스를 구현하는 컬렉션 클래스들

- Set<E> 인터페이스를 구현하는 컬렉션 클래스들의 공통된 특성을 먼저 이해하고, 이를 기반으로 HashSet<E> 클래스와 TreeSet<E> 클래스의 사용방법을 소개한다.

 

ㅁ Set<E> 인터페이스의 특성과 HashSet<E> 클래스

- List<E> 인터페이스를 구현하는 클래스들과 달리 Set<E>를 구현하는 클래스들은 데이터의 저장순서를 유지하지 않는다.

- List<E> 인터페이스를 구현하는 클래스들과 달리 Set<E>를 구현하는 클래스들은 데이터의 중복저장을 허용하지 않는다.

 

- Set<E>의 특성은 수학에서 말하는 '집합'의 특성이다.

 

- SetInterfaceFeature.java

 

import java.util.Iterator;

import java.util.HashSet;

 

class SetInterfaceFeature {

    public static void main(String[] args) {

   

        HashSet<String> hSet = new HashSet<String>();

        hSet.add("First");

        hSet.add("Second");

        hSet.add("Third");

        hSet.add("First");

 

        System.out.println("저장된 데이터 수 : " + hSet.size() );

 

        Iterator<String> itr = hSet.iterator();

        while(itr.hasNext() )

             System.out.println(itr.next() );

    }

}

[실행 결과]

저장된 데이터 수 : 3

Second

Third

First

 

- First가 반복해서 저장되지 않았음. 그리고 문자열이 저장된 순서에 상관없이 출력되고 있다.

 

 

ㅁ Set<E> 인터페이스를 구현하는 컬렉션 클래스 중에서 저장순서를 유지하는 예외로 LinkedHashSet<E>라는 클래스도 존재한다. 그러나 이는 예외적이다. 집합에는 순서가 존재하지 않고, 요소가 중복되지 않는다.

 

 

ㅁ 동일한 데이터인지 아닌지를 구분하는 기준은?

 

- HashSetEqualityOne.java

 

import java.util.Iterator;

import java.util.HashSet;

 

class SimpleNumber {

    int num;

 

    public SimpleNumber(int n) { num = n; }

 

    public String toString() { return String.valueOf(num); }

}

 

class HashSetEqualityOne {

    public static void main(String[] args) {

 

        HashSet<SimpleNumber> hSet = new HashSet<SimpleNumber> ();

        hSet.add(new SimpleNumber(10) );

        hSet.add(new SimpleNumber(20) );    //  (1)

        hSet.add(new SimpleNumber(20) );    //  (2)

   

        System.out.println("저장된 데이터 수 : " + hSet.size() );

 

        Iterator<SimpleNumber> itr = hSet.iterator();

        while(itr.hasNext() )

              System.out.println(itr.next() );

    }

}

[실행 결과]

저장된 데이터 수 : 3

20

10

20

- 실행 결과를 보면 (1)과 (2)에서 생성한 인스턴스를 다른 인스턴스로 간주하고 있다.

이는 HashSet <E> 클래스가 equals 메소드의 호출결과hashCode 메소드의 호출결과를 가지고 동등 비교를 하기 때문이다.

 

 

ㅁ 해시 알고리즘과 hashCode 메소드

- HashSet<E> 클래스를 제대로 활용하기 위해서는 간단하게나마 해시 알고리즘을 이해해야 한다.

 

       num % 3

 

이것도 해시 알고리즘이라고 볼 수 있다. (알고리즘이라고 복잡한 것만은 아니다)

이 알고리즘에 대입한 연산결과를 가지고 다음 수를 분류하기.

 

3, 5, 7, 12, 25, 31

 

위의 정수들이 하나의 집합을 구성한다고 가정할 때,  이 집합을  나머지 연산의 결과 0, 1, 2에 따라서 세 개의 분류로 나누어 묶을 수 있다. 

 

- 이렇게 부류가 나뉜 상태에서, 위의 집합에 정수 5가 존재하는지 확인하는 가장 효율적인 방법은

모든 정수들이 3으로 나눈 나머지를 기준으로 나뉘어 있으니

우선 찾고자 하는 정수 5를 3으로 나머지 연산하여 속하는 부류를 먼저 찾는 것이 가장 효율적인 방법이다.

 

5%3 = 2

 

이로써 나머지 연산의 결과가 0, 그리고 1인 부류는 검색의 대상에서 제외되었다.

즉 검색의 대상이 확 줄었다. 이것이 해시 알고리즘의 장점이다.

 

- 위에서 보인 해시 연산의 결과 값(%연산의 결과) 0, 1, 2를 가리켜 '해시 값'이라 하며, 이 해시 값을 기준으로 데이터를 구분한다.

- 참고로 해시 알고리즘은 데이터의 종류 및 성격에 따라서 다양하게 설계될 수 있다.

 

- 검색 1단계: 정수 5의 해시값을 계산한다.

- 검색 2단계: 해시값에 속하는 부류 내에서 정수 5가 존재하는지 하나씩 확인한다.

 

이렇게 두 단계를 거치는 이유는 검색의 효율성과 이로 인한 속도의 향상을 위한 것이다. 

HashSet<E> 클래스의 장점은 "매우 빠른 검색속도"이다.

 

그런데 HashSet<E>는 검색과 직접 관련된 메소드는 제공하지 않는다. 

HashSet<E>의 데이터 저장은 항상 검색의 과정을 동반한다. 데이터의 중복을 허용하지 않기 때문이다. 

즉 데이터를 저장하기 앞서 동일한 데이터가 이미 저장되어 있는지 우선 검색을 해야 한다. 

때문에 "매우 빠른 검색속도"는 "매우 빠른 데이터의 저장"으로 이어진다. 

 

- 그렇다면 위에서 정리한 검색 1단계와 검색 2단계를 HashSet<E> 클래스는 어떠한 방식으로 진행할까?

검색 1단계: Object 클래스의 hashCode 메소드의 반환 값을 해시 값으로 활용

검색 2단계: Object 클래스의 equals 메소드의 반환 값을 이용해서 내용비교

 

정리하면 hashCode 메소드의 반환값을 이용해서 검색의 부류를 결정하고(검색의 범위를 줄이고), 해당 부류 내에 존재하는 데이터들과의 내용비교(인스턴스 변수의 값이 완벽히 일치하는지 확인하는 비교)는 equals 메소드를 통해서 진행한다.

- 따라서 (1)과 (2)의 두 인스턴스가 동일한 인스턴스로 인식이 되도록 하려면, 다음 두 메소드를 적절히 오버라이딩 해야 한다.

 

public int hashCode()

public boolean equals(Object obj)

 

Object 클래스의 hashCode 메소드는 인스턴스가 다르면 구성 내용에 상관없이 전혀 다른 해시값을 반환하도록 정의되어 있다. 따라서 예제 HashSetEqualityOne.java의 실행결과를 보인 것이다.

equals 메소드도 내용 비교가 아닌 참조값만 비교하도록 정의되어 있다.(알다시피)

 

ㅁ 두 메소드를 적절히 오버라이딩 하여 두 개의 인스턴스를 동일한 인스턴스로 인식시키기

- HashSetEqualityTwo.java

 

import java.util.Iterator;

import java.util.HashSet;

 

class SimpleNumber {

    int num;

 

    public SimpleNumber(int n) { num = n; }

 

    public String toString() { return String.valueOf(num); }

 

    public int hashCode() { return num%3; }

 

    public boolean equals(Object obj) {

        SimpleNumber comp = (SimpleNumber) obj;

        if(comp.num == num)

            return true;

        else

            return false;

    }

}

 

class HashSetEqualityOne {

    public static void main(String[] args) {

 

        HashSet<SimpleNumber> hSet = new HashSet<SimpleNumber> ();

        hSet.add(new SimpleNumber(10) );

        hSet.add(new SimpleNumber(20) );    //  (1)

        hSet.add(new SimpleNumber(20) );    //  (2)

   

        System.out.println("저장된 데이터 수 : " + hSet.size() );

 

        Iterator<SimpleNumber> itr = hSet.iterator();

        while(itr.hasNext() )

              System.out.println(itr.next() );

    }

}

[실행 결과]

저장된 데이터 수 : 2

10

20

 

- hashCode()

이렇게 간단히 오버라이딩해도 된다. 중요한 것은 내용이 동일한 두 인스턴스가 동일한 해시값을 반환하도록 정의하는 것이기 때문이다. 

- equals(Object obj)

내용비교를 하도록 정의하였다. 해시값이 동일한 두 인스턴스는 이 메소드를 통해서 내용이 완벽히 일치하는지 확인하게 된다. 

- 참고로 String 클래스의 hashCode 메소드와 equals 메소드는 내용비교를 진행하도록 적절히 오버라이딩되어 있다. 따라서 예제 SetInterfaceFeature.java에서는 내용비교의 결과가 출력된 것이다.

 

 

ㅁ 문제

- hashCode 메소드와 equals 메소드의 오버라이딩

- 다음 클래스의 두 인스턴스를 HashSet<E>에 저장할 때, 두 인스턴스의 데이터(name & age)가 완전히 동일하다면, 하나만 저장되도록 hashCode 메소드와 equals 메소드를 오버라이딩 하자. 

 

class Person {

    String name;

    int age;

 

    public Person(String name, int age) {

        this.name = name;

        this.age = age;

    }

    

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

 

    // public int hashCode() { return age; }

    public int hashCode() { return name.hashCode()+age%7; }

 

    /* public boolean equals(Object obj) {

        Person p = (Person) obj;

        if(p.name.equals(name) )     //   String 내용 비교니까. String끼리 ==하면 주소값 비교(값을 비교하는거라). 기본값끼리 ==하면 내용비교.

              return true;

        else

              return false;

    } */

    public boolean equals(Object obj) {

        Person p = (Person) obj;

        if( p.name.equals(name) && p.age == age )

              return true;

        else

              return flase;

    }

    

}

 

class Main {

    public static void main(String[] args) {

    

        HashSet<Person> hSet = new HashSet<Person>();

        hSet.add(new Person("이진호", 10) );

        hSet.add(new Person("이진호", 20) );

        hSet.add(new Person("김명호", 20) );

        hSet.add(new Person("김명호", 15) );

        hSet.add(new Person("이진호", 20) );

        hSet.add(new Person("김명호", 20) );

 

        System.out.println("저장된 데이터 수 : " + hSet.size() );

 

        Iterator<Person> itr = hSet.iterator();

        while(itr.hasNext() )

             System.out.println(itr.next() );

    }

}

 

[실행 결과]는 다음과 같아야 한다.(순서는 구현하는 방법에 따라 다 다를것)

저장된 데이터 수 : 4

김명호(20세)

김명호(15세)

이진호(10세)

이진호(20세)

 

- 내가한 방식(빨간색)으로 하는 것은 좋은 답안은 아니다. 

앞서 설명한 HashSet<E> 클래스가 진행하느 ㄴ검색의 1, 2단계를 이해한다면 위오 ㅏ같으 ㄴ형태의 구현도 가능하다느 ㄴ것을 이해할 수 있을 것이다.

- 단 이렇게 정의하면 eqauls 메소드만을 이용한 두 인스턴스의 내용비교는 불가능하다. 그래서 그리 좋은 답안이 아니다.

또 age를 그냥 해시 값으로 사용하면 데이터의 부류가 많이 나뉘기에 그만큼 성능은 떨어진다.

 

 

ㅁ TreeSet <E> 클래스의 이해와 활용

- Set<E> 인터페이스를 구현하는 TreeSet<E> 클래스 소개.

- 인스턴스의 대소 비교라는 중요한 주제에 대해 생각하기.

 

- TreeSet<E> 클래스는 트리(Tree)라는 자료구조를 기반으로 구현되어 있는데, '트리'는 데이터를 정렬된 상태로 저장하는 자료구조이다. 따라서 이를 기반으로 구현된 TreeSet<E> 클래스 역시 데이터를 정렬된 상태로 유지한다.

(이는 저장순서를 유지하는 것과 의미가 완전히 다르다).

 

 

- SortTreeSet.java

 

import java.util.Iterator;

import java.util.TreeSet;

 

class SortTreeSet {

    public static void main(String[] args) {

   

        TreeSet<Integer> sTree = new TreeSet<Integer> ();

        sTree.add(1);

        sTree.add(2);

        sTree.add(4);

        sTree.add(3);

        sTree.add(2);

 

        System.out.println("저장된 데이터 수 : " + sTree.size() );

 

        Iterator<Integer> itr = sTree.iterator();

        while(itr.hasNext() )

              System.out.println(itr.next() );

    }

}

[실행 결과]

저장된 데이터 수 : 4

1

2

3

4

- 컬렉션 프레임워크 인스턴스인(gpt) sTree에 총 다섯 개의 Integer 인스턴스를 저장하고 있다. (오토박싱)

여기서 중요한 것은 저장될 때마다 데이터가 정렬된다는 사실이다.

- 참고로 iterator 메소드가 반환하는 반복자는 정렬된 데이터를 오름차순으로 참조한다.

- 중복된 데이터의 저장을 허용하지 않는다는 사실은 쉽게 확인이 가능하다.

 

- 정렬의 기준은?

iterator 메소드가 반환하는 반복자는 오름차순의 참조를 보장한다.

숫자의 경우에는 정렬의 기준에 대해 논란의 여지가 없다. 숫자의 크기가 정렬의 유일한 기준이다.

 

 

ㅁ 그러나 아래에 정의된 클래스의 인스턴스가 저장대상이라면 이야기는 달라진다.

 

class Person {

    String name;

    int age;

    public Person(String name, int age) {

         this.name = name;

         this.age = age;

    }

}

 

위 클래스의 인스턴스가 다음과 같이 생성되었다.

 

Person man1 = new Person("James", 24);

Person man2 = new Person("Michael", 15);

Person man3 = new Person("John", 29);

 

이 상황에서는 정렬의 기준을 나름대로 정할 수밖에 없는 상황이다.

이렇듯 인스턴스의 정렬기준은 대부분의 상황에서 프로그래머가 직접 정의해야 한다.

자바에서는 아래의 인터페이스 구현을 통해 정렬의 기준을 프로그래머가 직접 정의할 것을 요구한다.

 

interface Comparable<T> { 

    int compareTo(T obj);

}

 

위의 Comparable<T> 인터페이스의 compareTo 메소드는 다음의 내용을 근거로 정의되어야 한다.

 

ㅇ 인자로 전달된 obj가 작다면 양의 정수를 반환해라.

ㅇ 인자로 전달된 obj가 크다면 음의 정수를 반환해라.

ㅇ 인자로 전달된 obj와 같다면 0을 반환해라.

 

여기서 말하는 크고 작음에 대한 기준이 세워지면 이는 정렬의 기준이 세워지는 것이다.

예를 들어서 Person 클래스에 대한 크고 작음의 기준을 다음과 같이 세웠다고 가정하자.

 

인터페이스 변수 age의 값이 큰 인스턴스가 작은 인스턴스보다 크다.

 

그렇다면 정렬의 기준은 '나이의 많고 적음'이 된다. 

 

- 따라서 Comparable<T> 인터페이스의 compareTo 메소드를 구현할 때는 다음과 같이 정의하면 된다.

ㅇ 인자로 전달된 obj의 나이가 작다면 양의 정수를 반환해라

ㅇ 인자로 전달된 obj의 나이가 크다면 음의 정수를 반환해라

ㅇ 인자로 전달된 obj와 나이가 같다면 0을 반환해라

 

이러한 형태로 위의 Person 클래스가 Comparable<T> 인터페이스를 구현한다면, Person 인스턴스는 비로소 TreeSet<E>에 저장할 수 있게 된다. TreeSet<E> 역시 compareTo 메소드의 호출결과를 참조하여 정렬하기 때문에,

TreeSet<E>에 저장되는 인스턴스는 반드시 Comparable<T> 인터페이스를 구현하고 있어야 한다.

 

이렇게 정의해 놓으면 TreeSet<E> 내부적으로 오름차순 정렬을 하건 내림차순 정렬을 하건 신경쓰지 않아도 된다. 

iterator 메소드가 반환하는 반복자는 오름차순 참조를 보장하기 때문이다.

 

앞서 예제 SortTreeSet.java에서 Integer 인스턴스를 TreeSet<E>에 저장했는데, 이는 Integer 클래스가 Comparable<T> 인터페이스를 구현하고 있기 때문에 가능한 일이었다.

 

 

- compareTo 메소드를 재정의할 때, 보통은 다음과 같은 규칙에 따라 구현합니다:

  • 양수 반환: 현재 객체(this)가 매개변수로 전달된 객체보다 크다는 것을 나타냅니다. 따라서, (현재 객체가) 정렬 순서상 (매개변수로 온 객체보다) 뒤에 위치해야 한다는 의미입니다.
  • 음수 반환: 현재 객체(this)가 매개변수로 전달된 객체보다 작다는 것을 나타냅니다. 따라서, 정렬 순서상 앞에 위치해야 한다는 의미입니다.
  • 0 반환: 두 객체가 같다는 것을 나타냅니다. 이 경우, 순서가 변경되지 않습니다.

 

 

ㅁ 인스턴스의 비교 기준을 정의하는 Comparable<T> 인스턴스의 구현 1

- 위에서 보인 Person 클래스의 정렬기준을 나이로 정하고 실제로 Comparable<T> 인터페이스를 구현해보자.

 

class Person implements Comparable<Person> {

    String name;

    int age;

 

    public Person(String name, int age) {

         this.name = name;

         this.age = age;

    }

 

    public int compareTo(Person p) {

         if( age > p.age ) { return 1; }

         else if( age < p.age ) {return -1; }

         else return 0;

    }

}

 

- TreeSet<E>는 Person 인스턴스가 저장될 때마다 기존에 저장된 인스턴스와의 비교를 위해서 compareTo 메소드를 빈번히 호출하여, 이 때 반환되는 값을 기반으로 정렬을 진행할 것이다.

 

 

ㅁ String 클래스의 compareTo 메소드

- 13장에서 String 클래스의 compareTo 메소드를 설명한 적 있는데, 이 역시 comparable<T> 인터페이스의 구현 과정에서 정의된 메소드이다. 즉 String 클래스의 compareTo 메소드는 사전편찬 순서를 정렬의 기준으로 정의되어 있다.

 

 

ㅁ 위에서 정의한 Person 클래스의 인스턴스가 TreeSet<E>에 저장될 때, 나이를 기준으로 정렬됨을 확인하기 위한 예제이다.

 

- ComparablePerson.java

 

import java.util.Iterator;

import java.util.TreeSet;

 

class Person implements Comparable<Person> {

    String name;

    int age;

 

    public Person(String name, int age) {

         this.name = name;

         this.age = age;

    }

 

    public void showData() {

        System.out.printf("%s %d \n", name, age); 

    } 

 

    public int compareTo(Person p) {

         if( age > p.age ) { return 1; }

         else if( age < p.age ) {return -1; }

         else return 0;

    }

}

 

class ComparablePerson {

    public static void main(String[] args) {

        

        TreeSet<Person> sTree = new TreeSet<Person> ();

         sTree.add(new Person("Lee", 24) );

         sTree.add(new Person("Hong", 29) );

         sTree.add(new Person("Choi", 21) );

 

        Iterator<Person> itr = sTree.iterator();

        while( itr.hasNext() )

               itr.next().showData();

    }

}

[실행 결과]

Choi 21

Lee 24

Hong 29

 

 

ㅁ 인스턴스의 비교 기준을 정의하는 Comparable 인스턴스의 구현 2

- String 클래스는 사전편찬 순으로 정렬되도록 Comparable<T> 인터페이스를 구현하고 있다.

따라서 문자열을 TreeSet<E>에 저장할 경우 사전편찬 순으로 정렬이 이뤄진다.

- 그런데 이번에는 문자열을 사전편찬 순서가 아닌, 길이 순으로 정렬해서 TreeSet<E>에 저장하려 한다.

- 그리하여 String 클래스의 Wrapper 클래스를 일단 다음과 같이 정의하였다.

 

class MyString {

    String str;

    public MyString(String str) { this.str = str; }

 

    public int getLength() { return str.length(); }

    public String toString() { return str; }

}

 

- 이 클래스의 인스턴스들이 TreeSet<E>에 저장될 때, 인스턴스 변수 str이 참조하는 문자열의 길이를 기준으로 정렬되기 원한다. 따라서 Comparable<T> 인터페이스를 다음과 같이 정의하였다.

 

- ComparableMyString.java

 

import java.util.TreeSet;

import java.util.Iterator;

 

class MyString implements Comparable<MyString> {

    String str;

    public MyString(String str) { this.str = str; }

 

    public int getLength() { return str.length(); }

    public String toString() { return str; }

 

    public int compareTo(MyString mStr) {

        if(getLength() > mStr.getLength() ) { return 1; }

        else if(getLength() < mStr.getLength() ) { return -1; }

        else { return 0; }

 

        // return getLength() - mStr.getLength();       이렇게 한 줄로 표현 가능.

    }

}

 

class ComparableMyString {

    public static void main(String[] args) {

       

          TreeSet<MyString> tSet = new TreeSet<MyString> ();

          tSet.add(new MyString("Orange") );

          tSet.add(new MyString("Apple") );

          tSet.add(new MyString("Dog") );

          tSet.add(new MyString("Individual") );

    

          Iterator<MyString> itr = tSet.iterator();

          while(itr.hasNext() )

                System.out.println(itr.next() );

    }

}

[실행 결과]

Dog

Apple

Orange

Individual

 

- MyString 인스턴스는 TreeSet<MyString>에 문자열의 길이를 기준으로 정렬이 이뤄진다.

- tSet.add로 저장을 할 때마다, 길이 순으로 정렬이 된다. 

- iterator 메소드가 반환하는 '반복자'는 오름차순으로 데이터를 출력한다.

 

 

 

ㅁ Comparator<T> 인터페이스를 기반으로 TreeSet<E>의 정렬 기준 제시하기

- 방금전의 예제를 하면서, 문자열을 사전편찬 순서가 아닌 길이 순으로 정렬해서 TreeSet<E>에 저장하고 싶어서,

이를 위해 MyString이라는 String의 Wrapper 클래스를 정의했는데, TreeSet<E>의 정렬 기준을 변경하기 위해서 MyString이라는 별도의 클래스를 정의했었는데,

그러지 않고 TreeSet<String> 인스턴스에게 사전편찬 순서 말고 길이순으로 문자열을 정렬하게금 하기 위해,

정의된 것이 Comparator<T> 인터페이스이다. 이 인터페이스는 다음과 같이 정의되어 있다.

 

interface Comparator <T> {

    int compare(T obj1, T obj2);

    boolean equals(Object obj);

}

 

위 인터페이스에서 equals 메소드는 신경 쓰지 않아도 된다. 

이 인터페이스를 구현하는 모든 클래스는 Object 클래스를 상속하기 때문에, (equals메소드를 내가 작성하지 않으면) Object 클래스의 equals 메소드가 (상속된 것이 내려와서) 위의 equals 메소드를 구현하는 꼴이 되기 때문이다. 

따라서 compare 메소드만 신경을 쓰면 된다. compare 메소드의 구현방법은 앞서 소개한 compareTo 메소드의 구현방법과 유사하다.

obj1이 크면 양수를, obj2가 크면 음수를, 같다면 0을 반환하면 된다.

물론 크고 작음에 대한 기준은 여러분이 결정할 몫이다.

 

 

- IntroComparator.java

 

import java.util.TreeSet;

import java.util.Iterator;

import java.util.Comparator;

 

class StrLenComparator implements Comparator < String > {

    

    public int compare(String str1, String str2) {

         if( str1.length() > str2.length() ) { return 1; }

         else if(str1.length() < str2.length() ) { return -1; }

         else return 0;

 

         // return str1.length() - str2.length();

    }

}

 

class IntroComparator {

    public static void main(String[] args) {

          TreeSet<String> tSet = new TreeSet<String> (new StrLenComparator() );

          tSet.add("Orange");

          tSet.add("Apple");

          tSet.add("Dog" );

          tSet.add("Individual" );

    

          Iterator<String> itr = tSet.iterator();

          while(itr.hasNext() )

                System.out.println(itr.next() );

    }

}

[실행 결과]

Dog

Apple

Orange

Individual

 

- TreeSet<E> 인스턴스 생성과정에서 Comparator<T> 인터페이스를 구현하는 인스턴스의 참조 값이 전달되었다. 

이렇게 되면, TreeSet<E>는 생성자를 통해 전달된 인스턴스의 compare 메소드를 호출하여 정렬을 진행한다. 

 

 

ㅁ 예제

- API 문서를 참조해서 TreeSet<E>의 정렬된 데이터를 내림차순으로 정렬해서 출력하라.

- 예제 IntroComparator.java의 실행결과가 역순으로 출력되도록 예제를 변경해보자.

단, StrLenComparator 클래스는 변경하면 안 된다. 

 

- IntroComparator2.java

 

import java.util.TreeSet;

import java.util.Iterator;

import java.util.Comparator;

 

class StrLenComparator implements Comparator < String > {

    

    public int compare(String str1, String str2) {

         if( str1.length() > str2.length() ) { return 1; }

         else if(str1.length() < str2.length() ) { return -1; }

         else return 0;

 

         // return str1.length() - str2.length();

    }

}

 

class IntroComparator {

    public static void main(String[] args) {

          TreeSet<String> tSet = new TreeSet<String> (new StrLenComparator() );

          tSet.add("Orange");

          tSet.add("Apple");

          tSet.add("Dog" );

          tSet.add("Individual" );

    

          Iterator<String> itr = tSet.descendingIterator();

          while(itr.hasNext() )

                System.out.println(itr.next() );

    }

}

[실행 결과]

Individual

Orange

Apple

Dog

 

- iterator 메소드를 그대로 두고, Comparator의 compare 메소드에서 반환하는 값의 부호를 반대로 바꾸어도 동일한 효과를 얻을 수 있습니다.

 

 

 

ㅁ Map<K, V> 인터페이스를 구현하는 컬렉션 클래스들

- 이 인터페이스를 구현하는 컬렉션 클래스들의 데이터 저장방식을 가리켜 key-value 방식이라 한다. 

 

ㅁ key-value 방식의 데이터 저장과 HashMap<K, V> 클래스

- key는 데이터를 찾는 열쇠(이름)이다. value는 찾고자 하는 실질적인 데이터를 의미한다.

- 앞서 보였던 Collection<E>를 구현하는 컬렉션 클래스들이 value만 저장하는 구조였다면,

Map<K, V>를 구현하는 컬렉션 클래스들은 value를 저장할 때, 이를 찾는데 사용되는 key를 함께 저장하는 구조이다.

- Map<K, V> 인터페이스를 구현하는 대표적인 클래스로 HashMap<K, V>와 TreeMap<K, V>가 정의되어 있다.

 

ㅁ HashMap<K, V>

- IntroHashMap.java

 

import java.util.HashMap;

 

class IntroHashMap {

     public static void main(String[] args) {

 

        HashMap<Integer, String> hMap = new HashMap<Integer, String> ();

 

        hMap.put(new Integer(3), "나삼번");

        hMap.put(5, "윤오번");

        hMap.put(8, "박팔번");

 

        System.out.println("6학년 3반 8번 학생 : " + hMap.get(new Integer(8)) );

        System.out.println("6학년 3반 5번 학생 : " + hMap.get(5) );

        System.out.println("6학년 3반 3번 학생 : " + hMap.get(3) );

 

        hMap.remove(5);

        System.out.println("6학년 3반 5번 학생 : " + hMap.get(5) );

    }

}

[실행 결과]

6학년 3반 8번 학생 : 박팔번 

6학년 3반 5번 학생 : 윤오번 

6학년 3반 3번 학생 : 나삼번 

6학년 3반 5번 학생 : null

 

- HashMap<K, V> 클래스의 인스턴스 생성에는 key에 해당하는 자료형 정보(k)와 value에 해당하는 자료형 정보(v)가 필요하다.

- put 메소드는 데이터의 저장에 사용된다. 첫 인자로 key가, 두번째 인자로 value가 전달되어야 한다.

- get 메소드로 데이터를 참조하고 있다. 참조하고자 하는 데이터의 key가 전달되어야 key에 해당하는 데이터가 반환된다.

- remove 메소드는 데이터의 삭제에 사용된다. 

- key에 해당하는 데이터가 존재하지 않으면 null이 반환된다.

 

- value에 상관없이 중복된 key의 저장은 불가능 하다.

- value는 같더라도 key가 다르면 둘 이상의 데이터 저장도 가능하다. 

 

 

ㅁ TreeMap<K, V> 클래스

- HashSet<E>가 해시 알고리즘을 기반으로 구현되어 있듯이, HashMap<K, V> 역시 해시 알고리즘을 기반으로 구현되어 있다. 따라서 HashSet<E>의 장점인 '매우 빠른 검색속도'는 HashMap<K, V>에도 그대로 반영이 된다.

- 마찬가지로 TreeMap<K, V> 역시 '트리' 자료구조를 기반으로 구현이 되어 있다. 따라서 데이터는 정렬된 상태로 저장된다.

 

- IntroTreeMap.java

 

import java.util.TreeMap;

import java.util.Iterator;

import java.util.NavigableSet;

 

class IntroTreeMap {

    public static void main(String[] args) {

 

        TreeMap<Integer, String> tMap = new TreeMap<Integer, String> ();

        tMap.put(1, "data1");

        tMap.put(3, "data3");

        tMap.put(5, "data5");

        tMap.put(2, "data2");

        tMap.put(4, "data4");

 

        NavigableSet<Integer> navi = tMap.navigableKeySet();

 

        System.out.println("오름차순 출력...");

        Iterator<Integer> itr = navi.iterator();

        

        while(itr.hasNext() )

               System.out.println(tMap.get(itr.next() ) );

 

        System.out.println("내림차순 출력...");

        itr = navi.descendingIterator();

        

        while(itr.hasNext() )

               System.out.println(tMap.get(itr.next() ) );

    }

}

[실행 결과]

오름차순 출력... 

data1 

data2 

data3 

data4 

data5 

내림차순 출력... 

data5 

data4 

data3 

data2 

data1

 

- tMap.put()으로 총 5개의 데이터를 저장하고 있다. HashMap<K, V>와 달리 정렬되어 저장된다.

단 정렬 대상은 key이지 value가 아니다. 데이터는 key를 기준으로 참조가 이뤄지기 때문이다.

- navigableKeySet() 메소드가 호출되면, 인터페이서 NavigableSet<E>를 구현하는 인스턴스(인스턴스의 참조값)가 반환된다. 이 때 E는 key의 자료형인 Integer가 되며, 반환된 인스턴스에는 10~14행 까지 저장한 데이터들의 key 정보가 저장되어 있다.

- NavigableSet<E> 인터페이스는 Set<E> 인터페이스를 상속한다.

즉 navigableKeySet 메소드가 반환하는 인스턴스를 대상으로 반복자를 얻기 위해서 iterator 메소드의 호출이 가능하다.

그리고 이렇게 해서 얻은 반복자로 저장된 모든 key에 접근이 가능하다.

- (while 반복문) 반복자를 통해서 key를 반환하고, 이 key를 인자로 다시 value를 반환해서 출력하는 과정을 보이고 있다. 

- 위 예제의 navigableKeySet 메소드의 호출과 이 때 반환되는 인스턴스에 대해서 생소하게 느껴졌을 수 있다. 그러나 해설대로 이해하고 예제에서 보이는대로 활용하면 된다.

 

 

ㅁ TreeMap<K, V>의 전체 데이터 검색

- TreeMap<K, V>는 Collection<E>가 아닌 Map<K, V>를 구현하는 컬렉션 클래스이니, 저장되어 있는 전체 데이터를 검색하는 방식에 차이가 있음이 당연하다.

- 그리고 참으로 재밌는 것은, TreeMap<K, V>에 저장된 전체 데이터의 참조 과정에서 호출한 navigableKeySet 메소드가 반환하는 인스턴스가 Set<E> 인터페이스를 구현한다는 사실이다. KEY는 중복이 불가능하기 때문에 집합의 성격을 띤다. 

때문에 이러한 KEY를 저장하는 인스턴스는 Set<E> 인터페이스를 구현하고 있는 것이다.

(전체 데이터 검색 방법이 비슷하다는 걸 말하는 듯?)