많은 분들은 그냥 equals(), hashCode(), toString() 이거 그냥 쓰면 되는 거 아니야?? 왜 재정의를 해야 해?
사실 저거 뭐 어떻게 쓰는지도 몰라~라고 하시는 분들이 있습니다.
저도 예전엔 그러기도 했구요.
그래서 이번 포스팅에선 equals() 재정의 이유와 방법을 소개하겠습니다.
equals()
언제 재정의 해야 하나?
클래스 간 논리적 동치성을 확인해야 할 때 재정의 해야 합니다.
왜 재정의를 해야 하나?
참조형 객체는 Object 클래스를 기본으로 상속하고 있으니 Object에서 제공해주는 equals() 그냥 사용하면 안 되나??
라고 생각하시는 분들이 많으실텐데 Object에서 제공하는 equals는 단순히 같은 객체인지 아닌지를 판단하고 있기 때문입니다.
public boolean equals(Object obj) {
return (this == obj);
}
재정의하지 않고 Object에서 제공해주는 equals로 동일한 id, name을 갖고있는 student, student2를 비교해보겠습니다.
@Getter
public class Student {
String id;
String name;
@Builder
public Student(String id, String name) {
this.id = id;
this.name = name;
}
}
@Test
void test_student_equals() {
Student student = Student.builder().id("effort").name("guy").build();
Student student2 = Student.builder().id("effort").name("guy").build();
assertFalse(student.equals(student2));
}
논리적으로 같은 값을 가지고 있음에도 false가 나오는 걸 볼 수 있습니다.
꼭 재정의를 해야 하나?
아래와 같은 경우는 재정의할 필요가 없습니다.
1) 각 인스턴스가 본질적으로 고유하다.
2) 인스턴스의 논리적 동치성을 비교할 일이 없다.
3) 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
4) 클래스가 private이나 package-private이고 eqauls 메소드를 호출할 일이 없다.
재정의 하기 전 알아야 할 규약
기본 바탕 : null이 아닌 모든 참조 값들에 대해
반사성(reflexivity) : x.equals(x)는 반드시 true
대칭성(symmetry) : x.equals(y) == true면 y.equals(x) == true
추이성(transiticvity) : 값 x, y, z에 대해, x.equals(y) == true, y.equals(z) == true면 z.equals(x) == true
일관성(consistency) : x.equals(y)를 반복적으로 호출하면 true
null-아님 : x.equals(null)은 반드시 false
위 규약은 사실 어기는 게 더 힘든 거 같습니다.
재정의 방법
1) == 연산자를 사용해 자기 자신의 참조인지 확인한다.
2) instanceof 연산자로 입력이 올바른 타입인지 확인한다.
3) 입력을 올바른 타입으로 형변환한다.
4) 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
재정의 코드
아래 재정의 코드는 Lombok의 @EqualsAndHashCode 어노테이션 사용 시 생성되는 코드입니다.
@Getter
@EqualsAndHashCode
public class Student {
String id;
String name;
@Builder
public Student(String id, String name) {
this.id = id;
this.name = name;
}
}
public boolean equals(final Object o) {
if (o == this) { // 1)
return true;
} else if (!(o instanceof Student)) { // 2)
return false;
} else {
Student other = (Student)o; // 3)
if (!other.canEqual(this)) {
return false;
} else { // 4)
// id 비교
Object this$id = this.getId();
Object other$id = other.getId();
if (this$id == null) {
if (other$id != null) {
return false;
}
} else if (!this$id.equals(other$id)) {
return false;
}
// name 비교
Object this$name = this.getName();
Object other$name = other.getName();
if (this$name == null) {
if (other$name != null) {
return false;
}
} else if (!this$name.equals(other$name)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof Student;
}
재정의된 equals로 다시 테스트 해보겠습니다.
@Test
void test_student_equals() {
Student student = Student.builder().id("effort").name("guy").build();
Student student2 = Student.builder().id("effort").name("guy").build();
assertTrue(student.equals(student2));
}
재정의하니 논리적 동치성이 같다고 나오네요. 성공입니다.
추가 정보
null도 정상적인 참조 값으로 사용하는 경우
equals 재정의 규약을 보면 "기본 바탕 : null이 아닌 모든 참조 값들에 대해" 라는 문구가 전제로 깔려있는데
null을 사용해야 하는 경우 Objects.equals(Object, Object)로 비교해 NullPointerException 발생을 방지합니다.
equals와 == 차이점
== : 항등 연산자로 객체 인스턴스의 주소 값을 비교
equals() : 논리적 동치성을 비교
ex)
@Test
void equalsAnd() {
String s = new String("s");
String s1 = new String("s");
assertFalse(s == s1);
assertTrue(s.equals(s1));
}
primitive 타입 비교 방법
float, double을 제외한 기본 타입은 == 연산자로 비교한다.
float, double은 Float.compare, Double.compare를 이용해서 비교해야 한다. (부동소수 값을 다뤄야 하기 때문)
equals 재정의에 관련한 정보를 전부 정리해봤는데요.
요약하면, 논리적 동치성 비교해야 한다면 Lombok의 @EqualsAndHashCode를 사용하면 됩니다.
다음 포스팅에선 hashCode(), toString()에 대해 알아보겠습니다.
참고
- Joshua Bloch, Effective Java 3/E, 인사이트, 2018
'Java' 카테고리의 다른 글
[Java] Mockito 사용법 (3) - 스터빙 (Stubbing) (OngoingStubbing, Stubber) (0) | 2022.09.06 |
---|---|
[Java] Mockito 사용법 (2) - 설정, Mock 생성 (@Mock, @Spy, @InjectMocks) (0) | 2022.09.06 |
[Java] Mockito 사용법 (1) - Mock이란?, Mockito 소개 (1) | 2022.09.06 |
[Java] Apache Log4j 2.x 취약점 및 해결 방법 (Log4j2 remote code execution vulnerability) (8) | 2021.12.12 |
[Java] equals(), hashCode(), toString() 재정의 이유와 방법 (2) (0) | 2021.04.16 |
댓글