equals()와 hashcode()

eunsiver·2023년 6월 10일
0

equals()는 객체의 값의 일치여부(boolean)을 반환하는 타입이다.

hashCode()는 객체의 주소값(int)을 이용하여 객체 고유의 해시코드를 리턴하는 함수이다.


equals 메소드

어떤 두 참조 변수의 값이 같은지 다른지 동등 여부를 비교해야 할때 사용하는 것이 equals() 메서드이다.

대표적으로 String 타입의 변수를 비교할때 가장 많이 거론되는 메서드일 것이다.

String s1 = "Hello";
String s2 = "Hello";

System.out.println(s1 == s2); // 주소 비교 false
System.out.println(s1.equals(s2)); // 값 비교 true

그러면 문자열이 아닌 클래스 자료형의 객체 데이터일 경우 equals() 메소드는 어떻게 다뤄질까?

어렵게 생각할 필요 없다. 비교할 대상이 객체일 경우 객체의 주소를 이용하여 비교한다.

즉, 객체 자체를 비교할때는 == 이나 equals() 나 똑같다고 보면 된다.

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

public class Example {
    public static void main(String[] args) {
        Person person1 = new Person("홍길동");
        Person person2 = new Person("홍길동");

        System.out.println(person1 == person2); // == 은 객체타입인경우 주소값을 비교한다. 서로다른 객체는 다른 주소를 가지고 있기 때문에 false가 출력됨

        System.out.println(person1.equals(person2)) // equals또한 객체타입인경우 주소값을 비교하기 때문에 false가 출력된다.
    }
}

만일 객체 자료형을 비교를 할때, 주소 값이 아닌 객체의 필드값을 기준으로 동등 비교 기준을 변경하고 싶다면, equals 메서드를 오버라이딩해서 주소가 아닌 필드값을 비교하도록 재정의 해주면 된다.

import java.util.Objects;

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    // 객체 주소 비교가 아닌 Person 객체의 사람 이름이 동등한지 비교로 재정의 하기 위해 오버라이딩
    public boolean equals(Object o) {
        if (this == o) return true; // 만일 현 객체 this와 매개변수 객체가 같을 경우 true
        if (!(o instanceof Person)) return false; // 만일 매개변수 객체가 Person 타입과 호환되지 않으면 false
        Person person = (Person) o; // 만일 매개변수 객체가 Person 타입과 호환된다면 다운캐스팅(down casting) 진행
        return Objects.equals(this.name, person.name); // this객체 이름과 매개변수 객체 이름이 같을경우 true, 다를 경우 false
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동"); // 동명이인

        System.out.println(p1.equals(p2)); // true
    }
}

equals 메서드를 재정의할 때는 반드시 일반 규약을 따라야 한다.
다음은 Object 명세에 적힌 규약이다.

equals 재정의 규약

  • 반사성: null이 아닌 모든 참조 값 x에 대해 x.equals(x)는 true이다.

  • 대칭성: null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 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다.

equals를 재정의하려거든 hashcode도 재정의하라

대부분의 IDE Generate 기능에서도 equals와 hashCode를 같이 재정의해주며 lombok에서도 EqualsAndHashCode 어노테이션으로 같이 재정의해준다.

그렇다면 equals와 hashCode를 왜 같이 재정의해야 하는 걸까?

hashCode를 재정의하지 않으면?

  • hashcode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.

equals만 재정의할 경우

public class Car {
    private final String name;

    public Car(String name) {
        this.name = name;
    }

    // intellij Generate 기능 사용
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Objects.equals(name, car.name);
    }
}

Car 클래스에는 equals만 재정의해두었다.


public static void main(String[] args){
    Car car1 = new Car("foo");
    Car car2 = new Car("foo");
    
    // true 출력
    System.out.println(car1.equals(car2));
}

equals를 재정의했기 때문에 Car 객체의 name이 같은 car1, car2 객체는 논리적으로 같은 객채로 판단된다.

public static void main(String[] args) {
    List<Car> cars = new ArrayList<>();
    cars.add(new Car("foo"));
    cars.add(new Car("foo"));

    System.out.println(cars.size());
}

Car 객체를 2개 List cars에 넣어줬으니 출력 결과는 당연히 2 일 것이다.

그렇다면 이번엔 Collection에 중복되지 않는 Car 객체만 넣으라는 요구사항이 추가되었다고 가정해보자.

요구사항을 반영하기 위해 List에서 중복 값을 허용하지 않는 Set으로 로직을 바꿨다. 마찬가지로 아래 main 메서드의 출력 결과를 예측해보자.


  public static void main(String[] args) {
    Set<Car> cars = new HashSet<>();
    cars.add(new Car("foo"));
    cars.add(new Car("foo"));

    System.out.println(cars.size());
}

추가된 두 Car 객체의 이름이 같아서 논리적으로 같은 객체라 판단하고 HashSet의 size가 1이 나올 거라 예상했지만, 예상과 다르게 2가 출력된다.

hashCode를 equals와 함께 재정의하지 않으면 코드가 예상과 다르게 작동하는 위와 같은 문제를 일으킨다. 정확히 말하면 hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)을 사용할 때 문제가 발생한다.

hash 값을 사용하는 Collection(HashMap, HashSet, HashTable)은 객체가 논리적으로 같은지 비교할 때 아래 그림과 같은 과정을 거친다.

hashCode 메서드의 리턴 값이 우선 일치하고 equals 메서드의 리턴 값이 true여야 논리적으로 같은 객체라고 판단한다.

앞서 봤던 main 메서드의 HashSet에 Car 객체를 추가할 때도 위와 같은 과정으로 중복 여부를 판단하고 HashSet에 추가됐다. 다만 Car 클래스에는 hashCode 메서드가 재정의 되어있지 않아서 Object 클래스의 hashCode 메서드가 사용되었다.

Object 클래스의 hashCode 메서드는 객체의 고유한 주소 값을 int 값으로 변환하기 때문에 객체마다 다른 값을 리턴한다. 두 개의 Car 객체는 equals로 비교도 하기 전에 서로 다른 hashCode 메서드의 리턴 값으로 인해 다른 객체로 판단된 것이다.


hashCode 메소드

hashCode 메서드는 객체의 주소 값을 이용해서 해싱(hashing) 기법을 통해 해시 코드를 만든 후 반환하기 때문에 서로 다른 두 객체는 같은 해시 코드를 가질 수 없게 된다. 그래서 해시코드는 객체의 지문이라고도 한다.

엄밀히 말하면 해시코드는 주소값은 아니고, 주소값으로 만든 고유한 숫자값이라고 하는게 옳다.

  class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        // 객체 인스턴스마다 각기 다른 주해시코드(주소))를 가지고 있다.
        System.out.println(p1.hashCode()); // 622488023
        System.out.println(p2.hashCode()); // 1933863327
    }
}

hashCode 재정의 규약

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되데 항상 같은 값을 반환해야 한다.

  • equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.

  • (Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블 성능이 좋아진다.


hashCode 재정의

  
  public class Car {
    private final String name;

    public Car(String name) {
        this.name = name;
    }

    // intellij Generate 기능 사용
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

intellij 의 Generate 기능으로 Objects.hash 메서드를 호출하는 로직으로 hashCode 메서드 재정의

Objects.hash 메서드는 hashCode 메서드를 재정의하기 위해 간편히 사용할 수 있는 메서드이지만 속도가 느리다.

인자를 담기 위한 배열이 만들어지고 인자 중 기본 타입이 있다면 박싱과 언박싱도 거쳐야 하기 때문이다.

성능에 아주 민감하지 않는 대부분의 프로그램은 간편하게 Objects.hash 메서드를 사용해서 hashCode 메서드를 재정의해도 문제가 없다. 그러나 민감한 경우에는 직접 재정의해주는게 좋다.

hashCode 재정의 가이드


  1. equals()와 hashcode()에 대해 설명해 주세요.
    • 본인이 hashcode() 를 정의해야 한다면, 어떤 점을 염두에 두고 구현할 것 같으세요?
    • 그렇다면 equals() 를 재정의 해야 할 때, 어떤 점을 염두에 두어야 하는지 설명해 주세요.

참고

profile
Let's study!

0개의 댓글