본문 바로가기

Java/JAVA

자바 equals(), hashCode(), == 연산자 비교 및 개념 정리하기(객체 비교 구문 커스터마이징하는 방법)

반응형

자바에서 각각 객체가 동일한지 확인하는 방법

자바 프로그래밍에서 객체가 동일한지 확인하는 분기문은 상당히 많이 작성할 것이다.

예를 들면 '==' 연산자로 비교할 수도 있고 'equals()', 'hashCode()' 로 비교할 수도 있다.

이제 앞에서 언급한 3개의 방법의 원리를 정리하고 적용해본다.

== 연산자

== 연산자는 피연산자가 primitive type(int, double, boolean, ...)일 때는 값이 같은지 비교하고, 피연산자가 그 외 객체, reference type일 때 가리키는 주소가 같은지를 검사한다.

1
2
3
4
5
6
7
8
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);//true
String str3 = new String("hello");
String str4 = new String("hello");
String str5 = str4;
System.out.println(str3 == str4);//false
System.out.println(str4 == str5);//true


위의 예제가 String 객체라서 조금 어렵게 설명이 될 수 있다. (String 클래스는 조금 특별하므로...)

str1과 str2는 String을 리터럴로 생성한 객체로 heap에 생성된 "hello"를 같이 가리키고 있는 것이다.

따라서 str1이 가리키는 주소("hello"의 주소)와 str2가 가리키는 주소("hello"의 주소)가 같기 때문에 true를 리턴하는 것을 알 수 있다.

str3과 str4는 생성자를 이용해서 생성한 객체로 각각의 메모리에 "hello"라는 String을 만든 것과 같다.

아까와는 다르게 각각의 메모리(주소공간)에 "hello"가 존재하고 가리키고 있으므로 서로 주소가 달라 false를 리턴한다. 

str4와 str5의 경우는 str5는 str4가 가리키는 값(주소)를 대입했으므로 같은 주소를 가리킨다. 따라서 true를 리턴한다.

다시 정리하면 '==' 연산자는 두 객체가 같은 것을 가리킬 때만 true를 준다고 보면 된다.

equals()

equals는 내용이 같은지를 검사하는 메서드다.

명확하게는 default로 primitive type은 내용이 같은지 검사하고, reference type은 객체의 주소가 같은지 검사한다.

(Object클래스의 메서드이므로 모든 객체는 equals()메서드를 사용할 수 있다.)

'==' 연산자와 다른 점은 완전히 같은 객체를 가리키지 않아도 개발자가 true로 만들 수 있다는 것이다.

1
2
3
4
5
6
String str1 = "hello";
String str2 = "hello";
System.out.println(str1.equals(str2));//true
String str3 = new String("hello");
String str4 = new String("hello");
System.out.println(str3.equals(str4));//true
cs

str1과 str2는 같은 주소를 가리키고 있을 뿐만아니라 내용(값)도 같으므로 equals메서드의 결과 true를 리턴한다.

str3과 str4는 가리키는 주소는 달라도 내용(값)이 같으므로 equals메서드의 결과 true를 리턴한다.

* 하지만 String 클래스는 조금 특별하다고 했다.

자바에서 String 클래스 내부적으로 equals메서드를 오버라이드(재정의) 해서 이런 결과가 나타난 것이다.

즉, String클래스가 아닌 개발자가 생성한 클래스의 객체는 자바가 내용이 같은지를 판단하기는 어렵다.

다른 클래스로도 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person {
    private String name;
    private int age;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}


임의로 Person이라는 클래스를 만들어보았다.

여기서는 일반적으로 사용하는 getter/setter, 생성자, toString() 메서드만 추가했다.

1
2
3
4
Person person1 = new Person("jeong-pro"27);
Person person2 = new Person("jeong-pro"27);
System.out.println(person1 == person2);//false
System.out.println(person1.equals(person2));//false


결과는 둘다 false가 나온다.

'==' 연산자를 복습해보면 당연히 두 객체가 각각 다른 주소에 생성되었기 때문에 person1과 person2는 '==' 연산에 대해 false를 리턴한다.

그런데 내용이 같으면 true를 준다던 equals()메서드가 false를 리턴했다.

문제는 자바에서 내용이 같은지를 모른다는 것이다.

왜냐하면 개발자의 의도에 따라 name만 같으면 두 객체를 같게 볼 수도 있고 name, age 둘 다 같아야 같다고 볼지 모르기 때문이다.

따라서 equals() 메서드를 오버라이드(재정의)해서 두 객체의 내용이 같은지를 정의해줘야 올바르게(의도한대로) 작동한다.

* equals() 메서드를 재정의하지 않고 아래와 같이 쓸 수도 있다.

1
System.out.println(person1.getName().equals(person2.getName()) || person1.getAge() == person2.getAge());
cs

어떻게 보면 위와 같은 방법이 코드를 따라갈 때에는 더 명확하게 무엇을 비교하는지 알 수 있어서 좋을 수도 있을 것이다. (이게 포인트가 아니므로 일단 넘어간다)

 age와 name 모두 같아야 같은 것으로 확인하는 equals()메서드를 만들었다. (IDE가 만들어 주었다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Person other = (Person) obj;
    if (age != other.age)
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}
cs

equals 메서드를 오버라이드했더니 이제는 person1.equals(person2) 가 true를 리턴한다.

* 참고로 eclipse(IDE)에서는 equals() 메서드를 generate 시켜주는 기능을 가지고 있는데 자연스럽게 hashCode()도 함께 generate 시켜준다. 즉, equals()와 hashCode()를 같이 재정의하게 한다는 것이다.

hashCode()를 정리하기 전 미리 말하면,

equals()만 재정의해서는 안되고 반드시 equals()와 hashCode()를 함께 재정의해야만 부작용이 없다.

예를 들면 아래와 같은 부작용이 있을 수 있다.

equals만 재정의해서 어떤 두 객체가 같다고 했는데 hash를 사용하는 Collection(HashSet, HashMap, ...)에 넣을 때는 같다고 생각하지 않아서 문제가 생길 수 있다. 아래에서 확인해보자.

1
2
3
4
5
6
7
8
9
Set<Person> hset = new HashSet<>();
Person person1 = new Person("jdk"27);
Person person2 = new Person("jdk"27);
System.out.println("person1 : "+person1.hashCode());//2018699554
System.out.println("person2 : "+person2.hashCode());//1311053135
System.out.println(person1.equals(person2));//true
hset.add(person1);
hset.add(person2);
System.out.println(hset.size());//2
cs

person1과 person2의 해시코드가 다른 것을 위에서 확인할 수 있고 그 때문에 중복을 자동으로 없애주는 Set에 넣었음에도 불구하고 set의 사이즈는 2가 나와버린 것이다.

이런 문제를 모르고 코딩하다가는 나중에 꼬여버린 탓에 곤란을 겪을 수 있다. 따라서 equals와 hashcode는 반드시 함께 재정의해야 한다.

즉, equals로 같은 객체라면 반드시 hashCode도 같은 값이여야만 한다.

하지만 반대로 hashCode가 같은 값이더라도 equals로 같은 객체가 아닐 수 있다는 것을 유의해야 한다.

또한 아주 중요한 점이 같은 파라미터를 이용해야 한다는 것이다.

(* 실제 equals의 파라미터는 반드시 Object 타입이어야 한다. 내부적으로 비교하는 파라미터를 같게 하라는 의미.

파라미터 타입을 Object에서 다른 타입으로 바꿀 경우는 오버로딩으로 인식하여 기존의 equals 메서드가 남아있게 된다.)

예를들어 equals를 판단하는 파라미터에는 name만 이용했는데 hashcode에서는 age를 이용한다든지 name과 age를 같이 사용해버린다든지 하면 부작용이 많이 일어날 수 있다.

결론적으로 반드시 같은 파라미터를 이용하면 될 것이다.

hashCode

1
2
3
4
5
6
7
8
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + age;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}


hashCode()는 메모리에서 가진 hash주소 값을 기본적으로 반환해준다.

기본적으로 hash는 다른 객체여도 같을 '수'가 있기 때문에 비교에 적합하지 않으나 hash함수를 쓰는 collection같은 객체가 있으므로 함께 사용하는 것으로 이해하도록 한다.


참고 사이트

: https://nesoy.github.io/articles/2018-06/Java-equals-hashcode

반응형
  • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2018.12.12 19:09 신고

    equals() 메서드 제대로 만드는 방법

    1. 파라미터가 this 객체를 참조하면 true를 리턴한다.
    2. instanceof 연산자로 파라미터가 같은 타입인지 검사하고 다르면 false를 리턴한다.
    3. 파라미터 객체를 정확한 타입으로 변경한다. (instanceof 한 타입)
    4. 주요 멤버 변수에 대해 값이 같은지 검사한다.
    -> float, double을 제외한 primitive type은 '==' 연산자로 비교한다.
    -> float, double은 Float.floatToIntBits(), Double.doubleToLongBits()로 int, long으로 변환한 다음 '==' 연산자로 비교한다.
    -> Reference type의 경우 해당 object의 equals() 메서드로 비교한다. (필요한 경우 null check)

    위와 같이 하지않으면 어디선가 치명적인 에러를 만들 수 있습니다.

  • 익명 2020.02.29 20:59

    비밀댓글입니다

  • 익명 2020.04.14 08:55

    비밀댓글입니다

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.04.14 20:09 신고

      말씀하신 부분이 맞습니다.
      hashCode()의 리턴값이 int이기 때문에 int의 범위인 약 20억개의 값으로 나뉠 수 있는데요.
      만약 페이스북에 올라오는 글 객체를 해시함수르 해시코드화 시키면 글들이 20억개 이상, 3~40억개가 되면서 겹칠 수 밖에 없어집니다. (그 전에 겹칠 수도 있음)

      부가적으로 전혀 다른 객체여도 해시코드 값은 같아질 수 있습니다.
      class Post{
      private string title;
      //...
      }
      class Book{
      private string title;
      //...
      }
      포스트와 북 클래스가 둘다 title이란 문자열로만 해시코드를 만들고 해시 함수가 같다면 책 제목과 포스트 제목이 같을 경우 해시코드가 같을 수 있습니다.

  • 익명 2020.12.21 23:07

    비밀댓글입니다

    • Favicon of https://jeong-pro.tistory.com BlogIcon JEONG_AMATEUR 2020.12.22 00:30 신고

      1. equals(Object object) 메소드 시그니처를 직접 바꾸는게 아니라 내부적으로 .getClass()로 비교하기 때문에 같은 클래스를 사용해야한다는게 말이었습니다.
      같은 파라미터라기보다는 같은 '클래스'라고 하는게 오해의 소지가 적겠네요.

      2. 이름과 나이를 갖는 사람(Person) 객체 A(joy, 20), B(joy, 24)가 있다고 가정합니다.
      .equals에서는 이름(name)만 같으면 같은 사람으로 보고,
      hashcode에서는 이름(name)과 나이(age)를 이용했다면,
      이름(name=joy)이 같은 두 A, B객체에 대해서 equals는 같은 객체(true)라고 표현할 것이지만, 이름(name)과 나이(age)가 같은 객체를 해시코드를 사용하는 HashSet과 같은 컬렉션 객체에서는 다른 객체로 판단할 것입니다.
      이러한 경우는 일반적으로 의도한 설계가 아닐 것입니다.

    • BlogIcon :) 2020.12.23 05:15

      아 그말이였군요 :) 댓글 감사합니다

태그