[Java] Object 클래스와 equals(), hashCode()

2022. 11. 24. 22:16BackEnd

해당글은 아래 내용을 재구성하여 작성하여 보았습니다.

- 자바의 신

- 이펙티브 자바

- 자바 공식 도큐먼트


모든 클래스는 Object 클래스의 상속을 받는다. Object 클래스에 있는 메소드들을 통해서 클래스의 기본적인 행동을 정의할 수 있기 때문이다. ( 자바의 신 p.319 )

 

https://docs.oracle.com/javase/10/docs/api/java/lang/Object.html

위 공식 도큐먼트에서는 Thread 처리를 위한 메소드들과 함께 명시가 되어있다. 아래는 자바의 신 내용을 발췌하였다.

메소드 설명
protected Object clone() 객체의 복사본을 만들어 리턴
public boolean equals(Object obj) 현재 객체와 매개 변수로 넘겨받은 객체가 같은지 확인하여, 같으면 true, 다르면 false를 반환한다.
protected void finalize() 현재 객체가 더 이상 쓸모가 없어졌을 때 GC에 의해서 호출되는 메서드
public Class<?> getClass() 현재 객체의 Class 클래스의 객체를 리턴한다.
public int hashCode() 객체에 대한 해시 코드값을 리턴한다. 16진수로 제공되는 객체의 메모리 주사가 해시코드다.
public String toString() 객체를 문자열로 표현하는 값을 리턴한다.

 

위 내용중 hashCode()와 equals(Object obj)에 대해서만 조금 더 다뤄본다.

 

 


equals(Object obj)

 

equals()는 동등성 비교를 하기 위한 목적의 메소드입니다. 그리고 동일성을 비교하려면 '==' 연산자를 사용합니다. 객체는 동일성으로는 비교가 되지 않으므로 동등성을 비교하는 equals()를 사용하여야 한다.

 

동등성을 비교하기 위해서는 각 필드들을 각각 비교해야 한다. 자바의 신 책에 따르면 다섯 가지 조건을 만족시켜야 한다.

  • reflexive : null이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true여야 한다 -> 자기자신은 자기자신과 항상 동등하다.
  • symmetric : null이 아닌 x와 y 객체가 있을 때, y.equals(x)가 true이면 x.equals(y)도 반드시 true를 리턴해야 한다.
  • transitive : null이 아닌 x,y,z가 있다면 (마치 삼단 논법처럼) x.equals(y)가 true, y.equals(z)가 true이면 x.equals(z)는 반드시 true
  • consistent : null이 아닌 x,y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도 결과는 항상 이전 결과와 동일해야 한다.
  • null과의 비교 : null이 아닌 x라는 객체의 x.equals(null) 결과는 항상 false여야만 한다.

이펙티브자바 아이템 10에서도 동일한 내용을 언급하고 있는데 '동치관계(equivalence relation)를 구현하여 위 내용을 만족한다.'라고 명시되어 있다.

 

 

일반적으로 아래 상황 중에 하나에 해당한다면 재정의 하지 않는 것이 최선이라고 한다. ( 이펙티브 자바 출처 )

  • 각 인스턴스가 본질적으로 고유하다. ( 동작하는 개체를 표현하는 클래스 )
  • 인스턴스의 '논리적 동치성(위 내용)'을 검사할 일이 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
쉽게 이야기하면 논리적으로 클래스에 선언된 필드들이 정확하게 값이 일치해야 되는 상황에서 equals()를 쓰라는 이야기로 들린다.

이펙티브 자바에서는 양질의 equals 메서드 구현 방법을 단계별로 정리하였다.

 

  1. == 연산자를 사용해서 입력이 자기 자신의 참조인지 확인한다. 자기 자신이면 true를 반환한다. 이는 단순한 성능 최적화용이므로, 비교 작업이 복잡한 상황일 때 값어치를 할 것이다.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다. 그렇지 않다면 false를 반환한다. 이 때의 올바른 타입은 equals가 정의된 클래스인 것이 보통이지만, 가끔은 클래스가 구현한 특정 인터페이스가 될 수도 있다.
  3. 입력을 올바른 타입으로 형변환한다.
  4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다. 모든 필드가 일치하면 true, 그렇지 않으면 false를 반환한다.

이펙티브 자바에서 제시하는 예시이다. 구글링을 해봐도 일반적으로 아래오 같이 구현하는 예시들이 많다. 참고참고

public final class PhoneNumber {
	private final short areaCode, prefix, lineNum;
    
    public PhoneNumber(int areaCode, int prefix, int lineNum){
		this.areaCode = rangeCheck(areaCode, 999, "지역코드");
        this.prefix = rangeCheck(prefix, 999, "프리픽스");
        this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
    }
    
    private static short rangeCheck(int val, int max, String arg){
    	if(val < 0 || val > max)
        	throw new IllegalArgumentException(arg + ": " + val);
        return (short) val;
    }
    
    @Override
    public boolean equals(Object o) {
    	if( o == this )
        	return true;
        if( !(o instanceof PhoneNumber) )
        	return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
    }
	... //나머지 코드 생략
}

 

유념해야 될 부분은 equals() 메소드를 Overriding 할 때에는 hashCode()도 같이 Overriding 해야만 한다. 자바의 신에서는 'equals() 메서드의 값이 같다고해서 그 객체의 주소값이 같지는 않기 때문이다.'라고 언급하는데 이펙티브 자바에서는 조금 더 상세하게 언급한다.

 

Object 명세서 규약에 따르면 아래 3가지를 따른다.

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. ( 단 애플리케이션이 다시 실행한다면 이 값이 달라져도 상관없다. )
  • equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

자바의 신에 나오는 이야기와 종합하면 문제가 되는 규약은 2번째 규약이다. 똑같지 않을 수가 있다. 이런 문제는 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.

 

그 이유는 Hash가 붙은 자료구조들이 아래와 같은 방식으로 객체를 비교하기 때문이다.

 


hashCode() : 객체의 고유 값 ( 메모리 주소의 16진수 )을 리턴하는 메서드

 

hashCode()를 같이 재정의 해야하는 이유는 위에서 언급하였기에 Pass. 이펙티브 자바에서는 좋은 hashCode()를 작성하는 요령을 제시한다. 이상적인 해시 함수는 주어진 (서로 다른) 인스턴스들을 32비트 정수 범위에 균일하게 분배해야 하는 것을 말한다.

 

  1. int 변수 result를 선언한 후 값 c로 초기화 한다. 이 때, c는 해당 객체의 첫번째 핵심 필드를 단계 2.1 방식으로 계산한 해시코드다.
  2. 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.
    1. 해당 필드의 해시코드 c를 계산한다.
      1. 기본 타입 필드라면, Type.hashCode(f)를 수행한다. 여기서 Type은 해당 기본 타입의 박싱 클래스다.
      2. 참조 타입 필드이면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다. 계산이 더 복잡해질 것 같으면, 이 필드의 표준형을 만들어 그 표준형의 hashCode를 호출한다. 필드의 값이 null이면 0을 사용한다.
      3. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 모든 원소가 핵심 원소라면 Arrays.hashCode()를 사용한다.
    2. 단계 2.1에서 계산한 해시코드 c로 result를 갱신한다. 
  3. result를 반환한다.

 

이러한 방식으로 hashCode()를 재정의한다. 근데 두권의 책 모두에서 AutoValue 프레임워크나 IDE에서 제공하는 짱짱맨 기법을 적용하라고 조언한다.

 


두권의 책을 짜깁기하여 정리하였는데, 특히 이펙티브자바의 이야기는 좀 어렵기 때문에 정리를 다시금 잘 해야겠다는 생각이 듭니다. 결국 위 내용의 핵심을 다시금 정리하자면 아래의 순서로 되짚어 보려고 합니다.

 

- Object 클래스가 필요한 이유

- Object 클래스에서 equals(), hashCode()를 재정의 하는 이유

- 어떤 상황에서 재정의를 해야하는지?

- 재정의 시 주의사항

 

주의사항이나 재정의 방법은 세세하게 다 외우면 좋겠지만, 업무에서 필요시 참고를 하는 편이 좋을듯하고 핵심적인 내용만 맥락정보로 가지고 있으면 될 것 같습니다.

 


책 내용을 보면서 이해가 잘 되지 않는 부분은 망나니개발자님 블로그 참고하였고, 다만 대부분의 내용은 책의 내용이 공신력이 있다고 판단되어 책 내용 위주로 정리하였습니다.