以容器类为例子,可以观一叶而知秋,看看以前的前辈们是如何处理各种面向对象思想下的继承体系的。
读的源代码越多,就越要总结这个继承关系否则读的多也忘得快。
首先摆上一张图片:
看到这张图很多人就慌了,难道这些东西我都要全部学习?其实是也不是,其中的很多东西都很有学习的必要,但是学习的过程绝对不是一行一行背诵,每一个类有哪些方法。而怎么从大体上掌握这个继承体系呢?
以最基本的ArrayList<E>为例子。(JDK 1.8环境下)
从ArrayList<E>开始往上看继承体系:
ArrayList(C)--->AbstractList(A)--->AbstractCollection(A)--->Collection(I)--->Iterable(I)
|--->List(I)--->Collection(I)
首先这个继承体系有两条线,我们从上往下看
Iterable,也就是迭代器,是个接口。任何类如果实现了这个接口,就必须实现一个函数,这个函数返回一个迭代器对象。
从某种意义上来讲,这个接口不属于这个继承体系,因为这个接口的主要目的是让实现它的类带有一种功能:通过迭代器访问。因为Collection继承了这个接口,可以想到的是肯定每一个容器都能通过迭代器访问,那么为什么要这么设计呢?
一开始接触的容器比较简单,能通过某种方式遍历,比如通过一个Index来遍历。可是对于一些容器,没有明显的某个数值(或者索引或者下标)可以遍历。
这个问题再进一步就是这样:容器的底层实现各不相同,为了简化遍历这个操作,就有了迭代器,实现屏蔽其底层数据结构的遍历。你用别人写的容器的时候不需要知道它怎么实现的,就只需要拿到这个迭代器,然后遍历就可以了。容器的提供者(编写者)负责这个容器能拿出一个正确的迭代器。(通过实现Iterable接口)
看到这里就能明白,迭代器接口不是单单针对容器,假如你写一个让别人能按照你想法遍历的类,都可以用迭代器。
接下来,我们看看Collection类。这个类是一个接口,里面写了很多方法。除去继承自Object类的方法,和容器有关的主要有:add(),remove(),contains(),isEmpty(),iterator(),size(),toArray()。这里没有写完,也不需要写完。主要就是要明白,一般顶层的接口会有一个极其言简意赅的名字(Collection,List,Set,Executor等等),然后在里面提供最最最基本的操作。比如像这些操作,我想只要是个容器就应该提供吧。
接下来是一个中间层:AbstractCollection。这个抽象类是拿来干嘛的呢?其实一句话表述,它能尽可能实现那些可以实现的操作。这就有点不明白了,你说你上面是个接口,可以说啥都没有,你既然不知道底层的数据结构(使用数组实现的?还是链表?还是???),能在这个层面上实现什么呢?
其实不然,虽然现阶段,抽象类只能看到接口,完全不知道具体怎么实现,可是它知道一定会实现。
具体来说吧,这是AbstractCollection里面的一部分源代码:
1 public boolean contains(Object o) { 2 Iterator<E> it = iterator(); 3 if (o==null) { 4 while (it.hasNext()) 5 if (it.next()==null) 6 return true; 7 } else { 8 while (it.hasNext()) 9 if (o.equals(it.next())) 10 return true; 11 } 12 return false; 13 }
它虽然不知道你到底怎么实现了iterator(),但是它知道你一定会实现,因为是抽象方法。它在这个层面就直接拿来用了。而通过一个Iterator变量一个容器的代码,谁写基本都是这样。
总结来说,就是位置在继承体系中间的抽象类(如AbstractCollection)它主要作用是封装那些这个层面可以基本实现的差不多的代码。
再然后是AbstractList。这个抽象类类继承了AbstractCollection,同时实现了List接口。
既然前面已经通过抽象类封装了基本方法了,那么这个抽象类又是拿来干嘛的呢?很简单,之前的都是从一个Collection型的容器,功能特别单一,从这里开始就要进行分流了,很明显这个抽象类和它再往下的继承体系都是往List方向发展。
这里的List是个接口,继承自Collection接口,最主要是多了如下几个方法:
get(index),indexOf(Object),listIterator(),set(index,Object);
这说明一个什么?也就是说如果Collection是最基本的容器的话,List就是容器之中的线性表。可以类比数组。因为它的线性表特性,它的数据底层是通过首地址和偏移量的形式储存的,在这个层面上可以认为它是通过一个index(下标)对于上容器里面的元素的。所以对这个借口对应的容器来说,就不止是简单的添加删除了,它还可以做到在某个特定位置添加,修改,或者通过下标取得某个位置的值。
这里可以用简单的话来阐述我理解的这个地方的继承关系以及为什么要这么设计。
List继承自Collection说明它是一种容器,不过它不只单单是容器,它还有线性表性质。也就是说它跟高级了,从容器里面分离出来了一些。
而AbstractList为什么要继承List呢?它是在AbstractCollection基础上继承的List。也就是说,它是一个容器,可是它通过继承List和别的容器区分开来(如AbstractSet)。所以从这里看的话,List在继承体系中至少有这两个作用,1,分化继承线路,2,以后拿来给容器向上转型。
同时AbstractList还有一段有趣的代码:
1 private class Itr implements Iterator<E> { 2 3 int cursor = 0; 4 5 int lastRet = -1; 6 7 int expectedModCount = modCount; 8 9 public boolean hasNext() { 10 return cursor != size(); 11 } 12 13 public E next() { 14 checkForComodification(); 15 try { 16 int i = cursor; 17 E next = get(i); 18 lastRet = i; 19 cursor = i + 1; 20 return next; 21 } catch (IndexOutOfBoundsException e) { 22 checkForComodification(); 23 throw new NoSuchElementException(); 24 } 25 }
这段代码是AbstractList里面的迭代器的代码的节选。问题的核心是:get()方法根本没有:
1 abstract public E get(int index);
这还是利用了上面提到的思想:在这个层面还是完全不知道底层的真正数据结构和基本操作的真正实现方式。那么你的get(),set()函数肯定是写不出来的。但是在这个层面它为了尽可能地实现一些大同小异的代码,比如这里的Itr的实现,它既然认定这个抽象方法一定会被继承者实现,它就直接拿来用了。直接利用没有实现的get()方法来实现迭代器。这个过程真是和踢皮球一样:上层的AbstractCollection认为下层会实现迭代器,于是直接使用迭代器写出了Contains()方法。这一层认为下一层应该实现get()方法,于是它就直接用get()方法实现了迭代器。最后的结果是好的,最后一层理论上只需要关注自己切切实实的get(),set()等等方法。
AbstractList再往下就是Arraylist了。这就不用说了,底层的最终实现。封装的工具类,一般来说不需要再继承它只需要使用它。它不再是一个抽象类,里面很多方法得到了实现。应为它最终添加了底层的数据结构实现。所以对它来说一切都是可以明白的,到底那些方法怎么实现。
这都只是Arraylist这条路的一路下来的东西,很多别的继承结构都是基于这个体系的。