자바 객체를 해시테이블의 키 값으로 이용하자.


해시함수에 의해서 나온 해시 값, 즉 해시코드는 데이터를 고유하게 식별할 수 있다. 해시코드를 통해서 두 객체가 동일한 객체인지 여부를 확인할 수 있다. 이는 결국 두 객체의 동일성 (아이덴티티, Identity) 를 따지게 된다. 


일반적으로 우리가 HashMap, HashSet, Hashtable 과 같은 컬렉션 객체를 사용하는 경우 객체 의미상의 동등성 비교를 위해서 hashCode() 메소드를 호출한다. 


보통 나는 해시맵을 자주 이용하는데, 값의 중복 여부를 체크할 때 사용한다. 가령 아래와 같이 사용한다.


HashMap<String, Object> map = new HashMap<String, Object>();


map.put("호랑이", new Object());

map.put("사자", new Object());

map.put("원숭이", new Object());


String [] array = new String[]{"호랑이", "사자", "치타"};

for(int i = 0; i < array.length; i++){

if(map.get(array[i]) == null){

continue;

}


// 해당 array[i] 값은 해시맵에 포함되어있음.

}


더미 객체를 밸류로 두고 사용하는데, 왜 이렇게 사용하냐면 밸류는 중복이라던가 값의 유무가 아닌 단순 키 값을 맵에 넣기위한 하나의 임시 데이터일 뿐이고 실질적으로 중요한 것은 키 값이다. 키는 고유하게 하나의 값을 지니기 때문에 위와 같은 방법을 사용하는 것이다.


하지만 키 값을 단순히 String Class 또는 Wrapper Class 가 아닌 Object 형식으로 한다면 어떻게 하는 것이 좋을까?


이 의문에 해결점은 equals()hashCode() 에 있다. 


(1) equals() : 두 객체의 내용이 같은지 여부 (동등성, Equality)

객체와 객체가 동일한 것인지 비교하는 메소드이다. 객체 내의 멤버필드로 작용하는 요소들이 일부만 같고, 일부는 다른 경우가 있으며 오버라이딩을 통해서 비교 기준을 제시할 수 있다.


(2) hashCode() : 두 객체가 동일한 객체인지 여부 (동일성, Identity)

객체를 구별하기 위해 고유한 정수의 값으로 출력시켜주는 메소드이다. 난수처럼 보이지만 사실 데이터를 식별하기 위한 고정된 길이의 정수 값이다.


하나의 예제를 살펴보자.

  • 채팅방이 있다.

  • 해당 채팅방에는 여러 명의 사람들이 대화 진행하고 있다.

  • 해당 채팅방에는 여러 명의 사람들이 입장하고 퇴장하고 있다.

  • 채팅방에 입장하고 퇴장하는 인원들이 주어지고, 가장 마지막에 채팅방에 남아있는 사람은 누구누구인가.

이러한 요구사항을 받았다고 해보자. 그럼 나는 이런저런 생각을 할 것이다.
  1. 해시맵을 써서 중복을 제거해야겠다.
  2. 입장과 퇴장에 대해서 밸류로 구분지어주어야 겠다.
  3. 키 값은 사람이름으로 String 으로 해주면 되겠다.
사람 이름뿐만 아니라 성별, 주민등록번호가 있는 경우는 어떻게 할 것인가. 여기서 한가지 문제는 동등한 이름에 동등한 성별인데 주민등록번호가 다른경우는 완전히 다른 사람이라는 것이다. 

우선 Person 이라는 클래스를 하나 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * @since 2019 02 21
 * @author PASUDO
 *
 */
public class Person {
    
    private String name;            // 이름
    private String gender;            // 성별
    private String residentNumber;    // 주민등록번호
 
    public Person() {}
    
    public Person(String name, String gender, String residentNumber) {
        this.name = name;
        this.gender = gender;
        this.residentNumber = residentNumber;
    }
 
    /**
     * 이름 획득 <p>
     * @return
     */
    public String getName() {
        return name;
    }
 
    /**
     * 성별 획득 <p>
     * @return
     */
    public String getGender() {
        return gender;
    }
 
    /**
     * 주민등록번호 획득 <p>
     * @return
     */
    public String getResidentNumber() {
        return residentNumber;
    }
}
cs


그리고 해시테이블에 해당 사람들의 이름을 삽입한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
Hashtable<Person, Integer> chatRoomTable = new Hashtable<Person, Integer>();
 
/**
 * - new Person(이름, 성별, 주민번호(4자리))
 * - 1 :: 입장
 * - 2 :: 퇴장 
 */
 
chatRoomTable.put(new Person("안정환""남""1234"), 1);
chatRoomTable.put(new Person("손흥민""남""4423"), 1);
chatRoomTable.put(new Person("박지성""남""2463"), 1);
chatRoomTable.put(new Person("안정환""남""1234"), 0);
chatRoomTable.put(new Person("무리뉴""남""7777"), 1);
chatRoomTable.put(new Person("호날두""남""3984"), 1);
chatRoomTable.put(new Person("메시""남""6184"), 1);
chatRoomTable.put(new Person("호날두""남""3984"), 0);
chatRoomTable.put(new Person("손흥민""남""4423"), 0);
chatRoomTable.put(new Person("박지성""남""2463"), 0);
chatRoomTable.put(new Person("호날두""남""3984"), 1);
chatRoomTable.put(new Person("안정환""남""1234"), 1);
chatRoomTable.put(new Person("메시""남""6184"), 0);
chatRoomTable.put(new Person("이강인""남""5531"), 1);
cs


위의 내용을 살펴보면 채팅방에 남아있는 사람은 실제 값은 아래와 같다. 

  1. 이강인, 남, 5531

  2. 안정환, 남, 1234

  3. 호날두, 남, 3984

  4. 무리뉴, 남, 7777 


하지만 결과 값은 전혀 다르게 나타난다.

현재 채팅방에 남아있는 사람 1 ==> 안정환(1234)

현재 채팅방에 남아있는 사람 2 ==> 박지성(2463)

현재 채팅방에 남아있는 사람 3 ==> 호날두(3984)

현재 채팅방에 남아있는 사람 4 ==> 호날두(3984)

현재 채팅방에 남아있는 사람 5 ==> 메시(6184)

현재 채팅방에 남아있는 사람 6 ==> 무리뉴(7777)

현재 채팅방에 남아있는 사람 7 ==> 안정환(1234)

현재 채팅방에 남아있는 사람 8 ==> 이강인(5531)

현재 채팅방에 남아있는 사람 9 ==> 손흥민(4423)


분명히 박지성은 퇴장했는데 채팅방에 남아있다. 손흥민과 메시도 퇴장했는데도 불구하고 남아있다. new Person() 으로 키를 삽입하였기 때문에 새로운 객체를 넣었지만 사실 멤버필드로 보았을 때 같은 내용 값을 가진 동일한 객체이다.


따라서 Person 클래스 하단에 아래과 같이 추가해준다. equals() 메소드와 hashCode() 메소드를 오버라이딩한 내용이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
 * 해시코드를 획득하는 메소드 오버라이드 <p>
 */
@Override
public int hashCode() {
    
    final int prime = 31;
    
    int result = 1;
    
    result = prime * result + ((gender == null) ? 0 : gender.hashCode());
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    result = prime * result + ((residentNumber == null) ? 0 : residentNumber.hashCode());
    
    return result;
}
 
/**
 * 객체 비교 오버라이드 <p>
 */
@Override
public boolean equals(Object obj) {
    
    /** (1) 오브젝트 검사 **/
    if(this == obj) {
        return true;
    }
    
    if(obj == null) {
        return false;
    }
    
    /** (2) 해당 객체 소속 검사 **/
    if(getClass() != obj.getClass()) {
        return false;
    }
    
    /** (3) 해당 객체로 다운 캐스팅 후 멤버변수 검사 **/
    Person otherPerson = (Person) obj;
    
    if(gender == null && otherPerson.gender != null) {
        return false;
    }else if(!gender.equals(otherPerson.gender)) {
        return false;
    }
    
    if(name == null && otherPerson.name != null) {
        return false;
    }else if(!name.equals(otherPerson.name)) {
        return false;
    }
    
    if(residentNumber == null && otherPerson.residentNumber != null) {
        return false;
    }else if(!residentNumber.equals(otherPerson.residentNumber)) {
        return false;
    }
    
    return true;
}
cs


그리고 다시 한번 결과내용을 돌려보면 아래과 같이 결과값이 나타난다.

현재 채팅방에 남아있는 사람 1 ==> 무리뉴(7777)

현재 채팅방에 남아있는 사람 2 ==> 이강인(5531)

현재 채팅방에 남아있는 사람 3 ==> 호날두(3984)

현재 채팅방에 남아있는 사람 4 ==> 안정환(1234)


정상적으로 출력되었다.

이제 Object 를 키 값으로 중복처리가 가능한 것이다.


전체 코드는 하단에 있다.


Person Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package blog.hashtable;
 
/**
 * @since 2019 02 21
 * @author PASUDO
 *
 */
public class Person {
    
    private String name;            // 이름
    private String gender;            // 성별
    private String residentNumber;    // 주민등록번호
 
    public Person() {}
    
    public Person(String name, String gender, String residentNumber) {
        this.name = name;
        this.gender = gender;
        this.residentNumber = residentNumber;
    }
 
    /**
     * 이름 획득 <p>
     * @return
     */
    public String getName() {
        return name;
    }
 
    /**
     * 성별 획득 <p>
     * @return
     */
    public String getGender() {
        return gender;
    }
 
    /**
     * 주민등록번호 획득 <p>
     * @return
     */
    public String getResidentNumber() {
        return residentNumber;
    }
 
    /**
     * 해시코드를 획득하는 메소드 오버라이드 <p>
     */
    @Override
    public int hashCode() {
        
        final int prime = 31;
        
        int result = 1;
        
        result = prime * result + ((gender == null) ? 0 : gender.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((residentNumber == null) ? 0 : residentNumber.hashCode());
        
        return result;
    }
    
    /**
     * 객체 비교 오버라이드 <p>
     */
    @Override
    public boolean equals(Object obj) {
        
        /** (1) 오브젝트 검사 **/
        if(this == obj) {
            return true;
        }
        
        if(obj == null) {
            return false;
        }
        
        /** (2) 해당 객체 소속 검사 **/
        if(getClass() != obj.getClass()) {
            return false;
        }
        
        /** (3) 해당 객체로 다운 캐스팅 후 멤버변수 검사 **/
        Person otherPerson = (Person) obj;
        
        if(gender == null && otherPerson.gender != null) {
            return false;
        }else if(!gender.equals(otherPerson.gender)) {
            return false;
        }
        
        if(name == null && otherPerson.name != null) {
            return false;
        }else if(!name.equals(otherPerson.name)) {
            return false;
        }
        
        if(residentNumber == null && otherPerson.residentNumber != null) {
            return false;
        }else if(!residentNumber.equals(otherPerson.residentNumber)) {
            return false;
        }
        
        return true;
    }
}
 
cs


Main Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package blog.hashtable;
 
import java.util.Hashtable;
 
public class HashTableTester {
    
    public static void main(String[]args) {
        
        /**
         * Hashtable 의 Key 를 객체로 사용하기.
         */
        
        HashTableTester tester = new HashTableTester();
        
        tester.testingHashTableKeyByObject();
    }
    
    /**
     * 오브젝트를 이용한 해시테이블 키 사용 <p>
     */
    public void testingHashTableKeyByObject() {
        
        /**
         * 채팅방을 생각하자. 채팅방에는 여러 명의 사람이 대화를 하고 있으며, <p>
         * 
         * 해당 채팅방에는 여러 명의 사람들이 들어왔다 나가기를 반복한다. <p>
         * 
         * 등장인물은 아래와 같다. <p>
         * 
         * 안정환, 손흥민, 박지성, 이강인, 무리뉴, 호날두, 메시 <p>
         * 
         * 채팅방 입장 :: 1 <p>
         * 
         * 채팅방 퇴장 :: 0 <p>
         */
        
        /**
         * [-- 순서 --]
         * 
         * (1) 안정환 입장
         * (2) 손흥민 입장
         * (3) 박지성 입장
         * (4) 안정환 퇴장
         * (5) 무리뉴 입장
         * (6) 호날두 입장
         * (7) 메시 입장
         * (8) 호날두 퇴장
         * (9) 손흥민 퇴장
         * (10) 박지성 퇴장
         * (11) 호날두 입장
         * (12) 안정환 입장
         * (13) 메시 퇴장
         * (14) 이강인 입장
         */
        
        Hashtable<Person, Integer> chatRoomTable = new Hashtable<Person, Integer>();
        
        /**
         * - new Person(이름, 성별, 주민번호(4자리))
         * - 1 :: 입장
         * - 0 :: 퇴장 
         */
        
        chatRoomTable.put(new Person("안정환""남""1234"), 1);
        chatRoomTable.put(new Person("손흥민""남""4423"), 1);
        chatRoomTable.put(new Person("박지성""남""2463"), 1);
        chatRoomTable.put(new Person("안정환""남""1234"), 0);
        chatRoomTable.put(new Person("무리뉴""남""7777"), 1);
        chatRoomTable.put(new Person("호날두""남""3984"), 1);
        chatRoomTable.put(new Person("메시""남""6184"), 1);
        chatRoomTable.put(new Person("호날두""남""3984"), 0);
        chatRoomTable.put(new Person("손흥민""남""4423"), 0);
        chatRoomTable.put(new Person("박지성""남""2463"), 0);
        chatRoomTable.put(new Person("호날두""남""3984"), 1);
        chatRoomTable.put(new Person("안정환""남""1234"), 1);
        chatRoomTable.put(new Person("메시""남""6184"), 0);
        chatRoomTable.put(new Person("이강인""남""5531"), 1);
        
        /**
         * 현재 채팅방에 남아있는 사람들 확인
         */
        checkPersonInChatRoom(chatRoomTable);
    }
    
    /**
     * 채팅방에 남아있는 사람 체크 <p>
     * @param paramChatRoom
     */
    private void checkPersonInChatRoom(Hashtable<Person, Integer> paramChatRoom) {
        
        int count = 0;
 
        for(Person person : paramChatRoom.keySet()) {
 
            boolean isEnter = (paramChatRoom.get(person) == 0) ? false : true;
            
            /** 퇴장 continue **/
            if(!isEnter) {
                continue;
            }
            
            System.out.println("현재 채팅방에 남아있는 사람 " + (++count) + " ==> " + person.getName() + "(" + person.getResidentNumber() + ")");
        }
        
    }
}
 
cs


추가적으로 Hashtable 이 어떻게 작동되는지 간략히 알고자 한다면 하단 링크를 참고

https://pasudo123.tistory.com/225?category=764421


+) 추가사항 (20190222)

위의 경우에서 만약 객체의 중복을 처리하고자 한다면 해시셋(Hashset) 을 이용하여도 무방하다. 해시맵이나 해시테이블의 경우에는 데이터의 효율적인 저장 및 탐색을 위함이 컸다면 중복에 대한 집합 처리로써는 해시셋이 유용하다.


여기서 짚고 넘어갈 점은 해시셋을 이용하더라도 내부적으로 해시맵이 구현되어 있다는 사실이다. 아래는 해시셋의 add() 메소드의 일부이다. 


private static final Object PRESENT = new Object();



public boolean add(E e){


  return map.put(e, PRESENT) == null;


}


add() 메소드를 구현하지만, 사실 내부에는 맵의 키로 저장하고자 하는 데이터를 삽입하고, 밸류로써는 Dummy 객체를 사용한다는 사실을 확인할 수 있다.


+) 추가사항 (20190307)

해시셋 혹은 해시테이블을 통해서 값의 중복을 제거한다고 위에 작성하였다. 객체의 중복을 제거한다고 하였을 때, 해당 멤버필드의 모든 값에 대해 equals() 또는 hashCode() 메소드를 오버라이딩 할 것인지 고민하여야 한다.


만약에

사람이라는 클래스에 대해서 멤버필드가 아래와 같이 존재한다고 가정하자.

  1. 이름
  2. 성별
  3. 나이
  4. 주민등록번호
이런 멤버필드에서 unique 하게 사용할 수 있는 멤버필드는 유일하게 주민등록번호다. 세상에는 "김진수" 라는 이름을 가진 무수한 사람들이 있으며 그 사람들 속에서 같은 나이와 같은 성별을 가진 사람이 분명히 존재한다. 여기서 김진수라는 인물이 개명을 한다면? 여기서 김진수라는 인물이 성별을 바꾼다면? 과 같은 부분을 고려했을때 equals() 와 hashCode() 메소드를 일부 멤버필드에만 적용해서 사용할 수 있을 것이다. 


Posted by doubler
,