文章目录
关于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循环 | 300 | 0 |
3000 | 0 | |
30000 | 0 | |
迭代器遍历 | 300 | 1 |
3000 | 1 | |
30000 | 2 |
LinkedList遍历结果汇总
遍历方式 | 数量级 | 耗时(毫秒) |
普通for循环 | 300 | 1 |
3000 | 9 | |
30000 | 489 | |
迭代器遍历 | 300 | 0 |
3000 | 1 | |
30000 | 2 |
由以上的结果汇总,我们可以得知:
- 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 < 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()
方法,仅仅只是对cursor
,lastRet
字段的值进行修改,并没有在行为上作出过多耗时的操作。因此,对于
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)
不成立时,也即要查找的元素位置在整个链表的后半部分,此时从链表尾开始往前逐个进行查找 -
这么做的
目的是为了快速查找到目标元素
。
-
-
尽管在一开始就根据链表的长度截半进行查找,但一旦链表的元素过多时,毕竟链表的查找总是逐个进行的,所以仍然会出现查找速度过慢的问题。
-
LinkedList以普通for循环遍历时,由于调用的是get(int index),
假设我们从i=0开始遍历,遍历到i=10,那么LinkedList内部在get的时候,
实际上会跑(1+2+3+4+…+10)遍。
而通过迭代器遍历,获取一个元素,不再需要从头或者从尾来取得,而是返回ListItr迭代器维护的next成员字段。从i=0开始遍历,遍历到i=10,那么迭代器就只需要跑10次就可以。
-
// 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
方法中看到,对于整个元素的获取过程,仅仅只是改变lastReturned
,next
的值,当前元素的获取是通过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) {
}