数据结构为什么使用接口定义?

其实后来才想到,抽象类也是可以作为数据结构的抽象层的,但是问题是 Java 是单继承的语言,如果使用抽象类作为抽象层,这就会导致一个数据结构只能为某种或某系列数据结构,这样的关系只能是树形的。

而这是不符合常理的——你可以把一个 List 看作栈,看作队列,看作数组,甚至看作 Map 和 Set(这当然可以实现!只要提供相同接口,管它里面是什么实现!只是不能达到哈希表的查询速度罢了)现实中各个数据结构的关系应当是一种有向无环图的关系,一个具体的底层实现对应什么数据结构实际上是取决于我们如何看待它的。而看待它的方式实际上也就是对它的操作方式,也就是接口。所以 Java 将接口作为数据结构的定义是比较合理的。

Java 容器框架中的接口为

框架中的类为

这张图似乎已经过时了,ArrayQueue 这个类没了,现在要使用队列应当使用 Queue 作为声明类型,LinkedList 或 ArrayList,ArrayDeque 作为实际类型。

Stack 没有出现在这里,因为它是 vector 实现的,老黄历了。

这是不符合我的常识的,为什么类似 Queue,List 这样的数据结构要用接口来定义,而不是用一个抽象类之类的来定义?

我认为,这里牵扯到了……数据结构的本质问题,一个数据结构究竟是因为什么而成为这个数据结构?比如讨论队列,我们知道,队列能够在头部删除元素,能够在尾部删除元素,队列可以用数组实现,也可以用链表实现……再比如讨论二叉堆,堆有两个基本方法 swim 和 sink,它们实现删除堆顶,插入新元素等功能,我们可以使用数组实现,也可以使用树实现。

答案是显然的,一个数据结构是因为对它的操作方法(算法)从而成为这个数据结构,因为它的具体实现的变化并不影响这个数据结构的实质(这么说是有点形而上学的,我认为接口与实现的关系,就如形式与内容的关系,而不是瓶与酒的关系,它们之间是会互相制约的)。

在这里,将数据结构定义为接口,将接口和实现分开,这似乎是现代容器类库常用的……设计模式。

以 Abstract 开头的一些抽象类,是对这些接口的部分实现(从而方便类库实现者的编写,实现接口中所有方法有时候太麻烦了),这里实际上可以通过在接口的方法中使用 default 关键词实现相同的作用……为什么 Java 容器类设计者没有这样考虑?

因为当时还没有 default 关键字。

接口与实现分离,也能让我们能够方便地更改选择的实现,而不需要对代码进行太多改动,比如——

1
2
3
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

我们定义一个List<Integer>接口对象,赋给它一个ArrayList<Integer>。然后对它进行一些操作。然后经过几次迭代,我突然不想用ArrayList了,LinkedList更符合需求,这时候我们只需要更改一处地方即可。并且我们不需要担心其后会出现什么 bug,或者编译错误之类,因为这个接口对象所能够调用的方法是受其声明类型决定的。

还可以这样认为,声明类型是出于我们的需要,是从问题出发而选择的,声明类型就是在这个问题中,我们对这个对象的看待方式。比如,我们喝水时,我们是将水看待成一种能满足一定生理需求的东西,而不将它看待成氢氧化合物,不看待成液体,不看待成物质(当然,这些都是水的种概念),因为只有“满足一定生理需求的东西”才是最接近问题空间的。问题不是它究竟是什么(这种定义是可以无限地下的),而在于我们需要它是什么。