JavaRush /Java Blog /Random-KO /Equals 및 hashCode 계약 또는 무엇이든
Aleksandr Zimin
레벨 1
Санкт-Петербург

Equals 및 hashCode 계약 또는 무엇이든

Random-KO 그룹에 게시되었습니다
물론 대다수의 Java 프로그래머는 메소드가 서로 밀접하게 관련되어 있으며 클래스에서 이 두 메소드를 일관되게 재정의하는 것이 바람직하다는 것을 equals알고 있습니다. hashCode약간 더 적은 수의 사람들이 이것이 왜 그런지, 그리고 이 규칙을 어길 경우 어떤 슬픈 결과가 발생할 수 있는지 알고 있습니다. 나는 이러한 방법의 개념을 고려하고 그 목적을 반복하며 왜 그렇게 연결되어 있는지 이해할 것을 제안합니다. 나는 문제의 모든 세부 사항을 최종적으로 공개하고 더 이상 제3자 소스로 돌아가지 않기 위해 클래스 로딩에 관한 이전 기사와 마찬가지로 이 기사를 직접 썼습니다. 따라서 어딘가에 공백이 있으면 제거해야하기 때문에 건설적인 비판을 기쁘게 생각합니다. 아쉽게도 기사가 꽤 길어졌습니다.

재정의 규칙과 같음

Java에서는 동일한 출처의 두 객체가 논리적으로 동일equals() 하다는 사실을 확인하거나 거부하는 메서드가 필요합니다 . 즉, 두 개체를 비교할 때 프로그래머는 해당 개체의 유효 필드가 동일한지 여부를 이해해야 합니다 . 이 방법은 논리적 동등성을 의미 하므로 모든 필드가 동일해야 할 필요는 없습니다 . 그러나 때로는 이 방법을 특별히 사용할 필요가 없는 경우도 있습니다. 그들이 말했듯이 특정 메커니즘을 사용할 때 문제를 피하는 가장 쉬운 방법은 해당 메커니즘을 사용하지 않는 것입니다. 또한 계약을 위반하면 다른 개체 및 구조가 개체와 상호 작용하는 방식을 이해하는 통제력을 잃게 됩니다. 이후에 오류의 원인을 찾는 것은 매우 어려울 것입니다. equals()equals

이 메서드를 재정의하지 말아야 하는 경우

  • 클래스의 각 인스턴스가 고유한 경우.
  • 이는 데이터 작업을 위해 설계되기보다는 특정 동작을 제공하는 클래스에 더 많이 적용됩니다. 예를 들어 클래스와 같습니다 Thread. 이들에게는 equals클래스에서 제공하는 메서드를 구현하는 Object것만으로도 충분합니다. 또 다른 예는 열거형 클래스( Enum)입니다.
  • 실제로 클래스는 해당 인스턴스의 동등성을 결정할 필요가 없습니다.
  • 예를 들어, 클래스의 경우 java.util.Random클래스의 인스턴스를 서로 비교하여 동일한 난수 시퀀스를 반환할 수 있는지 여부를 결정할 필요가 전혀 없습니다. 단순히 이 클래스의 특성상 그러한 동작을 암시하지도 않기 때문입니다.
  • 확장하려는 클래스에 이미 자체 메소드 구현이 equals있고 이 구현의 동작이 적합할 경우.
  • 예를 들어, 클래스의 경우 Set구현 은 List각각 및 에 있습니다 . MapequalsAbstractSetAbstractListAbstractMap
  • equals그리고 마지막으로 클래스 범위가 private또는 인 경우 재정의할 필요가 없으며 package-private이 메서드가 절대 호출되지 않을 것이라고 확신합니다.

계약과 같음

메서드를 재정의할 때 equals개발자는 Java 언어 사양에 정의된 기본 규칙을 준수해야 합니다.
  • 반사성
  • 주어진 값에 대해 x표현식은 를 x.equals(x)반환해야 합니다 true.
    주어진 - 그런 의미x != null
  • 대칭
  • 주어진 값에 대해 xand 는 를 y반환 하는 경우에만 x.equals(y)반환해야 합니다 . truey.equals(x)true
  • 전이성
  • 지정된 값에 대해 xand y는 반환 및 반환 인 z경우 해당 값을 반환해야 합니다 . x.equals(y)truey.equals(z)truex.equals(z)true
  • 일관성
  • 두 개체를 비교하는 데 사용된 필드가 호출 간에 변경되지 않은 경우 반복된 x호출 yx.equals(y)이 메서드에 대한 이전 호출의 값을 반환합니다.
  • 비교 null
  • 주어진 값에 대해 x호출은 를 x.equals(null)반환해야 합니다 false.

계약 위반과 같습니다

Java Collections Framework의 클래스와 같은 많은 클래스는 메소드 구현에 의존하므로 equals()이를 무시해서는 안 됩니다. 이 방법의 계약을 위반하면 애플리케이션이 비합리적으로 작동할 수 있으며, 이 경우 그 이유를 찾기가 매우 어렵습니다. 재귀성의 원리에 따르면 모든 객체는 그 자체와 동등해야 합니다. 이 원칙을 위반하면 컬렉션에 개체를 추가한 후 메서드를 사용하여 검색할 때 contains()방금 컬렉션에 추가한 개체를 찾을 수 없습니다. 대칭 조건은 두 개체가 비교되는 순서에 관계없이 동일해야 함을 나타냅니다. 예를 들어 문자열 유형의 필드가 하나만 포함된 클래스가 있는 경우 equals이 필드를 메서드의 문자열과 비교하는 것은 올바르지 않습니다. 왜냐하면 역비교의 경우 메서드는 항상 값을 반환합니다 false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
전이성의 조건에 따르면 세 객체 중 두 개가 동일하면 이 경우 세 객체 모두가 동일해야 합니다. 이 원칙은 의미 있는 구성 요소를 추가하여 특정 기본 클래스를 확장해야 할 때 쉽게 위반될 수 있습니다 . Point예를 들어 좌표가 있는 클래스의 경우 점을 x확장 y하여 점의 색상을 추가해야 합니다. ColorPoint이렇게 하려면 적절한 필드를 사용하여 클래스를 선언해야 합니다 color. 따라서 확장 클래스에서 부모 메서드를 호출 equals하고 부모에서 좌표만 비교한다고 가정하면 x색상 y은 다르지만 좌표가 동일한 두 점은 동일한 것으로 간주되며 이는 잘못된 것입니다. 이 경우 파생 클래스에 색상을 구별하도록 가르쳐야 합니다. 이렇게 하려면 두 가지 방법을 사용할 수 있습니다. 그러나 하나는 대칭의 법칙을 위반 하고, 두 번째는 전이성을 위반하는 것입니다 .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
이 경우 호출은 point.equals(colorPoint)값을 반환 true하고 비교는 값 colorPoint.equals(point)을 반환합니다 false. "그것" 클래스의 객체를 기대합니다. 따라서 대칭의 법칙이 위반됩니다. 두 번째 방법은 점의 색상에 대한 데이터가 없는 경우 "블라인드" 검사를 수행하는 것입니다. 즉, 클래스가 입니다 Point. 또는 색상에 대한 정보가 있으면 색상을 확인하십시오. 즉, 클래스의 객체를 비교하십시오 ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    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)과 가 만족 p2.equals(p3)되더라도 p1.equals(p3)값을 반환합니다 false. 동시에 내 생각에는 두 번째 방법이 덜 매력적으로 보입니다. 어떤 경우에는 알고리즘이 블라인드되어 비교를 완전히 수행하지 않을 수 있으며 이에 대해 알지 못할 수도 있습니다. 약간의 시 일반적으로 제가 이해하는 바에 따르면 이 문제에 대한 구체적인 해결책은 없습니다. Kay Horstmann이라는 권위 있는 저자는 연산자 사용을 개체의 클래스를 반환하는 instanceof메서드 호출로 대체할 수 getClass()있으며 개체 자체 비교를 시작하기 전에 개체가 동일한 유형인지 확인해야 한다는 의견이 있습니다. , 공통 기원 사실에주의를 기울이지 마십시오. 따라서 대칭성전이성 규칙이 충족됩니다. 그러나 동시에 바리케이드 반대편에는 넓은 집단에서 그다지 존경받지 못하는 또 다른 작가인 Joshua Bloch가 서 있습니다. 그는 이 접근 방식이 Barbara Liskov의 대체 원칙을 위반한다고 믿습니다. 이 원칙은 "호출 코드는 기본 클래스를 알지 못한 채 하위 클래스와 동일한 방식으로 처리해야 한다 "고 명시합니다 . 그리고 Horstmann이 제안한 솔루션에서는 이 원칙이 구현에 따라 다르기 때문에 분명히 위반됩니다. 요컨대 문제가 어둡다는 것이 분명합니다. 또한 Horstmann은 자신의 접근 방식을 적용하는 규칙을 명확히 하고 클래스를 설계할 때 전략을 결정해야 한다고 평이한 영어로 쓰고 있으며, 동등성 테스트가 슈퍼클래스에 의해서만 수행되는 경우 다음을 수행하여 이를 수행할 수 있다는 점에 유의해야 합니다. 작업 instanceof. 그렇지 않은 경우 파생 클래스에 따라 검사의 의미가 변경되고 메서드 구현을 계층 구조 아래로 이동해야 하는 경우 메서드를 사용해야 합니다 getClass(). Joshua Bloch는 상속을 포기하고 클래스에 ColorPoint클래스를 포함시키고 해당 지점에 대한 구체적인 정보를 얻을 수 있는 Point액세스 방법을 제공함으로써 객체 구성을 사용할 것을 제안합니다. asPoint()이렇게 하면 모든 규칙을 위반하는 것을 피할 수 있지만 내 생각에는 코드를 이해하기가 더 어려워질 것입니다. 세 번째 옵션은 IDE를 사용하여 같음 메서드의 자동 생성을 사용하는 것입니다. 그런데 Idea는 Horstmann 생성을 재현하므로 슈퍼클래스나 그 하위 클래스에서 메서드를 구현하기 위한 전략을 선택할 수 있습니다. 마지막으로, 다음 일관성 규칙은 객체가 변경되지 x않더라도 y객체를 다시 호출하면 x.equals(y)이전과 동일한 값을 반환해야 한다고 명시합니다. 마지막 규칙은 어떤 객체도 와 같아서는 안 된다는 것입니다 null. 여기서 모든 것이 명확합니다 null. 이것은 불확실성입니다. 대상이 불확실성과 동일합니까? 명확하지 않습니다 false.

같음을 결정하는 일반 알고리즘

  1. this객체 참조 와 메소드 매개변수 가 같은지 확인하세요 o.
    if (this == o) return true;
  2. 링크가 정의되어 있는지 o, 즉 정의되어 있는지 확인하세요 null.
    나중에 객체 유형을 비교할 때 연산자가 사용되는 경우 instanceof이 매개변수가 반환되므로 이 항목을 건너뛸 수 false있습니다 null instanceof Object.
  3. 위의 설명과 자신의 직관에 따라 연산자 나 메소드를 this사용하여 객체 유형을 비교하세요 .oinstanceofgetClass()
  4. 하위 클래스에서 메서드가 equals재정의된 경우 반드시 호출해야 합니다.super.equals(o)
  5. 매개변수 유형을 o필수 클래스로 변환합니다.
  6. 모든 중요한 개체 필드를 비교합니다.
    • 기본 유형( float및 제외 double)의 경우 연산자 사용==
    • 참조 필드의 경우 해당 메소드를 호출해야 합니다.equals
    • 배열의 경우 순환 반복 또는 메소드를 사용할 수 있습니다.Arrays.equals()
    • 유형의 경우 float해당 래퍼 클래스의 비교 방법 을 double사용해야 하며Float.compare()Double.compare()
  7. 마지막으로 세 가지 질문에 답해 보세요. 구현된 메서드가 대칭 인가요 ? 전이적 ? 동의 ? 다른 두 가지 원칙( 재귀성과 확실성 ) 은 일반적으로 자동으로 수행됩니다.

HashCode 재정의 규칙

해시는 특정 시점의 상태를 설명하는 개체에서 생성된 숫자입니다. 이 숫자는 주로 Java와 같은 해시 테이블에서 사용됩니다 HashMap. 이 경우 객체를 기반으로 숫자를 얻는 해시 함수는 해시 테이블 전체에 걸쳐 요소가 상대적으로 균등하게 분포되도록 구현되어야 합니다. 또한 함수가 서로 다른 키에 대해 동일한 값을 반환할 때 충돌 가능성을 최소화합니다.

계약 해시 코드

해시 함수를 구현하기 위해 언어 사양은 다음 규칙을 정의합니다.
  • 동일한 객체에 대해 메서드를 hashCode두 번 이상 호출하면 값 계산과 관련된 객체의 필드가 변경되지 않은 경우 동일한 해시 값을 반환해야 합니다.
  • 두 개체에 대해 메서드를 호출하면 hashCode개체가 동일한 경우 항상 같은 숫자를 반환해야 합니다( equals이러한 개체에 대해 메서드를 호출하면 반환됨 true).
  • hashCode동일하지 않은 두 객체에 대해 메서드를 호출하면 서로 다른 해시 값을 반환해야 합니다. 이 요구 사항은 필수 사항은 아니지만 이를 구현하면 해시 테이블 성능에 긍정적인 영향을 미칠 것이라는 점을 고려해야 합니다.

equals 및 hashCode 메소드는 함께 재정의되어야 합니다.

위에서 설명한 계약에 따르면 코드에서 메서드를 재정의할 때는 equals항상 메서드를 재정의해야 합니다 hashCode. 실제로 클래스의 두 인스턴스는 서로 다른 메모리 영역에 있기 때문에 다르기 때문에 일부 논리적 기준에 따라 비교해야 합니다. 따라서 논리적으로 동등한 두 개체는 동일한 해시 값을 반환해야 합니다. 이러한 메서드 중 하나만 재정의되면 어떻게 되나요?
  1. equalshashCode아니오

    equals클래스에서 메서드를 올바르게 정의하고 hashCode해당 메서드를 클래스에 그대로 두기로 결정했다고 가정해 보겠습니다 Object. 그러면 메서드의 관점에서 볼 때 equals두 개체는 논리적으로 동일하지만 메서드의 관점에서는 hashCode공통점이 없습니다. 따라서 해시 테이블에 개체를 배치하면 키로 개체를 다시 가져오지 못할 위험이 있습니다.
    예를 들어 다음과 같습니다.

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    분명히 배치된 객체와 검색 중인 객체는 논리적으로는 동일하지만 두 개의 다른 객체입니다. 하지만 왜냐하면 우리가 계약을 위반했기 때문에 서로 다른 해시 값을 가지므로 해시 테이블 내부 어딘가에서 개체를 잃어버렸다고 말할 수 있습니다.

  2. hashCodeequals아니오.

    메서드를 재정의 hashCode하고 equals클래스에서 메서드 구현을 상속 하면 어떻게 될까요 Object? 아시다시피, equals기본 방법은 단순히 포인터를 객체에 비교하여 동일한 객체를 참조하는지 여부를 결정합니다. 모든 표준에 따라 메소드를 작성했다고 가정해 보겠습니다 hashCode. 즉, IDE를 사용하여 생성했으며 논리적으로 동일한 객체에 대해 동일한 해시 값을 반환합니다. 분명히 그렇게 함으로써 우리는 두 객체를 비교하기 위한 몇 가지 메커니즘을 이미 정의했습니다.

    따라서 이론적으로는 이전 단락의 예가 수행되어야 합니다. 하지만 여전히 해시 테이블에서 개체를 찾을 수 없습니다. 우리는 이것에 가깝지만 최소한 물체가 놓일 해시 테이블 바구니를 찾을 것이기 때문입니다.

    해시 테이블에서 개체를 성공적으로 검색하려면 키의 해시 값을 비교하는 것 외에도 키와 검색된 개체가 논리적으로 동일한지 확인하는 방법도 사용됩니다. 즉, equals메서드를 재정의하지 않고서는 수행할 수 있는 방법이 없습니다.

hashCode를 결정하기 위한 일반 알고리즘

여기서는 너무 걱정하지 말고 좋아하는 IDE에서 메서드를 생성하면 될 것 같습니다. 황금 비율, 즉 정규 분포를 찾기 위해 이러한 모든 비트가 오른쪽과 왼쪽으로 이동하기 때문에 이것은 완전히 완고한 친구들을 위한 것입니다. 개인적으로 같은 아이디어보다 더 잘하고 더 빠르게 할 수 있을지 의문입니다.

결론 대신

따라서 우리는 메소드가 Java 언어에서 잘 정의된 역할을 equals수행 하고 두 객체의 논리적 동등 특성을 얻도록 설계되었음을 알 수 있습니다. hashCode방법의 경우 equals이는 객체를 비교하는 것과 직접적인 관련이 있으며 hashCode, 간접적인 경우에는 해시 테이블이나 유사한 데이터 구조에서 객체의 대략적인 위치를 결정해야 하는 경우입니다. 개체 검색 속도를 높입니다. 계약 외에도 객체 비교와 관련된 또 다른 요구 사항이 있습니다 equals. hashCode이는 compareTo. Comparable_ equals_ 이 요구 사항에 따라 개발자는 항상 반환해야 x.equals(y) == true합니다 x.compareTo(y) == 0. 즉, 두 객체의 논리적 비교는 애플리케이션의 어느 부분에서도 모순되어서는 안 되며 항상 일관되어야 한다는 것을 알 수 있습니다.

출처

효과적인 Java, 제2판. 조슈아 블로흐. 아주 좋은 책을 무료로 번역했습니다. 전문가용 라이브러리인 Java입니다. 1권. 기본. 케이 호스트만. 이론이 조금 적고 실습이 더 많습니다. 그러나 모든 것이 Bloch만큼 자세하게 분석되지는 않습니다. 동일한 equals()에 대한 견해가 있지만. 그림의 데이터 구조. HashMap Java의 HashMap 장치에 대한 매우 유용한 기사입니다. 소스를 보는 대신.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION