迭代器Iterator與語法糖for-each
一、為什麼需要迭代器
設計模式迭代器
迭代器作用於集合,是用來遍歷集合元素的物件。迭代器迭代器不是Java獨有的,大部分高階語言都提供了迭代器來遍歷集合。實際上,迭代器是一種設計模式:
迭代器模式提供一種方法順序訪問一個聚合物件中的各個元素,而又不暴露其內部的表示。
迭代器封裝了對集合的遍歷,使得不用瞭解集合的內部細節,就可以使用同樣的方式遍歷不同的集合。
暴露細節的遍歷
要理解封裝遍歷的好處,必須理解暴露細節的遍歷帶來的壞處。
以下是兩個不同的集合介面,第一個是自定義集合,第二個是JDK的java.util.List
public interface IUserDefinedList <E> {
int length();
E getElement(int index);
}
public interface List<E> {
int size();
E get(int index);
}
分別使用for
迴圈對它們進行遍歷:
// 自定義List
for (int i = 0, len = ul.length(); i < len; i++) {
System.out.println(ul.getElement(i));
}
// java.util.List
for (int i = 0, size = ll.size(); i < size; i++) {
System.out.println(ll.get(i));
}
遍歷集合的程式碼與具體集合型別緊密耦合,不同型別的集合,必須寫出不同的遍歷程式碼,不可重用。緊耦合在良好的程式碼設計中是大忌,這時需要將遍歷邏輯抽離,封裝。這就是迭代器模式了。
二、封裝遍歷-迭代器
面向介面程式設計
面向介面程式設計是基本的設計原則,迭代器模式將遍歷封裝到介面,然後各個集合類可以以實現介面,或者組合介面實現類的方式,將遍歷封裝。
迭代器介面如下:
public interface Iterator<E> {
boolean hasNext();
E next();
}
然後就可以以統一的方式遍歷集合:
Iterator it = aIterator;
while (it.hasNext) {
it.next();
}
因為集合都實現了Iterator
介面,所以以上的遍歷程式碼是可以重用的,並且與具體集合型別鬆耦合。
Java迭代器
Java提供的Iterator
原理大致相同:
忽略Java8提供的預設方法forEachRemaining()
,java迭代器多了可以移除上一個元素的remove()
方法。
Java集合中有很多迭代器的具體實現,以ArrayList為例:
ArrayList的迭代器是以內部類的方式實現的,每次呼叫List的iterator()
方法,都會得到一個基於當前ArrayList物件狀態的迭代器:
public Iterator<E> iterator() {
return new 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];
}
每次呼叫next()
方法,都會呼叫
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
檢查所迭代的列表物件是否被修改過,modCount
是外部類的一個欄位,當呼叫外部類的add, remove, ensureCapacity
等方法,都會改變改欄位的值,而expectedModCount
是內部類Itr
初始化迭代器例項時,使用modeCount
賦值的欄位。這樣,在使用迭代器過程中,如果對正在迭代的物件呼叫了add, remove, ensureCapacity
等方法,再去呼叫迭代器的next()
,就會引發ConcurrentModificationException
異常了。
一個誘發異常的例子:
ArrayList<String> ls = new ArrayList<>();
Iterator<String> it = ls.iterator();
while (it.hasNext()) {
System.out.println(it.next());
ls.ensureCapacity(10); // 改變了集合物件modCount的值,下次呼叫next 丟擲異常
}
三、語法糖for-each
java中的for-each:
List<String> ls = new ArrayList<>();
for (String s: ls) {
System.out.println(s);
}
for-each其實只是java提供的語法糖。語法糖是程式語言提供的一些便於程式設計師書寫程式碼的語法,是編譯器提供給程式設計師的糖衣,編譯時會對這些語法特殊處理。語法糖雖然不會帶來實質性的改進,但是在提高程式碼可讀性,提高語法嚴謹性,減少編碼錯誤機會上確實做出了很大貢獻。
Java要求集合必須實現Iterable
介面,才能使用for-each語法糖遍歷該集合的例項。
JDK對該介面的描述是:
Implementing this interface allows an object to be the target of * the “for-each loop” statement.
介面如下:
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
忽略預設方法,該介面要求集合實現iterator()
方法,並返回一個迭代器物件Iterator
。這也是java中的集合通過實現Iterable
介面,組合Iterator
來提供迭代器,並不通過直接實現Iterator
介面的方式來提供集合迭代器的原因了。所以java集合迭代器的直接用法都如下:
List<String> ls = new ArrayList<>();
Iterator<String> it = ls.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
for-each的編譯器實現
for-each遍歷集合,實際上被翻譯如下:
for (I #i = Expression.iterator(); #i.hasNext(); ) {
{VariableModifier} TargetType Identifier =
(TargetType) #i.next();
Statement
}
當然,除了集合,for-each還可以遍歷陣列,翻譯如下:
T[] #a = Expression;
L1: L2: ... Lm:
for (int #i = 0; #i < #a.length; #i++) {
{VariableModifier} TargetType Identifier = #a[#i];
Statement
}
另外,使用javap命令,反編譯位元組碼,可以看到編譯器是怎樣處理for-each的。
public class Test {
public static void main(String[] args) {
List<String> ls = new ArrayList<>();
for (String s: ls) {
}
}
}
以上原始檔對應class檔案的反編譯彙編為:
C:\OTHERS\Working\ideaWork\utils\Z_practice\src\main\java>javap -c pattern\adapter\java_example\Test.class
Compiled from "Test.java"
public class pattern.adapter.java_example.Test {
public pattern.adapter.java_example.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #4, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_2
15: aload_2
16: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
21: ifeq 37
24: aload_2
25: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
30: checkcast #7 // class java/lang/String
33: astore_3
34: goto 15
37: return
}
不用關注全部彙編細節,只需要看到InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
就知道實際上是使用了迭代器。
至於陣列的for-each,也可以通過觀察class檔案的彙編程式碼理解。
四、最佳實踐
for-each遍歷的集合物件不能為
null
既然集合的for-each實際上是使用迭代器,呼叫集合物件的iterator()
方法獲得迭代器,那麼,對null
結合的for-each遍歷,勢必會跑出空指標異常。for-each中不能改變遍歷的集合
因為在使用迭代器遍歷集合時,不能夠改變集合,所以for-each遍歷改變集合時,同樣會引發ConcurrentModificationException
異常。