栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

java中foreach实现原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

java中foreach实现原理

概述

循环作为程序中经常使用的语句,在java5之后推出了新的for/in(foreach)循环方式以方便程序员编写(阅读)代码。这种方式并不是新的语法,只是语法糖。即编写的foreach循环的代码并不是直接转成字节码,而是由编译器先转成对应的语法,然后再转成字节码,可以理解成是编译器对一些语法的封装提供的另一种方便阅读编写功能代码的实现方式。java中提供的foreach语法糖其底层实现方式主要有两种:对于集合类或实现迭代器的集合使用迭代器的遍历方式,对于数组集合使用数组的遍历方法。

迭代器遍历模式

对于实现Iterator接口的集合,使用foreach实现循环功能的代码会被编译器转换成使用迭代器遍历集合的代码,然后再转成字节码。例如以下的程序,使用foreach循环遍历ArrayList集合,使用javac TestForEach.java生成字节码后,再使用javap -verbose TestForEach进行反编译,从反编译的结果来看,可以看出其底层是用迭代器模式进行遍历的。

import java.util.ArrayList;
import java.util.List;

public class TestForEach
{
    public static void main(String args[])
    {
 List nums = new ArrayList<>();
 nums.add(11);
 nums.add(22);
 nums.add(33);

 for (Integer num : nums)
 {
     System.out.println(num);
 }
    }
}

反编译结果如下,从中可以看出,在106118这十几行中是对集合进行遍历输出,在106行先使用`List.iterator()`接口生成迭代器,然后在109118中不断使用Iterator.hasNext()判断是否有下个元素,有则使用Iterator.next()接口获取下个元素并进行输出。

Classfile /C:/Users/zhchun/Desktop/TestForEach.class
  Last modified 2018-7-22; size 842 bytes
  MD5 checksum 45751115d8755b894835c52451125338
  Compiled from "TestForEach.java"
public class TestForEach
  SourceFile: "TestForEach.java"
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref   #13.#25 //  java/lang/Object."":()V
   #2 = Class#26     //  java/util/ArrayList
   #3 = Methodref   #2.#25  //  java/util/ArrayList."":()V
   #4 = Methodref   #9.#27  //  java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #5 = InterfaceMethodref #28.#29 //  java/util/List.add:(Ljava/lang/Object;)Z
   #6 = InterfaceMethodref #28.#30 //  java/util/List.iterator:()Ljava/util/Iterator;
   #7 = InterfaceMethodref #31.#32 //  java/util/Iterator.hasNext:()Z
   #8 = InterfaceMethodref #31.#33 //  java/util/Iterator.next:()Ljava/lang/Object;
   #9 = Class#34     //  java/lang/Integer
  #10 = Fieldref    #35.#36 //  java/lang/System.out:Ljava/io/PrintStream;
  #11 = Methodref   #37.#38 //  java/io/PrintStream.println:(Ljava/lang/Object;)V
  #12 = Class#39     //  TestForEach
  #13 = Class#40     //  java/lang/Object
  #14 = Utf8 
  #15 = Utf8 ()V
  #16 = Utf8 Code
  #17 = Utf8 LineNumberTable
  #18 = Utf8 main
  #19 = Utf8 ([Ljava/lang/String;)V
  #20 = Utf8 StackMapTable
  #21 = Class#41     //  java/util/List
  #22 = Class#42     //  java/util/Iterator
  #23 = Utf8 SourceFile
  #24 = Utf8 TestForEach.java
  #25 = NameAndType #14:#15 //  "":()V
  #26 = Utf8 java/util/ArrayList
  #27 = NameAndType #43:#44 //  valueOf:(I)Ljava/lang/Integer;
  #28 = Class#41     //  java/util/List
  #29 = NameAndType #45:#46 //  add:(Ljava/lang/Object;)Z
  #30 = NameAndType #47:#48 //  iterator:()Ljava/util/Iterator;
  #31 = Class#42     //  java/util/Iterator
  #32 = NameAndType #49:#50 //  hasNext:()Z
  #33 = NameAndType #51:#52 //  next:()Ljava/lang/Object;
  #34 = Utf8 java/lang/Integer
  #35 = Class#53     //  java/lang/System
  #36 = NameAndType #54:#55 //  out:Ljava/io/PrintStream;
  #37 = Class#56     //  java/io/PrintStream
  #38 = NameAndType #57:#58 //  println:(Ljava/lang/Object;)V
  #39 = Utf8 TestForEach
  #40 = Utf8 java/lang/Object
  #41 = Utf8 java/util/List
  #42 = Utf8 java/util/Iterator
  #43 = Utf8 valueOf
  #44 = Utf8 (I)Ljava/lang/Integer;
  #45 = Utf8 add
  #46 = Utf8 (Ljava/lang/Object;)Z
  #47 = Utf8 iterator
  #48 = Utf8 ()Ljava/util/Iterator;
  #49 = Utf8 hasNext
  #50 = Utf8 ()Z
  #51 = Utf8 next
  #52 = Utf8 ()Ljava/lang/Object;
  #53 = Utf8 java/lang/System
  #54 = Utf8 out
  #55 = Utf8 Ljava/io/PrintStream;
  #56 = Utf8 java/io/PrintStream
  #57 = Utf8 println
  #58 = Utf8 (Ljava/lang/Object;)V
{
  public TestForEach();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
  0: aload_0
  1: invokespecial #1    // Method java/lang/Object."":()V
  4: return 
      LineNumberTable:
 line 4: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
  0: new    #2    // class java/util/ArrayList
  3: dup    
  4: invokespecial #3    // Method java/util/ArrayList."":()V
  7: astore_1      
  8: aload_1
  9: bipush 11
 11: invokestatic  #4    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 14: invokeinterface #5,  2     // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
 19: pop    
 20: aload_1
 21: bipush 22
 23: invokestatic  #4    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 26: invokeinterface #5,  2     // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
 31: pop    
 32: aload_1
 33: bipush 33
 35: invokestatic  #4    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 38: invokeinterface #5,  2     // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
 43: pop    
 44: aload_1
 45: invokeinterface #6,  1     // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
 50: astore_2      
 51: aload_2
 52: invokeinterface #7,  1     // InterfaceMethod java/util/Iterator.hasNext:()Z
 57: ifeq   80
 60: aload_2
 61: invokeinterface #8,  1     // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
 66: checkcast     #9    // class java/lang/Integer
 69: astore_3      
 70: getstatic     #10   // Field java/lang/System.out:Ljava/io/PrintStream;
 73: aload_3
 74: invokevirtual #11   // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
 77: goto   51
 80: return 
      LineNumberTable:
 line 8: 0
 line 9: 8
 line 10: 20
 line 11: 32
 line 13: 44
 line 15: 70
 line 16: 77
 line 17: 80
      StackMapTable: number_of_entries = 2
    frame_type = 253 
   offset_delta = 51
   locals = [ class java/util/List, class java/util/Iterator ]
      frame_type = 250 
     offset_delta = 28

  }

因此,上面使用foreach方式遍历集合的程序与下面使用迭代器模式进行遍历的程序是一样的

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

public class TestForEach
{
    public static void main(String args[])
    {
 List nums = new ArrayList<>();
 nums.add(11);
 nums.add(22);
 nums.add(33);

 // 此处没有使用泛型,因为泛型在java中也是一种语法糖,只是编译器提供的一种检查,在运行期会擦除类型信息,其并不像C++那样在语法层面真正的支持泛型
 // 当然,为了良好的编码习惯,在平时的编码中应该使用泛型,即Iterator iter = nums.iterator();
 Iterator iter = nums.iterator();
 while (iter.hasNext()) 
 {
     Integer num = (Integer)iter.next();
     System.out.println(num);    
 }
    }
}
数组依次遍历模式

数组没有实现Iterator接口,但是又要支持foreach语法糖,所以就用了最原始的最基本的依次遍历数组中的每个元素的方式来实现。如下代码是数组用foreach方式实现的遍历。

public class TestForEach
{
    public static void main(String args[])
    {
 int[] nums = {11, 22, 33};
 for (int num : nums)
 {
     System.out.println(num);
 }
    }
}

同样使用javac TestForEach.java生成字节码后,再使用javap -verbose TestForEach进行反编译,输出结果如下。从中可以看出,从80~92这十几行是对数组进行遍历输出,这个过程没有使用迭代器,只是不断的对数进行出栈、比较、入栈、输出结果的操作。

Classfile /C:/Users/zhchun/Desktop/TestForEach.class
  Last modified 2018-7-22; size 528 bytes
  MD5 checksum 874d6164dd77ec1874a96f4adb7d884b
  Compiled from "TestForEach.java"
public class TestForEach
  SourceFile: "TestForEach.java"
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref   #5.#17  //  java/lang/Object."":()V
   #2 = Fieldref    #18.#19 //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref   #20.#21 //  java/io/PrintStream.println:(I)V
   #4 = Class#22     //  TestForEach
   #5 = Class#23     //  java/lang/Object
   #6 = Utf8 
   #7 = Utf8 ()V
   #8 = Utf8 Code
   #9 = Utf8 LineNumberTable
  #10 = Utf8 main
  #11 = Utf8 ([Ljava/lang/String;)V
  #12 = Utf8 StackMapTable
  #13 = Class#24     //  "[Ljava/lang/String;"
  #14 = Class#25     //  "[I"
  #15 = Utf8 SourceFile
  #16 = Utf8 TestForEach.java
  #17 = NameAndType #6:#7   //  "":()V
  #18 = Class#26     //  java/lang/System
  #19 = NameAndType #27:#28 //  out:Ljava/io/PrintStream;
  #20 = Class#29     //  java/io/PrintStream
  #21 = NameAndType #30:#31 //  println:(I)V
  #22 = Utf8 TestForEach
  #23 = Utf8 java/lang/Object
  #24 = Utf8 [Ljava/lang/String;
  #25 = Utf8 [I
  #26 = Utf8 java/lang/System
  #27 = Utf8 out
  #28 = Utf8 Ljava/io/PrintStream;
  #29 = Utf8 java/io/PrintStream
  #30 = Utf8 println
  #31 = Utf8 (I)V
{
  public TestForEach();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
  0: aload_0
  1: invokespecial #1    // Method java/lang/Object."":()V
  4: return 
      LineNumberTable:
 line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=6, args_size=1
  0: iconst_3      
  1: newarrayint
  3: dup    
  4: iconst_0      
  5: bipush 11
  7: iastore
  8: dup    
  9: iconst_1      
 10: bipush 22
 12: iastore
 13: dup    
 14: iconst_2      
 15: bipush 33
 17: iastore
 18: astore_1      
 19: aload_1
 20: astore_2      
 21: aload_2
 22: arraylength   
 23: istore_3      
 24: iconst_0      
 25: istore 4
 27: iload  4
 29: iload_3
 30: if_icmpge     53
 33: aload_2
 34: iload  4
 36: iaload 
 37: istore 5
 39: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
 42: iload  5
 44: invokevirtual #3    // Method java/io/PrintStream.println:(I)V
 47: iinc   4, 1
 50: goto   27
 53: return 
      LineNumberTable:
 line 5: 0
 line 6: 19
 line 8: 39
 line 6: 47
 line 10: 53
      StackMapTable: number_of_entries = 2
    frame_type = 255 
   offset_delta = 27
   locals = [ class "[Ljava/lang/String;", class "[I", class "[I", int, int ]
   stack = []
    frame_type = 248 
   offset_delta = 25

}

对于用foreach方式实现的数组遍历方式,与下面的依次遍历数组中每个元素的方式是一样的

public class TestForEach
{
    public static void main(String args[])
    {
 int[] nums = {11, 22, 33};
 for (int i = 0; i < nums.length; i++)
 {
     int num = nums[i];
     System.out.println(num);
 }
    }
}
其他

虽然foreach方便了程序的编写和阅读,是遍历集合和数组的一种好方式,但是使用foreach进行集合遍历时需要额外注意不能对集合长度进行修改,也就是不能对集合进行增删操作,否则会抛出ConcurrentModificationException异常。例如,下面程序会在执行第13行时抛出ConcurrentModificationException异常

import java.util.ArrayList;
import java.util.List;

public class TestForEach
{
    public static void main(String args[])
    {
 List nums = new ArrayList<>();
 nums.add(11);
 nums.add(22);
 nums.add(33);

 for (Integer num : nums)
 {
     if (num == 11)
     {
  // 此处使用集合中的remove操作,而不是迭代器中的remove操作,会导致迭代器中的expectedModCount和集合中的modCount变量不相等,从而导致在执行next()函数时抛出异常
  nums.remove((Integer)num);
     }
     else
     {
  System.out.println(num);
     }
 }
    }
}

虽然ArrayList的foreach底层用迭代器实现,迭代器也支持在遍历集合的过程中进行删除元素的操作,但是删除的函数必须是迭代器的函数,而不是集合自有的函数。至于上述代码为什么会抛出ConcurrentModificationException异常,可以从ArrayList中的迭代器类找到答案。ArrayList中部分源码如下所示

// ArrayList中删除函数源码
public boolean remove(Object o) {
    if (o == null) {
 for (int index = 0; index < size; index++)
     if (elementData[index] == null) {
  fastRemove(index);
  return true;
     }
    } else {
 for (int index = 0; index < size; index++)
     if (o.equals(elementData[index])) {
  fastRemove(index);
  return true;
     }
    }
    return false;
}

// ArrayList中删除函数源码
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
 System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

// ArrayList中迭代器函数源码
public Iterator iterator() {
    return new Itr();
}

// ArrayList中迭代器类源码
private class Itr implements Iterator {
    int cursor;// index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
 return cursor != size;
    }

    @SuppressWarnings("unchecked")
    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];
    }

    public void remove() {
 if (lastRet < 0)
     throw new IllegalStateException();
 checkForComodification();

 try {
     ArrayList.this.remove(lastRet);
     // 此处重新赋值,避免跳过下一个元素
     cursor = lastRet;
     lastRet = -1;
     // 此处重新赋值,避免下次调用next()函数时校验不通过抛出异常
     expectedModCount = modCount;
 } catch (IndexOutOfBoundsException ex) {
     throw new ConcurrentModificationException();
 }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer consumer) {
 Objects.requireNonNull(consumer);
 final int size = ArrayList.this.size;
 int i = cursor;
 if (i >= size) {
     return;
 }
 final Object[] elementData = ArrayList.this.elementData;
 if (i >= elementData.length) {
     throw new ConcurrentModificationException();
 }
 while (i != size && modCount == expectedModCount) {
     consumer.accept((E) elementData[i++]);
 }
 // update once at end of iteration to reduce heap write traffic
 cursor = i;
 lastRet = i - 1;
 checkForComodification();
    }

    final void checkForComodification() {
 if (modCount != expectedModCount)
     throw new ConcurrentModificationException();
    }
}

当执行for (Integer num : nums)语句时,会先调用ArrayList中的iterator()接口生成迭代器,而在初始化Itr类时会先将ArrayList对象中的modCount变量赋给Itr对象中的expectedModCount变量,在调用迭代器的next函数时会先调用checkForComodification函数进行校验,如果expectedModCount和modCount不相等则会抛出ConcurrentModificationException异常。在正常的集合遍历中,一般情况下,我们只使用迭代器中hasNext和next函数,并不会改变expectedModCount或者modCount的值,所以不会有问题,但是如果在遍历中调用了集合中自有的删除函数操作,则会改变modCount的值,从而导致expectedModCount与modCount不相等,进而在调用迭代器的next函数时进行校验不通过产生ConcurrentModificationException异常。而在遍历中调用迭代器的删除函数操作,由于其内部会在删除元素后对expectedModCount重新赋值,使其与modCount值相等,所以在遍历集合的过程中使用迭代器的删除函数操作不会有问题。

正确的在遍历集合过程中进行删除操作的方式如下

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

public class TestForEach
{
    public static void main(String args[])
    {
 List nums = new ArrayList<>();
 nums.add(11);
 nums.add(22);
 nums.add(33);

 Iterator iter = nums.iterator();
 while (iter.hasNext()) 
 {
     Integer num = iter.next();
     if (num == 11)
     {
  // 在迭代器遍历中,不能使用集合自有的删除操作,只能使用迭代器中的删除操作,否则会导致迭代器中的expectedModCount和集合中的modCount变量不相等,从而导致在执行next()函数时抛出异常
  //nums.remove((Integer)num);
  iter.remove();
     }
     else
     {
  System.out.println(num);   
     } 
 }
 
    }
}
参考资料

[1] 朱小厮. Java语法糖之foreach[J/OL]. https://blog.csdn.net/u013256816/article/details/50736498 , 2016-02-25

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号