การใช้งาน HashMap Java 8


93

ตามเอกสารลิงค์ต่อไปนี้: การใช้งาน Java HashMap

ฉันสับสนกับการใช้งานHashMap(หรือมากกว่านั้นคือการเพิ่มประสิทธิภาพในHashMap) คำถามของฉันคือ:

ประการแรก

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

เหตุใดจึงใช้ค่าคงที่เหล่านี้และอย่างไร ฉันต้องการตัวอย่างที่ชัดเจนสำหรับเรื่องนี้ พวกเขาได้รับประสิทธิภาพที่เพิ่มขึ้นจากสิ่งนี้อย่างไร?

ประการที่สอง

หากคุณเห็นซอร์สโค้ดของHashMapใน JDK คุณจะพบคลาสภายในแบบคงที่ต่อไปนี้:

static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
    HashMap.TreeNode<K, V> parent;
    HashMap.TreeNode<K, V> left;
    HashMap.TreeNode<K, V> right;
    HashMap.TreeNode<K, V> prev;
    boolean red;

    TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
        super(arg0, arg1, arg2, arg3);
    }

    final HashMap.TreeNode<K, V> root() {
        HashMap.TreeNode arg0 = this;

        while (true) {
            HashMap.TreeNode arg1 = arg0.parent;
            if (arg0.parent == null) {
                return arg0;
            }

            arg0 = arg1;
        }
    }
    //...
}

มันใช้ยังไง? ฉันเพียงต้องการคำอธิบายขั้นตอนวิธีการที่

คำตอบ:


226

HashMapมีที่เก็บข้อมูลจำนวนหนึ่ง ใช้hashCodeเพื่อกำหนดว่าจะใส่ถังเหล่านี้ลงในถังใด เพื่อความเรียบง่ายให้จินตนาการว่ามันเป็นโมดูลัส

หากแฮชโค้ดของเราคือ 123456 และเรามี 4 ที่เก็บข้อมูล123456 % 4 = 0รายการนั้นจะอยู่ในที่เก็บข้อมูลแรกคือที่เก็บข้อมูล 1

HashMap

หากฟังก์ชันแฮชโค้ดของเราดีควรจัดให้มีการกระจายอย่างสม่ำเสมอดังนั้นที่เก็บข้อมูลทั้งหมดจะถูกใช้อย่างเท่าเทียมกัน ในกรณีนี้ที่เก็บข้อมูลจะใช้รายการที่เชื่อมโยงเพื่อจัดเก็บค่า

ที่เก็บข้อมูลที่เชื่อมโยง

แต่คุณไม่สามารถพึ่งพาผู้คนในการใช้ฟังก์ชันแฮชที่ดีได้ ผู้คนมักจะเขียนฟังก์ชันแฮชที่ไม่ดีซึ่งจะทำให้เกิดการแจกแจงแบบไม่สม่ำเสมอ อาจเป็นไปได้ว่าเราอาจโชคไม่ดีกับปัจจัยการผลิตของเรา

แฮชแมปไม่ถูกต้อง

ยิ่งการกระจายตัวนี้น้อยเท่าไหร่เราก็ยิ่งขยับจากการดำเนินการ O (1) มากขึ้นเท่านั้นและยิ่งเราเข้าใกล้การดำเนินการ O (n)

การใช้งาน Hashmap จะพยายามลดปัญหานี้โดยจัดที่เก็บข้อมูลบางส่วนให้เป็นต้นไม้แทนที่จะเป็นรายการที่เชื่อมโยงหากที่เก็บข้อมูลมีขนาดใหญ่เกินไป นี่คือสิ่งที่TREEIFY_THRESHOLD = 8มีไว้สำหรับ หากถังมีสิ่งของมากกว่าแปดชิ้นควรกลายเป็นต้นไม้

ถังต้นไม้

ต้นไม้นี้เป็นต้นไม้สีแดง - ดำ เรียงลำดับตามรหัสแฮชก่อน หากรหัสแฮชเหมือนกันจะใช้compareToเมธอดในComparableกรณีที่อ็อบเจ็กต์ใช้อินเทอร์เฟซนั้นหรือรหัสแฮชประจำตัว

หากรายการถูกลบออกจากแผนที่จำนวนรายการในที่เก็บข้อมูลอาจลดลงจนไม่จำเป็นต้องใช้โครงสร้างต้นไม้นี้อีกต่อไป นั่นคือสิ่งที่UNTREEIFY_THRESHOLD = 6มีไว้สำหรับ หากจำนวนองค์ประกอบในที่เก็บข้อมูลลดลงต่ำกว่าหกรายการเราอาจกลับไปใช้รายการที่เชื่อมโยงได้เช่นกัน

ในที่สุดก็มีMIN_TREEIFY_CAPACITY = 64.

เมื่อแผนที่แฮชมีขนาดใหญ่ขึ้นแผนที่จะปรับขนาดโดยอัตโนมัติเพื่อให้มีที่เก็บข้อมูลมากขึ้น หากเรามีแผนที่แฮชขนาดเล็กโอกาสที่เราจะได้รับบัคเก็ตเต็มจำนวนนั้นค่อนข้างสูงเพราะเราไม่มีที่เก็บข้อมูลที่แตกต่างกันมากมายให้ใส่ของ การมีแผนที่แฮชที่ใหญ่ขึ้นจะดีกว่ามากโดยมีที่เก็บข้อมูลที่เต็มน้อยกว่า ค่าคงที่นี้บอกว่าอย่าเริ่มสร้างที่เก็บข้อมูลเป็นต้นไม้หากแผนที่แฮชของเรามีขนาดเล็กมากควรปรับขนาดให้ใหญ่ขึ้นก่อนแทน


เพื่อตอบคำถามของคุณเกี่ยวกับประสิทธิภาพที่เพิ่มขึ้นเราได้เพิ่มการเพิ่มประสิทธิภาพเหล่านี้เพื่อปรับปรุงกรณีที่เลวร้ายที่สุด ฉันแค่คาดเดา แต่คุณอาจจะเห็นการปรับปรุงประสิทธิภาพที่เห็นได้ชัดเนื่องจากการเพิ่มประสิทธิภาพเหล่านี้หากhashCodeฟังก์ชันของคุณไม่ดีมาก


3
การแจกแจงแบบไม่สม่ำเสมอไม่ใช่สัญญาณของฟังก์ชันแฮชที่ไม่ดีเสมอไป ข้อมูลบางประเภทเช่นStringมีช่องว่างที่มีค่ามากกว่าintแฮชโค้ดดังนั้นจึงหลีกเลี่ยงการชนกันไม่ได้ ตอนนี้มันขึ้นอยู่กับค่าจริงเช่นStrings จริงที่คุณใส่ลงในแผนที่ไม่ว่าคุณจะได้รับการแจกแจงแบบคู่หรือไม่ก็ตาม การกระจายที่ไม่ดีอาจเป็นผลมาจากความโชคร้าย
Holger

3
+1 ผมอยากจะเพิ่มว่าสถานการณ์ที่เฉพาะเจาะจงว่าวิธีการช่วยลดผลกระทบต้นไม้นี้เป็นกัญชาชนโจมตี DOS java.lang.Stringมีรูปแบบที่กำหนดและไม่เข้ารหัสhashCodeดังนั้นผู้โจมตีสามารถสร้างสตริงที่แตกต่างกันได้เล็กน้อยโดยใช้ hashCodes ที่ชนกัน ก่อนการเพิ่มประสิทธิภาพนี้อาจลดระดับการดำเนินการ HashMap เป็น O (n) -time ตอนนี้ก็ลดระดับเป็น O (log (n))
MikeFHay

1
+1 if the objects implement that interface, else the identity hash code.ฉันกำลังค้นหาส่วนอื่นนี้
หมายเลข 945

1
@NateGlenn รหัสแฮชเริ่มต้นถ้าคุณไม่ลบล้าง
Michael

ฉันไม่เข้าใจว่า "ค่าคงที่นี้บอกว่าอย่าเริ่มสร้างที่เก็บข้อมูลเป็นต้นไม้หากแผนที่แฮชของเรามีขนาดเล็กมากควรปรับขนาดให้ใหญ่ขึ้นก่อน" สำหรับMIN_TREEIFY_CAPACITY. หมายความว่า "เมื่อเราใส่คีย์ที่จะแฮชลงในที่เก็บข้อมูลแล้วซึ่งมีTREEIFY_THRESHOLDคีย์8 ( ) คีย์อยู่แล้วและหากมีMIN_TREEIFY_CAPACITYคีย์64 ( ) คีย์อยู่HashMapแล้วรายการที่เชื่อมโยงของที่เก็บข้อมูลนั้นจะถูกแปลงเป็นแผนผังสมดุล"
anir

16

เพื่อให้ง่ายขึ้น (เท่าที่ทำได้ง่ายกว่า) + รายละเอียดเพิ่มเติม

คุณสมบัติเหล่านี้ขึ้นอยู่กับสิ่งภายในจำนวนมากที่จะต้องทำความเข้าใจก่อนที่จะย้ายไปที่สิ่งเหล่านี้โดยตรง

TREEIFY_THRESHOLD -> เมื่อเดียวถังถึงนี้ (และจำนวนเกินMIN_TREEIFY_CAPACITY) ก็จะเปลี่ยนเป็นสีแดง / ดำโหนดสมดุลอย่างสมบูรณ์แบบ ทำไม? เนื่องจากความเร็วในการค้นหา ลองคิดในมุมกลับ:

จะต้องใช้เวลาไม่เกิน 32 ขั้นตอนในการค้นหารายการภายในถัง / ถังที่มีรายการInteger.MAX_VALUE

คำนำบางส่วนสำหรับหัวข้อถัดไป ทำไมคือจำนวนของถังขยะ / บุ้งกี๋เสมอพลังของทั้งสองที่ ? อย่างน้อยสองเหตุผล: เร็วกว่าการทำงานของโมดูโลและโมดูโลที่ตัวเลขติดลบจะเป็นลบ และคุณไม่สามารถใส่รายการลงในที่เก็บข้อมูล "เชิงลบ" ได้:

 int arrayIndex = hashCode % buckets; // will be negative

 buckets[arrayIndex] = Entry; // obviously will fail

แต่มีเคล็ดลับดีใช้แทนโมดูโล:

 (n - 1) & hash // n is the number of bins, hash - is the hash function of the key

นั่นคือความหมายเช่นเดียวกับการทำงานของโมดูโล มันจะเก็บบิตล่างไว้ สิ่งนี้มีผลลัพธ์ที่น่าสนใจเมื่อคุณทำ:

Map<String, String> map = new HashMap<>();

ในกรณีข้างต้นการตัดสินใจว่ารายการจะไปที่ใดโดยพิจารณาจากแฮชโค้ด4 บิตสุดท้ายของคุณเท่านั้น

นี่คือจุดที่การเพิ่มจำนวนถังเข้ามามีบทบาท ภายใต้เงื่อนไขบางประการ (จะต้องใช้เวลามากในการอธิบายรายละเอียดที่แน่นอน ) ที่เก็บข้อมูลจะมีขนาดใหญ่ขึ้นเป็นสองเท่า ทำไม? เมื่อบุ้งกี๋เป็นสองเท่าในขนาดที่มีมากขึ้นในหนึ่งบิตมาลงเล่น

ดังนั้นคุณจึงมี 16 ถัง - 4 บิตสุดท้ายของแฮชโค้ดเป็นตัวกำหนดว่ารายการจะไปที่ใด คุณเพิ่มถังเป็นสองเท่า: 32 ที่เก็บข้อมูล - 5 บิตสุดท้ายเป็นตัวตัดสินว่ารายการจะไปที่ใด

ดังนั้นกระบวนการนี้จึงเรียกว่าการแฮชซ้ำ สิ่งนี้อาจช้า นั่นคือ (สำหรับคนที่ใส่ใจ) เนื่องจาก HashMap นั้น "ล้อเล่น" ว่า: เร็วเร็วเร็วช้า มีการใช้งานอื่น ๆ - ค้นหาแฮชแมปแบบหยุดชั่วคราว ...

ตอนนี้UNTREEIFY_THRESHOLDเข้ามาเล่นแล้วหลังจากแฮชใหม่ ณ จุดที่บางรายการอาจจะย้ายออกจากถังขยะนี้กับคนอื่น ๆ (พวกเขาเพิ่มมากขึ้นอีกนิดกับ(n-1)&hashการคำนวณ - และย้ายยุทธดังกล่าวไปยังที่อื่น ๆบุ้งกี๋) UNTREEIFY_THRESHOLDและมันอาจจะถึงนี้ ณ จุดนี้จะไม่จ่ายเงินเพื่อให้ถังขยะเป็นred-black tree nodeแต่เป็นแบบLinkedListแทน

 entry.next.next....

MIN_TREEIFY_CAPACITYคือจำนวนที่เก็บข้อมูลขั้นต่ำก่อนที่ที่เก็บข้อมูลบางส่วนจะถูกเปลี่ยนเป็นต้นไม้


10

TreeNodeเป็นอีกทางเลือกหนึ่งในการจัดเก็บรายการที่อยู่ในถังเดียวของไฟล์HashMap. ในการใช้งานรุ่นเก่ารายการของ bin จะถูกเก็บไว้ในรายการที่เชื่อมโยง ใน Java 8 หากจำนวนรายการใน bin ผ่าน threshold ( TREEIFY_THRESHOLD) รายการเหล่านั้นจะถูกเก็บไว้ในโครงสร้างแบบทรีแทนที่จะเป็นรายการที่เชื่อมโยงเดิม นี่คือการเพิ่มประสิทธิภาพ

จากการใช้งาน:

/*
 * Implementation notes.
 *
 * This map usually acts as a binned (bucketed) hash table, but
 * when bins get too large, they are transformed into bins of
 * TreeNodes, each structured similarly to those in
 * java.util.TreeMap. Most methods try to use normal bins, but
 * relay to TreeNode methods when applicable (simply by checking
 * instanceof a node).  Bins of TreeNodes may be traversed and
 * used like any others, but additionally support faster lookup
 * when overpopulated. However, since the vast majority of bins in
 * normal use are not overpopulated, checking for existence of
 * tree bins may be delayed in the course of table methods.

ไม่ตรงความจริง หากพวกเขาผ่านTREEIFY_THRESHOLD และMIN_TREEIFY_CAPACITYจำนวนรวมของถังขยะเป็นอย่างน้อย ฉันพยายามพูดถึงสิ่งนั้นในคำตอบของฉัน ...
ยูจีน

3

คุณจะต้องเห็นภาพ: บอกว่ามีคีย์คลาสที่มีฟังก์ชัน hashCode () เท่านั้นที่ถูกแทนที่เพื่อให้ส่งคืนค่าเดียวกันเสมอ

public class Key implements Comparable<Key>{

  private String name;

  public Key (String name){
    this.name = name;
  }

  @Override
  public int hashCode(){
    return 1;
  }

  public String keyName(){
    return this.name;
  }

  public int compareTo(Key key){
    //returns a +ve or -ve integer 
  }

}

จากนั้นที่อื่นฉันกำลังแทรก 9 รายการลงใน HashMap โดยมีคีย์ทั้งหมดเป็นอินสแตนซ์ของคลาสนี้ เช่น

Map<Key, String> map = new HashMap<>();

    Key key1 = new Key("key1");
    map.put(key1, "one");

    Key key2 = new Key("key2");
    map.put(key2, "two");
    Key key3 = new Key("key3");
    map.put(key3, "three");
    Key key4 = new Key("key4");
    map.put(key4, "four");
    Key key5 = new Key("key5");
    map.put(key5, "five");
    Key key6 = new Key("key6");
    map.put(key6, "six");
    Key key7 = new Key("key7");
    map.put(key7, "seven");
    Key key8 = new Key("key8");
    map.put(key8, "eight");

//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry 

    Key key9 = new Key("key9");
    map.put(key9, "nine");

  threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.

                  key1
                 /    \
               key2   key3
              /   \   /  \

การข้ามต้นไม้จะเร็วกว่า {O (log n)} มากกว่า LinkedList {O (n)} และเมื่อ n เติบโตขึ้นความแตกต่างก็มีนัยสำคัญมากขึ้น


มันไม่สามารถสร้างต้นไม้ที่มีประสิทธิภาพได้เนื่องจากไม่มีวิธีเปรียบเทียบคีย์อื่นนอกเหนือจากรหัสแฮชซึ่งเหมือนกันทั้งหมดและวิธีการเท่ากับซึ่งไม่ช่วยในการสั่งซื้อ
user253751

@immibis แฮชโค้ดของพวกเขาไม่จำเป็นต้องเหมือนกัน พวกเขาค่อนข้างแตกต่างกัน ถ้าเรียนใช้มันก็ยังจะใช้จากcompareTo เป็นอีกกลไกหนึ่งที่ใช้ ComparableidentityHashCode
Michael

@ ไมเคิลในตัวอย่างนี้รหัสแฮชทั้งหมดจำเป็นต้องเหมือนกันและคลาสไม่ได้ใช้การเปรียบเทียบ identityHashCode จะไร้ค่าในการค้นหาโหนดที่ถูกต้อง
user253751

@immibis อ่าใช่ฉันแค่อ่าน แต่คุณพูดถูก ดังนั้นที่Keyไม่ใช้Comparable, identityHashCodeจะใช้ :)
ไมเคิล

@EmonMishra โชคไม่ดีเพียงแค่การมองเห็นจะไม่พอผมได้พยายามที่จะปกว่าในคำตอบของฉัน
Eugene

2

การเปลี่ยนแปลงในการดำเนิน HashMap ถูกที่ถูกเพิ่มเข้ามาด้วยJEP-180 มีวัตถุประสงค์เพื่อ:

ปรับปรุงประสิทธิภาพของ java.util.HashMap ภายใต้สภาวะที่มีการชนกันของแฮชสูงโดยใช้ต้นไม้ที่สมดุลแทนที่จะเป็นรายการที่เชื่อมโยงเพื่อจัดเก็บรายการแผนที่ ใช้การปรับปรุงเดียวกันในคลาส LinkedHashMap

อย่างไรก็ตามประสิทธิภาพที่บริสุทธิ์ไม่ได้เป็นเพียงการได้รับเท่านั้น นอกจากนี้ยังจะป้องกัน การโจมตี HashDoSในกรณีที่มีการใช้แผนที่แฮชเพื่อจัดเก็บอินพุตของผู้ใช้เนื่องจากต้นไม้สีแดง - ดำที่ใช้ในการจัดเก็บข้อมูลในที่เก็บข้อมูลมีความซับซ้อนของการแทรกกรณีที่เลวร้ายที่สุดใน O (log n) ต้นไม้ที่ถูกนำมาใช้หลังจากที่มีเกณฑ์ที่แน่นอนจะพบ - ดูคำตอบของยูจีน


-1

เพื่อให้เข้าใจถึงการใช้งานภายในของแฮชแมปคุณต้องเข้าใจการแฮช การแฮชในรูปแบบที่ง่ายที่สุดเป็นวิธีการกำหนดรหัสเฉพาะสำหรับตัวแปร / ออบเจ็กต์ใด ๆ หลังจากใช้สูตร / อัลกอริทึมใด ๆ กับคุณสมบัติของมัน

ฟังก์ชันแฮชที่แท้จริงต้องเป็นไปตามกฎนี้ -

“ ฟังก์ชันแฮชควรส่งคืนรหัสแฮชที่เหมือนกันทุกครั้งเมื่อใช้ฟังก์ชันกับวัตถุเดียวกันหรือเท่ากัน กล่าวอีกนัยหนึ่งวัตถุที่เท่ากันสองชิ้นต้องสร้างรหัสแฮชเดียวกันอย่างสม่ำเสมอ”


สิ่งนี้ไม่ตอบคำถาม
Stephen C
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.