1. 概述
Map
和 HashMap
的区别在于,前者是一个接口,而后者是一个实现。 但在本文中,我们将深入探讨为什么接口是有用的,并学习如何通过接口使代码更具灵活性,以及为什么对于同一个接口会有不同的实现。
2. 接口的目的
接口是一种定义行为的契约。每个实现特定接口的类都必须履行这个契约。 为了更好地理解,我们可以从现实生活中的例子来说明。想象一辆汽车。每个人心中都有不同的形象,但“汽车”这个词暗示了一些质量和行为。任何具备这些特性的对象都可以被称为汽车。这就是为什么我们每个人对汽车有不同的想象。
接口的工作原理相同。Map
是一个抽象概念,定义了某些质量和行为。只有拥有所有这些特性的类才能被称为 Map
。
3. 不同的实现
我们为 Map
接口提供不同的实现,原因与我们有不同型号的汽车相似。所有实现都有不同的用途。没有一种实现能全面地成为最佳选择,只有在特定目的下才有最好的实现。 尽管跑车速度快且看起来酷炫,但它并不是家庭野餐或家具店之旅的最佳选择。
HashMap
是 Map
接口的最简单实现,提供了基础功能。通常,这个实现能满足大部分需求。另外两个广泛使用的实现是 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());
}
}
}
这是一个完成任务的小类,但它存在一个问题:它只能与 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
在 Map
实现的分支上(这里并没有双关含义),因此可能缺少 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 上找到。