フードの下のJava HashMap

Java HashMapのガイド

1. 概要

この記事では、JavaでHashMapを使用する方法と、それが内部でどのように機能するかを見ていきます。

HashMapに非常によく似たクラスはHashtableです。 java.util.Hashtable class自体とdifferences between HashMap and Hashtableの詳細については、他のいくつかの記事を参照してください。

2. 基本的な使い方

まず、HashMapがマップであることの意味を見てみましょう。 マップはキーと値のマッピングです。つまり、すべてのキーが正確に1つの値にマップされ、キーを使用してマップから対応する値を取得できます。

単純に値をリストに追加しないのはなぜでしょうか。 なぜHashMapが必要なのですか? 単純な理由はパフォーマンスです。 リスト内の特定の要素を検索する場合、時間計算量はO(n)であり、リストがソートされている場合、たとえばバイナリ検索を使用してO(log n)になります。

HashMapの利点は、値を挿入および取得するための時間計算量が平均でO(1)であることです。 それをどのように達成できるかについては後で見ていきます。 まず、HashMapの使用方法を見てみましょう。

2.1. セットアップ

記事全体で使用する簡単なクラスを作成しましょう。

public class Product {

    private String name;
    private String description;
    private List tags;

    // standard getters/setters/constructors

    public Product addTagsOfOtherProdcut(Product product) {
        this.tags.addAll(product.getTags());
        return this;
    }
}

2.2. Put

これで、タイプStringのキーとタイプProductの要素を使用してHashMapを作成できます。

Map productsByName = new HashMap<>();

そして、HashMapに製品を追加します。

Product eBike = new Product("E-Bike", "A bike with a battery");
Product roadBike = new Product("Road bike", "A bike for competition");
productsByName.put(eBike.getName(), eBike);
productsByName.put(roadBike.getName(), roadBike);

2.3. Get

キーを使用してマップから値を取得できます。

Product nextPurchase = productsByName.get("E-Bike");
assertEquals("A bike with a battery", nextPurchase.getDescription());

マップに存在しないキーの値を見つけようとすると、nullの値が得られます。

Product nextPurchase = productsByName.get("Car");
assertNull(nextPurchase);

また、同じキーで2番目の値を挿入すると、そのキーに対して最後に挿入された値のみが取得されます。

Product newEBike = new Product("E-Bike", "A bike with a better battery");
productsByName.put(newEBike.getName(), newEBike);
assertEquals("A bike with a better battery", productsByName.get("E-Bike"));

2.4. キーとしてのヌル

HashMapを使用すると、nullをキーとして使用することもできます。

Product defaultProduct = new Product("Chocolate", "At least buy chocolate");
productsByName.put(null, defaultProduct);

Product nextPurchase = productsByName.get(null);
assertEquals("At least buy chocolate", nextPurchase.getDescription());

2.5. 同じキーを持つ値

さらに、同じオブジェクトを異なるキーで2回挿入できます。

productsByName.put(defaultProduct.getName(), defaultProduct);
assertSame(productsByName.get(null), productsByName.get("Chocolate"));

2.6. 値を削除する

HashMapからKey-Valueマッピングを削除できます。

productsByName.remove("E-Bike");
assertNull(productsByName.get("E-Bike"));

2.7. キーまたは値がマップに存在するかどうかを確認します

キーがマップに存在するかどうかを確認するには、containsKey()メソッドを使用できます。

productsByName.containsKey("E-Bike");

または、値がマップに存在するかどうかを確認するには、containsValue()メソッドを使用できます。

productsByName.containsValue(eBike);

この例では、両方のメソッド呼び出しがtrueを返します。 これらは非常によく似ていますが、これら2つのメソッド呼び出しのパフォーマンスには重要な違いがあります。 The complexity to check if a key exists is O(1), while the complexity to check for an element is O(n), as it’s necessary to loop over all the elements in the map.

2.8. HashMapを反復処理

HashMap内のすべてのキーと値のペアを反復処理する基本的な方法は3つあります。

すべてのキーのセットを反復処理できます。

for(String key : productsByName.keySet()) {
    Product product = productsByName.get(key);
}

または、すべてのエントリのセットを反復処理できます。

for(Map.Entry entry : productsByName.entrySet()) {
    Product product =  entry.getValue();
    String key = entry.getKey();
    //do something with the key and value
}

最後に、すべての値を反復処理できます。

List products = new ArrayList<>(productsByName.values());

3. キー

We can use any class as the key in our HashMap. However, for the map to work properly, we need to provide an implementation for equals() and*hashCode().* 製品をキー、価格を値としてマップを作成するとします。

HashMap priceByProduct = new HashMap<>();
priceByProduct.put(eBike, 900);

equals()メソッドとhashCode()メソッドを実装しましょう。

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (o == null || getClass() != o.getClass()) {
        return false;
    }

    Product product = (Product) o;
    return Objects.equals(name, product.name) &&
      Objects.equals(description, product.description);
}

@Override
public int hashCode() {
    return Objects.hash(name, description);
}

Note that hashCode() and equals() need to be overridden only for classes that we want to use as map keys, not for classes that are only used as values in a map.この記事のセクション5で、これが必要な理由を説明します。

4. Java 8以降の追加メソッド

Java 8は、HashMapにいくつかの機能スタイルのメソッドを追加しました。 このセクションでは、これらの方法のいくつかを見ていきます。

For each method, we’ll look at two examples.最初の例は、新しいメソッドの使用方法を示し、2番目の例は、以前のバージョンのJavaで同じ方法を実現する方法を示しています。

これらの方法は非常に単純なので、これ以上詳細な例は取り上げません。

4.1. forEach()

forEachメソッドは、マップ内のすべての要素を反復処理する機能スタイルの方法です。

productsByName.forEach( (key, product) -> {
    System.out.println("Key: " + key + " Product:" + product.getDescription());
    //do something with the key and value
});

Java 8より前:

for(Map.Entry entry : productsByName.entrySet()) {
    Product product =  entry.getValue();
    String key = entry.getKey();
    //do something with the key and value
}

記事Guide to the Java 8 forEachでは、forEachループについて詳しく説明しています。

4.2. getOrDefault()

getOrDefault()メソッドを使用すると、マップから値を取得するか、指定されたキーのマッピングがない場合はデフォルトの要素を返すことができます。

Product chocolate = new Product("chocolate", "something sweet");
Product defaultProduct = productsByName.getOrDefault("horse carriage", chocolate);
Product bike = productsByName.getOrDefault("E-Bike", chocolate);

Java 8より前:

Product bike2 = productsByName.containsKey("E-Bike")
    ? productsByName.get("E-Bike")
    : chocolate;
Product defaultProduct2 = productsByName.containsKey("horse carriage")
    ? productsByName.get("horse carriage")
    : chocolate;

4.3. putIfAbsent()

このメソッドを使用すると、指定されたキーのマッピングがまだない場合にのみ、新しいマッピングを追加できます。

productsByName.putIfAbsent("E-Bike", chocolate);

Java 8より前:

if(productsByName.containsKey("E-Bike")) {
    productsByName.put("E-Bike", chocolate);
}

私たちの記事Merging Two Maps with Java 8は、このメソッドを詳しく見ていきます。

4.4. merge()

また、merge(),を使用すると、マッピングが存在する場合は特定のキーの値を変更できます。それ以外の場合は、新しい値を追加できます。

Product eBike2 = new Product("E-Bike", "A bike with a battery");
eBike2.getTags().add("sport");
productsByName.merge("E-Bike", eBike2, Product::addTagsOfOtherProdcut);

Java 8より前:

if(productsByName.containsKey("E-Bike")) {
    productsByName.get("E-Bike").addTagsOfOtherProdcut(eBike2);
} else {
    productsByName.put("E-Bike", eBike2);
}

4.5. compute()

compute()メソッドを使用すると、特定のキーの値を計算できます。

productsByName.compute("E-Bike", (k,v) -> {
    if(v != null) {
        return v.addTagsOfOtherProdcut(eBike2);
    } else {
        return eBike2;
    }
});

Java 8より前:

if(productsByName.containsKey("E-Bike")) {
    productsByName.get("E-Bike").addTagsOfOtherProdcut(eBike2);
} else {
    productsByName.put("E-Bike", eBike2);
}

methods merge() and compute() are quite similar.compute() methodは、再マッピング用のkeyBiFunctionの2つの引数を受け入れることに注意してください。 また、merge()は、key、キーがまだ存在しない場合にマップに追加するdefault value、および再マッピング用のBiFunctionの3つのパラメーターを受け入れます。

5. HashMap内部

このセクションでは、HashMapが内部でどのように機能するか、たとえば、単純なリストの代わりにHashMapを使用する利点は何であるかを見ていきます。

これまで見てきたように、キーによってHashMapから要素を取得できます。 1つの方法は、リストを使用して、すべての要素を反復処理し、キーが一致する要素が見つかったら戻ることです。 このアプローチの時間と空間の両方の複雑さはO(n)になります。

With HashMap, we can achieve an average time complexity of O(1) for the put and get operations and space complexity of O(n).それがどのように機能するか見てみましょう。

5.1. ハッシュコードと等しい

HashMapは、そのすべての要素を反復処理する代わりに、キーに基づいて値の位置を計算しようとします。

素朴なアプローチは、可能な限り多くの要素を含むことができるリストを持つことです。 例として、キーが小文字であるとしましょう。 次に、サイズ26のリストがあれば十分です。キー「c」を使用して要素にアクセスする場合は、それが位置3にあることがわかり、直接取得できます。

ただし、より大きなキースペースがある場合、このアプローチはあまり効果的ではありません。 たとえば、キーが整数だったとしましょう。 この場合、リストのサイズは2,147,483,647である必要があります。 ほとんどの場合、要素がはるかに少ないため、割り当てられたメモリの大部分は未使用のままになります。

HashMap stores elements in so-called buckets and the number of buckets is called capacity.マップに値を配置する場合、HashMapはキーに基づいてバケットを計算し、そのバケットに値を格納します。 値を取得するために、HashMapはまったく同じ方法でバケットを計算します。

5.2. 衝突

For this to work correctly, equal keys must have the same hash, however, different keys can have the same hash。 2つの異なるキーに同じハッシュがある場合、それらに属する2つの値は同じバケットに格納されます。 バケット内では、値はリストに保存され、すべての要素をループすることで取得されます。 これのコストはO(n)です。

Java 8以降(JEP 180を参照)、バケットに8つ以上の値が含まれている場合、1つのバケット内の値が格納されるデータ構造がリストからバランスツリーに変更され、リストに戻されます。ある時点で、バケットに6つの値しか残っていない場合。 これにより、パフォーマンスがO(log n)に向上します。

5.3. 容量と負荷係数

複数の値を持つ多数のバケットを持つことを避けるために、バケットの75%(負荷係数)が空にならない場合、容量は2倍になります。 負荷係数のデフォルト値は75%で、デフォルトの初期容量は16です。 両方ともコンストラクターで設定できます。

5.4. putおよびget操作の要約

putおよびget操作がどのように機能するかを要約してみましょう。

When we add an element to the map,HashMapはバケットを計算します。 バケットにすでに値が含まれている場合、そのバケットに属するリスト(またはツリー)に値が追加されます。 負荷率がマップの最大負荷率よりも大きくなると、容量は2倍になります。

When we want to get a value from the map,HashMapはバケットを計算し、リスト(またはツリー)から同じキーで値を取得します。

6. 結論

この記事では、HashMapの使用方法と内部での動作について説明しました。 ArrayListと並んで、HashMapはJavaで最も頻繁に使用されるデータ構造の1つであるため、その使用方法と内部での動作について十分な知識を持っていると非常に便利です。 記事The Java HashMap Under the Hoodでは、HashMapの内部について詳しく説明しています。

いつものように、完全なソースコードはover on Githubで利用できます。