1. Objects 클래스 소개
본격적으로 시작해 봅시다! 매번 null 체크를 수동으로 하고 각 필드의 해시를 직접 계산하는 데 지쳤다면, 유틸리티 클래스 java.util.Objects가 큰 도움이 됩니다. 이 클래스의 목적은 객체 작업을 더 단순하고, 간결하며, 안전하게 만드는 것입니다.
정말 “만능 도구”입니다: 동등성 비교를 안전하게 수행하여 NullPointerException 위험을 피하고, 해시 코드를 편리하게 계산하며, 컴퍼레이터를 통한 비교와 인자에 대한 null 검사까지 지원합니다.
Objects.equals: null을 고려한 안전한 비교
단순히 a.equals(b)를 호출했다가 a가 null이면 NullPointerException이 발생합니다. 수동 체크는 번거롭죠. Objects.equals(a, b)는 다음과 같이 처리합니다:
- 둘 다 null이면 true를 반환합니다.
- 한쪽만 null이면 false를 반환합니다.
- 둘 다 null이 아니면 일반 equals를 호출합니다.
import java.util.Objects;
String a = null;
String b = "Java";
System.out.println(Objects.equals(a, b)); // false
String c = null;
System.out.println(Objects.equals(a, c)); // true
String d = "Java";
String e = "Java";
System.out.println(Objects.equals(d, e)); // true
왜 편리할까요? 코드는 더 짧고 깔끔하며, 우발적인 NPE로부터 안전합니다.
2. Objects.hash와 hashCode: 해시를 간결하게 계산하기
equals와 함께 hashCode를 재정의할 때, 특히 필드가 많으면 실수하기 쉽습니다. 수작업 코드는 종종 장황하고 취약합니다:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
메서드 Objects.hash가 문제를 해결합니다 — 간결하고 안전하며 null도 처리합니다:
import java.util.Objects;
public class Person {
private String name;
private int age;
// ... 생성자, getter 등
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
중요: Objects.hash는 varargs를 사용하고 배열을 생성합니다 — 극히 고성능이 필요한 경로에서는 수동 hashCode가 더 빠를 수 있습니다. 대부분의 애플리케이션에서는 차이가 미미합니다.
3. Objects.compare: 비교를 컴퍼레이터에 위임하기
때로는 미리 준비한 Comparator로 두 객체를 비교해야 합니다. comparator.compare(a, b)를 직접 호출하는 대신 다음을 사용할 수 있습니다:
int result = Objects.compare(a, b, comparator);
이 메서드는 다음과 같습니다:
- 객체가 같으면 0을 반환합니다.
- null을 어떤 non-null 객체보다 ‘작다’고 간주합니다.
- 그 외에는 전달된 컴퍼레이터의 로직에 위임합니다.
import java.util.Comparator;
import java.util.Objects;
class Person {
private String name;
Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person a = new Person("Anna");
Person b = new Person("Boris");
Comparator<Person> byName = Comparator.comparing(Person::getName);
System.out.println(Objects.compare(a, b, byName)); // <0, "Anna"가 "Boris"보다 작기 때문
System.out.println(Objects.compare(a, null, byName)); // >0, a != null 이므로
System.out.println(Objects.compare(null, b, byName)); // <0, null이 b보다 작음
System.out.println(Objects.compare(null, null, byName)); // 0
}
}
4. Objects.requireNonNull: ‘보이지 않는’ 오류를 예방하기
메서드가 오직 non-null 값만 받아야 한다면 바로 검사하세요. Objects.requireNonNull은 지정한 메시지와 함께 NullPointerException을 던집니다:
public void setName(String name) {
this.name = Objects.requireNonNull(name, "이름은 null일 수 없습니다");
}
5. 예시: Objects로 equals, hashCode, compareTo를 올바르게 구현하기
import java.util.Objects;
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = Objects.requireNonNull(name, "이름은 null일 수 없습니다");
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // 참조 동일성 비교
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
// null을 고려한 안전한 비교
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 간결하고 안전함
}
@Override
public int compareTo(Person other) {
// 먼저 이름으로, 그다음 나이로 비교
int cmp = name.compareTo(other.name);
if (cmp != 0) return cmp;
return Integer.compare(age, other.age);
}
}
이제 객체를 HashSet에 저장하고, HashMap의 키로 사용하며, 동등성 비교와 리스트 정렬(예: Collections.sort)을 정상적으로 수행할 수 있습니다.
6. 실전 적용: 코드 단축과 오류 감소
예시: 애플리케이션의 사용자 목록
올바른 equals/hashCode 덕분에 컬렉션 검색이 예측 가능하게 동작합니다:
import java.util.ArrayList;
import java.util.List;
List<Person> users = new ArrayList<>();
users.add(new Person("Anna", 25));
users.add(new Person("Boris", 30));
Person search = new Person("Anna", 25);
System.out.println(users.contains(search)); // true
예시: nullable 필드 다루기
클래스에 null일 수 있는 필드(예: 미들네임)가 있다면 Objects.equals와 Objects.hash를 사용하세요:
import java.util.Objects;
public class User {
private String firstName;
private String middleName; // null일 수 있음
private String lastName;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(firstName, user.firstName)
&& Objects.equals(middleName, user.middleName)
&& Objects.equals(lastName, user.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, middleName, lastName);
}
}
7. 표: Objects 클래스의 핵심 메서드
| 메서드 | 용도 | 사용 예 |
|---|---|---|
|
null을 고려한 두 객체의 안전한 비교 | |
|
여러 필드의 해시 코드를 간결하게 계산 | |
|
컴퍼레이터를 통한 비교, null에 안전 | |
|
null 검사, NullPointerException을 던짐 | |
|
null/비-null 검사(Stream API에서 유용) | |
8. Objects 메서드 사용 시 흔한 실수
오류 1: nullable 필드에 Objects.equals를 사용하지 않음. 필드를 equals로 직접 비교하면 NullPointerException이 발생할 수 있습니다. Objects.equals(middleName, other.middleName)를 사용하세요.
오류 2: 모든 필드를 hashCode에 반영하지 않음. equals에 참여하는 필드는 반드시 hashCode에도 포함되어야 합니다. 그렇지 않으면 HashSet/HashMap의 동작이 예측 불가능해집니다.
오류 3: 수동 hashCode 구현에 실수. 계수 선택과 null 체크를 올바르게 처리하기가 쉽지 않습니다. Objects.hash가 이를 대신해 줍니다. 성능 요구가 엄격하지 않다면 이를 사용하세요.
오류 4: 클래스의 계약상 필요한 곳에서 Objects.requireNonNull을 사용하지 않음. 필드가 null을 허용하지 않는다면 생성자/세터에서 바로 검사하세요 — 오류가 즉시 드러나며, 호출 스택 “깊은 곳”에서 뒤늦게 발견되지 않습니다.
오류 5: 배열에 Objects.hash를 사용함. 배열에는 Arrays.hashCode, 중첩 배열에는 Arrays.deepHashCode를 사용해야 합니다. 마찬가지로 내용 비교에는 Arrays.equals/Arrays.deepEquals가 있습니다.
GO TO FULL VERSION