Foreach(增强for)

来自Wikioe
跳到导航 跳到搜索


关于

For-Each 循环也叫增强型的 for 循环,或者叫 foreach 循环,是 JDK5.0 的新特性(其他新特性比如泛型、自动装箱等),For-Each 循环的加入简化了集合的遍历。

这种方式并不是新的语法,只是语法糖。即编写的 foreach 循环的代码并不是直接转成字节码,而是由编译器先转成对应的语法,然后再转成字节码,可以理解成是编译器对一些语法的封装提供的另一种方便阅读编写功能代码的实现方式。

语法

for(type element: array)
{
   System.out.println(element);
}

使用

  1. 遍历数组:
    public class ForeachTest
    {
        public static void main(String[] args)
        {
            int[] arr = {1, 2, 3, 4, 5};
            int[][] arr2 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
            
            System.out.println("---------- for ------------");
            for(int i=0; i<arr.length; i++)
            {
                System.out.println(arr[i]);
            }
            
            System.out.println("--------- foreach -------------");
            for(int element:arr)
            {
                System.out.println(element);
            }
            
            System.out.println("--------- foreach 遍历二维数组 -------------");
            for(int[] row : arr2)
            {
                for(int element : row)
                {
                    System.out.println(element);
                }
            }
        }
    }
    
  2. 遍历集合(三种方式):
    public class ForeachTest
    {
        public static void main(String[] args)
        { 
            List<String> list = new ArrayList<String>();
            list.add("a");
            list.add("b");
            list.add("c");
            
            System.out.println("---------- for -----------");
            for(int i = 0; i < list.size(); i++)
            {
                System.out.println(list.get(i));
                
            }
    		
            System.out.println("---------- foreach(实现了Iterable)-----------");
            for(String str: list)
            {
                System.out.println(str);
            }
            
            System.out.println("---------- Iterator -----------");
            for(Iterator<String> iter = list.iterator(); iter.hasNext();)
            {
                System.out.println(iter.next());
            }
        }
    }
    

原理

foreach 可以与任何实现了 Iterable 接口的对象一起工作。

(Iterable 接口提供了一个 Iterator 迭代器)

Java 中提供的 foreach 语法糖其底层实现方式主要有两种:

  1. 迭代器遍历模式:对于集合类(或实现迭代器的集合),使用“迭代器”的遍历方式;
    以下两组代码等效:
    import java.util.ArrayList;
    import java.util.List;
    
    public class TestForEach
    {
        public static void main(String args[])
        {
            List<Integer> nums = new ArrayList<>();
            nums.add(11);
            nums.add(22);
            nums.add(33);
    
            for (Integer num : nums)
            {
                System.out.println(num);
            }
        }
    }
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Iterator;
    
    public class TestForEach
    {
        public static void main(String args[])
        {
            List<Integer> nums = new ArrayList<>();
            nums.add(11);
            nums.add(22);
            nums.add(33);
    
            // 此处没有使用泛型,因为泛型在 java 中也是一种语法糖,只是编译器提供的一种检查,在运行期会擦除类型信息,其并不像C++那样在语法层面真正的支持泛型
            // 当然,为了良好的编码习惯,在平时的编码中应该使用泛型,即 Iterator<Integer> iter = nums.iterator();
            Iterator iter = nums.iterator();
            while (iter.hasNext()) 
            {
                Integer num = (Integer)iter.next();
                System.out.println(num);    
            }
        }
    }
    
  2. 数组依次遍历模式:对于数组(没有实现 Iterator 接口)使用最基本的依次遍历数组中每个元素的方式,来实现遍历。
    以下两组代码等效:
    public class TestForEach
    {
        public static void main(String args[])
        {
            int[] nums = {11, 22, 33};
            for (int num : nums)
            {
                System.out.println(num);
            }
        }
    }
    
    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 作为语法糖,在不同的场景实现方式不同,所以其性能分别近似于 for(对于数组) 和 Iterator(对于集合)。

由下代码可以看出:

  1. 对于集合(ArrayList、LinkList),性能表现:Iterator >= foreach > for;
    • 不要在 LinkList 中使用 for,效率太低;
  2. 对于数组,性能表现:foreach >= for;
    • (数组不支持 Iterator)
package com.eijux;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;

public class TestFor {
    ArrayList<Integer> arrayList = new ArrayList<>();
    LinkedList<Integer> linkedList = new LinkedList<>();

    String[] strings = new String[100000000];

    public TestFor() {
        for (int i = 0; i < 100000; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        for (int i = 0; i < 100000; i++) {
            strings[i] = "eijux" + i;
        }
    }

    public void testArrayList() {
        int number;
        long start, end, result;

        System.out.println("========== ArrayList ==========");

        start = System.currentTimeMillis();
        for (int i = 0; i < arrayList.size(); i++) {
            number = arrayList.get(i);
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("ArrayList for循环消耗时间:" + result);


        start = System.currentTimeMillis();
        for (int i : arrayList) {
            number = i;
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("ArrayList foreach循环消耗时间:" + result);

        start = System.currentTimeMillis();
        Iterator<Integer> itr = arrayList.iterator();
        while (itr.hasNext()) {
            number = itr.next();
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("ArrayList Iterator循环消耗时间:" + result);
    }

    public void testLinkList() {
        int number;
        long start, end, result;

        System.out.println("========== LinkList ==========");

        start = System.currentTimeMillis();
        for (int i = 0; i < linkedList.size(); i++) {
            number = linkedList.get(i);
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("LinkList for循环消耗时间:" + result);

        start = System.currentTimeMillis();
        for (int i : linkedList) {
            number = i;
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("LinkList foreach循环消耗时间:" + result);

        start = System.currentTimeMillis();
        Iterator<Integer> itr = linkedList.iterator();
        while (itr.hasNext()) {
            number = itr.next();
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("LinkList Iterator循环消耗时间:" + result);
    }

    public void testArray() {
        String string;
        long start, end, result;

        System.out.println("========== Array ==========");

        start = System.currentTimeMillis();
        for (int i = 0; i < strings.length; i++) {
            string = strings[i];
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("String[] for循环消耗时间:" + result);

        start = System.currentTimeMillis();
        for (String str : strings) {
            string = str;
        }
        end = System.currentTimeMillis();
        result = end - start;
        System.out.println("String[] foreach循环消耗时间:" + result);
    }

    public static void main(String[] args) {
        (new TestFor()).testArrayList();
        (new TestFor()).testLinkList();
        (new TestFor()).testArray();
    }
}

结果:

========== ArrayList ==========
ArrayList for循环消耗时间42
ArrayList foreach循环消耗时间9
ArrayList Iterator循环消耗时间7
========== LinkList ==========
LinkList for循环消耗时间22322
LinkList foreach循环消耗时间8
LinkList Iterator循环消耗时间5
========== Array ==========
String[] for循环消耗时间21
String[] foreach循环消耗时间11

为什么不能在 foreach 中增加、删除集合元素?

使用 foreach 进行集合遍历时需要额外注意不能对集合长度进行修改,也就是不能对集合进行增删操作,否则会抛出 ConcurrentModificationException 异常。

因为:
1、在 Iterator 可以利用自身(迭代器的)方法修改集合;
2、在 Iterator 不能使用集合的方法修改集合,否则会导致下一次循环时“集合的 modCount != 迭代器的 expectedModCount”,从而出现上述异常;

但是,在 foreach 中不能调用 Iterator 的方法操作集合,所以“不能在 foreach 中增加、删除集合元素”(可以使用 Iterator 或 for)。
  1. 错误方式:
    import java.util.ArrayList;
    import java.util.List;
    
    public class TestForEach
    {
        public static void main(String args[])
        {
            List<Integer> 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);
                }
            }
        }
    }
    
  2. 正确方式:【使用 Iterator】
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Iterator;
    
    public class TestForEach
    {
        public static void main(String args[])
        {
            List<Integer> nums = new ArrayList<>();
            nums.add(11);
            nums.add(22);
            nums.add(33);
    
            Iterator<Integer> 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);   
                } 
            }       
        }
    }
    

集合的“modCount”迭代器的“expectedModCount”

Iterator 的每次迭代(以及每个方法)执行前,都会判断“modCount”与“expectedModCount”是否相等,然后才进行下一步操作,否则抛出“ConcurrentModificationException”异常。

域含义:

  1. modCount:用于记录“修改集合”的次数。
    • 定义于抽象类“AbstractList<E>”(ArrayList、LinkedList 等直接或间接继承于该抽象类);
    • “修改集合”包括:改变集合大小(“增加”、“删除”);改变集合内容(“修改”:“clear”、“replace”、“sort”,但不包括“set”);
      比如:
      • ArrayList :add、remove、trimToSize、ensureCapacity,与 clear、replace、sort 等,以及相关方法。【但不包括“set”的相关方法】
      • LinkedList:link、unlink、add、remove、poll、offer、push、pop,与 clear 等,以及相关方法。【但不包括“set”的相关方法】
  2. expectedModCount:用于记录 Iterator 初始化时,集合的 modCount 值。
    • 定义于集合类的迭代器(内部类“private class ListItr implements ListIterator<E>”);


修改集合时:

  1. 通过“集合方法”修改:modCount 增加,expectedModCount 不变;
  2. 通过“Iterator 方法”修改:modCount 增加,expectedModCount 增加(或与 modCount 同步);
判断 modCount 与 expectedModCount 相等的意义?    fail-fast策略

java.util.ArrayList 不是线程安全的,在迭代过程中,通过判断 modCount 跟 expectedModCount 是否相等,就可以获知在使用迭代器的过程中是否有其他线程修改了 list。

所以,当需要遍历那些非线程安全的数据结构时,尽量使用迭代器。(或 foreach)

ArrayList 内部迭代器源码

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        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;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> 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();
        }
    }

关于“foreach 不能修改集合元素本身,但是可以更改集合元素的属性”???

网上有如上说法:“如果是对象集合,不能修改集合元素本身,但是可以更改集合元素的属性”

比如:
    有集合 ArrayList<User> users = new ArrayList<>();
    
    // 非法:
    for (User user : users) {
         user = new User("Eijux", 26, "成都");
         ...
    }
    
    // 合法:
    for (User user : users) {
         user.setAddress("成都");
         ...
    }

这一现象其实是由于“Java 值传递”[1]导致,此处:

  1. 实参(为:集合元素),形参(为:user);
  2. 因为“Java值传递”,“形参”与“实参”指向内存中的同一对象地址;
    即:“形参”的值(内存地址)与“实参”(集合元素、实际对象)一致。
  3. 所以“user.setAddress()”修改“形参”的属性,也就是修改了“实参”的属性;
    该方法并不会导致“modCount != expectedModCount”。
以上,实现了:“更改集合元素的属性”,但仍然并未“修改集合元素本身(将引用指向其他对象)”。

因为,在 foreach 中并没有修改实参(集合元素),而是修改了形参(user)。

简而言之:效果是这个效果,但说法似乎并不准确???

元素属性被修改,是由于“Java值传递”导致,和 foreach 并无相关。


P.S.:
1、基础类型集合:元素是基础类型;——(修改元素,即修改基础类型的数据)
2、对象集合:元素是引用类型;——(修改元素,即修改引用类型的数据,指向新的对象)

显然,无论何种集合,foreach 中修改元素本身都是不可能的。(“modCount != expectedModCount”)