HashMap Java 8 uygulaması


93

Aşağıdaki bağlantı belgesine göre: Java HashMap Uygulaması

HashMap(Veya daha doğrusu bir geliştirmenin HashMap) uygulanmasıyla karıştırıldım . Sorgularım:

ilk olarak

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

Bu sabitler neden ve nasıl kullanılır? Bunun için bazı net örnekler istiyorum. Bununla nasıl bir performans kazancı elde ediyorlar?

İkincisi

HashMapJDK'da kaynak kodunu görürseniz, aşağıdaki statik iç sınıfı bulacaksınız:

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;
        }
    }
    //...
}

Nasıl kullanılır? Algoritmanın bir açıklamasını istiyorum .

Yanıtlar:


228

HashMapbelirli sayıda kova içerir. hashCodeBunları hangi kovaya koyacağını belirlemek için kullanır . Basitlik uğruna, bunu bir modül olarak hayal edin.

Karma kodumuz 123456 ise ve 4 paketimiz varsa, 123456 % 4 = 0öğe ilk kova olan Kova 1'e gider.

HashMap

İşlevimiz hashCodeiyiyse, tüm kovaların bir şekilde eşit olarak kullanılması için eşit bir dağılım sağlamalıdır. Bu durumda paket, değerleri depolamak için bağlantılı bir liste kullanır.

Bağlı Kovalar

Ancak, iyi hash işlevlerini uygulamak için insanlara güvenemezsiniz. İnsanlar genellikle zayıf hash fonksiyonları yazacak ve bu da eşit olmayan bir dağılımla sonuçlanacaktır. Ayrıca girdilerimizle şanssız kalmamız da mümkündür.

Bozuk hashmap

Bu dağılım ne kadar azsa, O (1) işlemlerinden o kadar uzaklaşıyoruz ve O (n) işlemlerine doğru ilerliyoruz.

HashMap uygulaması, paketler çok büyük hale gelirse, bazı paketleri bağlantılı listeler yerine ağaçlarda organize ederek bunu azaltmaya çalışır. Bu ne TREEIFY_THRESHOLD = 8içindir. Bir kova sekizden fazla öğe içeriyorsa, bir ağaç haline gelmelidir.

Ağaç Kovası

Bu ağaç , muhtemelen bazı en kötü durum garantileri sunduğu için seçilen Kırmızı-Siyah bir ağaçtır . Önce hash koduna göre sıralanır. Karma kodlar aynıysa, nesneler bu arabirimi uygularsa, aksi takdirde kimlik karma kodu compareToyöntemini kullanır Comparable.

Girişler haritadan kaldırılırsa, paketteki girişlerin sayısı bu ağaç yapısına artık gerek kalmayacak şekilde azalabilir. Bunun UNTREEIFY_THRESHOLD = 6için var. Bir kepçedeki eleman sayısı altının altına düşerse, bağlantılı bir listeye geri dönebiliriz.

Son olarak MIN_TREEIFY_CAPACITY = 64,.

Bir karma haritanın boyutu büyüdüğünde, daha fazla gruba sahip olmak için kendini otomatik olarak yeniden boyutlandırır. Küçük bir HashMap'imiz varsa, çok dolu kovalar elde etme olasılığımız oldukça yüksektir, çünkü içine bir şeyler koymak için çok fazla farklı kovaya sahip değiliz. Daha az dolu daha fazla kova ile daha büyük bir HashMap'e sahip olmak çok daha iyidir. Bu sabit, temelde HashMap'imiz çok küçükse ağaçlara kova yapmaya başlamamamızı söylüyor - bunun yerine önce daha büyük olacak şekilde yeniden boyutlandırılması gerekiyor.


Performans kazancı hakkındaki sorunuza cevap vermek için, bu optimizasyonlar en kötü durumu iyileştirmek için eklenmiştir. hashCodeİşleviniz çok iyi olmasaydı, muhtemelen bu optimizasyonlar nedeniyle yalnızca gözle görülür bir performans artışı göreceksiniz .

Kötü hashCodeuygulamalara karşı koruma sağlamak için tasarlanmıştır ve aynı zamanda kötü bir aktörün aynı kovaları işgal eden girdileri kasten seçerek bir sistemi yavaşlatmaya çalışabileceği çarpışma saldırılarına karşı temel koruma sağlar .


3
Eşit olmayan bir dağılım her zaman zayıf hash fonksiyonlarının bir işareti değildir. Bazı veri türleri, örneğin String, intkarma koddan çok daha büyük bir değer alanına sahiptir , bu nedenle, çarpışmalar kaçınılmazdır. Şimdi bu, gerçek değerler gibi String, haritaya koyduğunuz gerçek değerlere bağlı, eşit bir dağılım olsun veya olmasın. Kötü bir dağıtım, kötü şansın sonucu olabilir.
Holger

3
+1, bu ağaç yaklaşımının azalttığı belirli bir senaryonun hash çarpışması DOS saldırısı olduğunu eklemek isterim . java.lang.Stringdeterministik, kriptografik olmayan bir özelliğe sahiptir hashCode, böylece saldırganlar hashCode'larla çakışan farklı Dizeler oluşturabilir. Bu optimizasyondan önce, bu HashMap işlemlerini O (n) -zamanına indirgeyebilirdi, şimdi sadece onları O (log (n)) olarak indirgiyor.
MikeFHay

1
+1, if the objects implement that interface, else the identity hash code.bu başka kısmı arıyordum.
Sayı945

1
@NateGlenn varsayılan hash kodunu geçersiz kılmazsanız
Michael

"Bu sabit temelde, hash haritamız çok küçükse ağaçlara kovalar yapmaya başlamamamızı söylüyor - bunun yerine önce daha büyük olacak şekilde yeniden boyutlandırılmalı." için MIN_TREEIFY_CAPACITY. Bu, "Hashing uygulanacak bir anahtarı zaten 8 ( TREEIFY_THRESHOLD) anahtar içeren kovaya eklediğimizde ve içinde zaten 64 ( MIN_TREEIFY_CAPACITY) anahtar varsa HashMap, bu paketin bağlantılı listesi dengeli ağaca dönüştürülür."
anir

16

Daha basit anlatmak gerekirse (daha basit yapabildiğim kadarıyla) + biraz daha ayrıntı.

Bu özellikler, doğrudan onlara geçmeden önce, anlaşılması çok güzel olan birçok iç şeye bağlıdır.

TREEIFY_THRESHOLD -> tek bir kova buna ulaştığında (ve toplam sayı aşıldığında MIN_TREEIFY_CAPACITY), mükemmel dengelenmiş bir kırmızı / siyah ağaç düğümüne dönüştürülür . Neden? Arama hızı nedeniyle. Bunu farklı bir şekilde düşünün:

alacaktı en fazla 32 adım ile bir kova / bin içinde Entry aramak için Integer.MAX_VALUE girdileri.

Bir sonraki konu için biraz giriş. Neden çöp kutusu / kova sayısı her zaman ikinin üssüdür ? En az iki neden: modulo işleminden daha hızlı ve negatif sayılarda modulo negatif olacaktır. Ve "negatif" bir kovaya bir Giriş koyamazsınız:

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

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

Bunun yerine modulo yerine kullanılan güzel bir numara var:

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

Yani anlamsal olarak modulo işlemiyle aynıdır . Düşük bitleri tutacaktır. Bunu yaptığınızda bunun ilginç bir sonucu vardır:

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

Yukarıdaki durumda, bir girişin nereye gideceğine dair karar, yalnızca hashcode'unuzun son 4 bitine .

Kovaları çoğaltmanın devreye girdiği yer burasıdır. Belirli koşullar altında ( tam ayrıntıları açıklamak çok zaman alır ), kovaların boyutu iki katına çıkar. Neden? Kovaların boyutu iki katına çıktığında, bir bit daha devreye giriyor .

Yani 16 paketiniz var - hashcode'un son 4 biti bir girişin nereye gideceğine karar verir. Kovaları ikiye katlarsınız: 32 kova - son 5 bit, girişin nereye gideceğine karar verir.

Bu nedenle bu işleme yeniden hashing denir. Bu yavaşlayabilir. Yani (önemseyen kişiler için) HashMap şu şekilde "şakadır": hızlı, hızlı, hızlı, yavaş . Başka uygulamalar da var - duraklamasız hashmap ara ...

Şimdi yeniden hashing işleminden sonra UNTREEIFY_THRESHOLD devreye giriyor. Bu noktada, bazı girişler bu bölmelerden diğerlerine geçebilir ( (n-1)&hashhesaplamaya bir bit daha eklerler ve bu nedenle diğer paketlere taşınabilirler ) ve buna ulaşabilir UNTREEIFY_THRESHOLD. Bu noktada, çöp kutusunu olduğu gibi tutmak işe yaramıyor red-black tree node, ancakLinkedList bunun yerine,

 entry.next.next....

MIN_TREEIFY_CAPACITY , belirli bir paket bir Ağaca dönüştürülmeden önceki minimum paket sayısıdır.


10

TreeNodetek bir bölmeye ait girdileri depolamanın alternatif bir yoludur HashMap. Daha eski uygulamalarda, bir kutunun girişleri bağlantılı bir listede saklanıyordu. Java 8'de, bir bölmedeki giriş sayısı bir eşiği ( TREEIFY_THRESHOLD) geçerse , bunlar orijinal bağlantılı liste yerine bir ağaç yapısında saklanır. Bu bir optimizasyondur.

Uygulamadan:

/*
 * 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.

tam olarak doğru değil . Geçerlerse TREEIFY_THRESHOLD VE toplam kutu sayısı en azdır MIN_TREEIFY_CAPACITY. Cevabımda bunu örtmeye çalıştım ...
Eugene

3

Görselleştirmeniz gerekir: Her zaman aynı değeri döndürmek için yalnızca hashCode () işlevi geçersiz kılınmış bir Sınıf Anahtarı olduğunu söyleyin

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 
  }

}

ve sonra başka bir yerde, tüm anahtarlar bu sınıfın örnekleri olan bir HashMap'e 9 giriş ekliyorum. Örneğin

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
              /   \   /  \

Ağaç geçişi, LinkedList {O (n)} 'den {O (log n)} daha hızlıdır ve n büyüdükçe, fark daha önemli hale gelir.


Etkili bir ağaç oluşturamaz çünkü anahtarları karşılaştırmanın hepsi aynı olan hashcode'ları ve sıralamaya yardımcı olmayan eşittir yöntemi dışında bir yolu yoktur.
user253751

@immibis Karma kodlarının aynı olması gerekmez. Oldukça farklılar. Sınıflar onu uygularsa, ek olarak kullanacaktırcompareTo danComparable . identityHashCodekullandığı başka bir mekanizmadır.
Michael

@Michael Bu örnekte tüm karma kodlar zorunlu olarak aynıdır ve sınıf Comparable'ı uygulamaz. IdentityHashCode, doğru düğümü bulmada değersiz olacaktır.
user253751

@immibis Ah evet, sadece gözden geçirdim ama haklısın. Yani, hem Keyuygulamıyor Comparable, identityHashCode:) kullanılacaktır
Michael

@EmonMishra ne yazık ki, sadece görsellik yeterli olmayacak bunu örtmeye çalıştım.
Eugene

2

HashMap uygulamasındaki değişiklik JEP-180 ile eklendi . Amaç şuydu:

Harita girişlerini depolamak için bağlantılı listeler yerine dengeli ağaçlar kullanarak yüksek karma çarpışma koşulları altında java.util.HashMap'in performansını artırın. LinkedHashMap sınıfında aynı iyileştirmeyi uygulayın

Ancak tek kazanç saf performans değildir. Aynı zamanda kullanıcı girdisini depolamak için bir karma haritanın kullanılması durumunda HashDoS saldırısını önleyecektir , çünkü kovadaki verileri depolamak için kullanılan kırmızı-siyah ağaç , O'da (log n) en kötü durum ekleme karmaşıklığına sahiptir. Ağaç, belirli bir kriter karşılandıktan sonra kullanılır - Eugene'nin cevabına bakın .


-1

Hashmap'in dahili uygulamasını anlamak için hashing'i anlamanız gerekir. Hashing en basit haliyle, özelliklerine herhangi bir formül / algoritma uyguladıktan sonra herhangi bir değişken / nesne için benzersiz bir kod atamanın bir yoludur.

Gerçek bir hash işlevi bu kurala uymalıdır -

"Hash fonksiyonu, fonksiyon aynı veya eşit nesnelere uygulandığında her seferinde aynı hash kodunu döndürmelidir. Diğer bir deyişle, iki eşit nesnenin tutarlı bir şekilde aynı karma kodu üretmesi gerekir. "


Bu soruya cevap vermiyor.
Stephen C
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.