随机访问和内存地址连续性
在对集合的访问中,一般会有两种访问方式:随机访问和顺序访问,不同的底层数据结构,在使用这两种访问方式时,效率是有区别的。一般来讲,内存地址连续的数据结构,在随机访问时的效率比内存地址不连续的要高。因为内存地址连续,可以通过下标很快的检索出对应的元素,反之由于内存地址不连续,在随机访问时,需要从内存地址起始位置按照链接关系一个一个的扫描,效率自然就降低了。典型的例子就是数组和链表。数组是一种内存地址连续的结构,对它的随机访问,效率比较高;链表是一种内存地址不连续的结构,对它的随机访问效率就比较低。
RandomAccess接口
在java语言中,为了方便开发者选择高效的访问方式,系统提供了一个RandomAccess接口,该接口用来标识对应的数据结构是否支持高效的随机访问。大多数以数组为底层结构的数据结构都实现了该接口。比如常见的ArrayList。在官方声明中,也对该接口进行了很详细的描述,原文如下:
* 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.
*
* <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.
*
* <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:
该声明对于RandomAccess接口进行很详细的描述,大致包括:
- 标识一个对象是否支持良好的随机访问策略
- 在满足1的前提下,随机访问策略比顺序访问的效率差不多高出一倍
- 如何判定一个集合或者数据结构是否满足这个接口的定义
在上述描述中的第三点,是如何确定一个对象是否应该去实现该接口,Java给出的依据是:
* for typical instances of the class, this loop:
* <pre>
* for (int i=0, n=list.size(); i < n; i++)
* list.get(i);
* </pre>
* runs faster than this loop:
* <pre>
* for (Iterator i=list.iterator(); i.hasNext(); )
* i.next();
* </pre>
也就是说,通过下标检索效率要比通过迭代器的检索效率高。接下来,我们测试下。
效率测试
测试用例1
针对实现了RandomAccess的结构,ArrayList进行测试。我们通过填充100W个整型对象到ArrayList中,然后对该ArrayList集合进行遍历。测试代码如下:
public class RandomAccessTest {
private final static int MAX = 10000000;
private static ArrayList<Integer> LIST = new ArrayList<>(MAX);
static {
for (int i = 0; i < MAX; i++) {
LIST.add(i);
}
}
public static void main(String[] args) {
loop();
iterator();
}
public static void loop() {
int size = LIST.size();
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
Integer integer = LIST.get(i);
}
long end = System.currentTimeMillis();
System.out.println("for loop cost " + (end - start));
}
public static void iterator() {
long start = System.currentTimeMillis();
Iterator<Integer> iterator = LIST.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("iterator loop cost " + (end - start));
}
}
我们看下测试结果:
可以看到,通过下标的访问速度明显比迭代器要高,在数据量变得更加庞大的时候,这种优越性会体现得更加明显。
测试用例2
下面,我们看下没有实现RandomAccess的结构LinkedList,它是一种实现了双端队列的数据结构,还是按照测试用例1的方式,先填充10W个元素,然后进行遍历,测试代码如下:
public class SequentialAccessTest {
private final static int MAX = 100000;
private static LinkedList<Integer> LIST = new LinkedList<>();
static {
for (int i = 0; i < MAX; i++) {
LIST.add(i);
}
}
public static void main(String[] args) {
loop();
iterator();
}
public static void loop() {
int size = LIST.size();
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
Integer integer = LIST.get(i);
}
long end = System.currentTimeMillis();
System.out.println("for loop cost " + (end - start));
}
public static void iterator() {
long start = System.currentTimeMillis();
Iterator<Integer> iterator = LIST.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("iterator loop cost " + (end - start));
}
}
我们看下测试结果:
结果很惊人,可以看到,通过下标的访问速度极其缓慢,在数据量变得更加庞大的时候,编译器甚至会长时间无响应。反观迭代器,效率极其高。这个case在一定程度上,在提醒我们,选择合适的访问方式是何等的重要。
结论
任何一种数据结构都有其优点和缺点,在特定的场景下选择合适的数据结构,会让系统的运行效率得到意想不到的提升。当然,如何去选择,需要软件开发者对数据结构有一定的了解。了解常用的数据结构特性,了解他们的实现思路,了解他们的时间空间消耗。这不是一件简单的事情,却是一件有意义的事情。