들어가기 전에
우리 눈에는 보이지 않지만 사실 모든 클래스들은 java.lang 패키지에 위치한 Object 클래스를 상속받고 있다. 클래스에 명시되어있지 않더라도 컴파일 시점에 상속 관계 설정을 해주기 때문이다.
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
그렇다면 자바는 왜 Object 클래스를 모든 클래스가 상속받게 했을까? 이유는 바로 Object의 메서드들에 있다. 배열을 포함한 모든 객체들은 Object 클래스의 메서드들을 구현하고 있다. 다시 말해서, 모든 클래스가 기본적으로 가지고 있어야 할 기능들을 Object가 정의하고 있는 것이다.
Object 클래스가 제공하는 메서드는 다음과 같다.
이번 포스팅에서 다룰 메서드는 equals()로 [Java] String의 불변성은 SCP로부터 에도 잠깐 이야기했던 내용이다. 자바를 공부하다 보면 '==' 연산자와 equals()의 차이점에 대한 공부를 하지 않을 수가 없는데 그 차이점은 동일성(물리적 동치성)과 동등성(논리적 동치성)에 있다. 두 객체를 비교할 때 해시코드 값으로 비교하느냐 혹은 논리적으로 두 객체가 같은 값을 지니고 있는지를 비교하느냐의 차이이다. 그렇다면 equals() 메서드의 내부는 어떻게 구현되어 있을까?
Object.equals()
equals() 메서드의 내부 구현을 보면 '==' 연산자로 객체를 비교한다.
public boolean equals(Object obj) {
return (this == obj);
}
eqauls() 메서드는 동등성 즉 논리적 동치성을 비교할 때 사용한다고 했다. 그렇다면 논리적으로 객체를 비교한다는 로직은 어디서 확인할 수 있을까? String 클래스의 equals() 메서드 내부 구현을 살펴보자.
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
- 두 객체의 해시코드 값을 비교하여 같다면 true를 반환한다. (물리적 동치성 확인)
- 비교하려는 객체가 String 타입이 맞는지 확인하고 일치하지 않는다면 false를 반환한다. (타입 일치 확인)
- Object 타입으로 넘어온 값을 명시적으로 String 타입으로 형변환해준 뒤 같은 인코딩 방식을 사용하는지 확인한다. 일치하지 않는다면 false를 반환한다. (인코딩 방식 일치 확인)
- 인코딩 방식이 Latin1인지 UTF16인지에 따라 각 클래스의 equals() 메서드를 통해 논리적으로 같은 객체인지 비교한다. (객체의 내부 값 일치 확인)
StringLatin1 클래스와 StringUTF16 클래스의 euals() 메서드를 보면 byte 배열로 문자열의 길이를 확인하고 내부의 값을 확인하는 것을 알 수 있다.
// StringLatin1.equals()
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
// StringUTF16.equals()
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}
즉, 동등성을 비교하는 로직을 확인하려면 Object.equals() 메서드를 재정의하여 각 클래스의 특성에 맞게 구현한 각각의 equals() 메서드를 확인해야 하는 것이다.
Example
Book이라는 클래스를 만들어 보자. 책 이름, 작가, 출판사의 정보를 가지고 있다.
public class Book {
private String name;
private String writer;
private String publisher;
public Book() {
}
public Book(String name, String writer, String publisher) {
this.name = name;
this.writer = writer;
this.publisher = publisher;
}
}
만약 아래와 같이 코드를 작성한다면 출력 결과는 어떨까?
public class BookTest {
public static void main(String[] args) {
Book book1 = new Book("데미안", "헤르만 헤세", "민음사");
Book book2 = new Book("데미안", "헤르만 헤세", "민음사");
Book book3 = new Book("데미안", "헤르만 헤세", "문학동네");
System.out.println(book1 == book3);
System.out.println(book1 == book2);
System.out.println(book1.equals(book3));
System.out.println(book1.equals(book2));
}
}
출력 결과
false
false
false
false
모두 해시코드 값으로 동일성을 비교하기 때문에 당연하게도 false 값이 나오게 된다. book1과 book2의 equals() 메서드의 결과 값이 true가 나올 수 있도록 equlas() 메서드를 재정의해보자. 이때 hashCode() 메서드도 재정의해주지 않으면 두 객체가 여전히 다른 해시코드 값을 반환하므로 false가 나오기 때문에 꼭 같이 재정의를 해줘야 한다.
IDE의 equals() 메서드 재정의 기능을 이용하여 아래와 같이 재정의해주었다.
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Book book = (Book)o;
return Objects.equals(name, book.name) && Objects.equals(writer, book.writer)
&& Objects.equals(publisher, book.publisher);
}
- 두 객체의 해시코드 값을 비교하여 같다면 true를 반환한다. (물리적 동치성 확인)
- 비교하려는 객체가 null이거나 클래스 타입이 맞지 않다면 false를 반환한다.
- Object 타입으로 넘어온 값을 명시적으로 Book 타입으로 형변환해준 뒤 Objects 클래스의 equals() 메서드를 호출하여 각각의 값을 비교한다.
Objects.equals() 클래스의 내부 구현은 다음과 같다. 문자열 리터럴을 비교하는 것과 같다고 생각하면 된다.
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
참고로 Objects 클래스는 null을 허용하기 때문에 값이 null일 경우에도 예외가 발생하지 않는데 만약 비교하려는 값이 둘 다 null일 경우에는 true를 반환해준다는 특징이 있다.
equals() 재정의
equals() 메서드를 재정의할 때는 지켜야 할 규약이 있다.
equals 메서드는 동치관계(equivalence relation)를 구현하며, 다음을 만족한다.
- 반사성(reflexivity) : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.
- 대칭성(symmetry) : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true이다.
- 추이성(transitivity) : null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true이다.
- 일관성(consistency) : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- non-null : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
출처 : 이펙티브 자바
이 규약을 정한 이유는 Object 클래스를 상속받는 모든 클래스가 equals() 메서드를 재정의할 수 있기 때문에 다른 방식으로 구현되었을 때 생길 수 있는 문제를 방지하기 위해서이다. 재정의할 때는 해당 객체의 필드들을 위의 모든 규약을 지켜서 비교하고 있는지 잘 확인해야 한다.
그렇다면 equals() 메서드는 항상 재정의해야 하는가? 대답은 "아니요."이다.
해당 객체가 동등성을 비교해야 할 일이 없고 상위 클래스의 equals() 메서드를 그대로 사용할 수 있는 경우라면 굳이 equals() 메서드를 재정의하지 않아도 된다.
정리
- 모든 클래스는 Object 클래스를 상속받는다.
- Object.equals() 메서드는 각 클래스의 특성에 맞게 재정의되어 사용된다.
- Object.equals() 메서드는 정해져있는 다섯 가지 규약에 맞게 재정의되어야 한다.
- Object.equals() 메서드는 필요한 경우에만 재정의한다.
- Objcet.equals() 메서드를 재정의할 때는 Object.hashCode() 메서드를 같이 재정의해야 한다.
면접 예상 질문
- 모든 클래스가 Object 클래스의 상속을 받는 이유는 무엇인가요?
- Object 클래스에서 객체를 처리하기 위해 제공하는 메서드는 어떤 것들이 있나요?
- 문자열을 '==' 논리 연산자보다 Object의 equals() 메서드로 비교해야 하는 이유는 무엇인가요?
- 객체의 동일성(물리적 동치성) 비교와 동등성(논리적 동치성) 비교는 어떤 차이점이 있나요?
- Object 클래스의 equals() 메서드를 재정의해야 할 때는 언제인가요?
- Object 클래스의 equals() 메서드를 재정의할 때 hashCode() 메서드와 같이 재정의해야 하는 이유는 무엇인가요?
참고
'Java' 카테고리의 다른 글
[Java] Object.clone() (0) | 2023.03.04 |
---|---|
[Java] Object.hashCode()와 Hashing, 그리고 Hash Collision (3) | 2023.02.20 |
[Java] Socket에 관하여 (0) | 2023.02.08 |
[Java] 다중 상속이 가지는 문제 (0) | 2023.02.03 |
[Java] 컴포지션을 선택하는 이유 (0) | 2023.01.31 |