equals 왜 override를 해야하는거지??
예를 들어, 위와 같은 Student 클래스가 있다고 가정하자.
이전의 나는 '에이 무조건 true만 출력되지~' 였다.
하지만 출력 했을 시 아래와 같은 콘솔창을 만날 수 있다.. 와 이게 뭐지?!?!?!
조사해보니 studentYoon 변수와 studentMoon 변수는 각각 다른 객체를 초기화해서 Heap Memory 영역에 따로 저장해두고 있다.
각 변수가 저장되는 메모리 영역에 대해 조금 알고 싶으면 아래 게시글을 참고하길 바란다.
두 객체를 비교하면 참조하는 메모리 주소가 같지 않아 당연히 false가 발생하는 것이다!!
이 때는 주로 Object 클래스에 정의된 equals() 메서드를 override 하여 사용한다고 한다.
override란 뭐지??
: 추상적으로 말하자면 부모 클래스 내 메서드를 재정의하는 것이다.
위 예시에서 내가 말하고자 하는 override는 부모클래스인 Object 클래스 내 메서드인 equals를 재정의하는 것이다.
Object 내 활용할 수 있는 메서드 키워드 - equals !!
: equals()와 hashCode() 는 모두 Object 클래스에 있는 메서드이다.
그렇기 때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode() 메서드를 상속받고 있다.
기존 Java의 Object 클래스에 정의된 boolean equals(Object obj) 메소드를 살펴보자.
equals() in Object class
: 기본적으로 2개의 서로 객체가 동일한지 검사하기 위해 사용된다.
조금의 스포를 하자면, 이는 후에 살펴볼 동일성(Identity)를 비교하기 위해 사용한다.
this와 obj를 == 으로 비교함으로써, 두 객체가 참조하는(가리키는) 메모리 주소가 같은지를 비교하고 있다.
⇒ 이 때문에 기본적으로 ‘Object’ 클래스의 equals()메소드는 == 연산자와 같은 기능을 수행하는 것이다.
그런데.. 과연 equals() 메서드를 어느 상황에서든지
override해야 하는건가? 아닐 때도 있지 않을까??
override할 필요가 없을 때
※ 해당 내용은 이펙티브 자바에서 가져온 일부 내용임을 밝힌다
- 싱글톤 패턴으로 class를 구현했을 때, enum 형식으로 class를 구현했을 때
- → 해당 class에 대한 instance는 본질적으로 고유하기 때문이다.
- 논리적 동치성(logical Identity, 동일성)을 검사할 필요가 없을 때→ “hello”와 “hello”는 같은 것으로 판단하기에도 충분하기 때문이다.
쉽게 말하자면, logical Identity := 두 자동차가 같은 번호판의 차인가(완전히 똑같은 차)? 를 묻는 것과 비슷하다고 이해하면 되겠다
또한, logical equality := 단순히 차가 같은 (종류의) 차인가? 를 묻는 것과 비슷하다고 이해하면 되겠다
- 참고) 더 쉽게 예를 들자면, 자동차 객체를 여러 개 찍어냈을 때를 들 수 있다.
- → 예를 들어, String을 비교할 때는 .equals()를 논리적 동치성으로 판단할 필요가 없다.
- 이미 상위 클래스에서 재정의한 equals가 있을 경우
- 예를 들어, list, set 의 상위 클래스인 AbstractList, AbstractSet에 이미 .equals()는 재정의가 되어 있다.
- 클래스가 private이거나 package-private이고 equals() 메서드를 호출할 일이 없을 때
- 예를 들어, 클래스가 public이면 외부에서 언제든지 해당 클래스의 인스턴스를 List, Set에 넣음으로써 .equals()가 호출되버릴 수 있는 것처럼 어떻게 쓰일지 예상할 수 없다.
그럼 논리적 동치성(logical Identity)을 검사할 필요가 없을 때인 String 클래스 내부에서
equals()를 실제로 어떻게 정의하고 있을까??
equals() in String class
: String 클래스에서 Java Object 클래스에 정의된 equals() 메서드를 override하고 있다
⇒ 객체가 가리키고 있는 heap영역의 주소값이 아닌 문자열 값으로 비교함으로써 논리적 동등성(logical equality)을 판단할 수 있다.
코드를 자세히 까보도록 하자.
- 메모리 참조 확인
- 처음 if 문을 통해 this 즉, ‘현재 문자열’과 ‘anObject’가 메모리 상에서 같은 객체를 참조하고 있는지 확인한다.
⇒ 두 변수가 정확히 같은 객체를 가리키고 있다면, String은 immutable한 타입이기 때문에 정확히 같은 내용을 가리키므로 바로 true를 반환한다.
2. String 내부 인코딩 비교
- anObject가 String 형식이라면, String으로 안전하게 casting한다.
- 이후, coder()를 통해 ‘현재 문자열’과 ‘aString’이 같은 문자 인코딩을 사용하는지 확인한다
- 참고) Java에서 문자열은 Latine-1 or UTF-16으로 encoding 하여 문자열을 효율적으로 저장하고 처리한다.
- 만약 ‘현재 문자열’이 Latine-1()으로 encoding을 사용한다면, StringLiatin1.equals() 메서드를 호출하고, 그렇지 않다면(:= UTF-16() encoding을 사용한다면), StringUTF16.equals() 메소드를 호출한다. 참고) Java에서 문자열 내 ASCII만 포함되어 있으면 → Latine-1 encoding
Java에서 문자열 내 다국어 문자(유니코드)가 포함되어 있으면 → UTF-16 encoding
참고)
- 보통 Latin-1으로 encoding된 문자열은 각 문자를 1 byte로 표현한다.
- 하지만, UTF-16 encoding은 각 문자를 2 byte로 표현한다.
- ⇒ 따라서 Latin-1 encoding으로 된 문자열을 처리할 때, 효율적인 메모리 사용과 동시에 더 빠른 메모리 액세스를 예상할 수 있을 것이다.
StringLatin1 내부 equals() 코드
- byte 배열로 데이터를 입력으로 받는다.
- for문을 통해 두 배열의 index의 요소를 순차적으로 비교하고 있다.
⇒ 이로써, 객체가 아닌 문자열의 값을 비교해서 논리적 동등성을 판단하는 것이다!!
실제 코드로도 알아보자!! - equals() override
이 논리적 동치성(logical Identity)를 검사할 필요가 없는 경우, 아래와 같이 Object 클래스에 정의된 equals()를 override함으로써 논리적 동등성(logical equality)을 판단할 수 있다.
예를 들어, Student 객체를 아래와 같이 정의했다고 가정하자.
이 때, 해당 Student 클래스 내부에 Object 클래스에 정의된 .equals()를 아래와 같이 override 함으로써 logical equality을 비교할 수 있는 것이다.
테스트 코드
- 그럼 equals()를 override하였을 때, 정상적으로 논리적 동치성과 논리적 동등성을 만족하는지 확인해도록 해보자.
결과
- 아래와 같이 모두 정상적으로 통과됨을 알 수 있었다!!
- 사실 Spring boot @Lombok을 이용하면 내부적으로 equals()를 override하고 있어서 이렇게까지 할 필요는 없긴하다.
hashCode() override
위에서 equals()를 override를 하였는데, 찾아보니 이를 선수하면 hashCode() 또한 override해야 한다고 한다.
왜 그럴까.. hashCode 규약을 살펴보도록 하자.
hashCode() 규약
- 두 객체에 대한 equals가 같다면, hashCode의 값 또한 반드시 같아야 한다.
- 두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만, 가능한 한 다른 값을 리턴하는 것이 좋다.
- 해시 테이블 성능을 고려해 성능저하를 막기 위해
- 해시 테이블 내 key 값에 list형태로 hashCode가 들어감으로써, 리스트 순회로 인한 O(1) → O(n)의 성능 저하
- 즉, 해시 충돌로 인한 성능 저하가 발생
- 참고) 해시 충돌이 발생 시 처리하는 방법으로 체이닝 기법이 존재한다.
즉, equals()를 override 시, 해시 충돌을 막고 일관성을 보장하기 위해 hashCode()를 같이 override해야 한다.
int hashCode()
해당 객체의 (heap) 메모리 주소를 통해 hashing 기법을 통해 해시 코드를 만든 후 반환한다.
참고) 엄연히 말하면 주소값으로 만들어진 고유한 숫자값이다.
참고) Runtime 시, 객체의 유일한 integer 값을 반환한다.
⇒ 서로 같은 객체로 판단되어야만 같은 hashCode()를 가질 수 있다.
참고) 서로 다른 객체는, 무슨 일이 있어도 같은 hashCode()를 가질 수 없기 때문에 hashCode()를 객체의 지문이라고도 한다.
equals()와 hashCode의 관계
hashCode 규약을 더 자세히 이해하기 위해 둘 간의 관계를 알아보자.
먼저, 이를 이해하기 위해 객체의 일관성에 대해 알 필요가 있다.
객체의 일관성이란?
객체의 일관성(Object Consistency)은 객체지향 프로그래밍에서 객체의 상태가 일관되게 유지되는 것을 의미한다. 즉, 객체가 생성된 후에도 그 구조와 특성이 변하지 않고 일관된 상태로 남아있어야 한다.
Object 클래스에 정의된 equals()와 hashCode()
이미 Object 클래스에 정의된 equals()는 논리적 동치성을 비교하고 있기 때문에, hashCode()또한, equals()메서드를 통해 객체의 일관성을 보장하고 있다.
하지만!
equals() 메서드를 override한 equals() 메서드는 객체가 참조하고 있는 메모리 주소를 정확히 비교하는 게 아니라, 값을 비교하기 때문에 두 객체는 “같다”로 판단을 한다.
그런데, 기존 Object 클래스에 정의된 hashCode()는 객체의 메모리 주소가 다르기 때문에 “다르다”고 판단을 내리게 된다.
이 때문에 hashCode 규약을 지키지 않게 됨으로써 데이터 일관성이 Hash table 상 성능저하가 발생하는 것이다.
따라서
hashCode()도 override를 통해 재정의 해줌으로써 객체의 일관성을 보장하고, Hash table 간 성능저하를 막아야 한다.
hashCode() override 하지 않았을 시 테스트
테스트 결과
- override한 equals() 메서드는 두 Student 객체가 같은 메모리 주소를 가지지 않더라도, 같은 객체로 인지하는 반면
- 기존 Object 클래스에 정의된 hashCode()는 두 객체가 같은 메모리 주소를 가지지 않으므로 다른 hashCode()를 반환한다.
- 따라서, Set에 저장이 될 때 같은 객체로써 하나로 저장이 되는 게 아니라 서로 다른 hashCode() 값을 가지고 있기 때문에 1개의 객체가 아닌, 총 2개의 객체가 저장이 되는 것이다.
hashCode() override 한 후 테스트
Student 객체 내 hashCode() override
테스트 결과
- 기존 Object 클래스에 정의된 hashCode()를 override함으로써, 객체의 메모리 주소(정확히는 객체가 가리키는 메모리 주소)가 같지 않더라도 서로 같은 hashCode()값을 가지도록 한다.
- Set에 저장이 될 때는 같은 hashCode 값을 가지고 있기 때문에 같은 데이터로 인식되어 중복 방지 처리가 정상적으로 됨으로써 전체 Set size가 1개가 나옴을 확인할 수 있다.
정리
논리적 동일성(동치성)
: 같은 메모리 주소를 참조하는지 여부를 확인
논리적 동등성
: 메모리 주소가 아닌 두 개체의 값이 동일한지 여부를 확인
Object 클래스에 정의된 equals()
그런데 만약, 이렇게 Object 클래스에 정의된 equals() 메서드를 사용하면 논리적 동등성(logical equality)을 확인할 수 없다. 즉, 두 객체의 내용이 같은지 확인할 수는 없다.
equals() override
Object 클래스에 정의된 equals() 메서드를 아래와 같이 override함으로써 논리적 동등성(logical equality)을 확인할 수 있다.
Object 클래스에 정의된 hashCode()
해당 객체의 (heap) 메모리 주소를 통해 hashing 기법을 통해 해시 코드를 만든 후 반환하며, 이로써 서로 다른 객체는 같은 hashCode()값을 가질 수 없다.
hashCode() override
기존 Object 클래스에 정의된 hashCode()는 해당 객체의 메모리 주소를 활용하기 때문에 Object 클래스에 정의된 equals() 메서드를 override한 값과 다른 결과를 내게 되므로, 데이터 일관성을 깨지게 된다. 이를 방지하기 위해 hashCode()를 override해야 한다.
참고) Object 클래스에 정의된 메서드 중 override 할 수 있는 대표적인 메서드는 equals(), hashCode(), toString(), clone(), finalize()가 있다.
참고
https://www.yes24.com/Product/Goods/65551284
https://mangkyu.tistory.com/101
https://howtodoinjava.com/java/basics/java-hashcode-equals-methods/
'Java' 카테고리의 다른 글
JVM (0) | 2023.07.10 |
---|---|
System.out.println을 쓰면 안되는 이유 (0) | 2023.07.06 |
Java Exception ( feat. Checked, UnChecked ) (0) | 2023.06.26 |
Java (0) | 2023.06.23 |