1. 概要

MapとHashMapの違いは、最初のものがインターフェースであり、2番目が実装であるということです。 ただし、この記事では、もう少し深く掘り下げて、インターフェイスが役立つ理由を説明します。 また、インターフェイスを使用してコードをより柔軟にする方法と、同じインターフェイスに対して異なる実装を使用する理由についても学習します。

2. インターフェイスの目的

インターフェイスは、動作のみを定義するコントラクトです。 特定のインターフェースを実装する各クラスは、このコントラクトを満たす必要があります。それをよりよく理解するために、実際の例を取り上げることができます。 車を想像してみてください。 人それぞれの心の中には異なるイメージがあります。 車という用語は、いくつかの性質と動作を意味します。 これらの性質を持つオブジェクトはすべて、車と呼ぶことができます。 だから私たち一人一人が違う車を想像したのです。

インターフェイスは同じように機能します。 マップは、特定の品質と動作を定義する抽象化です。 マップになることができるのは、これらすべての品質を備えたクラスのみです。

3. さまざまな実装

車種が異なるのと同じ理由で、Mapインターフェースの実装も異なります。 すべての実装は異なる目的を果たします。 全体として最適な実装を見つけることは不可能です。 ある目的のための最良の実装だけがあります。 スポーツカーは速くてかっこいいですが、家族でのピクニックや家具店への旅行には最適ではありません。

HashMap は、 Map インターフェースの最も単純な実装であり、基本的な機能を提供します。 ほとんどの場合、この実装はすべてのニーズをカバーします。 他に広く使用されている2つの実装は、 TreeMap であり、LinkedHashMapは追加機能を提供します。

これは、より詳細ですが完全ではない階層です。

4. 実装へのプログラミング

コンソールでHashMapのキーと値印刷したいとします。

public class HashMapPrinter {

    public void printMap(final HashMap<?, ?> map) {
        for (final Entry<?, ?> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

これは仕事をする小さなクラスです。 ただし、1つの問題があります。 HashMapでのみ機能します。したがって、 Mapによって参照されるメソッドTreeMapまたはHashMapにパスしようとすると、コンパイルが発生します。エラー:

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        HashMap<String, String> hashMap = new HashMap<>();
        TreeMap<String, String> treeMap = new TreeMap<>();

        HashMapPrinter hashMapPrinter = new HashMapPrinter();
        hashMapPrinter.printMap(hashMap);
//        hashMapPrinter.printMap(treeMap); Compile time error
//        hashMapPrinter.printMap(map); Compile time error
    }
}

なぜそれが起こっているのかを理解してみましょう。 どちらの場合も、コンパイラは、このメソッド内で HashMap 固有のメソッドが呼び出されないことを確認できません。

TreeMap の別のブランチにあります地図実装(しゃれは意図されていません)、したがって、で定義されているいくつかのメソッドが不足している可能性があります HashMap。 

2番目のケースでは、タイプ HashMapの実際の基になるオブジェクトにもかかわらず、それはMapインターフェースによって参照されます。 したがって、このオブジェクトは、 Map で定義されたメソッドのみを公開でき、HashMap。では公開できません。

したがって、HashMapPrinterは非常に単純なクラスですが、具体的すぎます。 このアプローチでは、マップの実装ごとに特定のプリンターを作成する必要があります。

5. インターフェイスへのプログラミング

多くの場合、初心者は「インターフェイスへのプログラム」または「インターフェイスに対するコード」という表現の意味について混乱します。 次の例を考えてみましょう。これにより、もう少し明確になります。 引数のタイプを可能な限り最も一般的なタイプであるMap:に変更します。

public class MapPrinter {
    
    public void printMap(final Map<?, ?> map) {
        for (final Entry<?, ?> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

ご覧のとおり、実際の実装は同じままでしたが、唯一の変更点は引数のタイプです。 これは、メソッドがHashMapの特定のメソッドを使用しなかったことを示しています。 必要なすべての機能は、 Map インターフェイス、つまりメソッド entrySet()ですでに定義されています。

その結果、この小さな変更によって大きな違いが生まれました。 これで、このクラスは任意のMap実装で機能します。

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        HashMap<String, String> hashMap = new HashMap<>();
        TreeMap<String, String> treeMap = new TreeMap<>();

        MapPrinter mapPrinter = new MapPrinter();
        mapPrinter.printMap(hashMap);
        mapPrinter.printMap(treeMap);
        mapPrinter.printMap(map);
    }
}

インターフェイスへのコーディングは、Mapインターフェイスの任意の実装で機能する多用途のクラスを作成するのに役立ちました。このアプローチにより、コードの重複を排除し、クラスとメソッドの目的を明確にすることができます。

6. インターフェイスを使用する場所

全体として、引数は可能な限り最も一般的なタイプである必要があります。 前の例で、メソッドのシグネチャを変更するだけでコードがどのように改善されるかを見ました。 同じアプローチが必要なもう1つの場所は、コンストラクターです。

public class MapReporter {

    private final Map<?, ?> map;

    public MapReporter(final Map<?, ?> map) {
        this.map = map;
    }

    public void printMap() {
        for (final Entry<?, ?> entry : this.map.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}

このクラスは、コンストラクターで正しい型を使用したという理由だけで、 Map、の任意の実装で機能します。

7. 結論

要約すると、このチュートリアルでは、インターフェースが抽象化とコントラクトの定義に最適な手段である理由について説明しました。 可能な限り最も一般的なタイプを使用すると、コードの再利用と読み取りが容易になります。 同時に、このアプローチはコードの量を減らします。これは常にコードベースを単純化するための良い方法です。

いつものように、コードはGitHubから入手できます。