1. 概述

MapHashMap 的区别在于,前者是一个接口,而后者是一个实现。 但在本文中,我们将深入探讨为什么接口是有用的,并学习如何通过接口使代码更具灵活性,以及为什么对于同一个接口会有不同的实现。

2. 接口的目的

接口是一种定义行为的契约。每个实现特定接口的类都必须履行这个契约。 为了更好地理解,我们可以从现实生活中的例子来说明。想象一辆汽车。每个人心中都有不同的形象,但“汽车”这个词暗示了一些质量和行为。任何具备这些特性的对象都可以被称为汽车。这就是为什么我们每个人对汽车有不同的想象。

接口的工作原理相同。Map 是一个抽象概念,定义了某些质量和行为。只有拥有所有这些特性的类才能被称为 Map

3. 不同的实现

我们为 Map 接口提供不同的实现,原因与我们有不同型号的汽车相似。所有实现都有不同的用途。没有一种实现能全面地成为最佳选择,只有在特定目的下才有最好的实现。 尽管跑车速度快且看起来酷炫,但它并不是家庭野餐或家具店之旅的最佳选择。

HashMapMap 接口的最简单实现,提供了基础功能。通常,这个实现能满足大部分需求。另外两个广泛使用的实现是 TreeMapLinkedHashMap,它们提供了额外的功能。

这里有一个更详细但不完整的层次结构:

4. 针对实现编程

设想我们要在控制台上打印 [HashMap] 的键值对:

public class HashMapPrinter {

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

这是一个完成任务的小类,但它存在一个问题:它只能与 HashMap 对象一起工作。因此,如果试图将 Map 类型的 TreeMapHashMap 传递给方法,编译时会出错:

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 特定方法的调用。

TreeMapMap 实现的分支上(这里并没有双关含义),因此可能缺少 HashMap 定义的一些方法。

在第二种情况下,尽管实际底层对象是 HashMap 类型,但它通过 Map 接口引用。因此,这个对象只能暴露 Map 接口定义的方法,而不是 HashMap 的方法。

因此,尽管 HashMapPrinter 类相当简单,但它过于具体。 这种方法要求我们为每个 Map 实现创建一个特定的 Printer 类。

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. 如何使用接口

总的来说,参数应尽可能具有最通用的类型。我们在前一个例子中看到了,只需简单改变方法签名就能改进代码。另一个应该采取同样方法的地方是构造函数:

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 上找到。