본문 바로가기
[JAVA]/JAVA 기본

자바의 Object 클래스와 equals(), hashCode() 메서드에 대해 알아보자

by 황원용 2023. 8. 17.
728x90

Object 클래스

  • 자바에서 모든 클래스가 공통으로 가지는 상위 클래스이다.
  • 다르게 말하면 자바의 모든 클래스는 Java.lang.Object 클래스의 하위 클래스이다.
  • 객체를 표현하는 데 필요한 메서드를 제공한다.
  • 클래스가 명시적으로 상위 클래스를 상속하지 않을 경우 컴파일러가 자동으로 Object 클래스를 상속하게 된다.
  • 따라서, 하나의 클래스가 다른 클래스를 상속받는다고 하더라도 올라가다 보면 그 맨 위는 Object 클래스인 것이다.

 

 

Object Class의 메서드

메서드 명 역할
clone() 객체를 복제하여 새로운 인스턴스를 생성
equals(Object obj) 해당 객체와 매개변수로 전달된 객체를 비교하여 동일한지 여부를 반환
hashCode() 해당 객체의 해시코드 값을 반환
toString() 해당 객체의 문자열 표현을 반환
getClass() 해당 객체의 클래스 정보를 반환
notify() 해당 객체에 대한 대기 중인 하나의 스레드를 깨움
notifyAll() 해당 객체에 대한 대기 중인 모든 스레드를 깨움
wait() 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦
wait(long timeout) 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦.
최대 대기 시간을 밀리초 단위로 설정 가능
wait(long timeout, int nanos) 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦.
최대 대기 시간을 밀리초와 나노초로 설정 가능
finalize() 가비지 컬렉션에 의해 객체가 메모리에서 해제되기 직전에 호출되는 메서드 (Defrecated)
  • Object 클래스는 필드를 가지지 않는다.
  • 이 글에서는 equals(), hashCode()에 대해 자세히 알아보려고 한다.

 

 

equals() 메서드

문자열 비교

String a = "intp";
String b = "intp";
String c = new String("intp");

System.out.println(a == b); // (1) true
System.out.println(a.equals(b)); // (2) true

System.out.println(a == c); // (3) false
  • equlas() 메서드는 보통 비교 연산자 '=='와 많이 비교가 된다.
  • 문자열의 경우 '=='는 데이터의 메모리 주소값이 같은지 여부를 비교하지만, equals() 메서드는 문자열의 값이 같은지 비교한다.
  • 따라서 위 코드에서 (2)는 문자열 값을 비교하기 때문에 true인 것이다.
  • 그런데 주소값을 비교하는 (1)은 왜 true인 것일까?
    • 자바에서 String 리터럴은 내부적으로 같은 값을 가진 경우에는 동일한 String 객체를 참조하기 때문이다.
    • String a와 b는 "intp"이라는 동일한 리터럴을 가지고 있음으로 같은 String 객체를 참조하는 것이다.

 

 객체 비교

Student student1 = new Student(1, "손흥민", "A+");
Student student2 = new Student(1, "손흥민", "A+");

System.out.println(student1 == student2); // (1) False
System.out.println(student1.equals(student2)); // (2) False
Assertions.assertEquals(student1, student2); // (3) Test failed
// expected: <study.object.Student@331ad6eb> but was: <study.object.Student@6cd6698b>
// Expected :study.object.Student@331ad6eb
// Actual   :study.object.Student@6cd6698b
  • 객체의 경우 equals() 메서드 역시 객체의 주소를 이용하여 비교한다.
  • 따라서 (1), (2)처럼 객체 간 비교에서는 ==, equals() 메서드 모두 주소값을 이용하여 비교하기 때문에 false가 나온다.
  • (3)을 통해 실제 참조하는 메모리 주소값이 같은 값이 있는 객체라도 다르다는 것을 확인할 수 있다.

 

 

equals 오버라이딩

  • 컴퓨터의 관점에서는 객체 간 비교시 메모리 주소값을 가지고 비교하기 때문에 주소가 일치하지 않으면 같은 객체가 아니라고 판단한다.
  • 그러나 사람의 관점에서는 같은 데이터라고 볼 수도 있다.
  • 필드값이 완전히 똑같기 때문이다.
  • 따라서 만약 객체 타입을 비교할 때 비교 기준을 객체의 주소값이 아닌  필드값으로 하고 싶다면 equals() 메서드의 오버라이딩을 통해 재정의해주면 된다.

 

 

예시 코드

 @Getter
@AllArgsConstructor
public class StudentOverriding {
    private int studentId;

    private String name;

    private String grade;

    /**
     * @param obj 비교 대상
     * @return 필드값 비교를 통해 값이 같은지 여부 리턴
     */
    @Override
    public boolean equals(Object obj) {
        // 두 비교 객체가 같은 객체일 경우 true
        if (this == obj) return true;

        // 객체가 Null이거나 Student를 포함하는 하위 클래스가 아니라면 false 리턴
        else if (obj == null || !(obj instanceof StudentOverriding)) return false;

        // 다운캐스팅을 통해 obj를 StudentOverriding 인스턴스로 변경 후
        // 비교 주체와 비교 대상의 필드값을 비교하여 모두 같으면 같은 객체로 취급함
        else {
            StudentOverriding studentOverriding = (StudentOverriding) obj;
            boolean result =
                    studentId == studentOverriding.getStudentId() &&
                    name.equals(studentOverriding.getName()) &&
                    grade.equals(studentOverriding.getGrade());

            return result;
        }
    }
}
  • 위와 같이 equals() 메서드를 오버라이딩하여 비교 주체와 대상의 필드값을 비교하는 방식으로 재정의했다.
  • a.equals(b)일 때 , a가 비교 주체이고 b가 비교 대상이다.

 

테스트 코드

@Test
public void studentOverridingEquals() {
    StudentOverriding studentOverriding1 = new StudentOverriding(2, "해리케인", "B");
    StudentOverriding studentOverriding2 = new StudentOverriding(2, "해리케인", "B");

    System.out.println(studentOverriding1.equals(studentOverriding2)); // true
    Assertions.assertEquals(studentOverriding1, studentOverriding2);
}
  • StudentOverriding의 경우 equals() 메서드를 재정의하였기 때문에 주소값이 아닌 필드값을 통해 비교한다.
  • 따라서 true를 리턴한다.

 

 

hashCode() 메서드

  • 객체의 주소값을 해싱하여 나온 해시 코드를 반환한다.
  • 주소값으로 만든 고유한 값이기 때문에 객체의 지문이라고도 한다.

 

@Test
public void studentHashCode() {
    Student student1 = new Student();
    Student student2 = new Student();

    System.out.println(student1.hashCode()); // 988637485
    System.out.println(student2.hashCode()); // 1324113830
}
  • 기본적으로 두 인스턴스의 주소값은 다르기 때문에 다른 주소값을 해싱한 결과는 같을 수 없다.

 

 

equals()와 hashCode() 메서드

  • 두 메서드는 같이 재정의해야한다.
  • equals() 메서드만을 재정의할 경우 자바의 컬렉션 프레임워크를 사용 시에 의도와는 다른 결과가 발생하기 때문이다.

 

equals() 메서드만 재정의

@Test
public void onlyEquals() {
    StudentOverriding studentOverriding1 = new StudentOverriding();
    StudentOverriding studentOverriding2 = new StudentOverriding();

    // List에 student 1,2 추가
    List<StudentOverriding> studentList = new ArrayList<>();
    studentList.add(studentOverriding1);
    studentList.add(studentOverriding2);

    // List 요소 개수 출력
    System.out.println(studentList.size()); // 2

    // Set에 student 1,2 추가
    Set<StudentOverriding> studentSet = new HashSet<>();
    studentSet.add(studentOverriding1);
    studentSet.add(studentOverriding2);

    // Set 요소 개수 출력
    System.out.println(studentSet.size()); // 2
}
  • 위 코드에서 StudentOverriding 클래스는 equals() 메서드만 재정의한 클래스이다.
  • 여기서 List와 Set에 StudentOverriding 클래스의 인스턴스를 요소로 추가하고 size() 메서드로 개수를 출력해 보면 List와 Set 모두 2가 나온다.
  • Set의 경우 중복을 제거하기 때문에 equals() 메서드로 같다고 나온 두 인스턴스는 중복으로 처리되어 1이 출력되어야 하는데 말이다.
  • 이렇게 동작하는 이유는 컬렉션은 객체가 논리적으로 같은지를 판단할 때 hashCode의 리턴값을 비교하게 되고 만약 같다면 그다음으로 equals() 메서드를 사용해 비교하기 때문이다.
  • 즉, hashCode() -> equals() -> true return의 순서로 동작하기 때문에 먼저 비교하는 hashCode()에서 서로 다른 객체라고 판단하고 비교 로직을 끝내버리는 것이다.
  • 따라서 equals() 메서드를 재정의하는 경우에는 hashCode()도 함께 재정의해야 한다. 

 

 

예시 코드

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StudentOverriding {
    private int studentId;

    private String name;

    private String grade;

	...
    
    @Override
    public int hashCode() {
        return Objects.hashCode(studentId); // studentId 필드의 해시코드를 반환한다.
    }
}
  • 필드값 중 하나인 studentId는 unique 하기 때문에 이 값을 기준으로 해시코드를 반환하도록 했다.
  • 이제 hashCode() 메서드에서 동일한 studentId를 비교하는 경우 같은 해시코드가 나와 같은 객체라고 판단할 것이고,
  • 그다음 equals() 메서드에서 필드값 전체를 비교하는 로직이 진행될 것이다.

 

테스트 코드

@Test
public void studentOverridingHashCode() {
    StudentOverriding studentOverriding1 = new StudentOverriding(2, "해리케인", "B");
    StudentOverriding studentOverriding2 = new StudentOverriding(2, "해리케인", "B");

    // List에 student 1,2 추가
    List<StudentOverriding> studentList = new ArrayList<>();
    studentList.add(studentOverriding1);
    studentList.add(studentOverriding2);

    // List 요소 개수 출력
    System.out.println(studentList.size()); // 2

    // Set에 student 1,2 추가
    Set<StudentOverriding> studentSet = new HashSet<>();
    studentSet.add(studentOverriding1);
    studentSet.add(studentOverriding2);

    // Set 요소 개수 출력
    System.out.println(studentSet.size()); // 1
}
  • studentOverrriding1, 2의 필드값은 모두 동일하다.
  • 위에서 설명한 대로 먼저 hashCode() 메서드로 studentId의 해시코드를 비교하는데 이때 studentId를 기준으로 비교하기 때문에 같은 객체라고 판단할 것이다.
  • 다음으로 equals() 메서드 비교를 통해 두 인스턴스의 필드값인 {StudentId : 2}, {name : "해리케인"}, {grade : "B"}를 비교하면 같은 객체라고 최종 판단을 하고 이는 중복 객체라고 인식하게 됨으로 Set 특성상 컬렉션의 요소로 studentOverrriding1만 추가했을 것이다.
  • 따라서 Set 요소 개수는 중복이 제거된 1로 출력된다.

 

 

@EqualsAndHashCode 애너테이션

  • Lombok 라이브러리에 포함된 애너테이션 중에 하나이다.
    • 자바 프로젝트에 Lombok 라이브러리 의존성을 추가하면 사용할 수 있다.
  • 클래스의 객체들을 비교할 때 equals()와 hashCode()를 자동으로 생성해 준다. 
    • 클래스 안의 모든 필드들을 비교하여 equals() 및 hashCode() 메서드를 자동으로 생성하기 때문에 매우 편리하게 사용할 수 있다.
    • 이를 통해 객체를 생성하거나 변경할 때, equals() 및 hashCode() 메서드를 구현하는 코드를 줄일 수 있다.
  • @EqualsAndHashCode는 객체의 내용이 같다면 equals() 메서드를 통해 두 객체를 같은 것으로 판단한다.
    • (객체의 내용이 같다면) hashCode() 메서드를 사용하여 두 객체가 같은지 판단할 때는 같은 hashCode를 반환한다.
  • 만약 비교에서 제외되어야 하는 필드가 있는 경우 애노테이션 내의 exclude 또는 of 옵션을 사용하여 제외할 수 있다.

 

exclude

@EqualsAndHashCode(exclude = {"id","createdDate","modifiedDate"})
public class MyClass {
    private Long id;
    private String name;
    private Date createdDate;
    private Date modifiedDate;
}
  • 비교를 하고 싶지 않은 필드(id, 생성 시각, 수정 시각 등)가 있는 경우 위 코드와 같이 exclude 옵션을 사용하여 비교에서 제외할 필드를 선택할 수 있다.

 

of

@EqualsAndHashCode(of = {"id"})
public class MyClass {
    private Long id;
    private String name;
}
  • of 옵션의 경우 include와 같은 의미를 가진다.
  • 비교에 사용할 필드를 명시적으로 지정할 수 있다.
    • include 옵션에서 명시적으로 지정한 필드만을 비교하게 된다.

 

 

테스트 코드는 아래의 깃허브 레파지토리에서 확인할 수 있다.

https://github.com/wonyongg/test/tree/main/object

 

 

 

 

참고

뤼튼

https://inpa.tistory.com/entry/JAVA-☕-equals-hashCode-메서드-개념-활용-파헤치기

728x90