개발하다 보면 "객체가 같은지 확인해볼까?"라는 순간이 자주 옵니다. ==만 쓰면 되지 않냐고요? 사실 ==는 객체의 참조값(주소)를 비교하기 때문에, 우리가 진짜 원하는 값 비교는 안 되는 경우가 많습니다. 그때 등장하는 게 바로 equals입니다. 기본적으로 Object 클래스에서 제공되지만, 직접 정의해야 하는 경우도 꽤 많습니다.
1️⃣ 언제 equals를 정의해야 할까?
1. 객체의 "값"을 기준으로 비교하고 싶을 때
예를 들어, 같은 이름과 나이를 가진 사람이 같은 사람으로 간주되어야 한다면, equals를 정의해야 합니다. 그렇지 않으면 기본 equals는 객체의 주소값(물리적 동치)을 비교하기 때문에 다른 객체로 판단될 수 있습니다.
두 객체가 "물리적으로 같음"이 아니라 "논리적으로 같음"인지 확인해야 할 때
- 물리적 동치성: 두 객체의 참조(메모리 주소)가 같은지 비교합니다. (== 연산자 사용)
- 논리적 동치성: 두 객체의 속성(값)이나 상태가 같은지를 비교합니다. (equals 메서드 재정의 필요)
예를 들어, 두 개의 String 객체 "abc"가 논리적으로는 같지만, 실제로 메모리에서 다른 객체일 수 있습니다. 이런 경우 논리적 동치성을 확인하기 위해 equals 메서드를 사용해야 합니다. Java에서 기본적으로 제공되는 Object 클래스의 equals는 물리적 동치성을 확인합니다.
class Person {
private String name;
private int age;
// equals와 hashCode를 정의하지 않으면 기본 Object의 equals를 사용
}
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // false! (주소가 다르기 때문)
2. 컬렉션(Set, Map)에서 정상적으로 동작해야 할 때
`HashMap`이나 `HashSet`은 내부적으로 `equals`와 `hashCode`를 기반으로 객체를 구분합니다. `equals`를 제대로 정의하지 않으면, "똑같은 객체"를 여러 번 추가하거나 찾을 수 없는 문제가 생길 수 있습니다.
Set<Person> people = new HashSet<>();
people.add(p1);
people.add(p2);
System.out.println(people.size()); // 기대는 1, 실제는 2!
3. 객체를 "특정 기준"으로 비교해야 할 때
어떤 객체는 모든 필드가 아니라 특정 필드(예: ID)만 같으면 같은 것으로 간주하고 싶을 수도 있습니다 이럴 때 `equals`를 정의해서 비교 로직을 커스터마이징하면 됩니다.
class User {
private String id; // 유일한 식별자
private String name;
private int age;
public User(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 같은 객체 참조 시 true
if (o == null || getClass() != o.getClass()) return false; // 2. null 또는 클래스 다르면 false
User user = (User) o;
return id.equals(user.id); // 3. ID만 비교
}
@Override
public int hashCode() {
return id.hashCode(); // 4. ID를 기반으로 hashCode 생성
}
}
ID만 비교
- `equals`에서 id.equals(user.id)만 확인합니다. 이름이나 나이는 비교하지 않습니다.
- 따라서 ID가 같으면 `true`, 다르면 `false`를 반환합니다.
2️⃣ equals 정의하는 법
`equals`를 정의할 땐 `hashCode`도 꼭 함께 정의해야 합니다. 그래야 `Set`이나 `Map`에서 일관성 있는 동작을 보장할 수 있습니다.
다음은 `equals`를 정의할 때의 기본 규칙입니다.
- 반사성: x.equals(x)는 항상 true여야 함.
- 대칭성: x.equals(y)가 true면, y.equals(x)도 true여야 함.
- 추이성: x.equals(y)이고 y.equals(z)라면, x.equals(z)도 true여야 함.
- 일관성: x.equals(y)의 결과는 상태가 바뀌지 않는 한 항상 같아야 함.
- null 비교: x.equals(null)은 항상 false여야 함.
1. getClass()를 사용하는 경우
- 엄격한 클래스 일치를 요구
- 상속 관계에서 하위 클래스가 같은 것으로 간주되지 않기를 원하는 경우
- `getClass()`를 사용하면 현재 클래스와 정확히 동일한 클래스인지 확인합니다.
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 같은 객체인지 확인
if (o == null || getClass() != o.getClass()) return false; // 2. null 또는 클래스 불일치 확인
Person person = (Person) o; // 3. 형변환
return age == person.age && Objects.equals(name, person.name); // 4. 필드 값 비교
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 필드를 기반으로 해시코드 생성
}
}
장점:
- 클래스 일치에 대한 명확한 제약을 부여합니다.
- 상속 관계에서 equals의 불필요한 오작동을 방지할 수 있습니다.
단점:
- 상속을 사용하는 경우에는 다형성을 활용할 수 없게 됩니다.
2. instanceof를 사용하는 경우
- 상속 관계를 고려하여 객체를 비교
- 하위 클래스도 같은 객체로 간주될 수 있음
public class User {
private final String id;
public User(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 같은 객체인지 확인
if (o == null || !(o instanceof User)) return false; // 2. null 또는 타입 불일치 확인
User user = (User) o; // 3. 형변환
return Objects.equals(id, user.id); // 4. 필드 값 비교
}
@Override
public int hashCode() {
return Objects.hash(id); // 필드를 기반으로 해시코드 생성
}
}
장점:
- 다형성을 활용할 수 있어 상속 관계에서 유연한 비교가 가능합니다.
- 동일한 기준을 가진 하위 클래스를 동일한 객체로 처리하고 싶을 때 적합합니다.
단점:
- 상속 관계에서 잘못된 비교가 이루어질 가능성이 있습니다.
- 예를 들어, 하위 클래스가 추가적인 필드를 가질 때, 하위 클래스의 필드 비교 없이 equals가 true를 반환할 수 있습니다.
어떤 경우에 사용해야 할까?
- getClass()를 사용할 때:
- 상속 관계가 없거나, 상속이 있더라도 클래스 간의 명확한 구분이 필요할 때.
- 하위 클래스가 같은 객체로 간주되지 않아야 하는 경우.
- 예를 들어, User와 이를 상속받은 AdminUser가 있을 때, AdminUser가 User와 같게 취급되지 않아야 하는 경우.
- instanceof를 사용할 때:
- 상속 관계를 고려하여 동등성을 비교해야 하는 경우.
- 다형성을 활용하는 객체 계층에서 유연한 비교가 필요한 경우.
- 예를 들어, User와 이를 상속받은 PremiumUser가 있을 때, 같은 ID를 가지면 같은 객체로 간주해도 되는 경우.
정리
- getClass()는 상속 관계를 무시하고 클래스 일치를 요구하며, 상속 구조가 복잡하지 않은 경우 적합합니다.
- instanceof는 상속 관계를 고려하며, 다형성을 지원하는 계층 구조에서 유용합니다.
3️⃣ equals 정의 시 주의할 점
- 모든 필드를 비교해야 할까?
- 꼭 그런 건 아닙니다. 논리적으로 중요한 필드만 비교하면 됩니다. 예를 들어, 유저의 ID만 같으면 같은 유저로 간주할 수도 있습니다.
- 성능 문제
- 필드가 많거나 계산이 복잡하면 equals 호출이 느려질 수 있습니다. 필요한 최소한의 필드만 비교하는게 좋습니다.
'JAVA > 개념' 카테고리의 다른 글
Collection.forEach vs Stream.forEach (1) | 2024.11.20 |
---|---|
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |
스트림의 중간 연산, 종단 연산 (0) | 2024.11.18 |
자바 정적 팩토리 메서드 쉽게 이해하기 - 왜 사용하고 어떻게 활용할까? (0) | 2024.10.24 |