[エレガントなピット回避]新しいHashMap(list.size())でサイズを指定して、拡張による余分なオーバーヘッドを完全に回避できますか?

HashMapの初期容量を設定します

HashMapの初期容量の設定は、最適化の始まりにすぎません。

HashMapこれは、Javaの使用において非常に重要な位置を占めます。通常の使用では、多くのJavaプログラマーは、を定義するときにHashMap、hashMapの拡張(サイズ変更)によって発生する追加のオーバーヘッドを減らすために、初期容量を設定することを知っていると思います。私のようなこのコード(zi)(ji):

@Test
public void longLongAGo() {
    int count = 1000000;

    System.out.println("---------------- 不设置hashMap初始容量 ------------");
    long start = System.currentTimeMillis();
    HashMap<Integer, Object> map = new HashMap<>();
    for (int i = 0; i < count; i++) {
        map.put(i, UUID.randomUUID());
    }
    long end = System.currentTimeMillis();
    System.out.println("添加1000000个元素耗时:" + (end - start));

    System.out.println("---------------- 设置hashMap初始容量 -------------------");
    long start1 = System.currentTimeMillis();
    HashMap<Integer, Object> map1 = new HashMap<>(count);
    for (int i = 0; i < count; i++) {
        map1.put(i, UUID.randomUUID());
    }
    long end1 = System.currentTimeMillis();
    System.out.println("添加1000000个元素耗时:" + (end1 - start1));
}
复制代码

 

 

 

私の同僚は、初期化中にマップの容量を設定し、要素を追加するプロセス中に容量を自動的に拡張しないと言いました。これにより、パフォーマンスが大幅に向上します。これは、実際の結果からも当てはまります。

したがって、コレクションを初期化するときに、コレクションの初期値を指定すると、パフォーマンスを向上させることができます。

しかし、私は懐疑的な態度で、初期容量が設定されている場合と初期容量が設定されていない場合のhashMapの拡張数を比較しました。初期容量が1,000,000に設定されている場合、コンテナは期待どおりに拡張されず、拡張されます。 1回:

@SneakyThrows
@Test
public void testing() {
    int count = 1000000;

    System.out.println("---------------- 初始化hashMap容量为1000000 ------------");
    int resizeCount = 0;
    HashMap<Integer, Object> map = new HashMap<>(count);
    Method capacityMethod = map.getClass().getDeclaredMethod("capacity");
    capacityMethod.setAccessible(true);
    int capacity = (int) capacityMethod.invoke(map);
    System.out.println("初始容量:" + capacity);
    for (int i = 0; i < count; i++) {
        map.put(i, UUID.randomUUID());
        int curCapacity = (int) capacityMethod.invoke(map);
        if (curCapacity > capacity) {
            System.out.println("当前容量:" + curCapacity);
            resizeCount++;
            capacity = curCapacity;
        }
    }
    System.out.println("hashMap扩容次数:" + resizeCount);

    System.out.println("---------------- 不初始化hashMap容量 -------------------");
    resizeCount = 0;
    HashMap<Integer, Object> map1 = new HashMap<>();
    Method capacityMethod1 = map1.getClass().getDeclaredMethod("capacity");
    capacityMethod1.setAccessible(true);
    int capacity1 = (int) capacityMethod1.invoke(map1);
    System.out.println("初始容量:" + capacity1);
    for (int i = 0; i < count; i++) {
        map1.put(i, UUID.randomUUID());
        int curCapacity = (int) capacityMethod1.invoke(map1);
        if (curCapacity > capacity1) {
            System.out.println("当前容量:" + curCapacity);
            resizeCount++;
            capacity1 = curCapacity;
        }
    }
    System.out.println("扩容次数:" + resizeCount);
}
复制代码

hashMapcapacity()メソッドを直接呼び出すことはできないため、リフレクションを使用して、追加された各要素の容量の変化を表示し、hashMapの展開数を監視します。

//使用反射,调用hashMap的capacity()方法
Method capacityMethod = map.getClass().getDeclaredMethod("capacity");
capacityMethod.setAccessible(true);
int capacity = (int) capacityMethod.invoke(map);
复制代码

リフレクションに関しては、Javaの最も強力なテクノロジーの1つであるリフレクションをお読みください。リフレクションメカニズムの一般的な理解が得られます。

私はほとんど間違っていました、今上記のプログラムの実行結果に戻ります:

---------------- 初始化hashMap容量为1000000 ------------
初始容量:1048576
当前容量:2097152
hashMap扩容次数:1
---------------- 不初始化hashMap容量 -------------------
初始容量:16
当前容量:32
当前容量:64
当前容量:128
当前容量:256
当前容量:512
当前容量:1024
当前容量:2048
当前容量:4096
当前容量:8192
当前容量:16384
当前容量:32768
当前容量:65536
当前容量:131072
当前容量:262144
当前容量:524288
当前容量:1048576
当前容量:2097152
扩容次数:17
复制代码

見つかった操作の結果:

  • 初期容量が設定されたhashMap、初期容量は私が指定した1000000ではなく、1048576(2 ^ 20
  • hashMapの容量は固定されていません。拡張条件に達すると、16から32、64、128に拡張されます...(ハッシュは容量として現在の容量よりも大きい21乗を選択します
  • 初期容量が確立され、初期容量が1048576であっても、1,000,000個の要素が追加されると(1,000,000は1048576未満)、hashMapは1回拡張されます。

なんでこんなに紫色なの?上記の3つの発見を踏まえて、HashMapの拡張メカニズムを見てみましょう。

HashMap拡張メカニズム

まず、HashMapのいくつかのメンバー変数を見てください。

 

HashMapメンバー変数

 

 

  • DEFAULT_INITIAL_CAPACITY:デフォルトの初期容量は2 ^ 4 = 16です
  • DEFAULT_LOAD_FACTOR:デフォルトの負荷係数は0.75で、HashMapがどれだけいっぱいかを測定するために使用されます
  • 一時的なintサイズ:マップ内のk、vペアの数
  • 最終フロートloadFactor:負荷係数。デフォルト値は0.75です。
  • intしきい値:サイズ変更する次のサイズ値(容量×負荷率)。kとvの実際の数がしきい値を超えると、HashMapは容量を拡張します

別の方法を見てみましょうcapacity()

final int capacity() {
    return (table != null) ? table.length :
        (threshold > 0) ? threshold :
        DEFAULT_INITIAL_CAPACITY;
}
复制代码

これは何ですか?サイズ変数は以前に定義されていませんか?

これは、現在このバケットに多くのものがインストールされcapacityているHashMapバレル体积(このボリュームは大きい)と見なすことができますsize

バケットの容量がthreshold定義されており、デフォルトの容量は2の4乗、つまり16です。ソースコードは次のようになります。

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
复制代码

 

 

 

1 << 4は、4ビットを左にシフトすることを意味します。つまり、2 ^ 4 = 16です。

では、いつ拡張しますか?これは簡単に考えられます。hashMapのバケットにデータを入れます。バケット内のk、vペアの数がsizeバケットに近づくcapacityと、バケットが拡張されます。

前の例ではないような、ハッシュマップを示しているsizecapacityのみ膨張するが、到着capacity膨張に一定値の時間が、この値はthreshold時間、ハッシュマップを行っresize()て、ソースコードでこのルック。

 

HashMap拡張ポイントのソースコード

 

 

ソースコードの一部が折りたたまれており、主に容量に関連する部分が示されています。

それはときsizeよりも大きく成長しthreshold、ハッシュマップが実行されresize()、そしてthreshold = loadFactor * capacityHashMapのバケットは、そのボリュームを自動的に拡張したときに、このように、あなたが知ることができます。

HashMapの拡張を本当に避けてください

前に分析したようにsize > threshold、hashMapが展開されるとthreshold = loadFactor * capacityこの式を使用して、初期化時の方向がわかります。

まず、直接設定しないでください。loadFactor * capacityこの数値は2の累乗ではない可能性があり、HashMapで指定されるコンテナ容量は2の累乗である必要があります。この場合、loadFactor * capacity最初の2の累乗より大きい数値に設定します。 :

int initCapacity = 1 + (int) (count / 0.75);
HashMap<Integer, Object> map = new HashMap<>(initCapacity);
复制代码

1 + (int) (count / 0.75)この式は、HashMapソースコードから取得されます。

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
复制代码

このコードは本当に空飛ぶ妖精です!その目的は次のとおりです。着信容量の値に応じcapて、一連の妖精の操作を通じて、彼より2最初の累乗を取得し、それを返します。

これらはすべてバイナリビット操作であり、番号を順番に右にシフトしてから、元の値とのORを取ります任意の数を見つけてコードに代入して検証すると、結果はそれより2の累乗になります。

おそらく、署名されていない右シフト >>>または操作 |が高速になっているためです。

 

 

 

結果の検証

容量の計算式は以前に作成されていますが、正しいことを確認してください。

@SneakyThrows
@Test
public void perfect() {
    int count = 1000000;

    int initCapacity = 1 + (int) (count / 0.75);
    HashMap<Integer, Object> map = new HashMap<>(initCapacity);
    Method capacityMethod = map.getClass().getDeclaredMethod("capacity");
    capacityMethod.setAccessible(true);
    int capacity = (int) capacityMethod.invoke(map);
    System.out.println("jdk hashMap default capacity:" + capacity);
    int resizeCount = 0;
    for (int i = 0; i < count; i++) {
        map.put(i, UUID.randomUUID());
        int curCapacity = (int) capacityMethod.invoke(map);
        if (curCapacity > capacity) {
            System.out.println("当前容量:" + curCapacity);
            resizeCount++;
            capacity = curCapacity;
        }
    }
    System.out.println("hashMap扩容次数:" + resizeCount);
复制代码

動作結果:

 

 

 

拡張の数は0、完璧です!

数値initCapacity = 1333334をHashMaptableSizeForメソッドに代入すると、容量を2097152 = 2 ^ 21として計算できます。

初期容量を計算したくない-まだ別の方法があります

GuavaはオープンソースベースのJavaライブラリであり、Googleが多くのプロジェクトで使用している多くのコアライブラリが含まれています。このライブラリは、コーディングを容易にし、コーディングエラーを減らすためのものです。このライブラリは、コレクション、キャッシング、サポートプリミティブ、同時実行、共通アノテーション、文字列処理、I / O、および検証のための実用的なメソッドを提供します。

GuavaでHashMapを初期化する既製の方法があり、initCapacityを計算する必要はなく、テストするだけです。

最初にGuavaパッケージを紹介します。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>
复制代码

テスト:

@SneakyThrows
@Test
public void perfectWithGuava() {
    int count = 1000000;

    HashMap<Integer, Object> map = Maps.newHashMapWithExpectedSize(count);
    Method capacityMethod = map.getClass().getDeclaredMethod("capacity");
    capacityMethod.setAccessible(true);
    int capacity = (int) capacityMethod.invoke(map);
    System.out.println("guava hashMap default capacity:" + capacity);
    int resizeCount = 0;
    for (int i = 0; i < count; i++) {
        map.put(i, UUID.randomUUID());
        int curCapacity = (int) capacityMethod.invoke(map);
        if (curCapacity > capacity) {
            System.out.println("当前容量:" + curCapacity);
            resizeCount++;
            capacity = curCapacity;
        }
    }
    System.out.println("hashMap扩容次数:" + resizeCount);
}
复制代码

動作結果:

 

 

 

拡張せずにHashMapを作成することもできます!

キーコードを見てください:

... = Maps.newHashMapWithExpectedSize(count);

newHashMapWithExpectedSize(int)このソースコードは、HashMapと同様return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;にこの方法計算する必要があると思います。見てみましょう。

 

GuavaMapsのソースコード

 

 

おめでとうございます、あなたはすべて正しく推測することができます!

概要

  • 初期容量が設定されたハッシュマップ。実際の初期容量は必ずしも指定された値ではありませんが、HashMapによって内部的に計算されます。
  • hashMapの容量は固定されていません。拡張条件に達すると、16から32、64、128に拡張されます...(ハッシュは容量として現在の容量よりも大きい21乗を選択します
  • 初期容量が指定されている場合、hashMapが拡張されないとは思わないでください
  • hashMapの拡張を回避する方法は、1 + (int) (count / 0.75)計算された初期値を渡すことです。
  • Guavaも使用できますnewHashMapWithExpectedSize(int count)

おすすめ

転載: blog.csdn.net/weixin_51204715/article/details/108866607