이번 글에서는 equals와 hashCode에 대해서 다뤄볼 예정이다.
❗ equals 메서드란?
두 인스턴스의 주소 값을 비교하여 같은 인스턴스인지를 확인하고, 같다면 true 다르다면 false를 반환한다.
public class EX {
public static void main(String[] args) {
Person test1 = new Person("Test1");
Person test2 = new Person("Test1");
System.out.println(test1.equals(test1)); // true
System.out.println(test2.equals(test1)); // false
}
public static class Person {
private String name;
public Person(final String name) {
this.name = name;
}
}
}
◼ test1과 test2는 이름은 같지만, 객체의 주소가 다르기 때문에 equals 비교 시 false가 출력된다.
❗ equals 메서드 재정의해야 하는 상황
❕❕ 싱글톤 객체가 아니면서, 논리적인 동치성을 확인하고 싶을 때, 재정의한다.
즉, 다른 객체이지만 같은 정보를 가지고 있다면 동일하다고 판단해야 할 때!
❗ equals의 일반 규약
null이 아닌 모든 참조 값 x, y, z에 대해서,
◼ 반사성: x.equals(x) // true -> 자기 자신과 같아야 한다.
◼ 대칭성: x.equals(y) // true 일 때, y.equals(x) // true -> 서로에 대한 동치 여부가 같아야 한다.
◼ 추이성: x.equals(y) // true 이고, y.equals(z) // true이면, x.equals(z) // true -> x.equals(y)가 참이고 y.equals(x)가 참이면, x.equals(z)는 참이다.
◼ 일관성: x.equals(y) // true 반복해도 값이 동일 -> x.equals(y)가 참일 때, 반복해서 호출해도 결과는 항상 true여야 한다.
◼ non-null: x.equals(null) // false -> NullpointerException이 발생하지 않고 false를 반환해야 한다.
❗ equals의 좋은 재정의 방법
public class EX {
public static void main(String[] args) {
Person test1_1 = new Person("Test1", 10);
Person test1_2 = new Person("Test1", 10);
Person test1_3 = new Person("Test1", 10, "SOMTHING");
System.out.println(test1_1.equals(test1_1)); // true
System.out.println(test1_1.equals(test1_2)); // true
System.out.println(test1_2.equals(test1_1)); // true
System.out.println(test1_1.equals(test1_3)); // false
}
public static class Person {
private String name;
private int age;
private String department = null;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public Person(final String name, final int age, final String department) {
this.name = name;
this.age = age;
this.department = department;
}
@Override
public boolean equals(final Object o) {
// 1. == 연산자를 통해 자기 자신의 참조인지 확인
if (o == this) {
return true;
}
// 2. instanceof 연산자로 입력이 올바른 타입인지 확인
if (!(o instanceof Person)) {
return false;
}
// 3. 입력을 올바른 타입으로 형변환
Person person = (Person) o;
// 4. 필드가 참조 타입일 경우 equals()로 비교
if (!this.name.equals(person.name)) {
return false;
}
// 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
if (this.age != person.age) {
return false;
}
// 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
return Objects.equals(this.department, person.department);
}
}
}
❗ equals 재정의 시 참고 사항
◼ 논리적인 동치성을 비교하는 것이 아니면 재정의하지 말자.
◼ 재정의하는 경우 위의 일반 규약을 지키면서, 핵심 필드를 전부 포함시키자.
◼ 핵심 필드를 포함시킬 때, 다를 경우가 많은 필드부터 비교하는 것이 성능에 좋다.
◼ 객체의 논리적인 상태와 관련 없는 필드는 비교하면 안된다.
❗ hashCode 메서드란?
객체를 식별할 수 있는 유니크 값을 반환하는 메서드로, 반환된 값은 중복되지 않는 고유의 값이다.
public class EX {
public static void main(String[] args) {
Person test1_1 = new Person("Test1", 10);
Person test1_2 = new Person("Test1", 10);
System.out.println(test1_1.equals(test1_1)); // true
System.out.println(test1_1.equals(test1_2)); // true
System.out.println(test1_1.hashCode()); // 2003749087
System.out.println(test1_2.hashCode()); // 1324119927
}
public static class Person {
private String name;
private int age;
private String department = null;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public Person(final String name, final int age, final String department) {
this.name = name;
this.age = age;
this.department = department;
}
@Override
public boolean equals(final Object o) {
// 1. == 연산자를 통해 자기 자신의 참조인지 확인
if (o == this) {
return true;
}
// 2. instanceof 연산자로 입력이 올바른 타입인지 확인
if (!(o instanceof Person)) {
return false;
}
// 3. 입력을 올바른 타입으로 형변환
Person person = (Person) o;
// 4. 필드가 참조 타입일 경우 equals()로 비교
if (!this.name.equals(person.name)) {
return false;
}
// 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
if (this.age != person.age) {
return false;
}
// 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
return Objects.equals(this.department, person.department);
}
}
}
◼ 해당 코드를 살펴보면, equals 비교 시 참이지만 hashCode를 비교하면 다른 값이 출력된다.
❗ hashCode 메서드를 정의하지 않는다면?
public class EX {
public static void main(String[] args) {
Person 원본 = new Person("Test1", 10);
Person 비교대상 = new Person("Test1", 10);
System.out.println(원본.equals(비교대상)); // true
Set<Person> personSet = new HashSet();
personSet.add(원본);
System.out.println(personSet.contains(비교대상)); // false
}
public static class Person {
private String name;
private int age;
private String department = null;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public Person(final String name, final int age, final String department) {
this.name = name;
this.age = age;
this.department = department;
}
@Override
public boolean equals(final Object o) {
// 1. == 연산자를 통해 자기 자신의 참조인지 확인
if (o == this) {
return true;
}
// 2. instanceof 연산자로 입력이 올바른 타입인지 확인
if (!(o instanceof Person)) {
return false;
}
// 3. 입력을 올바른 타입으로 형변환
Person person = (Person) o;
// 4. 필드가 참조 타입일 경우 equals()로 비교
if (!this.name.equals(person.name)) {
return false;
}
// 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
if (this.age != person.age) {
return false;
}
// 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
return Objects.equals(this.department, person.department);
}
}
}
◼ 원본과 비교대상은 같다고 판단해서(equals로 인해) HashSet에 원본을 넣고 비교대상이 존재하는지 확인한다면, false가 출력되게 된다.
❗ hashCode 메서드를 정의한다면?
public class EX {
public static void main(String[] args) {
Person 원본 = new Person("Test1", 10);
Person 비교대상 = new Person("Test1", 10);
System.out.println(원본.equals(비교대상)); // true
Set<Person> personSet = new HashSet();
personSet.add(원본);
System.out.println(personSet.contains(비교대상)); // true
}
public static class Person {
private String name;
private int age;
private String department = null;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public Person(final String name, final int age, final String department) {
this.name = name;
this.age = age;
this.department = department;
}
@Override
public boolean equals(final Object o) {
// 1. == 연산자를 통해 자기 자신의 참조인지 확인
if (o == this) {
return true;
}
// 2. instanceof 연산자로 입력이 올바른 타입인지 확인
if (!(o instanceof Person)) {
return false;
}
// 3. 입력을 올바른 타입으로 형변환
Person person = (Person) o;
// 4. 필드가 참조 타입일 경우 equals()로 비교
if (!this.name.equals(person.name)) {
return false;
}
// 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
if (this.age != person.age) {
return false;
}
// 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
return Objects.equals(this.department, person.department);
}
@Override
public int hashCode() {
return Objects.hash(this.name, this.age, this.department);
}
}
}
◼ 원본 객체를 HashSet에 넣고 비교대상 객체로 포함되어 있는지 확인해도 true가 반환된다.
❗ hashCode 메서드를 재정의할 때 고려사항
◼ Objects.hash() 메서드의 내부 구현이다.
◼ 핵심 필드를 빠트리지 않고 넣고 해당 메서드를 사용하는 것이 좋다.
◼ HashCode 값의 생성 규칙을 API 사용자에게 자세히 공표해서는 안된다.
'이팩티브 자바' 카테고리의 다른 글
[Effective] try-with-resources (1) | 2023.03.03 |
---|---|
[Effective] 정적 팩터리 메서드 (0) | 2023.03.02 |