您的位置:首页 > 编程语言 > Java开发

源码分析-java-AbstractList-subList、RandomAccess、equals和hashCode

2016-07-19 15:43 204 查看
上一个小结说完了AbstractList的迭代器Itr和ListItr的实现。

这一小结把RandomAccess和subList说完。

RandomAccess

首先说明一些RandomAccess接口。

这个接口没有任何需要实现的方法。这中类型的接口其实在java中比较常见。

API文档

List实现的标记方法用来指示他们支持快速随机访问(通常是常数时间)。这个接口的主要目的是允许通用方法提供良好的性能,当这个方法应用在随机或者序列访问list中。

用于操作随机访问列表的(比如ArrayList)的算法在应用在顺序访问列表(比如LinkedList)的时候会产生另外的一个算法。通用的list算法通常应该检测给定的List是不是可以实现RandomAccess,在不是RandomAccess实例的时候提供性能一般的算法,在实现了RandomAccess的时候提供加速的算法。

这个使得随机和顺序访问更加容易混淆。例如有些List在变大的时候提供接近线性的访问性能,但是在实践中是常数的。这样的List应该需要标识实现了该接口。从经验上来看。如果一个List实例实现了这个接口。那么它使用get迭代的速度是快于Iterator的next方法的。

从AbstractList类的代码上并没有实现这样的接口,但是并没有限制AbstractList的子类实现RandomAccess接口。这是非常合理的,因为Abstract的get方法并没实现get方法。而是需要子类来实现的。而是否实现了RandomAccess也是根据get的实现方式来确定。

而且从下文看出虽然AbstractList没有实现RandomAccess,但是对于AbstractList的subList在创建新的subList的时候进行了判断。

subList

API文档

返回一个这里从fromIndex到toIndex之间的一个list的独立的视图,返回的列表是基于原表的。原表的任何非结构性改动都会反映在这个subList中,反之亦然。返回的列表支持所有传入列表的方法。

这个方法消除了显示的范围操作(比如数组操作中普遍存在的排序)。任何一个期望将List的范围作为参数传递的操作都可以使用subList。比如说从一定范围内删除list的语句

list.subList(from, to).clear();


类似的语句可以从indexOf和lastIndexOf构造,其他的所有Collection的方法都可以应用在subList中。

如果所支持的list被通过非本sublist的方法而进行了结构性调整,那通过这个方法返回的列表的语义是没有办法确定的。(结构调整是指那些改变list大小的方式,或者是扰动了迭代器使得其得出错误结果的动作)

这个实现返回了一个AbstractList的子类。这个子类在私有域中存有,subList在原list中的的偏置(offset)以及subList的大小(可以在其周期内修改)以及原List的modCount。这里有两种不同种类的子类,一个是实现了RandomAccess接口的子类,一个是普通的为实现RandomAccess的子类。需要说明的是如果原List是RandomAccess的哪返回的子类才会是RandomAccess的。

subList的set、get、add、remove、addAll和removeRage方法都在检查过边界,并偏置了位移之后,委托给了对应的原list方法。对于addAll(Collection c)方法只是单纯的返回了addAll(size,c)

ListIterator返回了一个包装对象类型覆盖了原List Iterator的。Iterator方法单纯了返回了listIterator(),size方法单纯的返回了size域。

所有的方法都实现检查了modCount是否等于它所期望的值。如果不是则抛出一个异常。

其实源码上的东西基本上也都被API文档说的差不多了。但是还有一些问题有继续探讨的价值。

首先一个size的问题:

在源码中的SubList的size是以一个私有域的形式固定下来的,当然它也提供了size方法。这一点和AbstractList有所不同。Abstract的所有用到size的地方都是调用了size方法。而不是设定了一个私有域。至于为何这样做一个可能的结论,AbstractList的size是依赖具体实现的。如果保留了私有域size则没有办法很好的初始化。因此将这个任务交给子类,而在所有用大小的地方采用调用size的方法实现。这样可以实现更好的通用性。

另一个问题是实际上modCount并不是一个私有域而是继承自AbstractList的protected类型。

第三个问题是在使用ListIterator(int)的方法的时候实现了一个新的匿名类,这个新类的私有域保留了ListIterator。并作为一个ListIterator接口引用返回。初始化的方式采用了私有域变量的初始化。(关于初始化的顺序是一个有意思的话题需要另外在写一篇说明)但是初始化的方法实际上还是调用了SubList所保存的私有域的ListIterator(int)的方法,只是改变了index的偏置。

equals

典型的equals的编写代码的方式:

public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;

ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}


判定o和当前对象引用的是不是同一个对象,如果是的直接返回true

判断o是不是List对象,如果不是List的实例,那就直接返回。此外如果没有这句话的话这样会导致直接生成迭代器的时候会产生错误。

生成两个迭代器。分别对量迭代器进行分别比较各个迭代器的元素。

当迭代器中某一个迭代器结束之后,结束循环体,并且还需要保证两个迭代器都同时结束才返回true。

没有什么特别需要说明的,很多地方也都说明白了如何判断两个元素相等。主要需要注意的一点应该就是泛型的使用了。另外在比较的时候使用E的equals方法。而不能使用Object。

hashCode

public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}


这也是典型的hashCode的集合的写法。散列实际上说是区别不同的集合的方法。当然是需要每个元素都按照顺序依次计算的。按照数据结构里面的说法这个实际上算是多项式的算法。

还有需要说明的一点是为什么需要采用31作为系数。

首先当然需要采用质数,最好是一个比较大的质数。因为这样当需要散列的关键字数量太大的时候更不容易出现冲突的情况。而采用31是考虑到这样做是相当于把原来的数左移5位,然后减一,这样计算效果要快的多。据说大部分的jvm都对此进行过优化。因此使用31为系数的hash算法在java里非常常见。

关于hashCode为什么异地要取尽可能大的质数我找到两个网页说的比较靠谱。

https://segmentfault.com/q/1010000000593741

http://www.cnblogs.com/Jochebed/p/4615865.html

总的来说之所以取质数是为了避免在同一个散列中的元素有相同的联系和性质,

而有些说法说取不接近2的多次幂的方法可能是为了避免和计算机存储信息格式的问题以及安全方面的考量,不过我对这方面不了解。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 文档 源码