728x90
Object 클래스
- 자바에서 모든 클래스가 공통으로 가지는 상위 클래스이다.
- 다르게 말하면 자바의 모든 클래스는 Java.lang.Object 클래스의 하위 클래스이다.
- 객체를 표현하는 데 필요한 메서드를 제공한다.
- 클래스가 명시적으로 상위 클래스를 상속하지 않을 경우 컴파일러가 자동으로 Object 클래스를 상속하게 된다.
- 따라서, 하나의 클래스가 다른 클래스를 상속받는다고 하더라도 올라가다 보면 그 맨 위는 Object 클래스인 것이다.
Object Class의 메서드
메서드 명 | 역할 |
clone() | 객체를 복제하여 새로운 인스턴스를 생성 |
equals(Object obj) | 해당 객체와 매개변수로 전달된 객체를 비교하여 동일한지 여부를 반환 |
hashCode() | 해당 객체의 해시코드 값을 반환 |
toString() | 해당 객체의 문자열 표현을 반환 |
getClass() | 해당 객체의 클래스 정보를 반환 |
notify() | 해당 객체에 대한 대기 중인 하나의 스레드를 깨움 |
notifyAll() | 해당 객체에 대한 대기 중인 모든 스레드를 깨움 |
wait() | 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦 |
wait(long timeout) | 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦. 최대 대기 시간을 밀리초 단위로 설정 가능 |
wait(long timeout, int nanos) | 해당 객체에 대해 호출한 스레드를 잠시 중지시키고 대기 상태로 만듦. 최대 대기 시간을 밀리초와 나노초로 설정 가능 |
finalize() | 가비지 컬렉션에 의해 객체가 메모리에서 해제되기 직전에 호출되는 메서드 (Defrecated) |
- Object 클래스는 필드를 가지지 않는다.
- 이 글에서는 equals(), hashCode()에 대해 자세히 알아보려고 한다.
equals() 메서드
문자열 비교
String a = "intp";
String b = "intp";
String c = new String("intp");
System.out.println(a == b); // (1) true
System.out.println(a.equals(b)); // (2) true
System.out.println(a == c); // (3) false
- equlas() 메서드는 보통 비교 연산자 '=='와 많이 비교가 된다.
- 문자열의 경우 '=='는 데이터의 메모리 주소값이 같은지 여부를 비교하지만, equals() 메서드는 문자열의 값이 같은지 비교한다.
- 따라서 위 코드에서 (2)는 문자열 값을 비교하기 때문에 true인 것이다.
- 그런데 주소값을 비교하는 (1)은 왜 true인 것일까?
- 자바에서 String 리터럴은 내부적으로 같은 값을 가진 경우에는 동일한 String 객체를 참조하기 때문이다.
- String a와 b는 "intp"이라는 동일한 리터럴을 가지고 있음으로 같은 String 객체를 참조하는 것이다.
객체 비교
Student student1 = new Student(1, "손흥민", "A+");
Student student2 = new Student(1, "손흥민", "A+");
System.out.println(student1 == student2); // (1) False
System.out.println(student1.equals(student2)); // (2) False
Assertions.assertEquals(student1, student2); // (3) Test failed
// expected: <study.object.Student@331ad6eb> but was: <study.object.Student@6cd6698b>
// Expected :study.object.Student@331ad6eb
// Actual :study.object.Student@6cd6698b
- 객체의 경우 equals() 메서드 역시 객체의 주소를 이용하여 비교한다.
- 따라서 (1), (2)처럼 객체 간 비교에서는 ==, equals() 메서드 모두 주소값을 이용하여 비교하기 때문에 false가 나온다.
- (3)을 통해 실제 참조하는 메모리 주소값이 같은 값이 있는 객체라도 다르다는 것을 확인할 수 있다.
equals 오버라이딩
- 컴퓨터의 관점에서는 객체 간 비교시 메모리 주소값을 가지고 비교하기 때문에 주소가 일치하지 않으면 같은 객체가 아니라고 판단한다.
- 그러나 사람의 관점에서는 같은 데이터라고 볼 수도 있다.
- 필드값이 완전히 똑같기 때문이다.
- 따라서 만약 객체 타입을 비교할 때 비교 기준을 객체의 주소값이 아닌 필드값으로 하고 싶다면 equals() 메서드의 오버라이딩을 통해 재정의해주면 된다.
예시 코드
@Getter
@AllArgsConstructor
public class StudentOverriding {
private int studentId;
private String name;
private String grade;
/**
* @param obj 비교 대상
* @return 필드값 비교를 통해 값이 같은지 여부 리턴
*/
@Override
public boolean equals(Object obj) {
// 두 비교 객체가 같은 객체일 경우 true
if (this == obj) return true;
// 객체가 Null이거나 Student를 포함하는 하위 클래스가 아니라면 false 리턴
else if (obj == null || !(obj instanceof StudentOverriding)) return false;
// 다운캐스팅을 통해 obj를 StudentOverriding 인스턴스로 변경 후
// 비교 주체와 비교 대상의 필드값을 비교하여 모두 같으면 같은 객체로 취급함
else {
StudentOverriding studentOverriding = (StudentOverriding) obj;
boolean result =
studentId == studentOverriding.getStudentId() &&
name.equals(studentOverriding.getName()) &&
grade.equals(studentOverriding.getGrade());
return result;
}
}
}
- 위와 같이 equals() 메서드를 오버라이딩하여 비교 주체와 대상의 필드값을 비교하는 방식으로 재정의했다.
- a.equals(b)일 때 , a가 비교 주체이고 b가 비교 대상이다.
테스트 코드
@Test
public void studentOverridingEquals() {
StudentOverriding studentOverriding1 = new StudentOverriding(2, "해리케인", "B");
StudentOverriding studentOverriding2 = new StudentOverriding(2, "해리케인", "B");
System.out.println(studentOverriding1.equals(studentOverriding2)); // true
Assertions.assertEquals(studentOverriding1, studentOverriding2);
}
- StudentOverriding의 경우 equals() 메서드를 재정의하였기 때문에 주소값이 아닌 필드값을 통해 비교한다.
- 따라서 true를 리턴한다.
hashCode() 메서드
- 객체의 주소값을 해싱하여 나온 해시 코드를 반환한다.
- 주소값으로 만든 고유한 값이기 때문에 객체의 지문이라고도 한다.
@Test
public void studentHashCode() {
Student student1 = new Student();
Student student2 = new Student();
System.out.println(student1.hashCode()); // 988637485
System.out.println(student2.hashCode()); // 1324113830
}
- 기본적으로 두 인스턴스의 주소값은 다르기 때문에 다른 주소값을 해싱한 결과는 같을 수 없다.
equals()와 hashCode() 메서드
- 두 메서드는 같이 재정의해야한다.
- equals() 메서드만을 재정의할 경우 자바의 컬렉션 프레임워크를 사용 시에 의도와는 다른 결과가 발생하기 때문이다.
equals() 메서드만 재정의
@Test
public void onlyEquals() {
StudentOverriding studentOverriding1 = new StudentOverriding();
StudentOverriding studentOverriding2 = new StudentOverriding();
// List에 student 1,2 추가
List<StudentOverriding> studentList = new ArrayList<>();
studentList.add(studentOverriding1);
studentList.add(studentOverriding2);
// List 요소 개수 출력
System.out.println(studentList.size()); // 2
// Set에 student 1,2 추가
Set<StudentOverriding> studentSet = new HashSet<>();
studentSet.add(studentOverriding1);
studentSet.add(studentOverriding2);
// Set 요소 개수 출력
System.out.println(studentSet.size()); // 2
}
- 위 코드에서 StudentOverriding 클래스는 equals() 메서드만 재정의한 클래스이다.
- 여기서 List와 Set에 StudentOverriding 클래스의 인스턴스를 요소로 추가하고 size() 메서드로 개수를 출력해 보면 List와 Set 모두 2가 나온다.
- Set의 경우 중복을 제거하기 때문에 equals() 메서드로 같다고 나온 두 인스턴스는 중복으로 처리되어 1이 출력되어야 하는데 말이다.
- 이렇게 동작하는 이유는 컬렉션은 객체가 논리적으로 같은지를 판단할 때 hashCode의 리턴값을 비교하게 되고 만약 같다면 그다음으로 equals() 메서드를 사용해 비교하기 때문이다.
- 즉, hashCode() -> equals() -> true return의 순서로 동작하기 때문에 먼저 비교하는 hashCode()에서 서로 다른 객체라고 판단하고 비교 로직을 끝내버리는 것이다.
- 따라서 equals() 메서드를 재정의하는 경우에는 hashCode()도 함께 재정의해야 한다.
예시 코드
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StudentOverriding {
private int studentId;
private String name;
private String grade;
...
@Override
public int hashCode() {
return Objects.hashCode(studentId); // studentId 필드의 해시코드를 반환한다.
}
}
- 필드값 중 하나인 studentId는 unique 하기 때문에 이 값을 기준으로 해시코드를 반환하도록 했다.
- 이제 hashCode() 메서드에서 동일한 studentId를 비교하는 경우 같은 해시코드가 나와 같은 객체라고 판단할 것이고,
- 그다음 equals() 메서드에서 필드값 전체를 비교하는 로직이 진행될 것이다.
테스트 코드
@Test
public void studentOverridingHashCode() {
StudentOverriding studentOverriding1 = new StudentOverriding(2, "해리케인", "B");
StudentOverriding studentOverriding2 = new StudentOverriding(2, "해리케인", "B");
// List에 student 1,2 추가
List<StudentOverriding> studentList = new ArrayList<>();
studentList.add(studentOverriding1);
studentList.add(studentOverriding2);
// List 요소 개수 출력
System.out.println(studentList.size()); // 2
// Set에 student 1,2 추가
Set<StudentOverriding> studentSet = new HashSet<>();
studentSet.add(studentOverriding1);
studentSet.add(studentOverriding2);
// Set 요소 개수 출력
System.out.println(studentSet.size()); // 1
}
- studentOverrriding1, 2의 필드값은 모두 동일하다.
- 위에서 설명한 대로 먼저 hashCode() 메서드로 studentId의 해시코드를 비교하는데 이때 studentId를 기준으로 비교하기 때문에 같은 객체라고 판단할 것이다.
- 다음으로 equals() 메서드 비교를 통해 두 인스턴스의 필드값인 {StudentId : 2}, {name : "해리케인"}, {grade : "B"}를 비교하면 같은 객체라고 최종 판단을 하고 이는 중복 객체라고 인식하게 됨으로 Set 특성상 컬렉션의 요소로 studentOverrriding1만 추가했을 것이다.
- 따라서 Set 요소 개수는 중복이 제거된 1로 출력된다.
@EqualsAndHashCode 애너테이션
- Lombok 라이브러리에 포함된 애너테이션 중에 하나이다.
- 자바 프로젝트에 Lombok 라이브러리 의존성을 추가하면 사용할 수 있다.
- 클래스의 객체들을 비교할 때 equals()와 hashCode()를 자동으로 생성해 준다.
- 클래스 안의 모든 필드들을 비교하여 equals() 및 hashCode() 메서드를 자동으로 생성하기 때문에 매우 편리하게 사용할 수 있다.
- 이를 통해 객체를 생성하거나 변경할 때, equals() 및 hashCode() 메서드를 구현하는 코드를 줄일 수 있다.
- @EqualsAndHashCode는 객체의 내용이 같다면 equals() 메서드를 통해 두 객체를 같은 것으로 판단한다.
- (객체의 내용이 같다면) hashCode() 메서드를 사용하여 두 객체가 같은지 판단할 때는 같은 hashCode를 반환한다.
- 만약 비교에서 제외되어야 하는 필드가 있는 경우 애노테이션 내의 exclude 또는 of 옵션을 사용하여 제외할 수 있다.
exclude
@EqualsAndHashCode(exclude = {"id","createdDate","modifiedDate"})
public class MyClass {
private Long id;
private String name;
private Date createdDate;
private Date modifiedDate;
}
- 비교를 하고 싶지 않은 필드(id, 생성 시각, 수정 시각 등)가 있는 경우 위 코드와 같이 exclude 옵션을 사용하여 비교에서 제외할 필드를 선택할 수 있다.
of
@EqualsAndHashCode(of = {"id"})
public class MyClass {
private Long id;
private String name;
}
- of 옵션의 경우 include와 같은 의미를 가진다.
- 비교에 사용할 필드를 명시적으로 지정할 수 있다.
- include 옵션에서 명시적으로 지정한 필드만을 비교하게 된다.
테스트 코드는 아래의 깃허브 레파지토리에서 확인할 수 있다.
https://github.com/wonyongg/test/tree/main/object
참고
뤼튼
https://inpa.tistory.com/entry/JAVA-☕-equals-hashCode-메서드-개념-활용-파헤치기
728x90
'[JAVA] > JAVA 기본' 카테고리의 다른 글
자바에서 중복 요소를 남김없이 모두 제거하는 방법 (3) | 2024.03.21 |
---|---|
자바에서 대용량 엑셀 데이터를 읽어들이는 ExcelParser를 만들어보자. (0) | 2023.08.11 |
정적 팩토리 메서드의 특징과 사용법을 예제로 이해하기 (0) | 2023.06.20 |
Comparable과 Comparator.comparing() 간단 정리 (0) | 2023.06.14 |
NPE와 Optional에 대해 간단히 알아보기 (0) | 2023.06.13 |