专栏原创出处:github-源笔记文件 (opens new window) ,github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。
Java 核心知识专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 核心知识 (opens new window)
# 一、前言
本节内容主要研究 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
Copied!
# 三、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
Copied!
# 四、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)
Copied!
- 从字节码层面分析,发现 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]; }
Copied!
- 如果我们用 for 时,只需要根据下标值,取对应的数据即可
public E get(int index) { Objects.checkIndex(index, size); return elementData(index); }
Copied!
# 五、RandomAccess 接口让我们尽量使用 for 循环
- RandomAccess 接口是干什么的?
public interface RandomAccess { }
Copied!
我们可以看到这个接口没有任何实现,它仅仅起一个标识的作用,标识这个集合是否支持随机访问。类似于 Serializable, Cloneable
。
- 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); } } }
Copied!
现在我们知道为什么 Set 接口没有实现 RandomAccess 接口了。这也是为什么 Set 不能通过下标取值的原因之一。
Set 底层一般通过 Map 集合的 Key 实现的。我们知道 Map 作为一个哈希表底层使用链表节点存储。因此不支持随机访问。这也是为什么 Set 没有
get(int index)
方法的根本原因。
# 专栏更多文章笔记
Gitalking ...