JavaRush /Java Blog /Random-KO /같음 및 hashCode 메서드: 사용 사례

같음 및 hashCode 메서드: 사용 사례

Random-KO 그룹에 게시되었습니다
안녕하세요! equals()오늘 우리는 Java의 두 가지 중요한 메소드인 과 에 대해 이야기하겠습니다 hashCode(). 우리가 그들을 만난 것은 이번이 처음이 아닙니다. JavaRush 과정 시작 시 에 대한 짧은 강의가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()클래스에 정의됩니다 Object. 그러나 일반적인 형태에서는 거의 사용되지 않습니다.
public boolean equals(Object obj) {
   return (this == obj);
}
equals()이것이 클래스에서 메소드가 정의되는 방식입니다 Object. 링크의 동일한 비교. 그는 왜 이렇게 만들어졌을까? 그렇다면 언어 작성자는 프로그램의 어떤 객체가 동일하다고 간주되고 어떤 객체가 동일하지 않은지 어떻게 알 수 있습니까? :) 이것이 메서드의 주요 아이디어입니다 equals(). 클래스 작성자가 이 클래스 개체의 동등성을 확인하는 특성을 직접 결정합니다. 이렇게 하면 equals()클래스의 메서드가 재정의됩니다. “특성을 스스로 정의한다”는 말의 의미를 잘 이해하지 못한다면 예를 들어보자. 다음은 person의 간단한 클래스입니다 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 생물학적 테스트 결과(단순화를 위해 코드 번호 형식)의 다섯 가지 특성을 가지고 있습니다. 다음 중 어떤 특성이 우리 프로그램에서 쌍둥이 친척을 식별할 수 있다고 생각하시나요? 메소드는 &  hashCode: 사용법 - 2물론 생물학적 테스트만이 보증을 제공할 수 있습니다. 두 사람의 눈 색깔, 머리 모양, 코, 심지어 흉터까지 똑같을 수 있습니다. 세상에는 사람이 많고 우연의 일치를 피할 수 없습니다. 신뢰할 수 있는 메커니즘이 필요합니다. 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(.........);//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. 이 경우 추가 검사는 의미가 없습니다. 이 모든 것을 고려하면 우리의 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()그 자체이어야 합니다.
    우리는 이미 이 요구 사항을 고려했습니다. 우리의 방법은 다음과 같습니다:

    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() 메서드

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

    그렇지 않으면 우리의 방법은 의미가 없을 것입니다. hashCode()성능을 향상시키려면 앞서 말했듯 이 를 확인하는 것이 먼저 이루어져야 합니다. 해시 코드가 다르면 객체가 실제로 동일하더라도(메서드에서 정의한 대로 equals()) 확인 결과 false가 반환됩니다.

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

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

세 번째 규칙은 약간 혼란스럽습니다. 어떻게 이럴 수있어? 설명은 매우 간단합니다. 메서드가 를 hashCode()반환합니다 int. int32비트 숫자입니다. -2,147,483,648에서 +2,147,483,647까지 제한된 수의 값을 갖습니다. 즉, 숫자의 변형이 40억 개가 조금 넘습니다 int. 이제 지구상의 모든 살아있는 사람들에 대한 데이터를 저장하는 프로그램을 만들고 있다고 상상해보십시오. 각 사람은 자신만의 클래스 객체를 갖게 됩니다 Man. 지구상에는 약 75억 명의 사람들이 살고 있습니다. 즉, Man객체를 숫자로 변환하기 위해 아무리 좋은 알고리즘을 작성하더라도 숫자가 충분하지 않습니다. 우리에게는 선택의 여지가 45억 개뿐이고 그보다 더 많은 사람이 있습니다. 이는 우리가 아무리 노력하더라도 해시 코드가 일부 다른 사람들에게 동일할 것임을 의미합니다. 이러한 상황(서로 다른 두 개체의 해시 코드가 일치하는 상황)을 충돌이라고 합니다. 메서드를 재정의할 때 프로그래머의 목표 중 하나 hashCode()는 잠재적인 충돌 횟수를 최대한 줄이는 것입니다. 이 모든 규칙을 고려하여 hashCode()수업 방법은 어떤 모습일까요 ? Man이와 같이:
@Override
public int hashCode() {
   return dnaCode;
}
놀란? :) 예상치 못한 일이지만 요구 사항을 보면 우리가 모든 것을 준수한다는 것을 알 수 있습니다. 우리가 true를 반환하는 객체는 equals()에서 동일할 것입니다 hashCode(). 두 객체의 Man값이 동일 하면 equals(즉, 동일한 값을 가짐 dnaCode) 메소드는 동일한 숫자를 반환합니다. 좀 더 복잡한 예를 살펴보겠습니다. 우리 프로그램이 수집가 고객을 위해 고급 자동차를 선택해야 한다고 가정해 보겠습니다. 수집은 복잡한 일이고 그 안에는 많은 기능이 있습니다. 1963년산 자동차는 1964년산 자동차보다 가격이 100배 더 비쌉니다. 1970년에 나온 빨간색 자동차는 같은 해에 나온 같은 브랜드의 파란색 자동차보다 가격이 100배 더 비쌉니다. 메소드는 &  hashCode: 사용법 - 4첫 번째 경우에는 클래스를 사용하여 Man대부분의 필드(즉, 사람 특성)를 중요하지 않은 것으로 버리고 비교용 필드만 사용했습니다 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()재정의 할 때마다 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;
}
LuxuryAuto 클래스 equals()에 대한 메서드는 다음과 같습니다 . hashCode()메서드는 hashCode()변경되지 않고 그대로 유지되었으며 메서드에서 equals()필드를 제거했습니다 model. 이제 모델은 두 개체를 비교하는 특성이 아닙니다 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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
오류! 서로 다른 필드를 사용함으로써 우리 equals()hashCode()그들을 위해 설정된 계약을 위반했습니다! 두 개의 동일한 equals()객체에는 동일한 해시 코드가 있어야 합니다. 우리는 그들에게 다른 의미를 가지고 있습니다. 이러한 오류는 특히 해시를 사용하는 컬렉션으로 작업할 때 가장 놀라운 결과를 초래할 수 있습니다. 따라서 재정의할 때 equals()동일한 hashCode()필드를 사용하는 것이 정확합니다. 강의가 꽤 길었는데 오늘은 새로운 걸 많이 배웠네요! :) 이제 문제 해결로 돌아갈 시간입니다!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION