자바 프로그래밍을 하다 보면 객체 간의 비교를 해야할 때가 종종 있습니다. 이 때 동일성(Identity), 동등성(Equality)의 개념을 정확히 이해하는 것이 중요합니다. 두 개념은 비슷해 보이지만, 자바에서는 매우 다른 의미를 가집니다.
동일성 (Identity)
동일성 비교란 두 객체의 참조가 실제로 같은 객체를 가르키고 있는지를 확인하는 것입니다. 즉, 두 객체의 메모리 주소가 같은지를 비교하는 것이죠. 자바에서는 "==" 연산자를 사용하여 동일성을 비교합니다.
동등성 ( Equality )
반면, 동등성 비교는 두 객체가 논리적으로 같은가를 확인하는 것입니다. 이는 두 객체의 내용이 같은지를 보는 것이며, 이를 위해서는 equals() 메서드를 오버라이드 (재정의) 해야 합니다. Object 클래스의 기본 equals() 메서드는 내부적으로 == 연산자를 사용하여 동일성 비교를 수행하므로, 필요한 경우에는 equals 메서드를 적절히 재정의 해야 합니다.
예시
아래 예시는 Member 객체를 만들어 동일 및 동등성 비교를 하였습니다.
public class IdentityEquality {
public static void main(String[] args) {
Member member1 = new Member("member1", 1);
Member member2 = new Member("member1",1);
System.out.println(member1 == member2);
System.out.println(member1.equals(member2));
}
static class Member {
private String name;
private int age;
public Member(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Member(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return age == member.age && Objects.equals(name, member.name);
}
}
}
-- 결과
false
true
이 예시에서 Member 클래스는 name, age 필드를 가지며 equals 메서드를 오버라이드하여 값이 같으면 두 객체를 동등하다고 판단하도록 구현하였습니다. 보통 equals() 메서드를 재정의할 때는 IDE 툴의 도움을 받아 사용합니다.
equals() 메서드 재정의가 필요한 경우
equals() 메서드를 재정의 하는 것은 특정 상황에 따라 중요할 수 있습니다. 아래는 몇 가지 일반적인 경우입니다.
비즈니스 로직에 따른 객체 비교
객체가 동등함을 결정할 때 비즈니스 로직에 의존해야 하는 경우가 있습니다. 예를 들어, 사용자 정의 객체가 논리적으로 동일하다고 간주되어야 할 때 특정 필드들의 값을 비교해야 합니다. 'equals()'를 사용하면 이러한 논리적 동등성을 구현할 수 있습니다.
컬렉션에서의 객체 사용
HashTable, HashSet, HashMap 등 컬렉션 객체를 사용할 때, 객체의 동등성 비교가 중요합니다. 이러한 컬렉션들은 객체를 저장, 검색, 제거할 때 equals(), hasCode() 메서드를 사용합니다. 따라서 이 메서드들을 적절히 오버라이드하지 않으면 예상치 못한 동작이 발생할 수 있습니다.
public class Person {
private String name;
private 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;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (age != person.age) return false;
return Objects.equals(name, person.name);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person person1 = new Person("John", 25);
map.put(person1, "Developer");
// 다른 인스턴스이지만, 동등한 객체를 키로 사용하여 값을 검색
Person person2 = new Person("John", 25);
String occupation = map.get(person2);
System.out.println("Occupation: " + occupation);
}
}
이 코드에서 Person 클래스는 name, age 필드를 가지고 있습니다. equals(), hasCode() 메서드를 오버라이드 하여, 두 객체의 동등성이 name, age 필드의 값으로 결정되게 했습니다.
main 메서드에서는 HashMap에 person1 객체를 키로 사용하여 "Developer"라는 문자열을 저장합니다. 이후 person2라는 새로운 Person 인스턴스를 생성하였지만, person1과 동등한 name, age 값을 가집니다. equals(), hasCode()를 정의 하였기 때문에, person2를 사용하여 HashMap에서 person1을 키로 저장된 값을 검색할 수 있습니다.
객체의 유니크니스(unique-ness) 확인
데이터 중복을 허용하지 않는 상황에서, 예를 들어 데이터베이스의 유니크 키와 같은 역할을 하는 필드를 가진 객체들의 리스트를 관리할 때 equals(), hasCode()를 오버라이드하여 객체간의 동동성을 확인해 중복을 방지할 수 있습니다.
아래 예시는 Account 계정의 중복을 방지한 코드 입니다.
ublic class UserAccount {
private String name;
private String email;
public UserAccount(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserAccount that = (UserAccount) o;
return Objects.equals(name, that.name) && Objects.equals(email, that.email);
}
@Override
public int hashCode() {
return Objects.hash(name, email);
}
public static void main(String[] args) {
Set<UserAccount> accounts = new HashSet<>();
accounts.add(new UserAccount("user1", "user1@example.com"));
accounts.add(new UserAccount("user2", "user2@example.com"));
accounts.add(new UserAccount("user1", "user1@example.com")); // 중복 객체
System.out.println("총 계정 수: " + accounts.size()); // 중복 객체는 추가되지 않음
}
}
만약 위 코드에서 equals(), hasCode() 메서드를 재정의 하지 않았다면, 계정이 개수는 3개가 됩니다. (중복 허용)
값 객체 (Value Object)의 비교
불변의 값 객체를 사용하는 경우(날짜, 시간, 화폐 단위 등), 객체의 상태(객체가 담고 있는 값)가 그 객체의 정체성을 정의합니다. 이런 경우, 객체의 동등성 비교는 객체가 담고 있는 값의 동등성을 의미하기 때문에 equals()를 재정의 해야합니다.
값 객체는 어플리케이션에서 값을 사용되는 객체를 말합니다. 이들을 보통 변경 불가능(immutable)하며, 객체의 인스턴스 자체보다는 객체가 표현하는 값에 의미가 있습니다. 값 객체의 비교는 객체의 상태 즉, 객체가 가지고 있는 값을 비교합니다.
아래 코드는 String(불변객체)로 예시를 만들어 보았습니다.
public class StringExample {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1 == str2); // true, 리터럴 방식으로 생성된 String 객체는 같은 인스턴스를 참조
System.out.println(str1 == str3); // false, new 키워드로 생성된 String 객체는 새로운 인스턴스를 생성
System.out.println(str1.equals(str3)); // true, equals() 메서드는 객체의 내용을 비교
}
}
String class 내부적으로 equals()를 재정의 하고 있습니다.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
이 예시에서 볼 수 있듯이, str1과 str2 는 같은 리터럴 값으로 초기화되어 같은 메모리 주소를 참조합니다. 따라서 == 연산자로 비교 했을 때 true를 반환합니다. 반면 str3는 new 키워드를 사용하여 생성되어 별도의 메모리 주소를 가집니다. 다라서 str1 과 str3 를 == 비교하면 false가 됩니다. 그러나 equals 메서드는 객체의 내용을 비교하기 때문에 str1.equals(str3)는 true를 반환합니다.
String 객체의 이러한 동작 방식은 값 객체가 어떻게 동작해야 하는지 보여줍니다. 객체의 상태(값)이 같다면, 그 객체들은 동등하다고 간주되어야 합니다. 이는 String 외에도 모든 불변 객체에 적용되는 원칙 입니다.
'자바' 카테고리의 다른 글
String 클래스 (0) | 2024.04.04 |
---|---|
자바에서의 불변 객체 (Immutable Object) (1) | 2024.04.03 |
Parallel Stream (1) | 2024.02.06 |
Optional<T> (0) | 2024.02.06 |
Stream API (0) | 2024.02.02 |