컬렉션 프레임워크2

goose_bumps·2024년 8월 25일

Java

목록 보기
16/21

7. Set

Collection 상속받는 핵심 인터페이스 중 하나인 Set 인터페이스는 중복을 허용하지 않고 저장 순서를 유지하지 않는다는 공통점을 지닌다.
(LinkedHashSet는 중복은 허용하지 않지만 저장 순서는 유지됨)

구현 클래스로는 대표적으로 HashSet, TreeSet이 존재한다.

1) HashSet

Set 인터페이스의 특징대로 중복을 허용하지 않고, 저장 순서를 유지하지 않는다.
중복을 허용하지 않는다는 것은 구체적으로 요소를 추가할 때 add / addAll 사용할 때 이전에 추가했던 요소와 같은 요소일 경우 false를 반환하고 추가하지 않는다.
이때 같은 요소인지 아닌지는 equals와 hashCode 메서드를 사용하여 판별한다.

저장 순서를 유지하지 않으므로 출력 시 저장 순서와 다르게 출력될 수 있다. 만약, 저장 순서를 유지하고 싶다면 LinkedHashSet을 사용해야 한다.

주요 메서드

  • HashSet() : HashSet 객체 생성
  • HashSet(Collection c) : 컬렉션을 포함하는 HashSet 객체 생성
  • HashSet(int initialCapacity) : 초기 용량을 설정한 HashSet 객체 생성
  • HashSet(int initialCapacity, float loadFactor) : 초기 용량과 loadFactor를 설정한 HashSet 객체 생성
  • boolean add(Object o) : 새로운 객체 저장, 중복 시 false 반환
  • boolean addAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체들을 추가(합집합 개념)
  • void clear() : HashSet 저장된 모든 객체 삭제
  • Object clone() : HashSet 복제 후 반환(얕은 복사)
  • boolean contains(Object o) : 지정된 객체 포함 유무 반환
  • boolean containsAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체들은 포함하고 있는지 알려줌
  • boolean isEmpty() : HashSet이 비어있는지 유무 반환
  • Iterator iterator() : Iterator 반환
  • boolean remove(Object o) : 지정된 객체를 HashSet에서 삭제
  • boolean removeAll(Collection c) : 주어진 컬렉션에 저장된 모든 객체를 HashSet에서 삭제
  • boolean retainAll(Collection c) : 주어진 컬렉션에 저장된 객체와 동일한 객체들만 남기고 나머지는 모두 삭제
  • int size() : 저장된 객체의 개수 반환
  • Object[] toArray() : 저장된 객체들을 객체 배열의 형태로 반환
  • Object[] toArray(Object[] a) : HashSet에 저장된 객체들을 객체배열 a에 저장

여기서 loadFactor는 저장공간이 포화되기 전에 미리 용량을 확보하는 기능으로 loadFactor를 0.8로 설정하면 저장공간의 80%가 채워졌을 경우 용량이 2배로 늘어난다. 아무것도 입력하지 않을 경우 loadFactor는 75%가 기본값으로 설정된다.

자, 그럼 예제를 통해 HashSet이 중복을 허용하지 않는다는 것이 어떤건지 알아보자.

        HashSet set = new HashSet<>();
        set.add("1");
        set.add(new Integer(1));
        set.add("2");
        set.add("2");
        System.out.println(set); //[1, 1, 2]

2의 경우에는 중복이 되어서 한 개만 있지만 1의 경우에는 둘 다 추가된 것을 볼 수 있다.
"1"은 String의 인스턴스이고 new Integer(1)은 Wrapper의 인스턴스이기 때문에 내용이 같더라도 서로 다른 것으로 간주한 것이다.

다른 예시도 보자.

public class Main {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        set.add(1);
        set.add(1);
        set.add(new Person("Kim",27));
        set.add(new Person("Kim",27));
        set.add(new Person("Lee",30));

        System.out.println(set); //[1, Kim : 27, Kim : 27, Lee : 30]
    }
}

class Person{
    String name;
    int age;

    Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public String toString(){
        return name + " : " + age;
    }
}

Person의 인스턴스에 같은 내용("Kim",27)을 입력했으나 서로 다른 객체로 인식하여 중복된 것으로 판단하지 않았다. 왜 이런 것일까?
앞에서 설명했듯이 HashSet은 add할 때 equals와 hashCode 메서드를 통해 서로 다른 객체인지 구별한다고 했었다.
하지만, 모든 클래스는 Object 클래스를 상속받기 때문에 Person 클래스의 경우 Object 클래스의 equals와 hashCode가 실행된 것이다.

복습차원에서 다시 언급해보면, Object 클래스에 있는 equals는 두 객체의 주소값을 비교하여 true/false를 반환하고, hashCode도 마찬가지로 주소값을 기준으로 해시값을 반환한다.

만약, 저 예제에서 HashSet이 내용만 같으면 즉, 필드값만 같으면 같은 객체로 인식하게 하려면 equals와 hashCode를 오버라이딩 해야한다.

Person 클래스를 수정해보면

    @Override
    public boolean equals(Object o){
        if(o instanceof Person){
            Person tmp = (Person)o; //Person의 필드를 호출하기 위해 타입 캐스팅을 해주어야 함
            return this.name.equals(tmp.name) && this.age == tmp.age;
        }
        return false;
    }
    @Override
    public int hashCode(){
        return Objects.hash(name,age);
        //return (name+age).hashCode();
    }

equals()

비교 대상이 되는 객체가 Person 클래스의 객체거나 상속받는 경우면 if문을 실행시켜 Person 타입으로 변환시킨다. 그리고 각각의 name 필드값을 equals로 비교하면 둘 다 String 인스턴스이기 때문에 String의 equals가 호출된다.
(참고로, String 클래스의 equals 메서드는 문자열의 내용만을 비교한다)

&& 연산자를 사용하여 age 필드값도 같을 경우 즉, 둘 다 true가 나올 경우 true를 리턴하여 같은 객체로 인식하게 된다.

hashcode()

두 가지 방법이 있는데 name+age의 결과는 String이 되므로 String 클래스의 hashCode()를 실행시키는 것이다. String 클래스의 hashCode()도 문자열의 내용을 기준으로 해시코드 값을 반환한다. 즉, name과 age가 같다면 name+age의 문자열 내용도 같으므로 해시코드 값도 같다.
(java.lang 패키지 참고)

두 번째 방법은 Objects 클래스의 hash() 메서드를 사용하는 것이다.
가능하다면 이 방법을 사용하는 편이 좋다.

여기까지는 원리적인 이야기였고 중복을 허용하지 않는다는 특징을 이용하여 6개의 난수를 만들어보겠다.

       HashSet set = new HashSet();
       for(int i = 0; set.size() < 6; i++){
           set.add((int)(Math.random()*10)+1); // 1 ~ 10까지 난수
       }
        System.out.println(set); //[2, 3, 4, 6, 8, 10] //실행마다 결과 달라짐

여기서 한 가지만 알아두면 된다. 왜 i < 6 이런식으로 안하고 set.size() < 6으로 설정했을까?
이게 바로 Set의 특징 때문이다. 만약, i < 6으로 설정한다면 동일한 숫자가 추가되었을 경우 중복을 허용하지 않아 추가되지 않기 때문에 set은 6개를 채우지 못한다.

그렇다면, HashSet의 메서드 중 size()는 가지고 있는 객체의 수를 반환하기 때문에 그 개수가 6이 될때까지 반복시키는 방법은 set.size() < 6 인 것이다.

       Set set = new HashSet();
       for(int i = 0; i < 6; i++){
           set.add((int)(Math.random()*10)+1); // 1 ~ 10까지 난수
       }
        System.out.println(set); //[3, 4, 5, 8]

물론 우연히 6개를 모두 채우는 경우도 있겠지만, 매 실행마다 set에 6개의 숫자를 채우려면 범위 설정을 위와 같이 해야한다.

마지막으로 Set의 특성을 이용한 합집합을 만드는 예제를 만들어보겠다.

        HashSet setA = new HashSet();
        HashSet setB = new HashSet();
        HashSet setAB = new HashSet();
        
        setA.add("1");
        setA.add("2");
        setA.add("3");
        setA.add("4");
        
        setB.add("3");
        setB.add("4");
        setB.add("5");
        setB.add("6");

setA에는 1,2,3,4가 있고 setB에는 3,4,5,6이니 우리가 알고 있는 합집합 개념이라면 A와 B의 합집합은 1,2,3,4,5,6이 되야 한다.

set은 중복을 허용하지 않기 때문에 setA와 setB의 모든 요소를 setAB에 추가해버리면 알아서 A + B에서 교집합을 빼준다.

HashSet의 메서드를 사용한다면 addAll(Collection c)를 2번 사용하거나 iterator를 사용하여 추가해주면 된다.

        //방법1. iterator() 사용
        
        Iterator it = setA.iterator();
        while(it.hasNext()){
            setAB.add(it.next());
        }
        it = setB.iterator();
        while(it.hasNext()){
            setAB.add(it.next());
        }
        System.out.println(setAB); //[1, 2, 3, 4, 5, 6]
        //방법2. addAll(Collection c) 사용
        
        setAB.addAll(setA);
        setAB.addAll(setB);

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

2) TreeSet

이진 검색 트리라는 자료구조의 형태로 데이터를 저장하는 Set 인터페이스의 구현 클래스인 TreeSet에 대해 알아보자.

이진 검색 트리란 LinkedList 처럼 여러 개의 노드가 서로 연결되어 정렬되어 있는 구조로, 기본적으로 Root라는 하나의 노드에서부터 시작하여 부모-자식의 관계를 형성해간다.
부모-자식 관계를 상대적인 것이라 " 부모-자식(부모)-자식(부모)-...." 이런식의 구조가 되는 것이다.

여기서 Child1 < Parent < Child2 순으로 데이터의 크기가 분류된다. 즉, 1,2,3이라는 데이터는 이진 검색 트리에서 Child1에 1이 Child2에 3이 들어가고 Parent에는 2가 들어가게 된다.

정리하자면, 노드 기준 좌측으로 내려가는 노드는 작은 값, 우측으로 내려가는 노드는 큰 값이 되는 것이다.

이 구조로 계속 이어나가면 결국 좌측 마지막 레벨은 데이터 중 가장 작은 값이, 우측 마지막 레벨은 가장 큰 값이 된다.

자, 그러면 "그래서 이런 구조를 왜 알아야 하는데?"라고 생각할 수 있다.
일단, 위의 그림을 잘 기억하고 있자.

이진 검색 트리는 정렬, 검색, 범위 탐색(Range Search)에 높은 성능을 보인다. 데이터를 저장하거나 삭제하거나 수정해도 항상 저 구조에 맞춰 정렬되는 것이다.

예를 들어, {1,2,3,4,5}를 이진 트리에 저장하면

이렇게 정렬되는 것이다.

만약, "4보다 작은 값을 찾아줘!"라는 요청이 들어오면 이진 트리에서는 4가 있는 노드 좌측 가지 밑으로만 탐색하면 되므로 빠르게 찾을 수 있다.

이처럼 정렬되어 있는 구조는 탐색에 있어 유리하지만, 역으로 데이터 추가/삭제 시 모든 노드를 다시 이진 트리 구조에 맞게 정렬해야 하기 때문에 시간이 오래 걸린다.

TreeSet의 특징

  • 중복된 데이터 저장이 불가(Set 인터페이스의 특징)
  • 모든 노드는 최대 2개의 자식노드를 가질 수 있음
  • 검색, 범위탐색, 정렬에 유리
  • 데이터 추가/삭제에 시간이 오래 걸림
  • 좌측 자식 노드 < 부모 노드 < 우측 자식 노드

TreeSet 메서드

  • TreeSet() : 기본 생성자
  • TreeSet(Collection c) : 컬렉션 c를 저장하는 TreeSet 생성
  • TreeSet(Comparator comp) : 주어진 정렬조건으로 정렬하는 TreeSet 생성
  • boolean add(Object o) / addAll(Collection c) : 지정된 객체 o / 컬렉션 c 객체들을 추가
  • Object ceiling(Object o) : 지정된 객체 o와 같은 객체를 반환. 없을 경우 지정된 객체 o 보다 큰 값 중 가장 가까운 값 반환. 지정된 객체 o보다 큰 값도 없을 경우 null 반환
  • Object floor(Object o) : 지정된 객체 o와 같은 객체를 반환. 없을 경우 지정된 객체 o 보다 작은 값 중 가장 가까운 값 반환. 지정된 객체 o보다 작은 값도 없을 경우 null 반환
  • Object higher(Object o) : 지정된 객체 o보다 큰 값을 가진 객체 중 가장 가까운 값 반환. 큰 값이 없으면 null 반환
  • Object lower(Object o) : 지정된 객체 o보다 작은 값을 가진 객체 중 가장 가까운 값 반환. 작은 값이 없으면 null 반환
  • void clear() : 저장된 모든 객체 삭제
  • boolean remove(Object o) : 지정된 객체 삭제
  • Object clone() : TreeSet을 복제하여 반환
  • boolean contains(Object o) / containsAll(Collection c) : 지정된 객체 o / 주어진 컬렉션 c의 객체들이 포함되어 있는지 반환
  • NavigableSet descendingSet() : TreeSet에 저장되어 있는 요소들을 역순으로 정렬해서 반환
  • Object first() / pollFirst() : 정렬된 순서에서 첫 번째 객체 반환(=가장 좌측 하단의 데이터)
  • Object last() / pollLast() : 정렬된 순서에서 마지막 객체 반환(=가장 우측 하단의 데이터)
  • SortedSet headSet(Object o) : 지정된 객체보다 작은 값의 객체들을 반환(같은 값은 포함 안함)
  • NavigableSet headSet(Object toElement, boolean inclusive) : headSet()과 기능은 동일하되, inclusive를 true로 설정 시 같은 값 포함
  • SortedSet tailSet(Object o) : 지정된 객체보다 큰 값의 객체들을 반환(같은 값은 포함)
  • boolean isEmpty() : TreeSet 비어있는지 확인 후 반환
  • boolean retainAll(Collection c) : 주어진 컬렉션과 공통된 요소 외에 전부 삭제
  • int size() : TreeSet의 데이터 개수 반환
  • SortedSet subSet(Object fromElement, Object toElement) : 범위 검색 후 반환(toElement는 범위에 포함 X)
  • Navigable subSet(Object fromElement, boolean fromInclusive, Object toElement, boolean toInclusive) : subSet과 기능 동일하되, inclusive 설정에 따라 element 포함 여부 결정
		TreeSet set = new TreeSet();
		set.add("ABC");
		set.add("@#$");
		set.add("abc");
		set.add("bbk");
		set.add("dop");
		set.add("1");
		set.add("2");
		set.add("3");
		System.out.println(set); //[1, 2, 3, @#$, ABC, abc, bbk, dop]
		System.out.println(set.subSet("a", "c")); //[abc, bbk]

subset을 사용하여 a ~ c 사이의 범위의 데이터를 반환하였다. 대문자와 특수기호는 소문자보다 앞서기 때문에 해당되지 않는다.
참고로 문자열의 정렬순서는 특수기호 < 숫자 < 대문자 < 소문자 순이다.

위에서 언급했던 기억해두자고 했던 그림을 코드로 구현해보겠다.

		TreeSet set = new TreeSet();
		for(int i = 1; i < 6; i++) {
			set.add(i);
		}
		System.out.println(set);
		System.out.println("4보다 작은 값 : " + set.headSet(4));
		System.out.println("4보다 큰 값 : " + set.tailSet(4));
		
		System.out.println("4보다 작은 값(4포함) : " + set.headSet(4, true));

[1, 2, 3, 4, 5]
4보다 작은 값 : [1, 2, 3]
4보다 큰 값 : [4, 5]
4보다 작은 값(4포함) : [1, 2, 3, 4]

마지막 줄에서는 headSet에 inclusive를 true로 설정하여 같은 값인 4도 범주에 포함할 수 있는 것을 나타낸 것이다.

8. Map

Map 인터페이스는 Key-Value 쌍으로 데이터를 묶어 저장한다.
저장 순서는 유지되지 않으며, Key는 중복을 허용하지 않지만, Value는 중복을 허용한다.

ex) "name" : "Kim", "name" : "Lee" (X)
"name" : "Kim", "Administrator" : "Kim" (O)

만약, 기존에 저장된 Key와 중복된 Key와 다른 Value를 저장하면 기존의 값은 없어지고 마지막에 저장된 값으로 저장된다.

Map 인터페이스 메서드

  • void clear() : Map의 모든 객체 삭제
  • boolean containsKey(Object Key) : 지정된 key 객체와 일치하는 key 객체가 있는지 Map에서 찾고 true/false 반환
  • boolean containsvalue(Object value) : 지정된 value 객체와 일치하는 value 객체가 있는지 Map에서 찾고 true/false 반환
  • Set entrySet() : Map에 저장되어 있는 key-value 쌍을 Map.Entry 타입의 객체로 저장한 Set으로 반환
  • boolean equals(Object o) : 동일한 Map인지 비교
  • Object get(Object Key) : 지정한 key에 해당하는 value를 반환
  • int hashCode() : 해시코드 반환
  • boolean isEmpty() : Map이 비어있는지 확인 후 결과 반환
  • Object put(Object Key, Object Value) : Map에 Key-Value쌍을 추가
  • void putAll(Map m) : 지정된 Map의 모든 key-value쌍을 추가
  • Object remove(Object Key) : 지정된 key와 일치하는 key-value 삭제
  • int size() : Map에 저장된 key-value 쌍의 개수 반환
  • Set keySet() : Map에 저장된 모든 key객체를 반환
  • Collection values() : Map에 저장된 모든 value 객체를 반환

모든 key와 value 객체를 반환하는 keySet, values를 보면 반환타입이 각각 Set, Collection인 것을 알 수 있는데, 이는 Key의 경우 중복을 허용하지 않기 때문에 Set 타입으로, Value는 중복을 허용하기 때문에 Collection 타입으로 반환하는 것이다.

entrySet()을 사용하면 Map.Entry 타입의 객체로 저장된 Set을 반환할 수 있다고 되어 있는데 뒤에서 다루겠지만 Iterator를 반환하기 위해 Set에 저장하는 것이다.

Map.Entry 인터페이스

Map 인터페이스는 내부에 Map.Entry 라는 인터페이스를 정의하고 있다. 이건 어디에 사용되길래 내부적으로 인터페이스를 또 정의한걸까?

  • Key,Value 묶음 관리 : Key와 Value를 분리된 개념이 아닌 둘을 묶어서 하나의 단위로 취급하는데 이를 Entry라고 한다.
  • 효율적인 처리 : Map을 순회하면서 즉, 반복문으로 key-value를 처리할 때 Entry 객체를 통해 한 번에 다룰 수 있다. 만약, Entry가 없으면 key,value를 따로따로 처리해야 하기 때문에 비효율적이다.

쉽게 설명하자면 어떤 Map 내부에 Key에 어떠어떠한것들이 있고, Value에 어떠어떠한 것들이 있는 방식보다는, Entry 객체가 여러 개가 있고 그 객체 내부에는 각각 key-value 쌍으로 구성되어 있다고 생각하면 된다.

그래서 Map에 있는 데이터를 모두 순회할 경우 바로 Iterator를 반환하는 것은 불가능하며, 가장 먼저 entrySet()을 통해 Entry를 Set에 저장하는 것이다.

Map map = new HashMap();
map.put("name" , "Kim");
map.put("Age" , "12");

Set set = map.entrySet(); //Map의 데이터를 Map.Entry 타입의 Set으로 반환
Iterator it = set.iterator(); //Entry가 저장된 Set에서 Iterator 반환

while(it.hasNext()){
	Map.Entry entry = (Map.Entry)it.next(); //Iterator에 저장된 Entry를 하나씩 꺼내어 entry에 저장
    System.out.println("Key : " + entry.getKey() + ", Value : " + entry.getValue());
    }
    //Key : name, Value : Kim
    //Key : Age, Value : 12
    

여기서 굳이 while문 내부에 Map.Entry를 정의한 이유는 entry는 하나의 쌍만 저장 가능하기 때문에 Iterator가 순회하면서 하나의 Entry 객체씩 entry에 대입하는 것이다. 그래서, getKey, getValue를 사용하면 본인 순번이 된 Entry가 각각 Key,Value를 반환하는 것이다.


Entry 형태로 반환하지 않고는 Iterator에 Key-Value 쌍으로 저장할 수 없다. 만약, Set 타입으로 변환하지 않고 Map에서 바로 Iterator를 사용하려면 다음과 같이 해야 한다.

     Map map = new HashMap();
     map.put("name" , "Kim");
     map.put("Age", "12");

     Iterator it = map.keySet().iterator(); //Map에 저장된 모든 Key객체 반환
     Iterator it2 = map.values().iterator();//Map에 저장된 모든 Value객체 반환

     while(it.hasNext() && it2.hasNext()){
      System.out.println("Key : " + it.next() + ", Value : " + it2.next());
     }

Map.Entry 메서드

  • boolean equals(Object o) : 동일한 Entry인지 비교
  • Object getKey() : Entry의 Key 객체 반환
  • Object getValue() : Entry의 Value 객체 반환
  • int hashCode() : 해시코드 반환
  • Object setValue(Object value) : Entry의 value 객체를 지정된 객체로 변경

1) HashMap

Map 인터페이스를 구현하는 클래스 중 하나인 HashMap은 해싱기법을 사용하여 많은 양의 데이터를 검색하는데 뛰어난 성능을 발휘한다.

구버전인 Hashtable이 있는데 HashMap을 사용하는 편이 더 좋다.
(Hashtable은 Key,value에 null을 허용하지 않지만, HashMap은 허용한다)

HashMap은 Key,Value를 각각 Object 타입으로 저장하기 때문에 어떠한 객체도 저장이 가능하며, 하나의 Key로 검색했을 때 결과가 하나여야 하기 때문에 Key는 컬렉션 내에서 유일해야 한다(중복될 수 없다).

또한, 내부적으로 Entry라는 내부 클래스를 정의하기 때문에 하나의 Key에는 하나의 Value가 존재하며 서로 관련된 값이 된다.
만약, 이미 존재하는 Key-Value에 새로운 데이터를 추가할 경우 새로운 데이터가 이전 데이터를 덮어쓴다.

해싱을 통해 데이터를 검색하는 점 외에는 Map의 특징을 그대로 이어가며 메서드 또한 거의 비슷하다. 추가적인 메서드만 알아보자.

  • HashMap() : 객체 생성
  • HashMap(int initialCapacity) : 지정된 값을 초기 용량으로 하는 객체 생성
  • HashMap(int inttialCapacity, float loadFactor) : 지정된 초기용량과 load factor를 가지는 객체 생성
  • Object clone() : 현재 HashMap 객체 복제해서 반환
  • Object getOrDefault(Object Key, Object defaultValue) : 지정된 key에 해당하는 value를 반환. key를 못찾을 경우 defaultValue로 지정된 객체 반환
  • Object replace(Object Key, Object newValue) : 지정된 Key의 해당하는 Value를 지정된 newValue로 대체
  • Object replace(Object Key, Object oldValue, Object newValue) : 지정된 Key와 oldValue가 모두 일치하는 경우에만 oldValue를 newValue로 교체

HashMap을 응용하여 여러 예제들을 만들어보자.

사용자 계정 생성, 로그인 구현

public class UserAccountCheck {
    public static HashMap<String,String> map = new HashMap<>();

    public static void createuserAccount(String ID, String PASSWORD){
        map.put(ID,PASSWORD);
        System.out.println("계정이 생성되었습니다.");
    }

    public static void userAccountCheck(HashMap map){
        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("존재하지 않는 계정입니다.");
            }
            else if(!map.get(ID).equals(PASSWORD)){
                System.out.println("PASSWORD가 틀립니다.");
            }
            else{
                System.out.println("환영합니다.");
                break;
            }
        }
    }
}
import userAccountCheck.UserAccountCheck;
public class Main {
    public static void main(String[] args) {
        UserAccountCheck.createuserAccount("asdf","123456");

        UserAccountCheck.userAccountCheck(UserAccountCheck.map);
    }
}

계정이 생성되었습니다.
ID,PASSWORD를 입력하세요.
ID : asdf
PASSWORD : 0000

PASSWORD가 틀립니다.
ID,PASSWORD를 입력하세요.
ID : asdfg
PASSWORD : 1234

존재하지 않는 계정입니다.
ID,PASSWORD를 입력하세요.
ID : asdf
PASSWORD : 123456

환영합니다.

HashMap에 put을 사용하여 Key-Value 쌍으로 id,password를 입력하여 계정을 생성하고 containsKey(), get() 메서드를 사용하여 존재하는 계정인지 확인하는 예제이다.

마지막 예제는 지역별로 주민 정보를 분류하고 다시 주민등록번호와 이름으로 분류하는 기능을 구현하는 것이다.
여기서 핵심은 1차적으로 Key를 지역으로 설정하고 지역에 대한 Value가 다시 Key-Value 관계를 이루는 것이다.

public class ResidentInfo {
    public static HashMap map = new HashMap();

    public static void saveInfo(String location,String residentNum, String name){
        if(!map.containsKey(location)){
            map.put(location,new HashMap());
        }
        HashMap info = (HashMap) map.get(location);
        info.put(residentNum,name);
    }

    public static void showResidentInfo(){
        Set set = map.entrySet();
        Iterator it = set.iterator();

        while(it.hasNext()){
            Map.Entry entry = (Map.Entry)it.next();
            Set subset = ((HashMap)entry.getValue()).entrySet();
            Iterator subIt = subset.iterator();
            System.out.println(entry.getKey() + "[" + subset.size() + "]");

            while(subIt.hasNext()){
                Map.Entry subEntry = (Map.Entry)subIt.next();
                System.out.println("주민등록번호 : " + subEntry.getKey() + " 이름 : " + subEntry.getValue());
            }

        }
    }
}
public class Main {
    public static void main(String[] args) {
        ResidentInfo.saveInfo("인천","001122-1234567","홍길동");
        ResidentInfo.saveInfo("인천","023522-1324268","김철수");
        ResidentInfo.saveInfo("인천","054232-1200867","김자바");
        ResidentInfo.saveInfo("인천","094532-1234885","파이썬");
        ResidentInfo.saveInfo("인천","992022-1293067","김정보");
        ResidentInfo.saveInfo("인천","120324-1034869","이자바");
        ResidentInfo.saveInfo("서울","002822-1234567","홍길동");
        ResidentInfo.saveInfo("대전","000022-1324268","김철수");
        ResidentInfo.saveInfo("서울","053132-1200867","김자바");
        ResidentInfo.saveInfo("서울","022232-1234885","파이썬");
        ResidentInfo.saveInfo("대전","934332-1293067","김정보");
        ResidentInfo.saveInfo("대전","134324-1034869","이자바");

        ResidentInfo.showResidentInfo();
    }
}

대전[3]
주민등록번호 : 000022-1324268 이름 : 김철수
주민등록번호 : 934332-1293067 이름 : 김정보
주민등록번호 : 134324-1034869 이름 : 이자바
서울[3]
주민등록번호 : 002822-1234567 이름 : 홍길동
주민등록번호 : 053132-1200867 이름 : 김자바
주민등록번호 : 022232-1234885 이름 : 파이썬
인천[6]
주민등록번호 : 094532-1234885 이름 : 파이썬
주민등록번호 : 023522-1324268 이름 : 김철수
주민등록번호 : 001122-1234567 이름 : 홍길동
주민등록번호 : 054232-1200867 이름 : 김자바
주민등록번호 : 120324-1034869 이름 : 이자바
주민등록번호 : 992022-1293067 이름 : 김정보

여기서 의문이 들 수 있다.
지역이라는 Key는 유일해야 하니까 Value도 하나여야 하는데, 왜 하나의 지역에 대해 여러 정보를 가질 수 있을까??

조금 복잡하지만 정리하자면 다음과 같다.
"인천"이라는 Key에 대한 Value로 새로운 HashMap을 만든다. 이를 HashMap1이라고 하자.
HashMap1은 다시 주민등록번호를 Key로 하여 그에 대한 Value인 이름을 저장한다.
즉, 여러 주민등록번호에 대한 각각의 이름을 가질 수 있는 것이다.

"인천"이 Key는 맞지만 주민등록번호, 이름이 저장되어 있는 HashMap1 입장에서는 "인천"이라는 HashMap이기 때문에 여러 Key-Value 저장이 가능한 것이다.

"인천"이라는 Key가 여러 개의 Value를 가지는 것이 아니라는 말이다.


위에 saveInfo 메서드에서 if문으로 !map.containsKey(location)을 설정한 이유는 만약, 이러한 조건이 없다면 map.put(location, new HashMap()) 이 계속 실행되므로 결국 새로 덮어씌어지는 작업만 반복되는 것이다.

처음 "인천"에 대한 정보를 저장하면 그 이후에는 "아 이미 인천이 저장되어 있으니 새로 HashMap 생성은 하지 말고 인천HashMap에 Key-Value를 저장해야겠다"라고 판단하여 저장하는 것이다.

이 방식으로 HashMap 뿐만 아니라 ArrayList를 사용해도 무방하다.
어떠한 Key에 대한 Value로 ArrayList를 설정하고 그 ArrayList에 데이터를 계속 추가하는 것도 가능하다는 것이다.

이처럼 HashMap에서 복수의 데이터를 저장할 수 있는 이유는 HashMap이 Object 타입으로 Key,Value를 저장하기 때문에 가능한 것이다.

2) TreeMap

이름 그대로 이진트리검색 형태로 Key-Value 쌍을 저장하기 때문에 범위 검색과 정렬에 유리하다. 그 외에는 HashMap과 차이점이 없다.

TreeMap 메서드

  • Map.Entry firstEntry() : TreeMap에 저장된 첫 번째(가장 작은) Key,Value 반환
  • Map.Entry lastEntry() : TreeMap에 저장된 마지막(가장 큰) Key,Value 반환
  • Map.Entry pollFirstEntry() : TreeMap에 저장된 첫 번째(가장 작은) Key를 제거하면서 반환
  • Map.Entry pollLastEntry() : TreeMap에 저장된 마지막(가장 큰) Key를 제거하면서 반환

  • Object firstKey() : 첫 번째(가장 작은) Key를 반환
  • Object lastKey() : 마지막(가장 큰) Key를 반환

  • Map.Entry floorEntry(Object Key) : 지정된 Key와 일치하거나 작은 Key 중 제일 큰 Key-Value 반환. 없으면 null 반환
  • Map.Entry lowerEntry(Object Key) : 지정된 Key보다 작은 Key 중 제일 큰 Key-Value 반환. 없으면 null 반환
  • Map.Entry higherEntry(Object Key) : 지정된 Key보다 큰 Key 중 제일 작은 Key-Value 반환. 없으면 null 반환
  • Map.Entry ceilingEntry(Object Key) : 지정된 Key와 일치하거나 큰 Key 중 제일 작은 Key-Value 반환. 없으면 null 반환

  • Object floorKey(Object Key) : 지정된 Key와 일치하거나 작은 것 중에서 제일 큰 Key를 반환. 없으면 null 반환
  • Object lowerKey(Object Key) : 지정된 Key보다 작은 것 중에서 제일 큰 Key-value를 반환. 없으면 null 반환
  • Object higherKey(Object Key) : 지정된 Key보다 큰 것 중에서 제일 작은 Key-Value를 반환. 없으면 null 반환
  • Object ceilingKey(Object Key) : 지정된 Key와 일치하거나 큰 것 중에서 제일 작은 Key를 반환. 없으면 null 반환

  • SortedMap headMap(Object toKey) : TreeMap에 저장된 첫 번째 요소부터 toKey까지 범위의(toKey는 미포함) SortedMap 반환
  • NavigableMap headMap(Object toKey,boolean inclusive) : TreeMap에 저장된 첫 번째 요소부터 toKey까지 범위의 NavigableMap 반환. inclusive로 toKey의 포함여부 설정 가능
  • SortedMap subMap(Object fromKey, Object toKey) : 지정된 범위의 모든 요소들이 담긴 SortedMap 반환. toKey는 범위에 미포함
  • NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive) : 지정된 범위의 모든 요소들이 담긴 NavigableMap 반환. inclusive로 포함여부 설정
  • SortedMap tailMap(Object fromKey) : TreeMap에 저장된 fromKey부터 마지막까지 범위의(fromKey는 미포함) SortedMap 반환
  • NavigableMap tailMap(Object toKey,boolean inclusive) : TreeMap에 저장된 fromKey부터 마지막까지 범위의 NavigableMap 반환. inclusive로 fromKey의 포함여부 설정 가능

0개의 댓글