Java中的forin語句
forin的原理
forin語句是JDK5版本的新特性,在此之前,遍歷數組或集合的方法有兩種:通過下標遍歷和通過叠代器遍歷。先舉個例子:
@Test public void demo() { String arr[] = { "abc", "def", "opq" }; for (int i = 0; i < arr.length; i++) {//通過下標遍歷數組 System.out.println(arr[i]); } System.out.println("----------"); List<String> list = new ArrayList<String>(); list.add("abc"); list.add("def"); list.add("opq"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) {//通過叠代器遍歷集合 System.out.println(iterator.next()); } }
用JUnit進行單體測試,兩種方法的輸出結果是一樣的:
demo()運行效果
JDK5以後引入了forin語句,目的是為了簡化叠代器遍歷,其本質仍然是叠代器遍歷。forin語句的寫法很簡單:
for(數據類型 對象名 : 數組或集合名){
...
}
這裏的數據類型是數組或集合中的數據類型,接著聲明一個該數據類型的對象,用於代替數組或集合中的每一個元素(因此forin語句又稱為foreach語句),最後便是對該對象也就是數組或集合中元素的操作了。
修改上面的代碼,用forin語句遍歷剛才的數組和集合:
System.out.println("----------"); for (String s1 : arr) { System.out.println(s1); } System.out.println("----------"); for (String s2 : list) { System.out.println(s2); }
用JUnit進行單體測試,輸出的結果與之前相同:
demo()運行效果
需要註意的是,通過forin語句遍歷和通過叠代器遍歷是完全等價的。另外,在使用Eclipse進行編程的時候,可以使用alt
+/
進行快捷輸入生成下標遍歷的for循環語句或forin語句,十分方便。
下面講一個關於數組內存的問題,在上面的代碼中再添加一段:
System.out.println("----------");
for (String s3 : arr) {
s3 = "rst";
}
System.out.println(arr[0]);
如果按照常規的思維去理解,數組中的三個元素應該都被修改為了rst
rst
。然而並不是這樣的,用JUnit進行單體測試:
demo()運行效果
結果很明顯,輸出的是abc
、def
、opq
而非三個rst
,也是說數組中的三個元素並沒有被rst
替換。要解釋這個問題就要從Java中的內存講起,在Java中,方法中的引用位於堆空間,而對象則實例化在棧空間。數組{ "abc", "def", "opq" }
屬於方法中的引用,因此存儲在堆空間中,而s3
和arr
屬於實例化的對象,則應存儲在棧空間中。在String arr[] = { "abc", "def", "opq" };
這句代碼中,=
的作用就是將棧空間中的arr
指向堆空間中的數組,而forin語句的作用則是每循環一次就將堆空間中數組元素的值賦給棧空間中的s3
,而這些元素的值實際上不會發生改變。因此遍歷並輸出數組所有元素得到的結果與之前完全一樣。下圖可以幫助理解這個問題:
數組內存
forin的實現
如果一個對象想使用forin語句進行遍歷,則對象類必須滿足兩個條件:實現Iterable
接口和實現Iterator
方法。之所以ArrayList
集合類能夠實現forin語句遍歷,就是因為其滿足上述兩個條件:
Collection接口繼承Iterable接口
Collection接口實現Iterator方法
由於ArrayList
集合類繼承AbstractList
類,AbstractList
類繼承AbstractCollection
類,AbstractCollection
類又實現Collection
接口,因此ArrayList
集合類間接地實現了Iterable
接口和Iterator
方法。
現在我們試著編寫一個Phone
類,然後讓Phone
類對象能夠實現forin語句遍歷:
public class Phone implements Iterable<String> {//實現Iterable接口
String[] names = { "蘋果", "三星", "華為", "小米", "魅族" };
public Iterator<String> iterator() {//實現Iterator方法同時自定義叠代器
Iterator<String> iterator = new MyIterator();
return iterator;
}
class MyIterator implements Iterator<String> {
int index = 0;
public boolean hasNext() {
if (index >= names.length) {
return false;
}
return true;
}
public String next() {
String name = names[index];
index++;
return name;
}
public void remove() {
}
}
}
創建新的方法用於測試:
@Test
public void demo1(){
Phone phone = new Phone();//實例化Phone類對象
for (String s : phone) {//forin語句遍歷Phone類對象phone
System.out.println(s);
}
}
用JUnit進行測試,結果是正確的:
demo1()運行結果
forin刪除元素
再創建一個方法,這次對集合的元素進行一些改動,然後用兩種方法刪除包含字符a
的字符串。首先是通過下標遍歷集合:
@Test
public void demo2(){
List<String> list = new ArrayList<String>();
list.add("abc");
list.add("ade");
list.add("afg");
list.add("def");
list.add("opq");
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if (s.contains("a")){
list.remove(s);
}
}
System.out.println(list);
}
這段代碼看起來再正確不過,然而輸出結果卻是錯誤的:
demo2()運行效果
這是因為當刪除完第一個字符串abc
後,第二個字符串ade
會自動成為第一個字符串,因此當下標變成1
時,得到的字符串就不是ade
而是afg
了,字符串ade
並沒有被刪除掉,便會出現錯誤的結果。
為了防止通過下標刪除集合元素時產生類似的錯誤,每次刪除完元素後應將下標減一,即i--
。改正代碼後再次測試,結果就正確了:
demo2()運行效果
接著是用forin語句遍歷,很簡單地想到代碼應該為:
for (String s : list) {
if(s.contains("a")){
list.remove(s);
}
}
System.out.println(list);
然而事與願違,程序報錯了,拋出了一個異常:
程序報錯
這個異常為並發修改異常。我們將關註的焦點放在第三行錯誤信息上,可以發現是ArrayList
類中Itr
類(叠代器類)的next()
方法出現了異常,查看方法的聲明,會發現調用了checkForComodification()
方法,繼續查看聲明:
checkForComodification()方法聲明
這裏出現了兩個參數:modCount
和expectedModCount
,並且如果這兩個參數不等,則會拋出並發修改異常。expectedModCount
參數是集合的初始化長度,而modCount
參數則是集合的當前長度。回到ArrayList
類中Itr
類的聲明,會有這麽一段代碼:
集合長度初始化
也就是說,在集合初始化的時候,expectedModCount
與modCount
是相等的,但是一旦向集合中添加或者刪除了元素,兩者就不等了,也就會拋出異常。
要想解決拋出異常的問題,可以使用Itr
類中的remove()
方法,先查看方法的聲明:
remove()方法聲明
有一句代碼十分關鍵:expectedModCount = modCount;
。顯然調用remove()
方法能夠將expectedModCount
與modCount
置為相等,因此這樣能夠避免程序拋出並發修改異常。
用集合叠代器的remove()
方法刪除集合的元素:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.contains("a")) {
iterator.remove();
}
}
System.out.println(list);
用JUnit進行單體測試,結果自然是正確的:
demo2()運行效果
如果只需要刪除集合中的一個元素例如刪除字符串afg
,這時候就可以使用集合的remove()
方法進行刪除,但前提是刪除完之後必須用break
語句跳出循環:
for (String s : list) {
if (s.equals("afg")) {
list.remove(s);
break;
}
}
System.out.println(list);
demo2()運行效果
原理也很簡單,還記得之前介紹過forin語句就是叠代器遍歷嗎?用break
語句跳出循環使得叠代器無法調用next()
方法,從而也不會拋出並發修改異常了。
還有一種方法,拋出異常是由集合自身性質所決定的,如果采用不會拋出這類異常的集合不就能解決問題了嗎?JDK5版本引入了Copy-On-Write
容器的概念,CopyOnWrite
機制的理念就是:當我們往一個容器添加或刪除元素的時候,不直接往當前容器添加或刪除,而是先將當前容器進行Copy
,復制出一個新的容器,然後新的容器裏添加或刪除元素,在這之後再將原容器的引用指向新的容器。目前有CopyOnWriteArrayList
和CopyOnWriteArraySet
兩個實現類,因此我們可以采用CopyOnWriteArrayList
類:
List<String> list = new CopyOnWriteArrayList<String>();
list.add("abc");
list.add("ade");
list.add("afg");
list.add("def");
list.add("opq");
for (String s : list) {
if (s.contains("a")){
list.remove(s);
}
}
System.out.println(list);
用JUnit進行測試,結果是正確的:
demo2()運行效果
Java中的forin語句