Java의 모든 객체는 Object 클래스를 상속받고 있다.
Object 클래스는 객체의 동일성을 비교하기 위한 equals와 hashCode 메서드가 정의되어 있다.
따라서 Java의 모든 객체는 필요에 따라 equals와 hashCode를 재정의할 수 있다.
equals()
equals()는 두 객체의 값이 같은지 여부를 비교할 때 사용하는 메서드이다.
객체를 비교할 때 기본적으로 객체의 주소를 비교한다.
public static void main(String[] args) {
Car car1 = new Car(1111, "blue");
Car car2 = new Car(1111, "blue");
System.out.println(car1 == car2); //두 객체는 주소가 다르기 때문에 false 반환
System.out.println(car1.equals(car2)); //false 반환
//마찬가지로 Object.equals()는 기본적으로 두 객체의 주소를 비교한다.
}
equals() 오버라이딩
위에서 car1과 car2는 각각 다른 객체를 인스턴스화하여 힙 영역에 따로 저장해두고 있기 때문에 두 객체의 주소는 다르다. 따라서 == 비교와 equals() 비교 모두 false가 된다.
컴퓨터 또는 프로그램의 입장에서 둘은 다르지만, 이 데이터를 사용하는 사용자 입장에서 두 데이터가 같다고 볼 수 있다. 또는 의도적으로 이 두 객체를 비교했을 때 같다고 하기를 원할수도 있다.
이럴 때 equals 메서드를 오버라이딩해서 단순히 주소를 비교하는 것이 아닌 필드 값을 기준으로 비교하도록 할 수 있다.
public class Car{
int carNum;
String color;
public Car(int carNum, String color) {
this.carNum = carNum;
this.color = color;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; //==비교를 통해 현재 객체와 비교 대상이 같은지 확인
if (o == null || getClass() != o.getClass()) return false; //비교 대상이 null인지 확인 || 현재 객체와 비교 대상의 타입이 같은지 확인
Car car = (Car) o;//현재 객체의 타입으로 형변환하여 필드를 비교
return carNum == car.carNum && Objects.equals(color, car.color);
}
}
equals() 오버라이딩 규칙
equals 메서드 일반 규약
equals 메서드는 동치관계(equivalence relation)를 구현하며, 아래를 만족한다. (from. Effective Java)
- 아래에서 사용되는 모든 객체는 null이 아닌 것을 가정한다.
- 반사성(reflexivity) : 모든 참조 값 x에 대해 x.equals(x)는 true다.
- 대칭성(symmetry) : 모든 참조 값 x, y에 대해 x.equals(y)가 true면 y.equals(x)도 true다.
- 추이성(transitivity) : 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면 z.equals(x)도 true다.
- 일관성(consistency) : 모든 참조값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- not-null : 모든 참조 값 x에 대해 x.equals(null)은 false다.
equals 메서드 구현 방법
- '==' 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다. (위의 예제에서는 getClass()를 이용했다.)
- 입력을 올바른 타입으로 형변환 한다.
- 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
- 당연히 null이면 안된다.
대부분의 IDE에서는 equals와 hashCode를 자동 생성해 주는 기능이 있다. ( IntelliJ - [Ctrl + insert] )
또는 Lombok의 @EqualsAndHashCode를 이용할 수도 있다.
hashCode()
hashCode() 메서드는 객체의 주소값을 이용한 해싱(hashing) 기법을 통해 해시 코드를 만든 후 반환한다.
따라서 서로 다른 객체는 같은 해시코드를 가질 수 없게 된다.
hashCode() 오버라이딩
@Override
public int hashCode() {
return Objects.hash(carNum, color);
}
Objects.hash() 메서드를 타고 들어가 보자.
Objects.hash()
//public class Objects
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
Arrays.hashCode()
아래와 같이 element == nulll 일 때는 0을, 아닐 때는 hashCode()를 재귀적으로 호출해 준다.
//public class Arrays
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());//재귀 호출 부분
return result;
}
31을 사용하는 이유는 홀수이면서 소수이기 때문이다. 만약 이 숫자가 짝수이고, 오버플로우가 발생한다면 정보를 잃기 때문이다.
Object.hashCode()
native 키워드를 확인할 수 있다. 이 키워드가 붙은 메서드는 OS가 가지고 있는 메서드를 말한다.
이 메서드는 OS에 C언어로 작성되어 있어서 안의 내용은 확인할 수 없고 사용할 수만 있다.
//public class Object
public native int hashCode();
equals()를 재정의 하려면 hashCode()도 재정의 해라
항상 equals()와 hashCode()는 함께 재정의 해야 한다. 그렇지 않으면 Java의 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 이용할 때 문제가 발생할 수 있다.
hash 값을 사용하는 Collection(HashMap, HashSet 등)은 객체가 동일한지 비교할 때 아래와 같은 과정을 거친다.
따라서 equals() 결과가 같더라도 hashCode()가 다르면 다른 객체로 인식하게 된다.
지금 당장 이런 자료구조를 사용하지 않더라도 언제 사용하게 될지 예측할 수 없기 때문에 equals()와 hashCode()는 항상 같이 재정의 하는 것이 원칙이다.
identityHashCode()
hashCode()의 본래 목적은 객체의 주소값을 기반으로 해싱하여 고유한 숫자 값(int)을 반환하는 것이다.
그런데 hashCode()를 overriding하게 되면 기존 객체 자체의 주소값(해시코드)을 얻을 수 없게 되기 때문에,
Java에서는 원래 해시코드 값을 반환하는 메서드를 제공한다.
System.identityHashCode(obj);
'JAVA & Spring > JAVA 이론' 카테고리의 다른 글
[Java] JAVA_HOME 자바 환경변수 설정 (0) | 2024.10.15 |
---|---|
[Java] 깊은 복사(Deep Copy) vs 얕은 복사(Shallow Copy) (0) | 2024.03.16 |
[Java / CS] JVM과 Java의 메모리 구조 (0) | 2024.02.16 |
[JAVA] 추상 클래스 (abstract class) (0) | 2023.05.18 |
[JAVA] 상속 (0) | 2023.05.17 |