作为程序员每天除了写很多 if else 之外,写的最多的也包含 for 循环了,都知道我们 Java 中常用的 for 循环有两种方式,一种是使用 for loop,另一种是使用 foreach,那如果问你,这两种方式哪一种效率最高,你的回答是什么呢?
首先我们先通过代码来实际测试一下,在计算耗时之前我们先创建一个大小集合,然后通过不断的获取集合中的内容来测试耗时。
- package com.example.demo;
-
- import java.util.ArrayList;
- import java.util.List;
-
- public class ForTest {
- public static void main(String[] args) {
-
- //获取一个指定大小的 List 集合
- List<Integer> list = getList(1000000);
-
- // 开启 for loop 耗时计算
- long startFor = System.currentTimeMillis();
- for (int i = 0; i < list.size(); i++) {
- Integer integer = list.get(i);
-
- }
- long costFor = System.currentTimeMillis() - startFor;
- System.out.println("for loop cost for ArrayList:" + costFor);
-
- // forEach 耗时计算
- long forEachStartTime = System.currentTimeMillis();
- for (Integer integer : list) {
- }
- long forEachCost = System.currentTimeMillis() - forEachStartTime;
- System.out.println("foreach cost for ArrayList:" + forEachCost);
-
- }
-
- public static List<Integer> getList(int size) {
- List<Integer> list = new ArrayList<>();
- for (int i = 0; i < size; i++) {
- list.add(i);
- }
- return list;
- }
-
- }
简单说明一下上面的带,先创建一个 List ,然后通过两种方式的遍历来计算耗时,根据集合的大小不同,我们进行运行会得到下面的一些测试数据,不同人的机器上面运行的时间会不一定,不过差距应该也不会太大。
| size= | 10000 | 100000 | 1000000 | 10000000 |
|---|---|---|---|---|
| for loop | 1 | 2 | 10 | 12 |
| for each | 1 | 3 | 17 | 34 |
通过上面的测试结果我们可以发现,在集合相对较小的情况下,for loop 和 foreach 两者的耗时基本上没有什么差别,当集合的数据量相对较大的时候,可以明显看的出来,for loop 的效率要比 foreach 的效率高。
至于为什么在大数据量的情况下 forEach 的效率要比 for 低,我们就要看下 forEach 的原理了。forEach 其实不是一种新的语法,而是一种 Java 的语法糖。在编译时,编译器会将这段代码转换成迭代器实现,并编译成字节码,我们可以再简单的看个 case,来实际看下字节码信息。
我们再编写一个简单的类,代码如下
- package com.example.demo;
-
- import java.util.ArrayList;
- import java.util.List;
-
- /**
- * <br>
- * <b>Function:</b><br>
- * <b>Author:</b>@author ziyou<br>
- * <b>Date:</b>2022-06-26 13:06<br>
- * <b>Desc:</b>无<br>
- */
- public class ForEachTest {
- List<Integer> list;
-
- public void main(String[] args) {
- for (Integer integer : list) {
-
- }
- }
-
-
- }
-
通过 javac ForEachTest.java 编译成 class 文件,再通过 javap -v ForEachTest 反编译,我们就会得到下面的字节码内容:
- Classfile /Users/silence/Downloads/demo/src/test/java/com/example/demo/ForEachTest.class
- Last modified 2022-6-26; size 643 bytes
- MD5 checksum 9cf01f7c8c87c2b4d62c39d437025b7f
- Compiled from "ForEachTest.java"
- public class com.example.demo.ForEachTest
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #8.#23 // java/lang/Object."<init>":()V
- #2 = Fieldref #7.#24 // com/example/demo/ForEachTest.list:Ljava/util/List;
- #3 = InterfaceMethodref #25.#26 // java/util/List.iterator:()Ljava/util/Iterator;
- #4 = InterfaceMethodref #27.#28 // java/util/Iterator.hasNext:()Z
- #5 = InterfaceMethodref #27.#29 // java/util/Iterator.next:()Ljava/lang/Object;
- #6 = Class #30 // java/lang/Integer
- #7 = Class #31 // com/example/demo/ForEachTest
- #8 = Class #32 // java/lang/Object
- #9 = Utf8 list
- #10 = Utf8 Ljava/util/List;
- #11 = Utf8 Signature
- #12 = Utf8 Ljava/util/List<Ljava/lang/Integer;>;
- #13 = Utf8 <init>
- #14 = Utf8 ()V
- #15 = Utf8 Code
- #16 = Utf8 LineNumberTable
- #17 = Utf8 main
- #18 = Utf8 ([Ljava/lang/String;)V
- #19 = Utf8 StackMapTable
- #20 = Class #33 // java/util/Iterator
- #21 = Utf8 SourceFile
- #22 = Utf8 ForEachTest.java
- #23 = NameAndType #13:#14 // "<init>":()V
- #24 = NameAndType #9:#10 // list:Ljava/util/List;
- #25 = Class #34 // java/util/List
- #26 = NameAndType #35:#36 // iterator:()Ljava/util/Iterator;
- #27 = Class #33 // java/util/Iterator
- #28 = NameAndType #37:#38 // hasNext:()Z
- #29 = NameAndType #39:#40 // next:()Ljava/lang/Object;
- #30 = Utf8 java/lang/Integer
- #31 = Utf8 com/example/demo/ForEachTest
- #32 = Utf8 java/lang/Object
- #33 = Utf8 java/util/Iterator
- #34 = Utf8 java/util/List
- #35 = Utf8 iterator
- #36 = Utf8 ()Ljava/util/Iterator;
- #37 = Utf8 hasNext
- #38 = Utf8 ()Z
- #39 = Utf8 next
- #40 = Utf8 ()Ljava/lang/Object;
- {
- java.util.List<java.lang.Integer> list;
- descriptor: Ljava/util/List;
- flags:
- Signature: #12 // Ljava/util/List<Ljava/lang/Integer;>;
-
- public com.example.demo.ForEachTest();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 13: 0
-
- public void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=4, args_size=2
- 0: aload_0
- 1: getfield #2 // Field list:Ljava/util/List;
- 4: invokeinterface #3, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
- 9: astore_2
- 10: aload_2
- 11: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
- 16: ifeq 32
- 19: aload_2
- 20: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
- 25: checkcast #6 // class java/lang/Integer
- 28: astore_3
- 29: goto 10
- 32: return
- LineNumberTable:
- line 17: 0
- line 19: 29
- line 20: 32
- StackMapTable: number_of_entries = 2
- frame_type = 252 /* append */
- offset_delta = 10
- locals = [ class java/util/Iterator ]
- frame_type = 250 /* chop */
- offset_delta = 21
- }
- SourceFile: "ForEachTest.java"
反编译的内容很多,不一一解释,可以看到这个字节码的一般含义是使用 getfield 命令获取变量,并调用 List.iterator 获取迭代器实例再调用 iterator.hasNext,如果返回 true,则调用 iterator.next 方法,这是迭代器遍历集合的实现逻辑。
写到这里有小伙伴就要问了,那以后遇到 List 集合我就用 for loop 了,不用 foreach了,毕竟前者的效率更好。那么接下来我们再看一个 case,这里我们把 ArrayList 换成 LinkedList,代码如下:
- public static List<Integer> getList(int size) {
- List<Integer> list = new LinkedList<>();
- for (int i = 0; i < size; i++) {
- list.add(i);
- }
- return list;
- }
| size= | 1000 | 10000 | 100000 |
|---|---|---|---|
| for loop | 27 | 129 | 7654 |
| For each | 2 | 2 | 15 |
从上面的数据可以很明显的看到,当在处理 LinkedList 的时候,for loop 明显就慢很多了。相信具体的原因大家也知道,ArrayList 底层是基于数组结构的,所以使用 for loop 操作起来会很快,时间复杂度是 O(1),但是 LinkedList 底层是链表结构,此时如果在想通过索引来操作数据,时间复杂度将是 O (n*n)。
所以具体使用哪种循环方式以及具体需要使用哪种数据结构,都需要根据实际的业务情况来选择,任何一种方案的存在都是合理的,你小伙你们认为呢?欢迎在评论区留言讨论。