equals()
และ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()
ในชั้นเรียนของคุณ หากคุณยังไม่เข้าใจความหมายของ "คุณกำหนดลักษณะเฉพาะของตัวเอง" มากนัก ลองดูตัวอย่างกัน นี่คือบุคคลคลาสเรียบง่าย - 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()
? เราจำเป็นต้องกำหนดมันใหม่ในชั้นเรียน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
มา
วิธีการของเราก็เป็นไปตามข้อกำหนดนี้เช่นกัน -
การขนส่ง
หากวัตถุสองชิ้นมีค่าเท่ากับวัตถุชิ้นที่สาม วัตถุทั้งสองจะต้องเท่ากัน
ถ้าa.equals(b) == true
และa.equals(c) == true
เช็คb.equals(c)
ควรคืนค่าเป็นจริงด้วย -
ความคงทน.
ผลลัพธ์ของงาน
equals()
ควรเปลี่ยนแปลงเฉพาะเมื่อฟิลด์ที่รวมอยู่ในนั้นเปลี่ยนแปลงเท่านั้น หากข้อมูลของวัตถุทั้งสองไม่มีการเปลี่ยนแปลง ผลลัพธ์ของการตรวจสอบequals()
ควรจะเหมือนกันเสมอ -
ความไม่เท่าเทียมกับ
null
.สำหรับอ็อบเจ็กต์ใด ๆ เช็ค
a.equals(null)
จะต้องคืนค่าเท็จ
นี่ไม่ใช่แค่ชุด "คำแนะนำที่เป็นประโยชน์" บางส่วน แต่เป็นสัญญาวิธีการ ที่เข้มงวด ซึ่งกำหนดไว้ในเอกสารของ Oracle
วิธีการ hashCode()
ตอนนี้เรามาพูดถึงวิธีการhashCode()
กัน เหตุใดจึงจำเป็น? เพื่อจุดประสงค์เดียวกันทุกประการ - การเปรียบเทียบวัตถุ แต่เรามีมันแล้วequals()
! ทำไมต้องใช้วิธีอื่น? คำตอบนั้นง่ายมาก: เพื่อปรับปรุงประสิทธิภาพการผลิต ฟังก์ชันแฮชซึ่งแสดงโดยเมธอด ใน Java hashCode()
จะส่งกลับค่าตัวเลขที่มีความยาวคงที่สำหรับอ็อบเจ็กต์ใดๆ ในกรณีของ Java วิธีการhashCode()
ส่งคืนชนิดจำนวน 32 บิต int
การเปรียบเทียบตัวเลขสองตัวด้วยกันจะเร็วกว่าการเปรียบเทียบวัตถุสองตัวโดยใช้เมธอด โดยequals()
เฉพาะอย่างยิ่งหากใช้หลายช่อง หากโปรแกรมของเราจะเปรียบเทียบวัตถุ จะง่ายกว่ามากถ้าใช้รหัสแฮช และเฉพาะในกรณีที่วัตถุมีค่าเท่ากันhashCode()
- ดำเนินการเปรียบเทียบequals()
ต่อ อย่างไรก็ตาม นี่คือวิธีการทำงานของโครงสร้างข้อมูลแบบแฮช—ตัวอย่างเช่น โครงสร้างที่คุณรู้จักHashMap
! วิธีการhashCode()
เช่นเดียวกับequals()
ถูกแทนที่โดยนักพัฒนาเอง และเช่นเดียวกับ for equals()
วิธีการนี้hashCode()
มีข้อกำหนดอย่างเป็นทางการที่ระบุไว้ในเอกสารของ Oracle:
-
หากวัตถุสองชิ้นเท่ากัน (นั่นคือเมธอด
equals()
คืนค่าจริง) วัตถุเหล่านั้นจะต้องมีรหัสแฮชเดียวกันไม่เช่นนั้นวิธีการของเราก็จะไร้ความหมาย การตรวจสอบโดย
hashCode()
อย่างที่เรากล่าวไว้ควรมาก่อนเพื่อปรับปรุงประสิทธิภาพ หากรหัสแฮชแตกต่างกัน การตรวจสอบจะส่งกลับเท็จ แม้ว่าจริง ๆ แล้ววัตถุจะเท่ากันก็ตาม (ตามที่เรากำหนดไว้ใน methodequals()
) -
หาก
hashCode()
มีการเรียกเมธอดหลายครั้งบนออบเจ็กต์เดียวกัน ก็ควรส่งคืนหมายเลขเดียวกันในแต่ละครั้ง -
กฎข้อที่ 1 ไม่ทำงานในทางกลับกัน ออบเจ็กต์สองรายการที่แตกต่างกันสามารถมีรหัสแฮชเดียวกันได้
hashCode()
ส่งint
คืน int
เป็นตัวเลข 32 บิต มีค่าจำนวนจำกัด - ตั้งแต่ -2,147,483,648 ถึง +2,147,483,647 กล่าวอีกนัยหนึ่ง มีตัวเลขมากกว่า 4 พันล้านรูปint
แบบ ทีนี้ลองจินตนาการว่าคุณกำลังสร้างโปรแกรมเพื่อจัดเก็บข้อมูลเกี่ยวกับสิ่งมีชีวิตทั้งหมดบนโลก แต่ละคนจะมีวัตถุในชั้นเรียนของMan
ตนเอง ผู้คนประมาณ 7.5 พันล้านคนอาศัยอยู่บนโลก กล่าวอีกนัยหนึ่ง ไม่ว่าMan
เราจะเขียนอัลกอริทึมเพื่อแปลงวัตถุเป็นตัวเลขได้ดีเพียงใด เราก็จะมีตัวเลขไม่เพียงพอ เรามีทางเลือกเพียง 4.5 พันล้านตัวเลือก และผู้คนอีกมากมาย ซึ่งหมายความว่าไม่ว่าเราจะพยายามแค่ไหน รหัสแฮชก็จะเหมือนกันสำหรับบางคน สถานการณ์นี้ (รหัสแฮชของวัตถุสองรายการที่ตรงกัน) เรียกว่าการชนกัน เป้าหมายประการหนึ่งของโปรแกรมเมอร์เมื่อแทนที่เมธอดhashCode()
คือการลดจำนวนการชนที่อาจเกิดขึ้นให้มากที่สุด hashCode()
วิธี การเรียนของเราจะ เป็นอย่างไร Man
โดยคำนึงถึงกฎเหล่านี้ทั้งหมด แบบนี้:
@Override
public int hashCode() {
return dnaCode;
}
น่าประหลาดใจ? :) โดยไม่คาดคิด แต่ถ้าคุณดูข้อกำหนดจะพบว่าเราปฏิบัติตามทุกอย่าง วัตถุที่เรา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 เราจะไม่ลงรายละเอียดคณิตศาสตร์ในขณะนี้ แต่สำหรับการอ้างอิงในอนาคต โปรดจำไว้ว่าการคูณผลลัพธ์ระดับกลางด้วยจำนวนคี่ที่มากพอจะช่วย "กระจาย" ผลลัพธ์ของแฮช ทำงานและจบลงด้วยวัตถุน้อยลงที่มีแฮชโค้ดเดียวกัน สำหรับวิธีการของเรา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()
แต่ละครั้ง ที่แทนที่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()
ยังคงไม่เปลี่ยนแปลง และ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