4 minute read

모든 객체의 공통 메서드

Object 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다.

Object에서 final이 아닌 메서드는 모두 재정의를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 모두 명확히 정의되어 있다.

메서드를 잘못 구현하면 대상 클래스가 이 규약을 준수한다고 가정한느 클래스를 오동작하게 만들 수 있다.

equals는 일반 규약을 지켜 재정의하라.

다음에서 열거한 상황 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.

  • 각 인스턴스가 본질적으로 고유하다.
  • 인스턴스의 ‘논리적 동치성’을 검사할 일이 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.

그렇다면 equals를 재저의해야 할 때는 언제일까?

→ 객체 식별성이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다.

equals가 논리적 동치성을 확인하도록 재정의해두면, 그 인스턴스는 값을 비교하길 원하는 Map의 키와 Set의 원소로 사용할 수 있게 된다.

값 클래스라 해도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스라면 equals를 재정의하지 않아도 된다.

다음은 equals 명세에 적힌 규약이다.

  • 반사성
    • 반사성은 단순히 말하면 객체는 자기 자신과 같아야 한다는 뜻이다. 이 요건을 어긴 클래스의 인스턴스를 컬렉션에 넣은 다음 contains 메서드를 호출하면 방금 넣은 인스턴스가 없다고 답할 것이다.
  • 대칭성
    • 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 것이다.
  • 추이성
    • 추이성은 a = b, b = c, c= a라는 뜻이다.
    • 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
  • 일관성
    • 일관성은 두 객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다.
    • 클래스를 작성할 때는 불변 클래스로 만드는 게 나을지를 심사숙고하자.
    • 불변 클래스로 만들기로 했다면 equals가 한번 같다고 한 객체와는 영원히 같다고 답하고, 다르다고 한 객체는 영원히 다르다고 답하도록 만들어야 한다.
    • 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다.
  • null-아님

equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
  3. 입력을 올바른 타입으로 형변환한다.
  4. 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다.
  5. float와 double을 제외한 기본 타입 필드는 == 연산자로 비교하고, 참조 타입 필드는 각각의 equals 세머드로, float와 double 필드는 각각 정적 메서드인 Float.compare(float, float), Double.compare(double, double)로 비교한다.
  6. Float.equals나 Double.equals를 사용할 수 있지만 오토박싱으로 인해 성능상 좋지 않다.

어떤 필드를 먼저 비교하느냐가 equals의 성능을 좌우하기도 한다. 최상의 성능을 바란다면 다를 가능성이 더 크거나 비교하는 비용이 싼 필드를 먼저 비교하자.

equals를 다 구현했다면 세 가지만 자문해보자. 대칭적인가? 추이성이 있는가? 일관적인가?

equals를 재정의할 땐 hashCode도 반드시 재정의하자

너무 복잡하게 해결하려 들지 말자.

꼭 필요한 경우가 아니라면 equals를 재정의하지 말자. 많은 경우에 Object의 equals가 여러분이 원하는 비교를 정확히 수행해준다. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.

equals를 재정의하려거든 hashCode도 재정의하라.

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.

그렇지 안흥면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 hashMap, Hashset같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.

  • equals 비교에 사용되는 정보가 변경되지 않앗다면, app이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 다시 실행한다면 이 값이 달라져도 상관없다.
  • equals(object)가 두 객체가 같다고 판단햇다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • equals(object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다.

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다.

toString을 항상 재정의하라.

toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다.

모든 구체 클래스에서 Object의 toString을 재정의하자. 상위 클래스에서 이미 알맞게 재정의한 경우는 예외다. toString을 재정의한 클래스는 사용하기도 즐겁고 그 클래스를 사용한 시스템을 디버깅하기 쉽게 해준다. toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 한다.

clone 재정의는 주의해서 진행해라.

Comparable을 구현할지 고려하라.

Comparable 인터페이스의 유일무이한 메서드인 compareTo를 알아보자. 이번 장에서 다른 다른 메서드들과 달리 compareTo는 Object의 메서드가 아니다.

compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다.

Comparable을 구현했다는 것은 그 클래스의 인스턴스들에 자연적인 순서가 잇음을 뜻한다.

그래서 Comparable을 구현한 객체들의 배열은 다음처럼 손쉽게 정렬할 수 잇다.

Arrays.sort(a);

compareTo 규약을 자세히 보자.

첫 번째 규약은 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.

두 번째 규약은 첫 번째가 두 번째보다 크고 두 번째가 세 번재보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.

마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.

이상의 세 규약은 compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다.

마지막 규약은 간단히 말하면 compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 것이다. 이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관되게 한다. compareTo의 순서와 equals의 결과가 일관되지 않은 클래스도 여전히 동작은 한다. 단, 이 클래스의 객체를 정려된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(collection, set, Map)에 정의된 동작과 엇박자를 낼 것이다. 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 놀랍게도 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문이다. 주의해야 한다.

자바 7부터는 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare을 이용하면 된다.

compareTo 메서드에서 관계 연산자 < 와 > 를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니. 이제는 추천하지 않는다.

자바 8에서는 Comparator 인터페이스가 비교자 생성 메서드를 이용해 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.

Comparator는 수많은 보조 생성 메서드드로 중무장하고 있다. long과 double용으로는 comparingInt와 thencomparingInt의 변형 메스드가 있다.

순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 < 와 > 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드가 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.