การสร้าง hashCode
เขียนฟังก์ชันแฮชที่ถูกต้อง
การสร้าง hashCode เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
เป้าหมายของ hashCode ที่ดี
hashCode() ที่ดีควร:
- ส่งคืนค่าเดียวกันสำหรับอ็อบเจ็กต์ที่เท่ากัน (ตามสัญญา)
- กระจายอ็อบเจ็กต์ที่ไม่เท่ากันไปยังค่าที่แตกต่างกันจำนวนมาก
- คำนวณได้โดยมีต้นทุนต่ำ
hashCode ที่ไม่ดีซึ่งส่งคืนค่าคงที่ยังคงปฏิบัติตามสัญญา แต่จะทำให้แมปกลายเป็นรายการเชื่อมโยงที่ทำงานช้า
public class Main {
public static void main(String[] args) {
// Legal but terrible: every object collides
System.out.println("constant hashCode is legal but kills performance");
}
}Objects.hash สำหรับกรณีทั่วไป
วิธีที่ง่ายและถูกต้องที่สุดคือ Objects.hash(field1, field2, ...)
เมธอดนี้จัดการค่าว่างและรวมฟิลด์ด้วยอัลกอริทึมมาตรฐาน ควรใช้ฟิลด์ชุดเดียวกันกับที่คุณเปรียบเทียบใน equals
import java.util.Objects;
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() { return Objects.hash(name, age); }
}
public static void main(String[] args) {
User a = new User("Ada", 36);
User b = new User("Ada", 36);
System.out.println(a.hashCode() == b.hashCode());
}
}ตัวคูณ 31 แบบดั้งเดิม
สำหรับการคำนวณแฮชด้วยตนเอง รูปแบบมาตรฐานคือคูณผลลัพธ์สะสมด้วย31 แล้วบวกค่าแฮชของแต่ละฟิลด์
31 เป็นจำนวนเฉพาะคี่ และ 31 * x ให้ผลเหมือนกับ (x << 5) - x ดังนั้น JVM จึงสามารถปรับให้เหมาะสมได้
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + age;
return result;
}
}
public static void main(String[] args) {
System.out.println(new User("Ada", 36).hashCode());
}
}การคำนวณแฮชของชนิดข้อมูลพื้นฐาน
ชนิดข้อมูลพื้นฐานแต่ละชนิดมีวิธีคำนวณแฮชที่แนะนำดังนี้:
int: ใช้ค่าของมันเองlong:(int)(value ^ (value >>> 32))boolean: 1 หรือ 0double:Double.hashCode(value)
public class Main {
public static void main(String[] args) {
long id = 4_000_000_000L;
int longHash = (int) (id ^ (id >>> 32));
System.out.println("long hash: " + longHash);
System.out.println("double hash: " + Double.hashCode(3.14));
System.out.println("bool hash: " + Boolean.hashCode(true));
}
}การคำนวณแฮชของอาร์เรย์
อย่าเรียก hashCode() โดยตรงกับอาร์เรย์ เพราะเมธอดนี้ใช้เอกลักษณ์ของอ็อบเจ็กต์ ไม่ใช่เนื้อหา
ใช้ Arrays.hashCode(arr) สำหรับอาร์เรย์ชั้นเดียว หรือ Arrays.deepHashCode(arr) สำหรับอาร์เรย์ซ้อนกัน
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println("identity equal: " + (a.hashCode() == b.hashCode()));
System.out.println("content equal: " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
}
}ทำให้ equals และ hashCode สอดคล้องกัน
ฟิลด์ที่ใช้ใน hashCode() ต้องเป็นสับเซตของฟิลด์ที่ใช้ใน equals() (ควรเป็นชุดเดียวกันทุกประการ)
หาก equals เปรียบเทียบฟิลด์มากกว่า hashCode อ็อบเจ็กต์ที่เท่ากันสองตัวก็ยังคงมีค่าแฮชเดียวกัน ซึ่งทำได้ตามสัญญา แต่หาก hashCode ใช้ฟิลด์ที่ equals ไม่ได้พิจารณา คุณจะละเมิดสัญญา
import java.util.Objects;
public class Main {
static class Coord {
final int x, y;
Coord(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
return o instanceof Coord c && c.x == x && c.y == y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Coord a = new Coord(3, 4), b = new Coord(3, 4);
System.out.println(a.equals(b) && a.hashCode() == b.hashCode());
}
}การเก็บค่าแฮชไว้ใช้ซ้ำ
สำหรับอ็อบเจ็กต์ที่ไม่เปลี่ยนแปลงและมีต้นทุนการคำนวณแฮชสูง คุณสามารถเก็บผลลัพธ์ไว้ในฟิลด์ได้
String ทำเช่นนี้ภายในอยู่แล้ว ควรทำก็ต่อเมื่ออ็อบเจ็กต์ไม่เปลี่ยนแปลงอย่างแท้จริง เพื่อไม่ให้ค่าที่เก็บไว้ล้าสมัย
import java.util.Objects;
public class Main {
static final class Key {
final String a, b;
private int hash; // 0 until computed
Key(String a, String b) { this.a = a; this.b = b; }
@Override public int hashCode() {
int h = hash;
if (h == 0) { h = Objects.hash(a, b); hash = h; }
return h;
}
}
public static void main(String[] args) {
Key k = new Key("x", "y");
System.out.println(k.hashCode());
System.out.println(k.hashCode());
}
}การกระจายมีความสำคัญ
hashCode ที่กระจายได้ดีจะกระจายคีย์ไปยังช่องต่าง ๆ อย่างสม่ำเสมอ ลองนับจำนวนค่าแฮชที่แตกต่างกันสำหรับกลุ่มอ็อบเจ็กต์ชุดหนึ่ง
ยิ่งมีค่าที่แตกต่างกันมาก การชนกันก็ยิ่งน้อยลง และแมปก็ยิ่งทำงานเร็วขึ้น
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
record Pair(int a, int b) {}
public static void main(String[] args) {
Set<Integer> hashes = new HashSet<>();
for (int i = 0; i < 100; i++) {
hashes.add(Objects.hash(i, i * 7));
}
System.out.println("distinct hashes: " + hashes.size());
}
}ตัวอย่างการกระจายแบบ Bad
การบวกฟิลด์โดยไม่คูณทำให้เกิดการชนกัน: (1,2) และ (2,1) ต่างก็มีค่าแฮชเป็น 3
ตัวคูณ 31 แก้ปัญหาความสมมาตรนี้ เพราะลำดับจึงมีผล
public class Main {
static int badHash(int a, int b) { return a + b; }
static int goodHash(int a, int b) { return 31 * a + b; }
public static void main(String[] args) {
System.out.println("bad (1,2): " + badHash(1, 2) + ", (2,1): " + badHash(2, 1));
System.out.println("good (1,2): " + goodHash(1, 2) + ", (2,1): " + goodHash(2, 1));
}
}ควรใช้ระเบียนสำหรับชนิดข้อมูลค่า
สำหรับโครงสร้างที่ใช้เก็บข้อมูลล้วน ๆ record จะสร้าง hashCode ที่ถูกต้องและกระจายได้ดีโดยอัตโนมัติ
ควรเขียน hashCode ด้วยตนเองก็ต่อเมื่อคุณต้องการความหมายการทำงานแบบกำหนดเอง หรือไม่สามารถใช้ระเบียนได้
public class Main {
record Money(long cents, String currency) {}
public static void main(String[] args) {
Money a = new Money(1099, "USD");
Money b = new Money(1099, "USD");
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}ประกอบทุกอย่างเข้าด้วยกัน
คลาสข้อมูลค่าที่สมบูรณ์: มีฟิลด์ที่ไม่เปลี่ยนแปลง, equals และ hashCode ที่ใช้ฟิลด์ชุดเดียวกัน และ toString ที่เรียบร้อย
import java.util.Objects;
public class Main {
static final class Version {
final int major, minor, patch;
Version(int major, int minor, int patch) {
this.major = major; this.minor = minor; this.patch = patch;
}
@Override public boolean equals(Object o) {
return o instanceof Version v && v.major == major && v.minor == minor && v.patch == patch;
}
@Override public int hashCode() { return Objects.hash(major, minor, patch); }
@Override public String toString() { return major + "." + minor + "." + patch; }
}
public static void main(String[] args) {
Version v = new Version(2, 1, 0);
System.out.println(v + " hash=" + v.hashCode());
}
}ตรวจสอบความเข้าใจ
ทดสอบทักษะการเขียน hashCode ของคุณ
ทบทวน
คุณได้เรียนรู้วิธีเขียน hashCode ให้ถูกต้องดังนี้:
- ใช้
Objects.hash(...)สำหรับกรณีทั่วไป - ใช้รูปแบบตัวคูณ 31สำหรับการคำนวณแฮชด้วยตนเอง
- คำนวณแฮชของอาร์เรย์ด้วย
Arrays.hashCodeไม่ใช่วิธีเริ่มต้น - ทำให้ฟิลด์ของ hashCode สอดคล้องกับ equals และควรใช้ระเบียน
ต่อไป มาดูกันว่า Java 8+ แปลงช่องที่มีการชนกันมากเป็นต้นไม้อย่างไร
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("hashCode recap done: " + Objects.hash("done"));
}
}คำถามที่พบบ่อย
บทเรียน “การสร้าง hashCode” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การสร้าง hashCode” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การสร้าง hashCode”
เขียนฟังก์ชันแฮชที่ถูกต้อง คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การสร้าง hashCode” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม
ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ