การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ
Java 8 ขึ้นไปรับมือการชนกันอย่างไร
การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหาการชนกัน
ก่อน Java 8 ช่องที่มีการชนกันจำนวนมากจะกลายเป็นรายการเชื่อมโยงที่ยาว การค้นหาในช่องนั้นจึงมีประสิทธิภาพลดลงเป็น O(n)
ผู้โจมตีอาจใช้ประโยชน์จากเรื่องนี้ด้วยคีย์ที่สร้างขึ้นมาโดยเฉพาะ เพื่อทำให้เกิดการปฏิเสธการให้บริการ โดยทำให้คีย์ทั้งหมดมีแฮชไปอยู่ในช่องเดียว
public class Main {
public static void main(String[] args) {
// All these strings can be made to collide in one bucket
System.out.println("FB".hashCode() == "Ea".hashCode());
}
}การแปลงเป็นต้นไม้ใน Java 8
Java 8 เพิ่มการแปลงเป็นต้นไม้ เมื่อช่องใดช่องหนึ่งมีรายการมากเกินไป รายการเชื่อมโยงจะถูกแปลงเป็นต้นไม้แดง-ดำแบบสมดุล
การค้นหาในช่องนั้นจึงกลายเป็น O(log n) แทนที่จะเป็น O(n)
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>();
for (int i = 0; i < 1000; i++) m.put(i, i);
System.out.println("Lookups stay fast: " + m.get(742));
}
}เกณฑ์: TREEIFY_THRESHOLD
ค่าคงที่ TREEIFY_THRESHOLD คือ8 ช่องจะถูกแปลงเป็นต้นไม้เมื่อมีรายการครบ 8 รายการ
แต่ยังมีเงื่อนไขที่สองด้วย นั่นคือตารางต้องมีขนาดอย่างน้อย MIN_TREEIFY_CAPACITY (64) มิฉะนั้นแมปจะปรับขนาดแทน
public class Main {
public static void main(String[] args) {
int TREEIFY_THRESHOLD = 8;
int MIN_TREEIFY_CAPACITY = 64;
System.out.println("Treeify when bucket size >= " + TREEIFY_THRESHOLD);
System.out.println("...and table capacity >= " + MIN_TREEIFY_CAPACITY);
}
}ปรับขนาดก่อน แล้วจึงแปลงเป็นต้นไม้
หากช่องมีรายการเกินขนาด แต่ตารางยังมีขนาดเล็ก (น้อยกว่า 64) HashMap จะปรับขนาดตารางก่อน
โดยปกติการปรับขนาดจะกระจายรายการใหม่และกำจัดจุดที่หนาแน่น ดังนั้นการแปลงเป็นต้นไม้จึงเป็นทางเลือกสุดท้ายสำหรับการกระจายแฮชที่แย่อย่างแท้จริง
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>(16);
for (int i = 0; i < 50; i++) m.put(i, i);
// Many resizes happened before any treeify would
System.out.println("size = " + m.size());
}
}การแปลงกลับจากต้นไม้
ต้นไม้ไม่ได้คงอยู่ถาวร หากการลบรายการทำให้ช่องมีจำนวนน้อยกว่า UNTREEIFY_THRESHOLD (6) ต้นไม้จะถูกเปลี่ยนกลับเป็นรายการเชื่อมโยง
ช่องว่างระหว่าง 8 (แปลงเป็นต้นไม้) และ 6 (แปลงกลับ) ช่วยหลีกเลี่ยงการสลับไปมาที่ขอบเขต
public class Main {
public static void main(String[] args) {
System.out.println("TREEIFY_THRESHOLD = 8");
System.out.println("UNTREEIFY_THRESHOLD = 6");
System.out.println("Gap prevents flip-flopping at the edge");
}
}ต้นไม้ต้องใช้ลำดับจากการเปรียบเทียบหรือเอกลักษณ์
ต้นไม้แดง-ดำต้องจัดลำดับรายการของตัวเอง HashMap จะเปรียบเทียบค่าแฮชก่อน หากค่าเท่ากันจะใช้ Comparable หากคีย์นำไปใช้งานได้ หรือใช้การตัดสินกรณีเสมอที่คงที่โดยอาศัยชื่อคลาสและเอกลักษณ์ของอ็อบเจ็กต์
คีย์ที่เป็น Comparable (เช่น String หรือชนิดจำนวนเต็ม) จะให้ลำดับในต้นไม้ที่ชัดเจนที่สุด
public class Main {
public static void main(String[] args) {
System.out.println("String is Comparable: " + ("a" instanceof Comparable));
System.out.println("Integer is Comparable: " + (Integer.valueOf(1) instanceof Comparable));
}
}ผลกระทบในทางปฏิบัติ
สำหรับโปรแกรมจริงส่วนใหญ่ที่มีค่าแฮชดีพอ คุณแทบจะไม่เห็นการแปลงเป็นต้นไม้ ช่องต่าง ๆ จะยังสั้นอยู่
การแปลงเป็นต้นไม้เป็นกลไกป้องกันที่จำกัดเวลาค้นหาในกรณีเลวร้ายที่สุดไว้ที่ O(log n) แม้การคำนวณแฮชจะไม่ดีหรือเกิดจากการโจมตีก็ตาม
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("alpha", 1);
m.put("beta", 2);
m.put("gamma", 3);
// Tiny buckets, plain linked lists, no trees needed
System.out.println(m.get("beta"));
}
}hashCode ค่าคงที่บังคับให้เกิดต้นไม้
หากคุณจงใจส่งคืน hashCode ค่าคงที่ คีย์ทุกตัวจะไปอยู่ในช่องเดียว เมื่อความจุมีค่า 64 ขึ้นไป ช่องนั้นจะถูกแปลงเป็นต้นไม้
นี่แสดงให้เห็นกลไกป้องกัน แต่เป็นสัญญาณว่าการออกแบบมีปัญหา ควรแก้ไข hashCode แทน
import java.util.HashMap;
import java.util.Map;
public class Main {
static class Bad implements Comparable<Bad> {
final int v;
Bad(int v) { this.v = v; }
@Override public int hashCode() { return 1; } // forces collisions
@Override public boolean equals(Object o) { return o instanceof Bad b && b.v == v; }
@Override public int compareTo(Bad o) { return Integer.compare(v, o.v); }
}
public static void main(String[] args) {
Map<Bad, Integer> m = new HashMap<>();
for (int i = 0; i < 100; i++) m.put(new Bad(i), i);
System.out.println("All in one bucket, still works: " + m.get(new Bad(50)));
}
}ต้นไม้ใช้หน่วยความจำมาก
โหนดต้นไม้มีขนาดใหญ่กว่าโหนดรายการเชื่อมโยงทั่วไป เพราะเก็บการอ้างอิงไปยังโหนดแม่ โหนดซ้าย โหนดขวา และสี
นี่เป็นอีกเหตุผลที่การแปลงเป็นต้นไม้เป็นทางเลือกสำรอง ไม่ใช่ค่าเริ่มต้น เพราะต้นไม้แลกหน่วยความจำกับความเร็วในกรณีเลวร้ายที่สุด
public class Main {
public static void main(String[] args) {
System.out.println("Node: hash, key, value, next");
System.out.println("TreeNode: + parent, left, right, prev, red flag");
System.out.println("=> trees cost more memory per entry");
}
}วิธีหลีกเลี่ยงการแปลงเป็นต้นไม้
คุณแทบจะไม่ต้องการพึ่งพาการแปลงเป็นต้นไม้ ควรหลีกเลี่ยงโดย:
- เขียน
hashCode()ที่กระจายได้ดี - ใช้ชนิดข้อมูลในตัวหรือระเบียนเป็นคีย์
- กำหนดขนาดแมปล่วงหน้าเพื่อลดการชนกัน
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
record Key(int a, int b) {}
public static void main(String[] args) {
Map<Key, Integer> m = new HashMap<>(256);
for (int i = 0; i < 200; i++) m.put(new Key(i, i * 31), i);
System.out.println("Even distribution, fast lookups: " + m.get(new Key(10, 310)));
}
}สรุปประสิทธิภาพ
ต้นทุนการดำเนินการของ HashMap:
- แฮชที่ดี: O(1) โดยเฉลี่ย
- ช่องแบบรายการเชื่อมโยง: O(n) ต่อช่องในกรณีเลวร้ายที่สุด
- ช่องที่แปลงเป็นต้นไม้: O(log n) ต่อช่อง
การแปลงเป็นต้นไม้ช่วยจำกัดกรณีเลวร้ายที่สุด แต่ hashCode ที่ดีจะทำให้คุณยังคงอยู่ในระดับ O(1)
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>(1 << 14);
for (int i = 0; i < 10000; i++) m.put(i, i);
System.out.println("10k entries, O(1) get: " + m.get(9999));
}
}ตรวจสอบความเข้าใจ
ทดสอบความรู้ของคุณเกี่ยวกับการแปลงเป็นต้นไม้
ทบทวน
คุณได้เรียนรู้วิธีที่ HashMap สมัยใหม่จัดการการชนกัน:
- ช่องจะแปลงเป็นต้นไม้เมื่อมี 8 รายการและความจุอย่างน้อย 64
- ต้นไม้ทำให้การค้นหาในกรณีเลวร้ายที่สุดเป็นO(log n)
- ช่องจะแปลงกลับจากต้นไม้เมื่อมีรายการน้อยกว่า 6 รายการ
- hashCode ที่ดีหมายความว่าคุณแทบไม่ต้องใช้กลไกป้องกันนี้
คุณเรียนจบหลักสูตรโครงสร้างภายในของ HashMap แล้ว
public class Main {
public static void main(String[] args) {
System.out.println("Treeification course complete");
}
}คำถามที่พบบ่อย
บทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ”
Java 8 ขึ้นไปรับมือการชนกันอย่างไร คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม
ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การทำงานของ HashMap
- สัญญาของ equals/hashCode
- การสร้าง hashCode
- การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ