CodeGym /Java Blog /무작위의 /equals 및 hashCode 메서드: 모범 사례
John Squirrels
레벨 41
San Francisco

equals 및 hashCode 메서드: 모범 사례

무작위의 그룹에 게시되었습니다
안녕! 오늘 우리는 Java의 두 가지 중요한 방법인 equals()및 에 대해 이야기할 것입니다 hashCode(). 우리가 그들을 만난 것은 이번이 처음이 아닙니다: CodeGym 과정은 다음에 대한 짧은 강의equals() 로 시작합니다 — 잊어버렸거나 전에 본 적이 없다면 읽어보십시오... equals 및 hashCode 메서드: 모범 사례 - 1오늘 강의에서 우리는 다음에 대해 이야기할 것입니다. 이러한 개념을 자세히 설명합니다. 그리고 저를 믿으세요. 우리는 할 이야기가 있습니다! 그러나 새 항목으로 이동하기 전에 이미 다룬 내용을 새로고침해 보겠습니다. 기억하듯이 참조를 비교하기 때문에 연산자를 사용하여 두 개체를 비교하는 것은 일반적으로 나쁜 생각 ==입니다 ==. 다음은 최근 수업에서 자동차를 사용한 예입니다.

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
콘솔 출력:

false
두 개의 동일한 개체를 만든 것 같습니다 Car. 두 자동차 개체의 해당 필드 값은 동일하지만 비교 결과는 여전히 거짓입니다. 우리는 이미 이유를 알고 있습니다. car1car2참조는 서로 다른 메모리 주소를 가리키므로 동일하지 않습니다. 그러나 우리는 여전히 두 참조가 아닌 두 개체를 비교하려고 합니다. 개체를 비교하는 가장 좋은 솔루션은 equals()방법입니다.

equals() 메서드

이 메서드를 처음부터 만들지 않고 재정의한다는 사실을 기억하실 수 있습니다. 메서드는 클래스 equals()에 정의되어 있습니다 Object. 즉, 일반적인 형태로는 거의 사용되지 않습니다.

public boolean equals(Object obj) {
   return (this == obj);
}
이것은 equals()메소드가 클래스에서 정의되는 방식입니다 Object. 이것은 다시 한 번 참조의 비교입니다. 왜 그렇게 만들었습니까? 음, 언어 작성자는 프로그램의 어떤 객체가 같은 것으로 간주되고 어떤 것이 그렇지 않은지 어떻게 알 수 있습니까? :) 이것이 방법의 요점입니다 equals(). 클래스의 객체의 동등성을 확인할 때 어떤 특성을 사용할지 결정하는 것은 클래스의 생성자입니다. equals()그런 다음 클래스의 메서드를 재정의합니다 . "특징 결정"의 의미를 잘 이해하지 못하는 경우 예를 들어 보겠습니다. 다음은 남자를 나타내는 간단한 클래스입니다. Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
두 사람이 일란성 쌍둥이인지 아니면 그냥 닮은지 판단하는 프로그램을 작성한다고 가정해 보겠습니다. 코 크기, 눈 색깔, 머리 모양, 흉터 유무, DNA 검사 결과(단순화를 위해 정수 코드로 나타냄)의 다섯 가지 특성이 있습니다. 이러한 특성 중 어떤 것이 우리 프로그램이 일란성 쌍둥이를 식별할 수 있게 해줄 것이라고 생각하십니까? equals 및 hashCode 메서드: 모범 사례 - 2물론 DNA 검사만이 보증을 제공할 수 있습니다. 두 사람이 같은 눈 색깔, 머리 모양, 코, 심지어 흉터까지 가질 수 있습니다. 세상에는 많은 사람들이 있고 거기에 도플갱어가 없다고 보장하는 것은 불가능합니다. 그러나 우리는 신뢰할 수 있는 메커니즘이 필요합니다. DNA 테스트 결과만이 정확한 결론을 내릴 수 있습니다. 이것은 우리의 방법에 무엇을 의미합니까 equals()? 우리는 그것을 재정의해야합니다Man우리 프로그램의 요구 사항을 고려한 클래스. 메서드는 int dnaCode두 개체의 필드를 비교해야 합니다. 동일하면 개체가 동일합니다.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
정말 그렇게 간단합니까? 설마. 우리는 무언가를 간과했습니다. 개체에 대해 개체 동등성을 설정하는 것과 관련된 하나의 필드만 식별했습니다 dnaCode. 이제 1개가 아니라 50개의 관련 필드가 있다고 상상해 보십시오. 그리고 두 개체의 50개 필드가 모두 같으면 개체가 같습니다. 이러한 시나리오도 가능합니다. 주요 문제는 50개의 필드를 비교하여 동등성을 설정하는 것이 시간 소모적이고 자원 집약적인 프로세스라는 것입니다. 이제 클래스 외에 에 존재하는 것과 정확히 동일한 필드를 Man가진 클래스가 있다고 상상해 보십시오 . 다른 프로그래머가 우리 클래스를 사용한다면 다음과 같은 코드를 쉽게 작성할 수 있습니다. WomanMan

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
이 경우 필드 값을 확인하는 것은 무의미합니다. 서로 다른 두 클래스의 개체가 있음을 쉽게 알 수 있으므로 두 클래스가 같을 수는 없습니다! 이는 메서드에 검사를 추가하여 equals()비교 대상의 클래스를 비교해야 함을 의미합니다. 우리가 그것을 생각한 것이 좋습니다!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
하지만 우리가 다른 것을 잊어버렸을까요? 흠... 최소한 객체를 자신과 비교하지 않는지 확인해야 합니다! 참조 A와 B가 동일한 메모리 주소를 가리키면 동일한 개체이므로 시간을 낭비하고 50개의 필드를 비교할 필요가 없습니다.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
에 대한 검사를 추가하는 것도 문제가 되지 않습니다 null. 어떤 개체도 와 같을 수 없습니다 null. 따라서 메서드 매개 변수가 null이면 추가 검사가 필요하지 않습니다. 이 모든 것을 염두에 두고 클래스 equals()에 대한 방법은 Man다음과 같습니다.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
위에서 언급한 모든 초기 점검을 수행합니다. 하루가 끝나면 다음과 같은 경우:
  • 우리는 같은 클래스의 두 객체를 비교하고 있습니다
  • 그리고 비교 대상은 같은 대상이 아닙니다.
  • 그리고 전달된 객체는null
...그런 다음 관련 특성의 비교를 진행합니다. 우리에게 이것은 dnaCode두 개체의 필드를 의미합니다. 메서드를 재정의할 때 equals()다음 요구 사항을 준수해야 합니다.
  1. 반사성.

    equals()메서드를 사용하여 개체를 자신과 비교하는 경우 true를 반환해야 합니다.
    우리는 이미 이 요구 사항을 준수했습니다. 우리의 방법은 다음을 포함합니다:

    
    if (this == o) return true;
    

  2. 대칭.

    이면 a.equals(b) == trueb.equals(a)반환해야 합니다 true.
    우리의 방법도 이 요구 사항을 충족합니다.

  3. 전이성.

    두 개체가 세 번째 개체와 같으면 서로 같아야 합니다. 및
    이면 true도 반환해야 합니다 .a.equals(b) == truea.equals(c) == trueb.equals(c)

  4. 고집.

    의 결과는 equals()관련 필드가 변경된 경우에만 변경되어야 합니다. 두 개체의 데이터가 변경되지 않으면 결과는 equals()항상 동일해야 합니다.

  5. 부등식 null.

    모든 개체에 대해 a.equals(null)false를 반환해야 합니다 .
    이것은 단지 "유용한 권장 사항" 집합이 아니라 Oracle 설명서에 명시된 엄격한 계약 입니다.

해시코드() 메서드

이제 방법에 대해 이야기합시다 hashCode(). 왜 필요한가요? 정확히 같은 목적을 위해 — 개체를 비교합니다. 그러나 우리는 이미 가지고 있습니다 equals()! 왜 다른 방법입니까? 대답은 간단합니다. 성능을 향상시키기 위해서입니다. 메서드를 사용하여 Java로 표현되는 해시 함수는 hashCode()모든 개체에 대해 고정 길이 숫자 값을 반환합니다. Java에서 hashCode()메서드는 모든 개체에 대해 32비트 숫자( int)를 반환합니다. equals()특히 해당 방법이 많은 필드를 고려하는 경우 두 숫자를 비교하는 것이 방법을 사용하여 두 개체를 비교하는 것보다 훨씬 빠릅니다 . 프로그램이 객체를 비교하는 경우 해시 코드를 사용하는 것이 훨씬 간단합니다. 방법 에 따라 개체가 동일한 경우에만 hashCode()비교가 다음으로 진행됩니다.equals()방법. 그건 그렇고, 이것은 해시 기반 데이터 구조가 작동하는 방식입니다. 예를 들어 익숙한 HashMap! 메서드 hashCode()는 메서드와 마찬가지로 equals()개발자가 재정의합니다. 그리고 와 마찬가지로 equals()hashCode()방법에는 Oracle 설명서에 명시된 공식 요구 사항이 있습니다.
  1. 두 개체가 같다면(즉, equals()메서드가 true를 반환하는 경우) 두 개체는 동일한 해시 코드를 가져야 합니다.

    그렇지 않으면 우리의 방법은 의미가 없을 것입니다. 위에서 언급했듯이 hashCode()성능을 개선하려면 먼저 확인해야 합니다. 해시 코드가 다르면 메서드를 정의한 방법에 따라 개체가 실제로 동일하더라도 검사는 false를 반환합니다 equals().

  2. 메서드 hashCode()가 동일한 개체에 대해 여러 번 호출되면 매번 같은 번호를 반환해야 합니다.

  3. 규칙 1은 반대 방향으로 작동하지 않습니다. 서로 다른 두 개체가 동일한 해시 코드를 가질 수 있습니다.

세 번째 규칙은 약간 혼란스럽습니다. 어떻게 이럴 수있어? 설명은 매우 간단합니다. 메서드 hashCode()는 를 반환합니다 int. An은 int32비트 숫자입니다. 값의 범위는 -2,147,483,648에서 +2,147,483,647까지로 제한됩니다. 즉, int. 이제 지구에 살고 있는 모든 사람들에 대한 데이터를 저장하는 프로그램을 만들고 있다고 상상해 보십시오. 각 사람은 자신의 Person개체에 해당합니다(클래스와 유사 Man). 지구상에는 약 75억 명의 사람들이 살고 있습니다. 즉, 우리가 변환을 위해 작성하는 알고리즘이 아무리 영리하더라도Personint에 대한 개체, 우리는 단순히 가능한 숫자가 충분하지 않습니다. 가능한 int 값은 45억 개에 불과하지만 그보다 훨씬 더 많은 사람들이 있습니다. 이것은 우리가 아무리 노력해도 일부 다른 사람들이 동일한 해시 코드를 갖게 된다는 것을 의미합니다. 이런 일이 발생하면(해시 코드가 서로 다른 두 개체에 대해 일치할 때) 우리는 이를 충돌이라고 합니다. 메서드를 재정의할 때 hashCode()프로그래머의 목표 중 하나는 잠재적인 충돌 수를 최소화하는 것입니다. 이 모든 규칙을 고려하면 hashCode()클래스에서 메서드는 어떻게 보 일까요 Person? 이와 같이:

@Override
public int hashCode() {
   return dnaCode;
}
놀란? :) 요구 사항을 보면 우리가 모두 준수한다는 것을 알 수 있습니다. 우리 equals()메서드가 true를 반환하는 개체도 에 따라 동일합니다 hashCode(). 두 Person객체가 in 에서 동일 하면 equals(즉, 동일한 를 가짐 dnaCode) 메서드는 동일한 숫자를 반환합니다. 좀 더 어려운 예를 들어보겠습니다. 프로그램이 자동차 수집가를 위해 고급차를 선택해야 한다고 가정합니다. 수집은 많은 특성을 지닌 복잡한 취미가 될 수 있습니다. 특정 1963년 자동차는 1964년 자동차보다 100배 더 비쌀 수 있습니다. 1970년식 빨간 차는 같은 해 같은 브랜드의 파란 차보다 100배 더 비쌀 수 있습니다. equals 및 hashCode 메서드: 모범 사례 - 4이전 예제에서 클래스를 사용하여 Person대부분의 필드(예: 인간 특성)를 중요하지 않은 것으로 버리고 다음 항목만 사용했습니다.dnaCode비교 필드. 우리는 지금 아주 특이한 영역에서 일하고 있으며, 거기에는 하찮은 세부 사항이 없습니다! 우리 수업은 다음과 같습니다 LuxuryAuto.

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
이제 비교에서 모든 필드를 고려해야 합니다. 모든 실수는 고객에게 수십만 달러의 비용이 들 수 있으므로 지나치게 안전한 것이 좋습니다.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
우리의 equals()방법에서 우리는 이전에 이야기한 모든 검사를 잊지 않았습니다. 그러나 이제 객체의 세 필드를 각각 비교합니다. 이 프로그램을 위해서는 절대적 평등, 즉 각 필드의 평등이 필요합니다. 어떻습니까 hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
우리 클래스의 필드 model는 문자열입니다. String클래스가 이미 메서드를 재정의하기 때문에 편리합니다 hashCode(). 필드의 해시 코드를 계산한 model다음 다른 두 숫자 필드의 합계를 추가합니다. Java 개발자는 충돌 수를 줄이기 위해 사용하는 간단한 트릭이 있습니다. 해시 코드를 계산할 때 중간 결과에 홀수 소수를 곱합니다. 가장 일반적으로 사용되는 숫자는 29 또는 31입니다. 지금은 수학적 미묘함을 탐구하지 않겠지만 나중에 중간 결과에 충분히 큰 홀수를 곱하면 해시 함수의 결과를 "확산"하는 데 도움이 되며, 결과적으로 동일한 해시 코드를 가진 개체의 수를 줄입니다. LuxuryAuto의 방법 hashCode()은 다음과 같습니다.

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
이 메커니즘의 모든 복잡성에 대한 자세한 내용은 StackOverflow의 이 게시물 과 Joshua Bloch의 책 Effective Java 에서 읽을 수 있습니다 . 마지막으로 언급할 가치가 있는 또 하나의 중요한 사항입니다. equals()and 메서드 를 재정의할 때마다 hashCode()이러한 메서드에서 고려되는 특정 인스턴스 필드를 선택했습니다. 이러한 메서드는 동일한 필드를 고려합니다. equals()그러나 및 에서 다른 필드를 고려할 수 있습니까 hashCode()? 기술적으로는 가능합니다. 그러나 이것은 잘못된 생각이며 그 이유는 다음과 같습니다.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
다음은 수업에 대한 우리 의 equals()hashCode()방법 입니다 LuxuryAuto. 메서드 hashCode()는 변경되지 않았지만 메서드 model에서 필드를 제거했습니다 equals(). 모델은 더 이상 메서드가 두 개체를 비교할 때 사용되는 특성이 아닙니다 equals(). 그러나 해시 코드를 계산할 때 해당 필드는 여전히 고려됩니다. 결과적으로 우리는 무엇을 얻습니까? 두 대의 자동차를 만들고 알아보자!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
오류! equals()및 방법 에 대해 서로 다른 필드를 사용함으로써 hashCode()우리는 그들에 대해 설정된 계약을 위반했습니다! 메서드 에 따라 동일한 두 개체는 equals()동일한 해시 코드를 가져야 합니다. 우리는 그들에 대해 다른 가치를 받았습니다. 이러한 오류는 특히 해시를 사용하는 컬렉션으로 작업할 때 절대적으로 믿을 수 없는 결과를 초래할 수 있습니다. 따라서 equals()및 를 재정의할 때 hashCode()동일한 필드를 고려해야 합니다. 이 수업은 다소 길었지만 오늘 많은 것을 배웠습니다! :) 이제 과제 해결로 돌아갈 시간입니다!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION