본문 바로가기
자바

[자바] .equals(), .hashCode() 메소드에 대해 알아보자

by kdohyeon (김대니) 2023. 2. 11.
반응형

자바 Object 클래스의 equals() 메소드 설명

들어가며

자바에서는 클래스를 선언할 때, 별다른 클래스를 상속받지 않으면 기본적으로 Object 클래스를 상속받게 된다. 따라서 Object 클래스는 모든 자바 클래스의 최상위 부모 클래스이다. Object 클래스는 필드가 없다. 메소드만 포함하고 있는데, 그 중 하나가 객체 비교를 위한 .equals() 메소드이며 코드는 아래를 참고하자.

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

객체 비교를 하는 equals() 메소드를 보면 객체의 주소값, 즉 참조값을 비교한다. 아래 테스트 코드에서의 o1 과 o2 는 서로 다른 참조값을 가지고 있기 때문에 "not equal" 이 출력된다.

@Test
void test() {
    Object o1 = new Object(); 
    Object o2 = new Object();

    if (o1.equals(o2)) {
        System.out.println("equal");
    } else {
        System.out.println("not equal"); // 출력
    }
}

참조값이 아닌 값 자체를 비교하고 싶다면?

자바에서 문자열 데이터 타입으로 사용되는 String 클래스 역시 Object 클래스를 상속받고 있다. String 의 경우, 참조값이 다르더라도 문자열 값이 동일하면 같은 객체로 판단할 수 있어야 한다. 즉, 논리적으로 동일한지 판단할 수 있어야 하기 때문에 Object 클래스의 .equals() 메소드를 오버라이딩하여 재정의 되어 있다.

public boolean equals(Object anObject) {
    if (this == anObject) { // 참조값이 동일하면 true
        return true;
    }

    return (anObject instanceof String aString) // 값 자체가 동일하면 true
        && (!COMPACT_STRINGS || this.coder == aString.coder)
        && StringLatin1.equals(value, aString.value);
}

동일한 값을 가지고 있는 두 String 변수가 있을 때, .equals() 를 활용하여 논리적으로 동일한지 판단할 수 있다.

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

a == b // false
a.equals(b) // true

.equals(), 커스텀 클래스에 대한 객체 비교

한 개 이상의 변수가 포함된 커스텀 클래스를 만들게 되면 논리적으로 동일한지 판단하는 로직이 필요하다. 예를 들어, 상품 (Product) 을 조회하여 중복을 제거하고 Set 에 담으려고 한다. 중복을 제거하기 위해서는 두 Product 객체가 논리적으로 동일한지 판단해야 하며 별도의 .equals() 메소드가 오버라이딩되어 구현되어 있지 않은 이상 Object 클래스에 존재하는 기본 메소드가 실행된다. 기본 .equals() 메소드는 객체의 참조값을 비교하기 때문에 논리적 비교를 위해서는 오버라이딩을 꼭 해주어야 한다.

@Getter
public class Product {
    private String productName;
    private String productDescription;

    @Override // 오버라이딩
    public boolean equals(Object o) {
        if (this == o) return true; // 참조값 비교
        if (!(o instanceof Product)) return false; // 객체 타입 비교

        Product product = (Product) o;

        if (!Objects.equals(product.getProductName(), this.productName)) return false; // 필드 비교
        if (!Objects.equals(product.getProductDescription(), this.productDescription)) return false; // 필드 비교

        return true;
    }
}

.hashCode(), 객체 해시코드

.equals() 메소드와 함께 같이 세트처럼 오버라이딩되어 구현되는 메소드는 .hashCode() 이다. 객체 해시코드는 객체를 식별할 수 있는 하나의 정수값인데, 객체의 주소값/참조값을 숫자 형태로 표현이 된 것이라고 생각하면 되고 객체마다 다른 값을 가지고 있다. HashMap, HashSet 등의 데이터 타입에서는 논리적 동등 비교를 위해 먼저 해시코드 값을 .hashCode() 로 먼저 비교하고, 해시코드 값이 동일하면 .equals() 메소드로 한번 더 확인한다.

public class ProductSample {
    public static void main(String[] args) {
        // 상품별 개수를 관리할 수 있는 맵을 생성
        Map<Product, Long> products = new HashMap<Product, Long>();

        // 상품명 "모니터", 설명 "27인치" 의 상품을 식별키로 입력
        products.put(new Product("모니터", "27인치"), 10);

        // 상품명 "모니터", 설명 "27인치" 의 식별키로 검색
        Long count = products.get(new Product("모니터", "27일치")); // null
    }
}

이렇게 조회를 하게 되면 count 에는 null 이 들어간다. 데이터를 입력할 때의 식별키와 조회할 때의 식별키가 서로 다른 참조값을 가지고 있다.

이 이슈를 해결하기 위해서는 .hashCode() 메소드도 Product 클래스에 재정의를 해주면 된다.

@Getter
public class Product {
    private String productName;
    private String productDescription;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Product)) return false;

        Product product = (Product) o;

        if (!Objects.equals(product.getProductName(), this.productName)) return false;
        if (!Objects.equals(product.getProductDescription(), this.productDescription)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = Objects.hashCode(productName);
        result = 31 * result + Objects.hashCode(productDescription);
        return result;
    }
}

핵심

  • 객체의 논리적 동등 비교를 위해서는 .equals().hashCode() 를 함께 재정의해줘야 한다.

참고 자료

반응형

댓글