2014년 7월 6일 일요일

Java Object의 hashCode()와 equals() 메소드

참고 :  http://www.javaworld.com/article/2074996/hashcode-and-equals-method-in-java-object---a-pragmatic-concept.html

java.lang.Object의 메소드인 hashCode()와 equals()는 보편적으로 사용되는 메소드는 아니지만 상황에 따라 오버라이드 되어 사용되기도 합니다. 오버라이드시 어떤 기능을 할 수 있는지 한번 알아보도록 하겠습니다.

hashCode()

Object에 포함된 기본 상태의 hashCode() 메소드는 객체에 대한 메모리 주소 매핑 정보를 기반으로 정수 값을 반환 합니다. Object 클래스의 소스를 보면 알 수 있겠지만 hashCode() 메소드는 다음과 같이 작성 되어 있습니다.

1
public native int hashCode();

hashCode() 메소드가 JVM상에 네이티브 코드로 직접 구현되어 있다는 것을 의미하며 메모리 주소를 특정한 지역 정보 형태로 제공한다는 것을 알 수 있습니다. 이런 특징이 있음에도 불구하고 이 메소드는 구현체에서 오버라이드 될 수 있습니다.

equals()

equals() 메소드는 두개의 객체가 동일한지 비교할 수 있는 특별한 메소드 입니다. 자바에는 동일함을 비교하는 방식이 두 종류가 있는데 하나는 "==" 연산자를 사용하는 것이며 다른 하나는 "equals()" 메소드 입니다. equals() 메소드의 경우 다음과 같이 작성 되어 있습니다.

1
2
3
public boolean equals(Object obj) {
        return (this == obj);
    }

오버라이드가 가능하다는 측면에서 둘의 차이를 좀더 생각해 보자면 객체의 비교에 있어 ".equals()"는 서로 같은 값을 갖는 관계를 이야기 하며 "==" 연산자의 경우 물리적으로 완전히 동일함을 이야기 한다고 볼 수 있을 것 입니다.

규약

hashCode()와 equals()는 결국 상호 연관관계에 있는 메소드로 둘은 동시에 오버라이드 되어야 하며 그렇지 않으면 API문서에 나와있는 메소드 규약을 위반하게 됩니다.

테스트

테스트를 위해 hashCode()와 equals()모두를 오버라이드한 클래스와 둘다 변경하지 않은 클래스를 작성 하였습니다.

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
package reference.value;
 
public class Robot {
 private int productNumber;
 private String modelName;
 
 public Robot(int productNumber, String modelName) {
  super();
  this.productNumber = productNumber;
  this.modelName = modelName;
 }
 
 public int getProductNumber() {
  return productNumber;
 }
 
 public void setProductNumber(int productNumber) {
  this.productNumber = productNumber;
 }
 
 public String getModelName() {
  return modelName;
 }
 
 public void setModelName(String modelName) {
  this.modelName = modelName;
 }
 
}

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
package reference.value;
 
public class CustomRobot {
 private int productNumber;
 private String modelName;
 
 public CustomRobot(int productNumber, String modelName) {
  super();
  this.productNumber = productNumber;
  this.modelName = modelName;
 }
 
 public int getProductNumber() {
  return productNumber;
 }
 
 public void setProductNumber(int productNumber) {
  this.productNumber = productNumber;
 }
 
 public String getModelName() {
  return modelName;
 }
 
 public void setModelName(String modelName) {
  this.modelName = modelName;
 }
 
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((modelName == null) ? 0 : modelName.hashCode());
  result = prime * result + productNumber;
  return result;
 }
 
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  CustomRobot other = (CustomRobot) obj;
  if (modelName == null) {
   if (other.modelName != null)
    return false;
  } else if (!modelName.equals(other.modelName))
   return false;
  if (productNumber != other.productNumber)
   return false;
  return true;
 }
 
}

이 둘의 차이를 알아보기 위해 다음과 같은 테스트 클래스를 작성하였습니다.

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
package reference.value;
 
import java.util.HashSet;
 
public class TestHashAndEqual {
 
 public static void main(String[] args) throws Exception {
  Robot r1 = new Robot(1999, "R2D2");
  Robot r2 = new Robot(1999, "R2D2");
  Robot r3 = r1;
   
  // toString in Object
  System.out.println("r1 is " + r1 + " , r2 is " + r2 );
  System.out.println("r1 == r2 result : " + (r1 == r2 ? true : false));
  System.out.println("r1 == r3 result : " + (r1 == r3 ? true : false));
  System.out.println("Equals between r1 and r2 : " + r1.equals(r2));
  System.out.println("HashCode r1 : " + r1.hashCode() + " , r2 : " + r2.hashCode());
  System.out.println("Identity HashCode r1 : " + System.identityHashCode(r1) + " , r2 : " + System.identityHashCode(r2));
   
  HashSet<Robot> rHS = new HashSet<Robot>();
   
  rHS.add(r1);
  rHS.add(r2);
  rHS.add(r3);
   
  System.out.println("HashSet Size : " + rHS.size());
   
  System.out.println("\n==============================\n");
   
  HashSet<CustomRobot> cRHS = new HashSet<CustomRobot&gt();
   
  CustomRobot cR1 = new CustomRobot(1999, "R2D2");
  CustomRobot cR2 = new CustomRobot(1999, "R2D2");
  CustomRobot cR3 = cR1;
   
  System.out.println("cR1 is " + cR1 + " , cR2 is " + cR2 );
  System.out.println("cR1 == cR2 result : " + (cR1 == cR2 ? true : false));
  System.out.println("cR1 == cR3 result : " + (cR1 == cR3 ? true : false));
  System.out.println("Equals between cR1 and cR2 : " + cR1.equals(cR2));
  System.out.println("HashCode cR1 : " + cR1.hashCode() + " , cR2 : " + cR2.hashCode());
  System.out.println("Identity HashCode cR1 : " + System.identityHashCode(cR1) + " cR2 : " + System.identityHashCode(cR2));
   
  cRHS.add(cR1);
  cRHS.add(cR2);
  cRHS.add(cR3);
   
  System.out.println("HashSet Size : " + cRHS.size());
   
 }
 
}

실행한 결과는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
r1 is reference.value.Robot@15db9742 , r2 is reference.value.Robot@6d06d69c
r1 == r2 result : false
r1 == r3 result : true
Equals between r1 and r2 : false
HashCode r1 : 366712642 , r2 : 1829164700
Identity HashCode r1 : 366712642 , r2 : 1829164700
HashSet Size : 2
 
==============================
 
cR1 is reference.value.CustomRobot@49b52c2 , cR2 is reference.value.CustomRobot@49b52c2
cR1 == cR2 result : false
cR1 == cR3 result : true
Equals between cR1 and cR2 : true
HashCode cR1 : 77288130 , cR2 : 77288130
Identity HashCode cR1 : 2018699554 cR2 : 1311053135
HashSet Size : 1
두 메소드의 오버라이드를 통해 내용면에서 같은 멤버변수의 값을 갖는 객체라면 물리적으로 동일하지 않더라도 논리적으로는 서로 같은 객체로 인식하는 것을 알 수 있습니다.

HashSet의 경우 중복을 배제하는 컬렉션 클래스이기 때문에 사이즈 또한 이에 기반하여 증가하는 것을 확인 할 수 있으며 HashMap의 경우 이를 이용하여 Key에 일반적으로 사용되는 String이 아닌 별도의 객체를 이용하여 키를 구성하는 것 또한 가능하게 됩니다.