프로그래밍/책

[이펙티브 자바, Effeective Java] 3장 모든 객체의 공통 메서드

시간이nullnull한 가장 2021. 12. 8. 12:30

아이템10, equals는 일반 규약을 지켜 재정의하라


equals는 논리적 동치성을 확인해야할 때 재정의한다.

 

*논리적 동치성 비교(equals)란 참조 타입(Reference Type) 변수를 비교하는 것이다. 더 정확히 말하면 비교할 핵심 값을 정하고, 핵심 값을 비교하여 두 객체가 서로 동등(equal)하다면 "논리적으로 같다"라고 한다. (https://javanitto.tistory.com/9)

 

equals 메서드를 재정의할 때는 반드시 일반 규약을 따라야 한다. 다음은 Object 명세에 적힌 규약이다.

  • 반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
  • 대칭성(symmetry): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
  • 추이성(transivity): null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true이면 x.equals(z)도 true이다.
  • 일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
  • null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.

 

equals를 재정의 할 때는 다음 단계를 거치도록 한다.

1. '==' 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다. 자기 자신이면 trye를 반환한다. 이는 단순한 성능 최적화용으로, 비교 작업이 복작한 상황일 때 값어치를 할 것이다.

2. instanceof 연산자로 입력이 올바른 타입인지 확인한다. 그렇지 않다면 false를 반환한다.

3. 입력을 올바른 타입으로 형변환한다. 앞서 2번에서 instanceof 검사를 했기 때문에 이 단계는 100% 성공한다.

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다. 모든 필드가 일치하면 true를, 하나라도 다르면 false를 반환한다. 2단계에서 인터페이스를 사용했다면 입력의 필드 값을 가져올 때도 그 인터페이스의 메서드를 사용해야 한다. 타입이 클래스라면 해당 필드에 직접 접근할 수도 있다.

@Override
public boolean equals(Object o){
    if(o == this)
    	return true;
    if(!(o instanceof MyClass))
    	return false;
    MyClass obj = (MyClass)o; 
    return obj.field1 == this.field1 && obj.field2 == this.field2 && ... ;
}

(https://snoop-study.tistory.com/109)

 

주의,

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

2 Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자. 아래 예시 참고

// 잘못된 예 - 입력 타입은 반드시 Object여야 한다!
public boolean equals(MyClass o) {
 ...
}

=> 이 메서드는 Object.equals를 재정의한 게 아니다. 입력 타입이 Object가 아니므로 재정의가 아니라 다중정의한 것이다.

 


아이템11 equals를 재정의하려거든 hashCode도 재정의하라


equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다. 다음은 Object 명세에서 발췌한 규약이다.

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

 

사실 equals나 hashCode를 재정의할 일이 없어서 와닿지 않았다.

 


아이템 12 toString을 항상 재정의하라


toString은 항상 클래스_이름@16진수로_표시한_해시코드를 반환하지만 객체를 println, printf, 문자열 연결 연산자(+), assert 구문에 넘길 때 혹은 디버거가 객체를 출력할 때마다 자동으로 불린다.

따라서 실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는게 좋다.

 

toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다. 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다.

만약 포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋다.

이에 따른 단점도 있다. 포맷을 명시하고 데이터가와 코드들이 쌓인 뒤 포맷이 바뀐다면 그동안의 기록이 문제가 된다.

포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.

중요한 것은, 포맷을 명시하든 아니든 재정의 의도는 명확히 밝혀야 한다는 점이다.

 


아이템 clone 13 재정의는 주의해서 진행하라


clone은 생성자처럼 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.

그러나 clone하려는 가변 상태를 갖는 필드에 대한 복제를 할 경우문제가 발생한다.

public class Stack implements Cloneable{
  private Object[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  public Stack() {
    this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(Object o) {}
  }
  ...

  @Override
  public Stack clone() {
    try {
      Stack result = (Stack) super.clone();
      result.elements = 
    } catch(CloneNotSupportedException e) {
    }
  }
}

 

 

(https://jaehun2841.github.io/2019/01/13/effective-java-item13/#%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-clone%EB%A9%94%EC%84%9C%EB%93%9C-%EC%9E%AC%EC%A0%95%EC%9D%98)

이 클래스가 단순히 clone메서드를 이용해 super.clone()만 실행하게 된다면 새로 생성된 객체가 원본 객체의 Object[] elements를 공유하게 된다.

 

가변 상태 필드가 final이 적용되어 있다면 또 다른 문제에 직면한다. Cloneable 아키텍처는 가변객체를 참조하는 필드는 final로 선언하라 라는 일반 용법과 충돌한다.

 

-> clone보다는 복사 생성자와 복사 팩머리 메서드를 사용하도록 하자.

 

*참고할것

얇은 복사(Shallow Copy)와 깊은 복사(Deep Copy)(https://jaehun2841.github.io/2019/01/13/java-object-copy/)

 


 

아이템14 Comparable을 구현할지 고려하라


compareTo는 이 객체와 주어진 객체의 순서를 비교한다. *compareTo(이 객제, 주어진 객체)


이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 리턴한다. 이 객체와 비교할 수 없는 타입이 주어지면 ClassCaseException을 던진다.

 

compareTo를 한 번도 사용한 적이 없어서 와닿지 않았다... ㅠㅠ 

 

*참고할 블로그

https://javabom.tistory.com/10