专栏原创出处:github-源笔记文件 github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。

Java 核心知识专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 核心知识

# 一、前言

本节内容主要研究 for、foreach 循环的底层实现原理,再比较两种实现方式的性能。最后通过 RandomAccess 接口说明 JDK 让我们怎么去识别集合是否支持随机访问。

随机访问表示,像数组那样,随便给定一个下标我们就可以访问内存的数据。而链式结构的存储只能顺序遍历各个链表节点访问。

# 二、for 循环底层实现

for 循环是对数值型数据出栈、改变值、比较的过程。

public void foriMethod() {
    for (int i = 0; i < 10; i++) {
        // ...
    }
}

———— 转为字节码,部分关键字节码如下:

 0 iconst_0         // 将 int 型 0 推送至栈顶
 1 istore_1         // 将栈顶 int 型数值存入第二个本地变量
 2 iload_1          // 将第二个 int 型本地变量推送至栈顶
 3 bipush 10        // 将单字节的常量值 10 推送至栈顶
 5 if_icmpge 14 (+9)// 比较栈顶两 int 型数值大小, 当结果大于等于 0 时跳转
 8 iinc 1 by 1      // 将指定 int 型变量增加指定值,此处 +1
11 goto 2 (-9)      // 无条件跳转至 计数器为 2 code,继续循环
14 return

# 三、foreach 循序底层实现

foreach 循环底层会把循环主体对象(实现了 Iterable 接口)转换为 Iterator 对象进行迭代器遍历。

public void foreachMethod(List<String> list) {
    for (String s : list) {
     }
}
// 等价于 for (Iterator i=list.iterator(); i.hasNext(); ) i.next();

———— 转为字节码,部分关键字节码如下:
    
 0 aload_1            // 将第二个引用类型本地变量 (list) 推送至栈顶
 1 invokeinterface #2 // 调用 iterator 方法 <java/util/List.iterator> count 1
 6 astore_2           // 将栈顶引用型数值存入第三个本地变量
 7 aload_2            // 将第三个引用类型本地变量推送至栈顶
 8 invokeinterface #3 // 调用 Iterator.hasNext <java/util/Iterator.hasNext> count 1
13 ifeq 29 (+16)      // 当栈顶 int 型数值等于 0 时跳转到 29 code
16 aload_2            // 将第三个引用类型本地变量推送至栈顶
17 invokeinterface #4 // 调用 Iterator.hasNext 方法返回当前取值对象 <java/util/Iterator.next> count 1
22 checkcast       #5 // 强转为 <java/lang/String>
25 astore_3           // 将栈顶引用型数值存入第四个本地变量
26 goto 7 (-19)       // 继续循环
29 return

# 四、foreach 与 for 性能比较

先下结论

尽量使用 for 循环,开销比 foreach 低。

对于 (int i = 0; i < list.size(); i++) ,长度尽量定义为变量,减少每次计算消耗。

—————————————————————————— foreach 生成字节码 ———————————————————————
for (String s : list) {}

 0 aload_1
 1 invokeinterface #2 <java/util/List.iterator> count 1
 6 astore_2
 7 aload_2
 8 invokeinterface #3 <java/util/Iterator.hasNext> count 1
13 ifeq 29 (+16)
16 aload_2
17 invokeinterface #4 <java/util/Iterator.next> count 1
22 checkcast #5 <java/lang/String>
25 astore_3
26 goto 7 (-19)

—————————————————————————— for 生成字节码 —————————————————————————
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
}

29 iconst_0
30 istore_2
31 iload_2
32 aload_1
33 invokeinterface #6 // 此处可优化为一个变量 list.size(),减少每次方法调用(如果编译器足够智能可能进行标量替换)
38 if_icmpge 58 (+20)
41 aload_1
42 iload_2
43 invokeinterface #7 <java/util/List.get> count 2
48 checkcast #5 <java/lang/String>
51 astore_3
52 iinc 2 by 1
55 goto 31 (-24)
  1. 从字节码层面分析,发现 foreach 会生成 Iterator 对象,而且在调用时实时创建的。并且在整个迭代过程中,hasNext 方法处理逻辑比较复杂。

以 ArrayList 源码为例:

public Iterator<E> iterator() {
        return new Itr(); // 每次调用实时创建返回
}

// ArrayList.Itr 类中的 next 方法如下:
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
  1. 如果我们用 for 时,只需要根据下标值,取对应的数据即可
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}

# 五、RandomAccess 接口让我们尽量使用 for 循环

  1. RandomAccess 接口是干什么的?
public interface RandomAccess { }

我们可以看到这个接口没有任何实现,它仅仅起一个标识的作用,标识这个集合是否支持随机访问。类似于 Serializable, Cloneable

  1. RandomAccess 接口有什么用?

举个例子:

List 接口有 ArrayList、LinkedList 两个实现。但是 LinkedList 底层是链表实现不支持随机访问,所以无法使用 for 循环,只能使用 foreach 循环遍历了。

public <T extends Object> void randomAccess(List<T> list) {
    if (list instanceof RandomAccess) {
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
        }
    } else {
        for (T t : list) {
            System.out.println(t);
        }
    }
}

现在我们知道为什么 Set 接口没有实现 RandomAccess 接口了。这也是为什么 Set 不能通过下标取值的原因之一。

Set 底层一般通过 Map 集合的 Key 实现的。我们知道 Map 作为一个哈希表底层使用链表节点存储。因此不支持随机访问。这也是为什么 Set 没有 get(int index) 方法的根本原因。

# 专栏更多文章笔记

最后修改时间: 2/17/2020, 4:43:04 AM