Language/Java

[Java] 자바의 객체 비교 메소드 equals 그리고 hashcode (eclipse 메소드 자동 완성)

KAispread 2022. 5. 25. 00:48
728x90
반응형

기본 자료형(byte, int, float, boolean 등)은 "==" 만으로 값이 같은지 확인할 수 있다.
하지만 객체는 == 연산자 만으로 두 객체가 같은지 판단할 수 없다.
객체에 "==" 를 사용할경우, 값이 아닌 주소값을 비교하기 때문이다.


 

  • equals() - 동등성 비교 후 boolean 값 (true | false)를 반환
    (기본 - 주소 비교)
  • hashCode() - 객체의 고유 값(int)을 반환
    (기본 - heap에 저장된 객체의 메모리 주소 값)
  • == 연산자 - 기본 자료형의 '값'을 비교하기위해 사용

 

 

equals()메소드의 필요성

자바에서 정수 값을 비교하기 위한 연산자는 == 이다.

int a = 7;
int b = 7;

if(a == b) { 
	System.out.println("a and b are same"); //"a and b are same"출력
}

위 코드에서, a와 b는 같은 값을 가지고 있으므로 if문 안의 문장이 실행된다. 이와같이 int와 같은 기본 자료형에서는 "==" 연산자로만 두 변수가 같은지 판별할 수 있다.

하지만 객체를 비교할때도 "=="연산자만으로 같은 객체인지 아닌지를 판별해낼 수 있을까? 예시를 보자.

public class Job {
   int salary;
   String name;
   
   public void setSalary(int a) {
      this.salary = a;
   }
   public void setName(String a) {
      this.name = a;
   }
}

다음과 같은 Job이라는 클래스가 있다.

public class JobCompare{

   public static void main(String[] args) {
      Job job1 = new Job();
      Job job2 = new Job();
      
      job1.setName("teacher"); // job2와 같은 값
      job1.setSalary(300);     // job2와 같은 값
      
      job2.setName("teacher"); // job1와 같은 값
      job2.setSalary(300);     // job1와 같은 값
      
      if(job1 == job2) {
         System.out.println("Job1 and job2 are same objects");
      } else if(job1 != job2) {
         System.out.println("Job1 and job2 are different objects");
      }
   }
}

JobCompare이라는 클래스에서 두개의 객체를 생성하고, 객체의 인스턴스로 각각 같은 값을 넣어주었다. 이 두개의 객체는 같은 인스턴스 변수를 가지고 있으므로 사실상 같은 객체라고 볼 수 있다. 그렇다면 job1==job2 가 true가 반환되어 if 문장이 실행될까?

else if 문장이 실행됨.

결과는 그렇지 않다. 그 이유는 "==" 연산자를 참조 자료형, 즉 객체에 사용할경우 '값'이 아닌 '주소값'이 같은지 비교하기 때문이다.

 

 

String 객체의 예외 경우

String 객체는 분명히 참조 자료형이다. 객체에 == 연산자를 사용하면 '값'이 아닌 '주소값'을 비교하므로, 다음 코드에서 if 문장이 실행되지 않는다고 생각 할 것이다.

String a = "A";
String b = "A";

if(a == b) { 
   System.out.println("a and b are same");
}

실행 결과

하지만 if 문장이 실행되었다. String 객체는 참조 자료형이지만 == 연산자를 통해서 값을 비교할 수 있다는 이야기인데, 왜 그럴까?
위의 코드를 보면 String a 에 "A"라는 문자열을 할당해주었다. 이후 String b에 "A"라는 기존에 존재하는 같은 문자열을 할당할경우, 기존에 "A"라는 문자열이 메모리의 한 부분에 저장되어있기 때문에, 메모리를 새로 할당하는 것이 아닌, 같은 주소값을 가리키게 된다. 따라서, String a와 String b의 주소값은 같아지게 되는 것이다.
== 연산자를 객체에 사용 할 경우 '주소값'을 비교하기 때문에 이와 같은 결과가 나온것이다.

String a = "A";
String b = new String("A");

if(a == b) { 
   System.out.println("a and b are same"); //실행되지 않음
}

하지만 다음과 같이 생성자를 이용하여 String 객체를 생성할 경우, 서로 다른 메모리 주소값을 가지게 된다.

 

 

equals() 메소드

equals() 메소드를 Override 하지 않았을 경우

== 연산자로 객체를 비교할 수 없기 때문에 자바에서는 equals()라는 함수를 제공한다.

equals(), hashcode(), toString() ... 등등의 메소드는 Object클래스에서 정의되어있고 Object클래스는 모든 참조 자료형의 상위 클래스이기때문에 이 메소드들을 사용 할 수 있다.


아까 예시로 사용했던 코드에서 객체를 비교하는 부분(==)을 equals() 메소드로 바꿔보자.

public class JobCompare{

   public static void main(String[] args) {
      Job job1 = new Job();
      Job job2 = new Job();
      
      job1.setName("teacher");
      job1.setSalary(300);
      
      job2.setName("teacher");
      job2.setSalary(300);
      
      if(job1.equals(job2)) {
         System.out.println("Job1 and job2 are same objects");
      } else {
         System.out.println("Job1 and job2 are different objects");
      }
   }
}

같은 인스턴스 변수를 가진 두 객체를 equals() 메소드로 비교해보았다. 결과는 다음과 같다.

else 문장이 실행됨.

if 문 안의 문장이 실행되길 바랬지만, 기대와는 다르게 else 문장이 실행된 모습이다.
그 이유는, Object 클래스에서 정의된 equals() 메소드는 객체의 '인스턴스 변수 값'을 비교하는 것이 아닌 hashCode() 값을 비교하기 때문이다. (hashCode() 메소드는 해당 객체의 주소값을 반환한다.)
결과적으로 두 객체가 같은 인스턴스 값을 가지기 때문에 실질적으로 같은 객체이더라도, 다른 생성자로 선언되었기때문에 '주소값'이 달라, 같은 객체인지 판별할 수가 없는 것이다.
따라서, equals() 메소드가 '주소값'이 아닌 '인스턴스 변수'들을 비교하여 같은 객체인지 판별하게끔 (Overriding을 통해) 바꾸어야한다.

 

 

equals() 메소드 재정의

이클립스에서 자동으로 생성해주는 equals() 메소드는 다음과 같다.

public class Job {
   
   // ... 생략
   
   @Override
   public boolean equals(Object obj) {
      if (this == obj) {
         return true;
      }
      if (!(obj instanceof Job)) {
         return false;
      }
      Job other = (Job) obj;
      if (name == null) {
         if (other.name != null) {
            return false;
         }
      } else if (!name.equals(other.name)) {
         return false;
      }
      if (salary != other.salary) {
         return false;
      }
      return true;
   }
   
}

첫 번째 if 문에서는 두 객체의 주소값이 같은지 비교한다. 주소값이 같다면 완전히 같은 객체이기 때문에 더 따질것도 없이 true를 반환하면서 메소드를 종료한다.
두 번째 if 문에서는 두 객체의 자료형이 같은지를 비교한다. (상속받은 경우에도 가능) 두 객체의 자료형이 같지 않다면 같은 객체로 볼 수 없으므로 false를 반환하면서 메소드를 종료한다.
Job other = (Job) obj; 명령을 통해 매개변수로 받은 객체가 자식 클래스일 경우 'Job'으로 자료형을 맞춰준다.
이후 세번째 if 문부터는 객체의 인스턴스 변수들이 일일이 같은 값을 가지고 있는지 비교하고, 다르다면 false를 반환하고 메소드를 종료한다.
마지막 문장까지 왔을 경우, 모든 인스턴스 변수들의 값이 같다는 의미이므로 true를 반환한다.

equals() 메소드를 Overriding해야 할 경우 지켜야 할 5가지 조건 (Oracle 공식 문서)

  • 재귀 : null이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true여야 한다.
  • 대칭 : null이 아닌 x와 y 객체가 있을 때 y.equals(x)가 true를 리턴했다면, x.equals(y)도 반드시 true를 리턴해야만 한다.
  • 타동적 : null이 아닌 x,y,z가 있을 때 x.equals(y)가 true를 리턴하고, y.equals(z)가 true를 리턴하면, x.equals(z)는 반드시 true를 리턴해야만 한다.
  • 일관 : null이 아닌 x와 y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도, x.equals(y)의 결과는 항상 true이거나 항상 false이여야 한다.
  • null과의 비교 : null이 아닌 x라는 객체의 x.equals(null)의 결과는 항상 false이여야 한다.

eclipse나 intelliJ같은 개발툴에서는 이와 같은 규칙을 지켜서 자동으로 메소드를 Overriding 해주기때문에 직접 메소드를 작성하는 것보다는 개발툴의 기능을 활용하는 것이 더욱 권장된다.

 

 

hashCode() 메소드

equals() 메소드를 Overriding 할 때는 반드시 hashCode() 메소드또한 같이 Overriding 해야한다.
두 객체를 equals() 메소드로 비교했을 때 true를 반환했다면, 두 객체의 hashCode() 리턴값도 서로 같아야하기 때문이다. (HashMap과 같은 자료형을 사용 할 때, hashCode()의 값을 비교하기 때문이다.)

따라서 eclipse에서도 equals() 메소드와 hashCode() 메소드를 같이 Overriding 하도록 되어있다.

eclipse의 메소드 자동완성 기능


아까 설명에서 hashCode() 메소드는 기본적으로 객체의 주소값을 리턴한다고 했다. equals() 메소드를 Overriding 할 때, 인스턴스 변수 값을 비교하여 두 객체가 같은지를 판별한 것 처럼, hashCode()메소드도 인스턴스 변수 값을 이용하여 같은 변수 값을 가질 경우 같은 hashCode() 리턴 값을 갖게끔 만들어야 한다.

 

hashcode() 메소드 재정의

마찬가지로 이클립스에서 자동으로 생성해주는 hashCode() 메소드는 다음과 같다.

public class Job {
   
   // ... 생략
   
   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      result = prime * result + salary;
      return result;
   }
}

인스턴스 변수를 이용하여 고유의 값을 리턴하도록 되어있다.
prime 상수 값이 31인 이유는, (약수가 자기 자신(31)과 1뿐인) '소수'이자 홀수이기 때문이다.
(31을 곱할경우, hashCode()의 리턴 값이 겹칠 확률이 상대적으로 적다.)

hashCode() 메소드를 Overriding해야 할 경우 지켜야 할 3가지 조건 (Oracle 공식 문서)

  • 자바 어플리케이션이 수행되는 동안에 어떤 객체에 대해서 이 메소드가 호출될 때에는 항상 동일한 int 값을 리턴해주어야 한다. 하지만, 자바를 실행할 때마다 같은 값이어야 할 필요는 없다.
  • 어떤 두개의 객체에 대하여 equals()메소드를 사용하여 비교한 결과가 true일 경우에, 두 객체의 hashCode() 메소드를 호출하면 동일한 int 값을 리턴해야만 한다.
  • 두 객체를 equals() 메소드를 사용하여 비교한 결과 false를 리턴했다고 해서, hashCode() 메소드를 호출한 int 값이 무조건 달라야 할 필요는 없다. 하지만, 이 경우에 서로 다른 int 값을 제공하면 hashtable의 성능을 향상시키는 데 도움이 된다.

마찬가지로, eclipse나 intelliJ같은 개발툴에서는 이와 같은 규칙을 지켜서 자동으로 메소드를 Overriding 해주기때문에 직접 메소드를 작성하는 것보다는 개발툴의 기능을 활용하는 것이 더욱 권장된다.

 

 

eclipse 메소드 자동 완성

eclipse와 같은 IDE에서는 이와 같은 메소드들을 편리하게 Override 할 수 있도록 아래와 같은 기능을 제공하고 있다.

소스코드 창에서 우클릭 후 Source -> Generate hashCode and equals() 클릭.

지시대로 클릭했을 경우 이 창이 뜨게된다.
1. hashCode()와 equals() 메소드에 사용될 인스턴스 변수를 정한다. (만약, salary라는 변수를 체크 해제할 경우 name 변수만 같아도 hashCode()와 equals()가 같은 객체로 판단한다.)
2. Overriding 되는 메소드를 추가할 위치를 선택한다. (default는 맨 마지막에 추가됨)
3. 위 옵션들을 체크/체크해제 함에 따라서 Overriding 되는 메소드의 코드 스타일이 바뀐다. (어떻게 설정하든 기능은 같다.)
이후, Generate를 누르면 다음과 같이 자동으로 Overriding 된다.

hashCode()와 equals()메소드가 Override됨.

 

참고
ㆍ"자바의 신 vol.1", 이상민 저자
ㆍhttps://nesoy.github.io/articles/2018-06/Java-equals-hashcode
728x90
반응형