ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3장 - 모든 객체의 공통 메서드
    📕 book/이펙티브 자바 2022. 10. 26. 21:19

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


    꼭 필요한 경우가 아니면 equals는 재정의 하지 않은 것이 좋다.

    equals를 재정의하지 않아도 되는 경우

    각 인스턴스가 본질적으로 고유

    • 값을 표현하는 것이 아닌 동작하는 객체를 표현하는 클래스 - ex) Thread
    • 값 클래스라도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스라면 재정의 하지 않아도 된다.

    인스턴스 논리적 동치성을 검사할 일이 없을 때

     

    상위 클래스에서 재정의한 equals가 하위 클래스에도 들어 맞을 때

    • ex) 대부분의 Set 구현체는 AbstractSet이 구현한 equals를 상속받아 사용
    • ex) List 구현체들은 AbstractList, Map 구현체들은 AbstractMap으로부터 상속받아 사용

    클래스가 private 이거나 package-private 이고 equals 메서드를 호출할 일이 없을 때

    • equals가 실수로라도 호출되는 것을 막고 싶다면 아래처럼 구현
    @Override
    public boolean equals(Object o) {
    	throw new AssertionError(); //호출 금지!
    }

    equals를 재정의해야 할 때

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

    객체 식별성

    • 두 객체가 물리적으로 같은가

    → 주로 값 클래스들( 프로그래머는 객체가 같은지가 아닌 값이 같은지를 알고 싶어 한다 )

    equals가 논리적 동치성을 확인하도록 재정의해두면, 그 인스턴스는 값을 비교하길 원하는 기대에 부응하는 것은 물론이고 Map의 키와 Set의 원소 로도 사용할 수 있다.

    ❗equals 메서드를 재정의할 때 따라야하는 일반 규약 - Object 명세에 적힌 규약

     💡 반사성 : null이 아닌 모든 참조 값 x에 대해, x.equals(x) 는 true 이다.
    • 객체는 자기 자신과 같아야 한다는 뜻
    💡 대칭성 : null이 아닌 모든 참조값 x, y에 대해, x.equals(y)가 true이면 y.equals(x)도 true이다.
    • 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다.
    public final class CaseInsensitiveString {
    	private final String s;
    		
    	public CaseInsensitiveString(String s) {
    		this.s = Objects.requireNonNull(s);
    	}
    
    	@Override public boolean equals(Object o) {
    		if (o instanceof CaseInsensitiveString)
    			return s.equalsIgnoreCase( ((CaseInsensitiveString) o).s ); //대소문자 구분x 비교
    		if (o instanceof String)
    			return s.equalsIgnoreCase( (String) o);
        }
    }

    CaseInsensitiveString 클래스가 CaseInsensitiveString 뿐 아니라 String 과도 비교를 시도

    CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    String s = "polish";

    위 상황에서 cis.equals(s) 는 true

    하지만, s.equals(cis)는 false를 반환한다.

    (String 은 CaseInsensitiveString 의 존재를 모르기 때문)

    ⇒ 대칭성을 위반

     

    💡 추이성 : null이 아닌 모든 참조값 x, y, z에 대해, x.equals(y)가 true이고, y.equals(z)도 true이면 x.equals(z)도 true이다.
    • 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면
    • 첫 번째 객체와 세 번째 객체도 같아야 한다.

    이 조건도 자칫하면 어기기 쉽다.

    public class Point {
    	private final int x;
    	private final int y;
    		
    	public Point(int x, int y) {
    		this.x = x;
    		this.y = y;
    	}
    }
    
    public class ColorPoint extends Point {
    	private final Color color;
    		
    	@Override public boolean equals(Object o) {
    		if (!(o instanceof Point))
    			return false;
    		if (!(o instanceof ColorPoint))
    			return o.equals(this);
    		return super.equals(o) && ((ColorPoint) o).color == color;
    	}
    }

    위 방식은 대칭성은 지켜주지만, 추이성을 깨버린다.

    ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
    Point p2 = new Point(1, 2);
    ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
    • p1.equals(p2) = true
    • p2.equals(p3) = true
    • p1.equals(p3) = false

    ❗ 해법

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

    getClass() 를 활용하는 방법은 리스코프 치환 원칙을 위배한다.

    @Override 
    public boolean equals(Object o) {
    	if(o == null || o.getClass() != getClass()) 
    		return false;
    	Point p = (Point) o;
    	return p.x == x && p.y == y;
    }

    리스코프 치환 원칙

    • 어떤 타입에 있어 중요한 속성이면 그 하위 타입에서도 중요하다.
    • 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.

    구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 하나 존재

    “상속 대신 컴포지션을 사용”

    • Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고
    • ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰 메서드를 public으로 추가
    💡 일관성 : null이 아닌 모든 참조값 x, y에 대해, x.equals(y)를 반복 호출하면 항상 true 혹은 항상 false를 반환한다.
    • 두 객체가 같다면 앞으로도 영원히 같아야 한다
    • (하나 혹은 두 객체 모두가 수정되지 않는 한)
    • 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해선 안된다.
     💡 null 아님 : null이 아닌 모든 참조값 x에 대해, x.equals(null)은 false이다.
    • 모든 객체가 null과 같지 않아야 한다
    • instanceof 연산자로 입력 매개변수가 올바른 타입인지 검사해야 한다.
    • ⇒ 입력이 null이면 무조건 false 반환
    • 입력이 null이면 타입 확인 단계에서 false를 반환하기 때문에 null 검사를 명시적으로 하지 않아도 된다.

    equals 메서드 단계별 구현 방법

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

    🧐 주의 사항

    • equals를 재정의할 땐 hashCode도 반드시 재정의 ⭐
    • 너무 복잡하게 해결하려고 하지 말자
    • Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
      • 이는 재정의가 아니라 다중정의를 한 것.

    👀 TIP

    • 어떤 필드를 먼저 비교하느냐에 따라 equals의 성능을 좌우한다
      • 다를 가능성이 더 크거나
      • 비교 비용이 싼 필드를 먼저 비교
    • 구글의 AutoValue 프레임워크를 사용하면 깔끔한 equals가 생성

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


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

    Object 명세에 있는 규약

    💡 1. equals 비교에 사용되는 정보가 변경되지 않았다면, 어플리케이션이 실행되는 동안 그 객체의 hashCode 메소드는 몇 번을 호출해도 항상 같은 값을 반환해야 한다. 단, 어플리케이션을 다시 실행한다면 달라져도 상관 없다.

     

     💡 2. equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.

     

     💡 3. equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

    hashCode 재정의를 잘못했을 때 크게 문제가 되는 조항은 2번째 이다.

    → 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.

    Map<PhoneNumber, String> m = new HashMap<>();
    
    m.put(new PhoneNumber(707, 867, 5309), "제니");
    
    m.get(new PhoneNumber(707, 867, 5309)) // -> null 반환

    2개의 PhoneNumber 인스턴스가 사용되었는데 hashCode를 재정의하지 않았기 때문에

    논리적 동치인 두 객체가 서로 다른 해시코드를 반환한다.

    좋은 hashCode를 작성 하는 요령

    1. int 변수 result를 선언한 후 값 c로 초기화
    2. 해당 객체의 나머지 핵심 필드 각각에 대해 다음 작업을 수행
      1. 해당 필드의 해시코드 c를 계산
        1. 기본 타입필드라면, Type.hashCode(c)를 수행
        2. 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면 이 필드의 hashCode를 재귀적으로 호출
        3. 필드가 배열이라면 핵심 원소 각각을 별도 필드처럼 다룬다.
      2. 2.1 에서 계산한 해시코드 c로 result를 갱신
        result = 31 * result + C;
    3. result 반환

    전형적인 hashCode 메서드

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

    ❓ 31 인 이유

    • 31이 홀수면서 소수(prime)이기 때문
    • 숫자가 짝수이고 오버플로가 발생한다면 정보를 잃게 된다.
    • 2를 곱하는 것은 시프트 연산과 같은 결과를 내기 때문
    • 결과적으로 31을 이용하면 shift 연산과 뺄셈으로 대체해 최적화할 수 있다.

    Objects 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인 hash를 제공

    → hashCode 함수를 단 한줄로 작성할 수 있다.

    → 하지만 속도는 더 느리다

    • 입력 타입중 기본타입이 있다면 박싱, 언박싱 과정을 거치기 때문
    • 성능에 민감하지 않은 상황에서만 사용하자

    💡toString 을 항상 재정의하라


    toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다.

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

    • 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면
    • 요약 정보를 담아야 함

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

    • 제공하지 않으면 이 정보가 필요한 프로그래머는 toString의 반환 값을 파싱할 수 밖에 없다.

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


    Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 의도한 목적을 제대로 이루지 못했다.

    문제 : clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected이다.

    • 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.

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

    • Cloneable 인터페이스는 Object의 protected 메서드 clone의 동작 방식을 결정해주는 역할
      • 객체를 복사할지, CloneNotSupportedException 을 던질지

    사용하기 어렵다

    Cloneable 인터페이스를 구현하고 clone 메서드를 오버라이드 했다면 끝일까? → ❌

    Cloneable 인터페이스를 구현한 클래스가 불변 객체만을 참조하는 경우는 아무 문제가 없다.

    하지만, 가변 객체를 참조하게 되면 원본 인스턴스와 복제 인스턴스 모두 동일한 가변 객체를 참조한다.

    ⇒ 복제 이후 문제가 발생할 수 있다. (불변식을 해친다)

    가변 객체를 복제하는 방법

    • 배열의 clone을 재귀적으로 호출 ⇒ 재귀 호출 대신 반복자를 써서 순회
    • 고수준 API를 사용

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

    요약하자면, Cloneable을 구현하는 모든 클래스는 clone을 재정의 해야한다.

    • 접근제한자는 public
    • 반환 타입은 클래스 자신으로 변경

    Cloneable을 이미 구현한 클래스를 확장한다면 clone을 잘 작동하도록 구현해야 한다.

    그렇지 않은 상황이라면 복사 생성자복사 팩터리라는 더 나은 객체 복사 방식을 제공

    💡Comparable을 구현할지 고려하라


    순서가 명확한 값 클래스 작성 시 반드시 Comparable 인터페이스를 구현하자

    compareTo 메서드의 일반 규약

    equals의 규약과 비슷

    • 이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 더 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
    • Comparable을 구현한 클래스는 모든 x, y에 대해 x.compare(y) == -(y.compare(x))여야 한다.
    • Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, x.compareTo(y) > 0 && y.compareTo(z) > 0이면 x.compareTo(z) > 0이다.
    • Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 x.compareTo(z) == y.compareTo(z)이다.
    • 권고사항! x.compareTo(y) == 0 == x.equals(y) 여야 한다.
    1. 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.

    2. 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다.

    3. 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.

    ⇒ 반사성, 대칭성, 추이성을 충족해야 한다!!

    compareTo 메서드로 수행한 동치성 테스트의 결과는 equals와 같아야 한다.

    Comparable vs Comparator

    Comparable : 일반적인 비교 (오름차순, 내림차순…)

    Comparator : 특별한 비교 (문자열 길이)

    Comparator는 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는데 멋지게 활용할 수 있다.

    private static final Comparator<PhoneNumber> COMPARATOR =
    	comparingInt((PhoneNumber) pn) -> pn.areaCode)
    		.thenComparingInt(pn -> pn.prefix)
    		.thenComparingInt(pn -> pn.lineNum);
    
    public int compareTo(PhoneNumber pn) 
    	return COMPARATOR.compare(this, pn);
    }

    참고 자료

    http://www.yes24.com/Product/Goods/65551284

    '📕 book > 이펙티브 자바' 카테고리의 다른 글

    7장 - 람다와 스트림  (0) 2022.11.07
    6장 - 열거타입과 애너테이션  (0) 2022.11.04
    5장 - 제네릭  (0) 2022.11.01
    4장 - 클래스와 인터페이스  (0) 2022.11.01
    2장 - 객체 생성과 파괴  (0) 2022.10.25

    댓글

Designed by Tistory.