java集合学习之List(二)随机访问RandomAccess接口和ArrayList和LinkedList遍历性能问题

时间:2021-12-08 17:59:31

ArrayList这个类是实现了RandomAccess接口的,RandomAccess接口和Serializable接口一样都是没有方法或者字段的,像是一个标志,

RandomAccess接口文档说明的是:Marker interface used by <tt>List</tt> implementations to indicate thatthey support fast (generally constant time) random access. [(标记接口用于List继承表名支持快速随机访问)]

这个接口在Collections类中用的很多,用于判断是否是RandomAccess接口的实例(其实就是多态的应用)[list instanceof RandomAccess]根据是否实现该接口,来选用不同的算

贴源码注释+自己的翻译:

/**
 * Marker interface used by <tt>List</tt> implementations to indicate that
 * they support fast (generally constant time) random access.  The primary
 * purpose of this interface is to allow generic algorithms to alter their
 * behavior to provide good performance when applied to either random or
 * sequential access lists.
 * 
 * 标记该接口用来被List实现指示他们支持快速随机访问. 这个接口的主要目的就是允许一般算法去修改他们的
 * 行为以提供更好的表现:当被应用与随机或者顺序方式列表时.
 *
 * <p>The best algorithms for manipulating random access lists (such as
 * <tt>ArrayList</tt>) can produce quadratic behavior when applied to
 * sequential access lists (such as <tt>LinkedList</tt>).  Generic list
 * algorithms are encouraged to check whether the given list is an
 * <tt>instanceof</tt> this interface before applying an algorithm that would
 * provide poor performance if it were applied to a sequential access list,
 * and to alter their behavior if necessary to guarantee acceptable
 * performance.
 *
 * 操作随机访问列表的lists时比如ArrayList的最好的算法当应用于顺序访问列表如LinkedList时,可能会产生二次项行为
 * (妈的,什么是二次项行为啊!韩老师说(a+b)^2展开就是)
 * 在应用算法之前通用列表算法鼓励去检查这个list是否是该接口的实例,如果应用于顺序访问列表时,
 * 则会提供较差的性能,并且在必要时去警告他们行为来保证可接受的性能.
 *
 * <p>It is recognized that the distinction between random and sequential
 * access is often fuzzy.  For example, some <tt>List</tt> implementations
 * provide asymptotically linear access times if they get huge, but constant
 * access times in practice.  Such a <tt>List</tt> implementation
 * should generally implement this interface.  As a rule of thumb, a
 * <tt>List</tt> implementation should implement this interface if,
 * for typical instances of the class, this loop:
 * <pre>
 *     for (int i=0, n=list.size(); i &lt; n; i++)
 *         list.get(i);
 * </pre>
 * runs faster than this loop:
 * <pre>
 *     for (Iterator i=list.iterator(); i.hasNext(); )
 *         i.next();
 * </pre>
 *    
 *  我们意识到在随机访问和顺序访问的区别通常是模糊的,比如,列表很大时,一些list的实现提供了
 *  渐进线性访问时间,但在实际中时间是不变的.这样的List应该实现该接口,
 *  作为一个经验法则,一个接口应该尽量实现这个这个接口,
 *  对于这个接口的实现使用for循环笔使用iterator循环更快
 *
 * <p>This interface is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @since 1.4
 */

 以上是对于RandomAccess接口的描述,一句话概括就是,在使用循环遍历List的时候应该判断下这个集合是否是RandomAccess的实例,如果是就是用for循环来操作,如果不是就是使用iterator迭代器来操作.可是为什么要这么做的?这要从数据结构的角度来说了

首先看一下实现了RandomAccess接口的ArrayList的接口,一般人都知道ArrayList就是一个数组

java集合学习之List(二)随机访问RandomAccess接口和ArrayList和LinkedList遍历性能问题

然后看下没有实现RandomAccess接口的LinkedList,顾名思义就是一个链表

先看一下链表和数组的区别:

数组像是身上都有了编号排成一排的人,你要找第几个直接根据编号去找,就是所谓的根据下标去查询,所以在查询的时候很快,但是如果要插入就变得很慢了,你还要给插入位置之后的其他人重新定义编号,删除同理,效率肯定慢.

链表就像是牵手站成一排的人,你要找第几个就要一个一个取数,从1数到n,但是插入的时候直接把人分开,重新牵手就可以,无需编号,删除同理.

所以如果用for循环遍历的话,贴个代码看看:

/**
 * Collections 测试RandomAccess随机访问速度
*/
List<String> l = new ArrayList<String>();
List<String> _l = new LinkedList<String>();
for(int i=0;i<100000;i++){
    l.add(String.valueOf(i));
    _l.add(String.valueOf(i));
}
long startTime = System.currentTimeMillis();
for(int i=0;i<l.size();i++){
    l.get(i);
}
System.out.println("count:"+(System.currentTimeMillis()-startTime));
startTime = System.currentTimeMillis();
for(int i=0;i<_l.size();i++){
    _l.get(i);
}
System.out.println("count:"+(System.currentTimeMillis()-startTime));
startTime = System.currentTimeMillis();
for(Iterator<String> it=_l.iterator();it.hasNext();){
    it.next();
}
System.out.println("count:"+(System.currentTimeMillis()-startTime));

输出结果就是:(ArrayList)count:0 (LinkedList)count:14760 (LinkedList)count:10

惊讶吧!开始分析!ArrayList不用管,就是根据下边直接去查,看下普通for循环下LinkedList为啥贼鸡儿慢!

看一下LinkedList的get方法

判断index在整体的前部还是后部,在前部就从前往后遍历,在后部就从后向前遍历,每次都这样从头遍历,能不慢么,算法复杂度有n^2了吧
Node<E> node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
 

 然后看下LinkedList的Iterator方法为啥挺快呢?通过实现类看到:

java集合学习之List(二)随机访问RandomAccess接口和ArrayList和LinkedList遍历性能问题

具体参考java集合学习之List(三)以LinkedList为例,debug看下迭代器的实现.

返回一个迭代器,然后判断是否有下一个节点,有的话就得到当前节点的next,不必再去多于的循环遍历.所以速度肯定很快.

-----------------------------------------------------------------------------------------------------

之前我的疑问在于这个LinkedList的Node节点是如何赋值的,今天又仔细看了下源码才他妈的看到的add的时候给Node复制的!!一定要看全啊....还是数据结构不熟悉