equals()
오늘 우리는 Java의 두 가지 중요한 메소드인 과 에 대해 이야기하겠습니다 hashCode()
. 우리가 그들을 만난 것은 이번이 처음이 아닙니다. JavaRush 과정 시작 시 에 대한 짧은 강의가equals()
있었습니다 . 잊어버렸거나 이전에 본 적이 없다면 읽어 보십시오. 오늘 수업에서는 이러한 개념에 대해 자세히 이야기하겠습니다. 이야기할 내용이 많습니다! 그리고 새로운 것으로 넘어가기 전에, 이미 다뤘던 내용을 다시 기억해 봅시다 :) 기억하시겠지만, " ==
" 연산자를 사용하여 두 객체를 비교하는 일반적인 방법은 나쁜 생각입니다. " ==
"는 참조를 비교하기 때문입니다. 다음은 최근 강의에서 자동차에 대한 예입니다.
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
. 두 시스템의 모든 필드는 동일하지만 비교 결과는 여전히 거짓입니다. 우리는 이미 그 이유를 알고 있습니다. 링크 car1
와 car2
메모리의 다른 주소를 가리키므로 동일하지 않습니다. 우리는 여전히 두 개의 참조가 아닌 두 개의 객체를 비교하고 싶습니다. 객체를 비교하는 가장 좋은 솔루션은 입니다 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 생물학적 테스트 결과(단순화를 위해 코드 번호 형식)의 다섯 가지 특성을 가지고 있습니다. 다음 중 어떤 특성이 우리 프로그램에서 쌍둥이 친척을 식별할 수 있다고 생각하시나요? 물론 생물학적 테스트만이 보증을 제공할 수 있습니다. 두 사람의 눈 색깔, 머리 모양, 코, 심지어 흉터까지 똑같을 수 있습니다. 세상에는 사람이 많고 우연의 일치를 피할 수 없습니다. 신뢰할 수 있는 메커니즘이 필요합니다. 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
클래스가 있다고 상상해 보세요 . 그리고 다른 프로그래머가 당신의 클래스를 사용한다면 그는 자신의 프로그램에 다음과 같은 내용을 쉽게 작성할 수 있습니다. Woman
Man
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()
다음 요구 사항을 준수해야 합니다.
-
반사성.
모든 객체는
equals()
그 자체이어야 합니다.
우리는 이미 이 요구 사항을 고려했습니다. 우리의 방법은 다음과 같습니다:if (this == o) return true;
-
대칭.
이면
a.equals(b) == true
을b.equals(a)
반환해야 합니다true
.
우리의 방법도 이 요구 사항을 충족합니다. -
전이성.
두 객체가 세 번째 객체와 동일하다면 두 객체는 서로 동일해야 합니다. 및
인 경우 검사 도 true를 반환해야 합니다.a.equals(b) == true
a.equals(c) == true
b.equals(c)
-
영구.
작업 결과는 작업
equals()
에 포함된 필드가 변경되는 경우에만 변경되어야 합니다. 두 개체의 데이터가 변경되지 않은 경우 검사 결과는equals()
항상 동일해야 합니다. -
와의 불평등
null
.모든 개체에 대해 검사는
a.equals(null)
false를 반환해야 합니다.
이것은 단지 "유용한 권장 사항" 집합이 아니라 Oracle 문서에 규정된 엄격한 방법 계약 입니다.
hashCode() 메서드
이제 방법에 대해 이야기 해 봅시다hashCode()
. 왜 필요한가요? 정확히 같은 목적으로 - 개체를 비교합니다. 하지만 우리는 이미 그것을 가지고 있습니다 equals()
! 왜 다른 방법이 있나요? 대답은 간단합니다. 생산성을 향상시키기 위해서입니다. Java에서 메소드로 표시되는 해시 함수는 hashCode()
모든 객체에 대해 고정 길이 숫자 값을 반환합니다. Java의 경우 이 메소드는 hashCode()
32비트 수의 유형을 반환합니다 int
. 두 숫자를 서로 비교하는 것은 equals()
특히 많은 필드를 사용하는 경우 메서드를 사용하여 두 개체를 비교하는 것보다 훨씬 빠릅니다. 우리 프로그램이 개체를 비교하는 경우 해시 코드를 사용하여 이를 수행하는 것이 훨씬 쉽고, 동일한 경우에만 으로 hashCode()
비교를 진행합니다 equals()
. 그건 그렇고, 이것은 해시 기반 데이터 구조가 작동하는 방식입니다. 예를 들어 여러분이 알고 있는 것과 같습니다 HashMap
! hashCode()
와 마찬가지로 이 메서드는 equals()
개발자가 직접 재정의합니다. 와 마찬가지로 equals()
이 메서드에도 hashCode()
Oracle 설명서에 지정된 공식 요구 사항이 있습니다.
-
두 개체가 동일한 경우(즉, 메서드가
equals()
true를 반환하는 경우) 두 개체의 해시 코드는 동일해야 합니다.그렇지 않으면 우리의 방법은 의미가 없을 것입니다.
hashCode()
성능을 향상시키려면 앞서 말했듯 이 를 확인하는 것이 먼저 이루어져야 합니다. 해시 코드가 다르면 객체가 실제로 동일하더라도(메서드에서 정의한 대로equals()
) 확인 결과 false가 반환됩니다. -
동일한 객체에 대해 메서드가
hashCode()
여러 번 호출되면 매번 동일한 숫자를 반환해야 합니다. -
규칙 1은 반대로 작동하지 않습니다. 서로 다른 두 객체가 동일한 해시 코드를 가질 수 있습니다.
hashCode()
반환합니다 int
. int
32비트 숫자입니다. -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배 더 비쌉니다. 첫 번째 경우에는 클래스를 사용하여 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()
필드를 사용하는 것이 정확합니다. 강의가 꽤 길었는데 오늘은 새로운 걸 많이 배웠네요! :) 이제 문제 해결로 돌아갈 시간입니다!
GO TO FULL VERSION