Java基础进阶系列-05之从for-each循环深入解读ArrayList与LinkedList不同遍历方式带来的影响,深度好文!

关于for-each循环

for-each循环(官方称之为“增强for语句”),通过完全隐藏迭代器或者索引变量,来规避混乱和出错的可能(例如在用普通for循环遍历集合或者数组时)。

for-each循环不仅是for循环的语法糖,还是iterator的语法糖。

参考Java8 Language Specification 14.14.2 增强for语句章节

Expression的类型必须是Iterable或数组类型,否则会产生编译时错误。

  • 如果Expression的类型是Iterable的子类型,那么就按如下方式转译
ArrayList<Integer> integers = new ArrayList();
  Iterator var2 = integers.iterator();
  
  while(var2.hasNext()) {
     Integer integer = (Integer)var2.next();
     System.out.println(integer);
   }
  • 否则,Expression必须具有数组类型。

增强for语句等价于下面形式的基本for语句:

int[] a = new int[]{1, 2, 4};
int[] var2 = a;
int var3 = a.length;

for(int var4 = 0; var4 < var3; ++var4) {
   int i = var2[var4];
   System.out.println(i);
 }

一、深入思考

(一)将局部变量的作用域最小化

参考《Effective Java》的第九章-通用编程,第57条-“将局部变量的作用域最小化”

将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

  • 1、要使局部变量的作用域最小化 ,最有力的方法就是在第一次要使用它的地方进行声明

  • 2、循环中提供了特殊的机会来将变量的作用域最小化,因此,如果在循环终止之后不再需要循环变量的内容, for循环就优先于 while 循环

    for (int i = 0, n = expensiveComputation(); i < n; i++) {
        // DO something with i;
        ... 
    }
    

(二)for-each循环Collection对象的陷阱

场景引入

假如List集合中有10个元素,我现在想从中删除3个元素,请问怎么做?

说明:下面Lists为com.google.common.collect.Lists

遍历Collection对象时尝试删除元素,出现ConcurrentModificationException异常

List<String> courseList = Lists.newArrayList("Chinese", "English", "Math", "PE");
for (String course : courseList) {
    if("PE".equalsIgnoreCase(course)) {
        courseList.remove(course);
    }
}

解决方案一:遍历时将待删除元素存放到临时集合,再调用removeAll来实现

List<String> testList = Lists.newArrayList("1杨", "1摩", "2卡", "1M","2o","1C","2H");

System.out.println("testList 删除前 = " + testList);
// 临时集合,用于存放待删除元素
List<String> tempList = new ArrayList<>();
for (String element : testList) {
    if (element.startsWith("1")) {
        tempList.add(element);
    }
}
testList.removeAll(tempList);
System.out.println("testList 删除后 = " + testList);

解决方案二:通过迭代器遍历时,进行删除操作

List<String> testList = Lists.newArrayList("1杨", "1摩", "2卡", "1M","2o","1C","2H");
Iterator<String> iterator = testList.iterator();
while (iterator.hasNext()) {
    if (iterator.next().startsWith("1")) {
        iterator.remove();
    }
}

解决方案三:Java8 Collection提供的removeIf

  • 1、入参不能为null,否则会抛出NullPointerException异常

  • 2、如果集合本身是不可修改的,那么执行removeIf方法会抛出UnsupportedOperationException异常

// java.util.Collection#removeIf
/**
 * Removes all of the elements of this collection that satisfy the given
 * predicate.
 *
 * @param filter a predicate which returns {@code true} for elements to be
 *        removed
 * @return {@code true} if any elements were removed
 * @throws NullPointerException if the specified filter is null
 * @throws UnsupportedOperationException if elements cannot be removed
 *         from this collection.  Implementations may throw this exception if a
 *         matching element cannot be removed or if, in general, removal is not
 *         supported.
 * @since 1.8
 */
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}
List<String> testList = Lists.newArrayList("1杨", "1摩", "2卡", "1M","2o","1C","2H");

System.out.println("testList 删除前 = " + testList);
testList.removeIf(element -> element.startsWith("1"));
System.out.println("testList 删除后 = " + testList);

(三)嵌套循环中使用迭代器的陷阱

public static void main(String[] args) {
    List<String> courseList = Lists.newArrayList("Chinese", "English", "Math", "PE");
    List<Integer> scoreList = Lists.newArrayList(1, 2, 3);
    List<Student> studentList = Lists.newArrayList();

    // 这是因为第二层循环内部对于第一个迭代器调用过多的next方法,而这本应该放到第二层循环外边进行的
    // for (Iterator<String> i = courseList.iterator(); i.hasNext(); ) {
    //     for (Iterator<Integer> j = scoreList.iterator(); j.hasNext(); ) {
    //         studentList.add(new Student(i.next(), j.next()));
    //     }
    // }

    // 修复上面bug的一种解决方案
    // for (Iterator<String> i = courseList.iterator(); i.hasNext(); ) {
    //     String course = i.next();
    //     for (Iterator<Integer> j = scoreList.iterator(); j.hasNext(); ) {
    //         studentList.add(new Student(course, j.next()));
    //     }
    // }

    // for-each专门为嵌套循环遍历定做
    for (String course : courseList) {
        for (Integer score : scoreList) {
            studentList.add(new Student(course, score));
        }
    }

    System.out.println("学生得分情况 = " + studentList);
}

(四)Collection对象为何能与for-each循环结合使用?

之所以Collection对象能够与for-each结合使用。

是因为Java5之后引入了java.lang.Iterable接口,该接口包含一个Iterator<T> iterator();方法,该方法返回的是实现了Iterator<T>的匿名内部类的实例,该匿名内部类可以遍历所有的元素。

/**
 * Implementing this interface allows an object to be the target of
 * the "for-each loop" statement.
 *
 * @param <T> the type of elements returned by the iterator
 * @jls 14.14.2 The enhanced for statement
 * @since 1.5
 */
public interface Iterable<T> {
  /**
   * Returns an iterator over elements of type {@code T}.
   *
   * @return an Iterator.
   */
   Iterator<T> iterator();
   
  /**
   * Performs the given action for each element of the {@code Iterable}
   * until all elements have been processed or the action throws an
   * exception.  Unless otherwise specified by the implementing class,
   * actions are performed in the order of iteration (if an iteration order
   * is specified).  Exceptions thrown by the action are relayed to the
   * caller.
   *
   * @param action The action to be performed for each element
   * @throws NullPointerException if the specified action is null
   * @implSpec <p>The default implementation behaves as if:
   * <pre>{@code
   *     for (T t : this)
   *         action.accept(t);
   * }</pre>
   * @since 1.8
   */
 default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
           for (T t : this) {
               action.accept(t);
           }
       }
   }
  • 1、实现了Iterable的实现类都可以作为for-each循环的目标对象

  • 2、因为List接口继承了Collection接口,因此也具备了Iterator接口的forEach默认方法实现,所以我们可以这样使用List的元素

// 参考自SonarQube源代码 org.sonar.server.authentication.HttpHeadersAuthentication
private static Map<String, String> getHeaders(HttpServletRequest request) {
  Map<String, String> headers = new HashMap<>(16);
 Collections.list(request.getHeaderNames()).forEach(header -> headers.put(header.toLowerCase(Locale.ENGLISH), request.getHeader(header)));
     return headers;
   }

迭代器为什么不是定义为一个类,而是一个接口?

  • 假设迭代器定义的是一个类,那么我们就可以创建对象,通过对象去调用方法来遍历集合。
    但是呢?
    我们来想一想:Java提供了那么多集合类,每一个集合类的数据结构都是不同的,即它们的存储方式和遍历方式是不同的,那么我们当定义迭代器去遍历时,就不好处理。
    所以才没有定义迭代器为类。

    • 但无论是哪种集合,都应该具备获取元素的操作,并且最好使用判断功能加以辅助。
      这样在获取元素之前,先判断,就不容易出错,也就是说集合中应该具备获取元素和判断的功能,而每一个集合的底层实现方式不太一样,所以我们将这两个功能提取出来,并不提供具体实现,将迭代器定义为接口。

    • 那么,真正的具体实现类在哪呢?
      在真正的具体子类中,以内部类的方式体现,具体要怎么做由子类决定。

// java.util.ArrayList#iterator
public Iterator<E> iterator() {
  return new Itr();
}
// java.util.ArrayList.Itr
private class Itr implements Iterator<E> {}
// java.util.ArrayList.ListItr
private class ListItr extends Itr implements ListIterator<E> {}
// java.util.LinkedList#listIterator
public ListIterator<E> listIterator(int index) {
  checkPositionIndex(index);
  return new ListItr(index);
}
// java.util.LinkedList.ListItr
private class ListItr implements ListIterator<E> {}

(五)以for循环与迭代器遍历ArrayList,LinkedList所带来的影响

/**
 * 使用普通for循环遍历
 *
 * @param list 待遍历的List集合
 * @param <T>  集合元素类型
 * @return 耗时(毫秒)
 */
public static <T> long traverseByLoop(List<T> list) {
    long startTime = System.currentTimeMillis();
    for (int i = 0, size = list.size(); i < size; i++) {
        list.get(i);
    }
    long endTime = System.currentTimeMillis();
    return endTime - startTime;
}

/**
 * 使用迭代器遍历List
 *
 * @param list 待遍历的List集合
 * @param <T>  集合元素类型
 * @return 耗时(毫秒)
 */
public static <T> long traverseByIterator(List<T> list) {
    Iterator<T> iterator = list.iterator();
    long startTime = System.currentTimeMillis();
    while (iterator.hasNext()) {
        iterator.next();
    }
    long endTime = System.currentTimeMillis();
    return endTime - startTime;
}

ArrayList遍历结果汇总

遍历方式数量级耗时(毫秒)
普通for循环3000
30000
300000
迭代器遍历3001
30001
300002

LinkedList遍历结果汇总

遍历方式数量级耗时(毫秒)
普通for循环3001
30009
30000489
迭代器遍历3000
30001
300002

由以上的结果汇总,我们可以得知:

  • ArrayList在普通for循环遍历场景下的耗时要比迭代器遍历场景要低,但差异不大
  • LinkedList在普通for循环遍历场景下的耗时随着数量级的提高,与迭代器遍历场景相比要高出许多倍

解读不同List在不同遍历方式下造成的差异

  • ArrayList底层数据结构:数组
// 不需要序列化的字段前添加关键字transient,序列化对象的时候,该字段就不会被序列化
  transient Object[] elementData;
  • LinkedList底层数据结构:双向链表
// java.util.LinkedList.Node
  private static class Node<E> {
     E item;
     Node<E> next;
      Node<E> prev;
   
      Node(Node<E> prev, E element, Node<E> next) {
         this.item = element;
          this.next = next;
          this.prev = prev;
      }
   }
RandomAccess接口里面的答案

List 实现所使用的标记接口,用来表明其支持快速(通常是固定时间)随机访问。

此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。

实际经验证明,如果是下列情况,则 List实现应该实现此接口,即对于典型的类实例而言。

/**
 * Marker interface used by <tt>List</tt> implementations to indicate that
 * they support fast (generally constant time) random access.  The primary
 * purpose of this interface is to allow generic algorithms to alter their
 * behavior to provide good performance when applied to either random or
 * sequential access lists.
 *
 * Generic list algorithms are encouraged to check whether the given list is  an <tt>instanceof</tt> this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable
 * performance.
 *
 * As a rule of thumb, a <tt>List</tt> implementation should implement this interface if, for typical instances of the class, this loop:
 * <pre>
 *     for (int i=0, n=list.size(); i &lt; n; i++)
 *         list.get(i);
 * </pre>
 * runs faster than this loop:
 * <pre>
 *     for (Iterator i=list.iterator(); i.hasNext(); )
 *         i.next();
 * </pre>
 *
 * @since 1.4
 */
public interface RandomAccess {
}

RandomAccess接口的体现

// java.util.Collections#binarySearch(java.util.List<? extends java.lang.Comparable<? super T>>, T)
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
	if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD) {
		return Collections.indexedBinarySearch(list, key);
	} else {
		return Collections.iteratorBinarySearch(list, key);
	}
}
// java.util.Collections#fill
public static <T> void fill(List<? super T> list, T obj) {
	int size = list.size();

	if (size < FILL_THRESHOLD || list instanceof RandomAccess) {
		for (int i=0; i<size; i++)
			list.set(i, obj);
	} else {
		ListIterator<? super T> itr = list.listIterator();
		for (int i=0; i<size; i++) {
			itr.next();
			itr.set(obj);
		}
	}
}

我们可以从Collections工具类里面的方法中得知,在二分搜索法等中,会对List的子类型进行判断,查看是否其是否支持快速的随机访问。然后根据不同的情况选择不同的策略进行。

对于RandomAccess接口实现类来说,通常采用普通for循环进行,否则采用迭代器的形式。

不论是从RandomAccess接口文档描述还是Collections工具方法,都得以验证,实现了RandomAccess接口的ArrayList,应该用普通for循环来遍历。

从ArrayList和LinkedList的get()方法中找到答案
  • ArrayList

    // java.util.ArrayList#get
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    // java.util.ArrayList#elementData
    E elementData(int index) {
        return (E) elementData[index];
    }
    

    ArrayList的get(int index)方法中,可以知道,如果处于普通for循环时,则直接通过索引下标就可以直接取得元素。

    // 下一个待返回元素的索引下标
    int cursor;
    // 上一个返回元素的索引下标,-1表示之前没有元素返回
    int lastRet = -1;
    
    // java.util.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];
    }
    

    虽然在增强for的转译下,变为迭代器进行元素遍历。

但是由于迭代器的next()方法,仅仅只是对cursorlastRet字段的值进行修改,并没有在行为上作出过多耗时的操作。

因此,对于ArrayList来说,不论是普通for循环还是for-each循环场景,性能影响都不会有太大影响,理论上,更应该用普通for循环,在开发中,选用何种遍历方式取决于个人开发习惯

  • LinkedList

    // java.util.LinkedList#get
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    // java.util.LinkedList#node
    Node<E> node(int index) {
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    
    • (size >> 1)的意思是size右移1位,可以理解为(size / 2)

      • index < (size >> 1)时,也就是说要查找的元素位置在整个链表的前半部分,此时从链表头开始往后逐个进行查找

      • index < (size >> 1)不成立时,也即要查找的元素位置在整个链表的后半部分,此时从链表尾开始往前逐个进行查找

      • 这么做的目的是为了快速查找到目标元素

    • 尽管在一开始就根据链表的长度截半进行查找,但一旦链表的元素过多时,毕竟链表的查找总是逐个进行的,所以仍然会出现查找速度过慢的问题。

// java.util.AbstractSequentialList#iterator
 public Iterator<E> iterator() {
     return listIterator();
 }
 // java.util.LinkedList#listIterator
 public ListIterator<E> listIterator(int index) {
     checkPositionIndex(index);
     return new ListItr(index);
 }
// java.util.LinkedList.ListItr#lastReturned
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
// java.util.LinkedList.ListItr#next
public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();
    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}

for-each循环场景中,对于LinkedList使用的是迭代器。

我们可以从java.util.LinkedList.ListItr#next方法中看到,对于整个元素的获取过程,仅仅只是改变lastReturnednext的值,当前元素的获取是通过next字段辅助获得的,不再需要像java.util.LinkedList#get方法那样遍历获取。

  • 因此,对于LinkedList的遍历,应该强制使用迭代器或者for-each来实现。

二、最佳实践与注意事项

(一)避免在循环条件中使用复杂表达式

循环过程中,循环条件会被反复执行计算。

不使用复杂表达式,而是让循环条件保持一个固定值,程序将会运行的更快,因此在循环条件中应该避免使用复杂的表达式。

例如:下面的代码中,将list.size()方法的返回值赋值给局部变量,从而避免循环过程中重复执行list.size()方法。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 1000000; i++) {
        list.add(i);
    }

    long startTime = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
        System.out.print(list.get(i));
    }
    long endTime = System.currentTimeMillis();

    long startTime2 = System.currentTimeMillis();
    for (int i = 0, len = list.size(); i < len; i++) {
        System.out.print(list.get(i));
    }
    long endTime2 = System.currentTimeMillis();

    System.out.println("==========");
    System.out.println("第一个耗时:" + (endTime - startTime));
    System.out.println("第二个耗时:" + (endTime2 - startTime2));
}

(二)避免在循环体中进行异常处理

  • 循环体中进行异常处理,耗时会比较大
for (int i = 0; i < 100; i++) {
    try {
        // Do something...
    } catch (Exception e) {
    }
}
  • 更好的编写方式
try {
    for (int i = 0; i < 10; i++) {
        // Do something...
    }
} catch (Exception e) {
}
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页