CodeGym /행동 /JAVA 25 SELF /Objects 클래스: equals, hashCode, hash 메서드

Objects 클래스: equals, hashCode, hash 메서드

JAVA 25 SELF
레벨 29, 레슨 1
사용 가능

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 클래스의 핵심 메서드

메서드 용도 사용 예
Objects.equals(a, b)
null을 고려한 두 객체의 안전한 비교
Objects.equals(a, b)
Objects.hash(a, b, ...)
여러 필드의 해시 코드를 간결하게 계산
Objects.hash(name, age)
Objects.compare(a, b, comparator)
컴퍼레이터를 통한 비교, null에 안전
Objects.compare(p1, p2, byNameComparator)
Objects.requireNonNull(obj[, msg])
null 검사, NullPointerException을 던짐
Objects.requireNonNull(name, "이름은 null일 수 없습니다")
Objects.isNull(obj) / Objects.nonNull(obj)
null/비-null 검사(Stream API에서 유용)
list.stream().filter(Objects::nonNull)

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가 있습니다.

1
과제
JAVA 25 SELF, 레벨 29, 레슨 1
잠금
카탈로그의 도서용 디지털 지문 생성 📚
카탈로그의 도서용 디지털 지문 생성 📚
1
과제
JAVA 25 SELF, 레벨 29, 레슨 1
잠금
이벤트 참가자를 이름순으로 정렬하기 🧑‍🤝‍🧑
이벤트 참가자를 이름순으로 정렬하기 🧑‍🤝‍🧑
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION