equals()
และhashCode()
. นี่ไม่ใช่ครั้งแรกที่เราพบพวกเขา: หลักสูตร CodeGym เริ่มต้นด้วยบทเรียนสั้นๆเกี่ยวกับ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()
วิธีการในชั้นเรียนของคุณ หากคุณไม่ค่อยเข้าใจความหมายของ "กำหนดลักษณะใด" ลองพิจารณาตัวอย่าง นี่คือคลาสง่ายๆ ที่เป็นตัวแทนของผู้ชาย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.
}
สมมติว่าเรากำลังเขียนโปรแกรมที่ต้องการตัดสินว่าคนสองคนเป็นฝาแฝดที่เหมือนกันหรือแค่หน้าตาเหมือนกัน เรามีลักษณะห้าประการ: ขนาดจมูก สีตา ทรงผม การมีแผลเป็น และผลการตรวจดีเอ็นเอ (เพื่อความง่าย เราจะแทนค่านี้เป็นรหัสจำนวนเต็ม) คุณคิดว่าลักษณะใดต่อไปนี้ที่ทำให้โปรแกรมของเราสามารถระบุฝาแฝดที่เหมือนกันได้ 
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(.........); // 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
ไม่ เสียหาย ดังนั้น หากพารามิเตอร์เมธอดเป็นโมฆะ แสดงว่าไม่มีประเด็นในการตรวจสอบเพิ่มเติม เมื่อคำนึงถึงสิ่งนี้แล้ว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
-
การเปลี่ยนแปลง
หากวัตถุสองชิ้นมีค่าเท่ากับวัตถุที่สาม วัตถุทั้งสองจะต้องเท่ากัน
ถ้าa.equals(b) == true
และa.equals(c) == true
จะb.equals(c)
ต้องคืนค่าจริงด้วย -
วิริยะ.
ผลลัพธ์ของ
equals()
ต้องเปลี่ยนเฉพาะเมื่อฟิลด์ที่เกี่ยวข้องมีการเปลี่ยนแปลง หากข้อมูลของวัตถุทั้งสองไม่เปลี่ยนแปลง ดังนั้นผลลัพธ์ของequals()
จะต้องเหมือนกันเสมอ -
อสมการกับ
null
.สำหรับอ็อบเจกต์ใด ๆ
a.equals(null)
ต้องส่งคืนค่าเท็จ
นี่ไม่ใช่แค่ชุดของ "คำแนะนำที่เป็นประโยชน์" แต่เป็นสัญญาที่เข้มงวดซึ่งกำหนดไว้ในเอกสารประกอบของ Oracle
วิธีการ hashCode()
ตอนนี้เรามาพูดถึงhashCode()
วิธีการ ทำไมถึงจำเป็น? เพื่อจุดประสงค์เดียวกันทุกประการ — เพื่อเปรียบเทียบวัตถุ แต่เรามีแล้วequals()
! ทำไมต้องใช้วิธีอื่น? คำตอบนั้นง่าย: เพื่อปรับปรุงประสิทธิภาพ ฟังก์ชันแฮชซึ่งแสดงในภาษาจาวาโดยใช้hashCode()
เมธอด จะส่งคืนค่าตัวเลขที่มีความยาวคงที่สำหรับออบเจกต์ใดๆ ใน Java hashCode()
เมธอดจะส่งคืนตัวเลข 32 บิต ( int
) สำหรับวัตถุใดๆ การเปรียบเทียบตัวเลขสองตัวนั้นเร็วกว่าการเปรียบเทียบวัตถุสองตัวโดยใช้equals()
เมธอด โดยเฉพาะอย่างยิ่งหากเมธอดนั้นพิจารณาหลายฟิลด์ ถ้าโปรแกรมของเราเปรียบเทียบอ็อบเจกต์ การทำเช่นนี้ทำได้ง่ายกว่ามากโดยใช้รหัสแฮช เฉพาะในกรณีที่วัตถุมีค่าเท่ากันตามhashCode()
วิธีการเท่านั้น การเปรียบเทียบจะดำเนินต่อไปที่equals()
วิธี. อย่างไรก็ตาม นี่คือวิธีการทำงานของโครงสร้างข้อมูลแบบแฮช ตัวอย่างเช่นHashMap
! เมธอดhashCode()
เช่นequals()
เมธอด ถูกแทนที่โดยผู้พัฒนา และเช่นเดียวกับ วิธีการ equals()
นี้hashCode()
มีข้อกำหนดอย่างเป็นทางการระบุไว้ในเอกสารประกอบของ Oracle:
-
ถ้าสองออบเจกต์มีค่าเท่ากัน (เช่น
equals()
เมธอดส่งคืนค่าจริง) ดังนั้นออบเจกต์เหล่านั้นต้องมีรหัสแฮชเดียวกันมิฉะนั้นวิธีการของเราจะไร้ความหมาย ดังที่เรากล่าวไว้ข้างต้น
hashCode()
ควรตรวจสอบก่อนเพื่อปรับปรุงประสิทธิภาพ หากรหัสแฮชต่างกัน การตรวจสอบจะส่งกลับค่าเท็จ แม้ว่าออบเจกต์จะเท่ากันตามวิธีที่เรากำหนดเมธอดequals()
ก็ตาม -
หาก
hashCode()
มีการเรียกใช้เมธอดหลายครั้งบนออบเจกต์เดียวกัน เมธอดนั้นจะต้องส่งกลับหมายเลขเดิมทุกครั้ง -
กฎข้อที่ 1 ไม่ได้ทำงานในทิศทางตรงกันข้าม วัตถุสองชิ้นที่แตกต่างกันสามารถมีรหัสแฮชเดียวกันได้
hashCode()
จะคืนค่าไฟล์int
. An int
เป็นตัวเลข 32 บิต มีช่วงค่าที่จำกัด: ตั้งแต่ -2,147,483,648 ถึง +2,147,483,647 กล่าวอีกนัยหนึ่ง มีค่าที่เป็นไปได้มากกว่า 4 พันล้านค่าสำหรับไฟล์int
. ลองจินตนาการว่าคุณกำลังสร้างโปรแกรมเพื่อจัดเก็บข้อมูลเกี่ยวกับผู้คนทั้งหมดที่อาศัยอยู่บนโลก แต่ละคนจะสอดคล้องกับวัตถุของตนเองPerson
(คล้ายกับMan
ชั้นเรียน) มีประมาณ 7.5 พันล้านคนที่อาศัยอยู่บนโลก กล่าวอีกนัยหนึ่ง ไม่ว่าเราจะเขียนอัลกอริทึมเพื่อการแปลงที่ชาญฉลาดเพียงใดPerson
คัดค้าน int เรามีตัวเลขที่เป็นไปได้ไม่เพียงพอ เรามีค่า int ที่เป็นไปได้เพียง 4.5 พันล้านค่า แต่มีคนมากกว่านั้นอีกมาก ซึ่งหมายความว่าไม่ว่าเราจะพยายามมากแค่ไหน บางคนก็จะมีรหัสแฮชเหมือนกัน เมื่อสิ่งนี้เกิดขึ้น (รหัสแฮชตรงกันสำหรับสองวัตถุที่แตกต่างกัน) เราเรียกว่าการชนกัน เมื่อแทนที่เมธอดhashCode()
หนึ่งในวัตถุประสงค์ของโปรแกรมเมอร์คือเพื่อลดจำนวนการชนที่อาจเกิดขึ้น การบัญชีสำหรับกฎเหล่านี้hashCode()
วิธีการจะมีลักษณะอย่างไรในPerson
ชั้นเรียน แบบนี้:
@Override
public int hashCode() {
return dnaCode;
}
น่าประหลาดใจ? :) หากคุณดูข้อกำหนด คุณจะเห็นว่าเราปฏิบัติตามข้อกำหนดทั้งหมด วัตถุที่equals()
วิธีการของเราคืนค่าจริงก็จะเท่ากันhashCode()
ตาม หากPerson
วัตถุทั้งสองของเรามีค่าเท่ากันequals
(นั่นคือ มีเหมือนกันdnaCode
) วิธีการของเราจะคืนค่าเป็นจำนวนเดียวกัน ลองพิจารณาตัวอย่างที่ยากขึ้น สมมติว่าโปรแกรมของเราควรเลือกรถหรูสำหรับนักสะสมรถ การสะสมอาจเป็นงานอดิเรกที่ซับซ้อนและมีลักษณะเฉพาะหลายอย่าง รถปี 1963 คันหนึ่งมีราคาสูงกว่ารถปี 1964 ถึง 100 เท่า รถสีแดงปี 1970 มีราคาสูงกว่ารถสีน้ำเงินยี่ห้อเดียวกันในปีเดียวกันถึง 100 เท่า 
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 เราจะไม่เจาะลึกรายละเอียดปลีกย่อยทางคณิตศาสตร์ในตอนนี้ แต่ในอนาคต โปรดจำไว้ว่าการคูณผลลัพธ์ระดับกลางด้วยจำนวนคี่ที่มากพอจะช่วย "กระจาย" ผลลัพธ์ของฟังก์ชันแฮช และ ดังนั้น ลดจำนวนวัตถุที่มีรหัสแฮชเดียวกัน สำหรับhashCode()
วิธีการของเราใน LuxuryAuto จะมีลักษณะดังนี้:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับความซับซ้อนทั้งหมดของกลไกนี้ได้ในโพสต์นี้บน StackOverflowรวมถึงในหนังสือEffective Javaโดย Joshua Bloch ในที่สุดจุดสำคัญอีกจุดหนึ่งที่ควรค่าแก่การกล่าวถึง ทุกครั้งที่เราลบล้างเมธอด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()
คุณควรพิจารณาฟิลด์เดียวกัน บทเรียนนี้ค่อนข้างยาว แต่คุณได้เรียนรู้มากมายในวันนี้! :) ตอนนี้ได้เวลากลับไปแก้งานแล้ว!
GO TO FULL VERSION