EffectiveJava 08. equals 메소드를 오버라이딩할 때 보편적인 규칙을 따르자.

Jae·2024년 3월 24일
0

Effective Java

목록 보기
9/11

equals를 오버라이드해서 사용할 수 있는데 생각보다 고려해야할 것이 많아 잘 알고 쓰는 것이 중요하며, 실수가 없이 하는 것이 중요한데 그 이유에 대해 학습한 내용을 정리하였다.

equals 오버라이딩이 필요하지 않은 경우

equals 오버라이딩은 규칙이 있기 때문에 구현함에 까다로움이 있다. 따라서 하지 않는 것이 좋은데 아래와 같은 경우 더더욱 기존에 구현되어 있는 것을 사용하는 것이 좋다.

  • 클래스가 각 인스턴스가 본래부터 유일한 경우
    활동하는 객체, 사용중인 객체(ex, thread) 라는 특성이 더 중요한 경우 사용한다. 객체 참조가 같으면 동일하다는 것을 증명하는 것이 중요하기에 값의 비교가 필요없으므로 사용하지 않는다.

  • 두 인스턴스가 논리적으로 같은지 검사하지 않아도 되는 클래스의 경우
    책에서는 java.util.Random 클래스에서 두 개의 Random 인스턴스가 같은 난수열을 만드는지 확인하기 위해 equals 메소드를 오버라이딩 할 수 있었을 것이고, 클래스 설계자는 클라이언트가 그런 기능을 원하지 않았다고 생각하기에 Object로부터 상속받은 equals를 그냥 쓰면 된다고 한다고 한다.

    내가 이해한 것은 난수클래스는 값이 변하는데 이걸 구현하는 로직이나 데이터 타입이라던지 참조값이 더 중요한 경우 앞의 인스턴스가 유일한 경우를 말하는 것으로 이해가 된다.

  • 수퍼클래스에서 equals 메소드를 이미 오버라이딩했고, 그 메소드를 그대로 사용해도 좋은 경우
    대표적으로 Set 인터페이스를 구현하는 대부분의 클래스의 경우 AbstractSet에 있는 이미 구현된 equals를 사용하고 있고 특별히 별도로 구현할 이유가 없어서 그대로 사용하는 것이 좋다.

  • private나 패키지 전용 클래스들은 이 클래스의 equals가 호출되면 안되기 때문에 사용하지 않고, 우연히라도 호출될 수 있기 때문에 예외처리를 해줘야한다. ;;

equals 오버라이드가 필요한 상황

인스턴스가 갖는 값을 비교하여 논리적으로 같은지 판단할 필요가 있는 클래스로써, Integer나 Date 클래스처럼 데이터의 참조 여부는 중요하지 않고 실제 갖고 있는 값의 판단이 중요하다. 참조형 데이터타입에서 값이 reference 데이터보다 value 데이터의 비교가 필요한 경우 오버라이딩해서 사용한다.
예외적으로 값클래스임에도 오버라이딩 해 줄 필요가 없는 클래스들이 있다. 각 데이터당 최대 하나의 객체만 존재하도록 인스턴스 제어를 사용하는 클래스이다. (싱글 톤 이야기인지?) 대표적으로 열거형이 있다. 이런 클래스는 논리적 값과 객체 참조가 동일하여, Object의 equals가 논리적인 equals 역할을 한다.

equals 메소드의 특징 - Object 클래스 명세 : JavaSE6

재귀성 (Refelexivity)

null이 아닌 모든 참조값에 대해 (x).equals(x) 는 true를 반환한다.

대칭성 (Symmetry)

null이 아닌 모든 참조값 x와 y에 대해 (y).equals.(x)가 true라면 (x).equals.(y)도 true여야한다.

문자열을 비교하는 부분을 오버라이딩한다고 할 때, 대소문자에 관계없이 비교될 수 있도록 작성하기 쉬울 거 같다.

code

public class Main {
  public static final class CaseInsensitiveString {

    private final String s;

    public CaseInsensitiveString(String s) {
      if (s == null) {
        throw new NullPointerException();
      }
      this.s = s;
    }
	// 의식적으로 대소문자를 무시하고 비교할 수 있도록 작성할 거 같음!
    @Override
    public boolean equals(Object obj) {
      if (obj instanceof CaseInsensitiveString) {
      // 대소문자 구분없이 비교하기
        return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
      }
      // 대소문자 구분없이 비교하기
      if(obj instanceof String) return s.equalsIgnoreCase((String) obj);
      return false;
    }
}

그치만 실행하게되면 둘은 false가 나오게 된다.

Result

AS-IS

TO-BE
equals 규칙을 위반한 것이기 때문에 수정해줘서 동일하도록 바꿔줘야한다.

이행성(Transitivity)

null이 아닌 참조값 x, y, z에 대해 서로 다른 두 개(x, z)가 다른 하나(y, (x == y), (y == z))와 true라면 서로 다른 두 개(x == z)는 true여야 한다.
(x).equals.(y) true
(y).equals.(z) true
-> (x).equals.(z) = true

public class Main {
  public static class Point {

    private final int x;
    private final int y;
	// 생성자 생략

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof Point)) {
        return false;
      }
      Point p = (Point) obj;

      return p.x == x && p.y == y;
    }
  }

  public static class ColorPoint extends Point {

    private final Color color;

    public ColorPoint(int x, int y, Color color) {
      super(x, y);
      this.color = color;
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof ColorPoint)) {
        return false;
      }
      return super.equals(obj) && ((ColorPoint) obj).color == color;
    }
  }
}
  public static void main(String[] args) {
    Point p = new Point(1, 4);
    ColorPoint cp = new ColorPoint(1, 4, Color.BLACK);
    System.out.println(p.equals(cp));
    System.out.println(cp.equals(p));
  }

Result

  true
  false

위의 코드와 같은 경우에 대칭성에 위배된다. 객체를 비교할 때와 색(값)을 비교하는 것이기 때문에 결과가 달라지는것 같다.

Fix : 대칭성 고려해서 수정

	// ColorPoint 클래스의 equals를 변경
    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof Point)) {
        return false;
      }
      if (!(obj instanceof ColorPoint)) {
        return obj.equals(this);
      }

      return super.equals(obj) && ((ColorPoint) obj).color == color;
    }

그치만 이 경우에도 이행성 위반행위가 생긴다.
객체지향 언어가 갖는 동등 관계 문제로 인해 발생한 경우인데 인스턴스 생성이 가능한 클래스의 서브클래스에 값 컴포넌트를 추가하면서 equals 규칙을 지킬 수 있는 방법은 없다.
어떻게 해본다고 해도 리스코프 대체 원칙에 어긋나게 된다.

다른 방식으로 상속보다는 컴포지션을 사용하자는 권고 원칙으로 이를 개선할 수도 있다.
또한, 추상클래스의 서브 클래스에는 값 컴포넌트를 추가를 통해 문제점을 개선할 수 있다.
-> 상속의 방법으로 해결하기.

일관성(Consistency)

null이 아닌 모든 참조 값 x와 y에 (x).equals(y)의 값이 변하지 않는다면 두 값은 계속해서 동일한 값을 나타내야 한다. 두 객체가 동일하다면, 값이 변경되지 않는 한, 객체들은 늘 같은 값을 보여준다는 것이다. 즉, 가변 객체의 값은 달라질 수 있지만 불변 객체라면 늘 동일한 값을 보여줘야 한다. 동일한 객체는 늘 동일하다고 보이고, 객체들이 그 상태를 유지하도록 작성해야 한다.

Null이 아님

equals를 오버라이딩할 때는, instanceof 연산자를 사용하여 값을 비교할 수 있는 같은 데이터 타입인지 비교를 해야하는데 이 과정에서 검증되는 부분이라 사용하지 않아도 되며, 타입 제대로 이뤄지지않으면 에러가 발생하기에 유의해야한다. null이 아닌 모든 참조값 x에 대해 x.equals(null)은 반드시 false를 반환해야한다.

equals 만들 때 고려 사항

  1. 객체의 값을 비교할 필요가 없고 참조만으로 같은 객체인지 비교 가능하다면 == 연산자를 사용하자.
  2. instanceof 연산자를 사용해서 전달된 인자가 올바른 타입인지 확인하자 그렇지 않다면 false를 반환하자.
  3. 인자 타입을 올바른 타입으로 변환한다.
  4. 클래스의 중요한 비교 부분 들은 빠뜨리지말고 현재 객체와 인자로 전달된 객체를 비교하자.
  5. 대칭적이며 이행적이고 일관성이 있는지 확인한다.

유의 사항

  • equals 메소드를 오버라이드할 때는 hashCode메소드도 항상 오버라이드한다.
  • equals는 필드 값이 같은지만 검사하면 되고, 깊게 생각하다보면 equals 계약 준수가 어려워진다.
  • equals 메소드의 인자 타입을 Object 대신 다른 타입으로 변경하지 말자 이 경우 오버라이딩이 아니라 오버로드한게 된다.

참고

effective java ed.2

0개의 댓글