이펙티브 자바 3장: 모든 객체의 공통 메서드

Adam·2024년 7월 24일
0

이펙티브 자바

목록 보기
2/10
post-thumbnail

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

그냥 두면 클래스의 인스턴스는 오직 자기 자신과만 같게 된다

다음과 같은 상황이면 equal는 재정의 하지 않는것이 좋다

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

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

equals를 재정의 해야할때 지켜야 하는 규약

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

동치관계: 집합을 서로 같은 원소들로 이루어진 부분집합으로 나누는 연산

equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다

구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다

→ 상속 대신 컴포지션을 사용하면 해당 문제를 우회할 수 있음

equals 메서드 구현 방법

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인
  2. instanceof 연산자로 입력이 올바른 타입인지 확인
  3. 입력을 올바른 타입으로 형변환
  4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사(모두 일치 시 true, 아닐 시 false)

equals 성능 향상법

  1. float과 double 제외한 기본 타입 필드는 == 연산자로 비교
  2. 비교하기 복잡한 필드를 가진 클래스는 그 필드의 표준형을 저장해둔 후 표준형끼리 비교
  3. 더 크거나 비교하는 비용이 싼 필드를 먼저 비교

equals 구현 시 주의 사항

  1. equals를 재정의할 땐 hashCode도 반드시 재정의
  2. 필드의 동치성만 검사해도 equals 규약을 어렵지 않게 지킬 수 있다
  3. Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자

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

  1. equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관된 값을 반환
  2. equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다
  3. equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다

전형적인 hashhCode 메서드

@Override public int hashCode(){
	int result = Short.hashCode(areaCode);
	result = 31 * result + Short.hashCode(prefix);
	result = 31 * result + Short.hashCode(lineNum);
	return result;
}

// Guava 라이브러리 사용 시 더 간단하나 성능은 조금 아쉬워진다
@Override public int hashCode(){
	return Objects.hash(lineNum, prefix, areaCode);
}

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

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세하게 공표하지 않아서 클라이언트가 이 값에 의존하지 않고 추후에 계산 방식을 바꿀 수 있게 해준다

toString을 항상 재정의하라

toString은 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다

모든 하위 클래스에서 toString을 재정의 해야 된다

toString을 잘 구현한 클래스는 사용하고 디버깅하기 쉬움

toString을 구현할 때 반환값의 포맷을 문서화할지 정해야 한다

포맷의 장점: 명확하고 읽기 편함

포맷의 단점: 해당 포맷에 얽매인다

포맷 명시 여부와 상관 없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하는게 좋다

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

Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다

clone 메서드는 사실상 생성자와 같은 효과를 낸다.

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

상속해서 쓰기 위한 클래스는 Cloneable을 구현해서는 안 된다

//하위 클래스에서 cloneable을 지원하지 못하게 하는 clone 메서드
@Override
protected final Object clone() throws CloneNotSupportedException{
	throw new CloneNotSupportedException();
}

Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화 해줘야 한다

final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다

Comparable을 구현할지 고려하라

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

compareTo 규약

sgn function: 이 객체와 주어진 객체의 순서를 비교, 이 객체가 주어진 객체보다 적으면 음의 정수, 같으면 0, 크면 양의 정수를 반환. 객체를 비교할 수 없으면 ClassCastException을 던짐

Comparable을 구현한 클래스는 다음과 같아야 한다

  1. sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
  2. 추이성 보장: (x.compareTo(y) > 0 && y.compareTo(z)>0)이면 x.compareTo(z)>0
  3. 모든 z에 대해 x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  4. 필수는 아니지만 (x.compareTo(y) == 0) == (x.equals(y))여야 한다

compareTo 메서드에서 관계 연산자 <와>를 사용하는 이전 방식은 오류를 유발하니 사용하지 않는다

클래스에 핵심 필드가 여러개라면 가장 핵심 필드부터 비교해 나간다

profile
Keep going하는 개발자

0개의 댓글