자바 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