使用Lambda表達式遍歷集合
Java8為Iterable接口新增了一個forEach(Consumer action)默認方法,該方法所需參數的類型是一個函數式接口,而Iterable接口是一個Collection接口的父接口,因此Collection集合也可以直接調用該方法。
當程序調用Iterable的forEach(Consumer action)遍歷集合元素時,程序會依次將集合元素傳給Consumer的accept(T t)方法(該接口中唯一的抽象方法)。正因為Consumer是函數式接口,因此可以使用Lambda表達式來遍歷集合元素。
public class CollectionEach {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");
//使用forEach()方法遍歷
books.forEach(obj-> System.out.println("迭代集合元素:"+obj));
}
}
使用Java8增強的Iterator遍歷集合元素
Iterator接口也是Java集合框架的成員,但它與Collection系列、Map系列的集合不一樣:Collection系列集合、Map系列集合主要用於盛裝其他對象,而Iterator則用於遍歷Collection集合中的元素,Iterator也被稱為迭代器。
Iterator接口隱藏了各種Collection實現類的底層細節,向應用程序提供了遍歷Collection集合元素的統一編程接口。Iterator接口里定義了如下4個方法:
- boolean hasNext():如果被迭代的集合元素還沒有被遍歷完,則返回true
- Object next():返回集合里的下一個元素
- void remove():刪除集合里上一次next方法返回的元素
- void forEachRemaining(Consumer action):這是Java8為Iterator新增的默認方法,該方法可以使用Lambda表達式來遍歷集合元素
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");
//獲取books集合對應的迭代器
Iterator it = books.iterator();
it.forEachRemaining(System.out::println);
運行結果如下:
水滸傳
三國演義
紅樓夢
下面程序示范了通過Iterator接口來遍歷集合元素。
public class IteratorTest {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");
//獲取books集合對應的迭代器
Iterator it = books.iterator();
// it.forEachRemaining(System.out::println);
while (it.hasNext()) {
//it.next()方法返回的數據類型是Object類型,因此需要強制類型轉換
String book = (String) it.next();
System.out.println(book);
if (book.equals("紅樓夢")) {
//從集合中刪除上一次next()方法返回的元素
it.remove();
}
//對book變量賦值,不會改變集合元素本身
book="測試";
}
System.out.println(books);
}
}
從上面代碼可看出,Iterator僅用於遍歷集合,Iterator本身不具有盛裝對線的能力。如果需要創建Iterator對象,則必須有一個被迭代的集合。沒有集合的Iterator仿佛無本之木,沒有存在的價值。
上面book="測試";
對迭代變量book進行賦值,但當再次輸出books集合時,會看到集合里的元素沒有任何改變。這就可以得到一個結論:當使用Iterator對集合元素進行迭代時,Iterator並不是把集合元素本身傳給了迭代變量,而是把集合元素的值傳給了迭代變量,所以修改迭代變量的值對集合元素本身沒有任何影響。
當使用Iterator迭代訪問Collection集合元素時,Collection集合里的元素不能被改變,只有通過Iterator的remove()方法刪除上一次next()方法返回的集合元素才可以;否則將會發生Exception in thread "main" java.util.ConcurrentModificationException
異常
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");
//獲取books集合對應的迭代器
Iterator it = books.iterator();
// it.forEachRemaining(System.out::println);
while (it.hasNext()) {
//it.next()方法返回的數據類型是Object類型,因此需要強制類型轉換
String book = (String) it.next();
System.out.println(book);
if (book.equals("三國演義")) {
//從集合中刪除上一次next()方法返回的元素
// it.remove();
books.remove(book);
}
//對book變量賦值,不會改變集合元素本身
book="測試";
}
上面books.remove(book);
代碼位於Iterator迭代塊內,也就是在Iterator迭代Collection集合過程中修改了Collection集合,所以程序將在運行時引發異常。
Iterator迭代器采用的是快速失敗(fail-fast)機制,一旦在迭代過程中檢測到該集合已經被修改(通常是程序中的其他線程修改),程序立即引發java.util.ConcurrentModificationException
異常,而不是顯示修改后的結果,這樣可以避免共享資源而引發的潛在問題。
注意:上面的程序如果改為刪除"紅樓夢"字符串,則不會引發異常,這樣可能有些讀者會"心存僥幸"地想:在迭代時好像也可以刪除集合元素啊。實際上這是一種危險的行為:對於HashSet以及后面的ArrayList等,迭代時刪除元素都會導致異常——只有在刪除集合中的某個特定元素時才不會拋出異常,這是由集合類的實現代碼決定的,程序員不應該這么做。
使用foreach循環遍歷集合元素
除了使用Iterator接口迭代訪問Collection集合里的元素之外,使用Java5提供的foreach循環迭代訪問集合元素更加便捷。
public class ForeachTest {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet();
books.add("三國演義");
books.add("水滸傳");
books.add("紅樓夢");
for (Object obj:books) {
//此處的book變量也不是集合元素本身
String book = (String)obj;
System.out.println(book);
if (book.equals("三國演義")){
//引發java.util.ConcurrentModificationException異常
books.remove(book);
}
}
System.out.println(books);
}
}
與使用Iterator接口迭代訪問集合元素類似的事,foreach循環中的迭代變量也不是集合元素本身,系統只是依次把集合元素的值賦給迭代變量,因此在foreach循環中修改迭代變量的值也沒有任何實際意義。
同樣,當使用foreach循環迭代訪問集合元素時,該集合也不能被改變,否則將引發ConcurrentModificationException異常。
使用Java8新增的Predicate操作集合
Java8為Collection集合新增了一個removeIf(Predicate filter)方法,該方法將會批量刪除符合filter條件的所有元素。該方法需要一個Predicate(謂詞)對象作為參數,Predicate也是函數式接口,因此可以使用Lambda表達式作為參數。
下面程序示范了使用Predicate來過濾集合
public class PredicateTest {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet<>();
books.add("輕量級Java EE企業應用實戰");
books.add("瘋狂Java講義");
books.add("瘋狂Ios講義");
books.add("瘋狂Ajax講義");
books.add("瘋狂Android講義");
//使用Lambda表達式(目標類型是Predicate)過濾集合
books.removeIf(ele->((String)ele).length()<10);
System.out.println(books);
}
}
上面books.removeIf(ele->((String)ele).length()<10);
調用了Collection集合的removeIf()方法批量刪除集合中符合條件的元素,程序傳入一個Lambda表達式作為過濾條件:所有長度小於10的字符串元素都會被刪除。運行結果是
[瘋狂Android講義, 輕量級Java EE企業應用實戰]
使用Predicate可以充分簡化集合的運算,假設依然有上面程序所示的books集合,如果程序有如下三個統計需求:
- 統計書名中出現"瘋狂"字符串的圖書數量
- 統計書名中出現"Java"字符串的圖書數量
- 統計書名長度大於10的圖書數量
如果采用傳統的編程方式來完成這些需求,則需要執行三次循環,但采用Predicate只需要一個方法即可。如下:
public class PredicateTest1 {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet<>();
books.add("輕量級Java EE企業應用實戰");
books.add("瘋狂Java講義");
books.add("瘋狂Ios講義");
books.add("瘋狂Ajax講義");
books.add("瘋狂Android講義");
//統計書名包含"瘋狂"子串的圖書數量
System.out.println(calAll(books,ele->((String)ele).contains("瘋狂")));
//統計書名包含"Java"子串的圖書數量
System.out.println(calAll(books,ele->((String)ele).contains("Java")));
//統計書名字符串長度大於10的圖書數量
System.out.println(calAll(books,ele->((String)ele).length()>10));
}
public static int calAll(Collection books, Predicate p){
int total =0;
for (Object obj:books) {
//使用Predicate的test()方法判斷該對象是否滿足Predicate指定的條件
if (p.test(obj)){
total++;
}
}
return total;
}
}
上面程序先定義了一個calAll()方法,該方法將會使用Predicate判斷每個集合元素是否符合特定條件——該條件通過Predicate參數動態傳入。
使用Java8新增的Stream操作集合
Java8還新增了Stream、InStream、LongStream、DoubleStream等流式API,這些API代表多個支持串行和並行聚集操作的元素。上面4個接口中,Stream是一個通用的流接口,而IntStream、LongStream、DoubleStream則代表元素類型為int、long、double的流。
Java8還為上面每個流式API提供了對應的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builer,開發者可以通過這些Builder來創建對應的流。
獨立使用Stream的步驟如下:
- 使用Stream或XXXStream的builder()類方法創建該Stream對應的Builder
- 重復調用Builder的add()方法向該流中添加多個元素
- 調用Builder的build()方法獲取對應的Stream
- 調用Stream的聚集方法
在上面4個步驟中,第4步可以根據具體需求來調用不同的方法,Stream提供了大量的聚集方法供用戶調用,具體可參考Stream或XXXStream的API文檔。對於大部分聚集方法而言,每個Stream只能執行一次。
public class IntStreamTest {
public static void main(String[] args) {
IntStream is = IntStream.builder().add(20).add(13).add(-2).add(18).build();
//下面調用聚集方法的代碼每次只能執行一行
System.out.println("is所有元素的最大值:"+is.max().getAsInt());
System.out.println("is所有元素的最小值:"+is.min().getAsInt());
System.out.println("is所有元素的總和:"+is.sum());
System.out.println("is所有元素的總數:"+is.count());
System.out.println("is所有元素的平均值:"+is.average());
System.out.println("is所有元素的平方是否都大於20:"+is.allMatch(ele->ele*ele>20));
System.out.println("is是否包含任一元素的平方大於20:"+is.anyMatch(ele->ele*ele>20));
//將is映射成一個新的Stream,新的Stream的每個元素是原Stream元素的2倍+1
IntStream newIs = is.map(ele -> ele * 2 + 1);
//使用方法引用的方式來遍歷集合元素
newIs.forEach(System.out::println);
}
}
如上面代碼中的很多行都使用了is,這樣就會產生Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
。所以上面的使用is的代碼,每次只能執行一行,要把其他使用is的代碼注釋掉。
除此之外,Java8允許使用流式API來操作集合,Collection接口提供了一個Stream()默認方法,該方法可返回該集合對應的流,接下來即可通過流式API來操作集合元素。由於Stream可以對集合元素進行整體的聚集操作,因此Stream極大地豐富了集合的功能。
例如,上一個知識點的示例程序中,該程序需要額外定義一個calAll()方法來遍歷集合元素,然后依次對每個集合元素進行判斷——這太麻煩了。如果使用Stream,即可直接對集合中所有元素進行批量操作。下面使用Stream來改寫這個程序:
public class CollectionStream {
public static void main(String[] args) {
//創建一個集合
Collection books = new HashSet<>();
books.add("輕量級Java EE企業應用實戰");
books.add("瘋狂Java講義");
books.add("瘋狂Ios講義");
books.add("瘋狂Ajax講義");
books.add("瘋狂Android講義");
//統計書名包含"瘋狂"子串的圖書數量
System.out.println(books.stream().filter(ele->((String)ele).contains("瘋狂")).count());
//統計書名包含"Java"子串的圖書數量
System.out.println(books.stream().filter(ele->((String)ele).contains("Java")).count());
//統計書名字符串長度大於10的圖書數量
System.out.println(books.stream().filter(ele->((String)ele).length()>10).count());
//先調用Collection對象的Stream()方法將集合轉化為Stream
//再調用Stream的mapToInt()方法獲取原有的Stream對應的IntStream
//調用forEach()方法遍歷IntStream中每個元素
System.out.println("集合中每個元素的長度:");
books.stream().mapToInt(ele->((String)ele).length()).forEach(System.out::println);
}
}
通過上面的代碼可以看出,程序只要調用Collection的Stream()方法即可返回該結合對應的Stream,接下來就可通過Stream提供的方法對所有集合元素進行處理,這樣大大地簡化了集合編程的代碼,這也是Stream編程帶來的優勢。
Java8改進的List接口和ListIterator接口
Java8為List集合增加了sort()和replaceAll()兩個常用的默認方法,其中sort()方法需要一個Comparator對象來控制元素排序,程序可以使用Lambda表達式來作為參數:而replaceAll()方法則需要一個UnaryOperator來替換所有集合元素,UnaryOperator也是一個函數式接口,因此程序也可以使用Lambda表達式作為參數。
public class ListTest {
public static void main(String[] args) {
List books = new ArrayList();
//向books集合中添加4個元素
books.add("輕量級Java EE企業應用實戰");
books.add("瘋狂Java講義");
books.add("瘋狂Ios講義");
books.add("瘋狂Ajax講義");
books.add("瘋狂Android講義");
//使用目標類型為Comparator的Lambda表達式對List集合排序
books.sort((o1,o2)->((String)o1).length()-((String)o2).length());
System.out.println(books);
//使用目標類型為UnaryOperator的Lambda表達式來替換集合中所有元素
//該Lambda表達式控制使用每個字符串的長度作為新的集合元素
books.replaceAll(ele->((String)ele).length());
System.out.println(books);
}
}
運行結果:
[瘋狂Ios講義, 瘋狂Java講義, 瘋狂Ajax講義, 瘋狂Android講義, 輕量級Java EE企業應用實戰]
[7, 8, 8, 11, 16]
上面程序中books.sort((o1,o2)->((String)o1).length()-((String)o2).length());
控制對List集合進行排序,傳給sort()方法的Lambda表達式指定的排序規則是:字符串長度越大,字符串越大,因此執行完該代碼后,List集合匯總的字符串會按由短到長的順序排列。
books.replaceAll(ele->((String)ele).length());
傳給replaceAll()方法的Lambda表達式指定了替換集合元素的規則:直接用集合元素(字符串)的長度作為新的集合元素。
與Set只提供一個iterator()方法不同,List還額外提供了一個listIterator()方法,該方法返回一個ListIterator對象,ListIterator接口繼承了Iterator接口,提供了專門操作List的方法。ListIterator接口在Iterator接口基礎上增加了如下方法:
- boolean hasPrevious():返回該迭代器關聯的集合是否還有上一個元素
- Object previous():返回該迭代器的上一個元素
- void add(Object o):在指定位置插入一個元素
拿ListIterator與普通的Iterator進行對比,不難發現ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator還可通過add()方法向List集合中添加元素(Iterator只能刪除元素)。下面示范ListIterator的用法:
public class ListIteratorTest {
public static void main(String[] args) {
String[] books = {"瘋狂Java講義","瘋狂ios講義","輕量級Java EE企業應用實戰"};
List bookList = new ArrayList();
for (int i = 0; i < books.length; i++) {
bookList.add(books[i]);
}
ListIterator lit = bookList.listIterator();
while (lit.hasNext()) {
System.out.println(lit.next());
lit.add("---------分隔符---------");
}
System.out.println("===========下面開始反向迭代=====");
while (lit.hasPrevious()) {
System.out.println(lit.previous());
}
}
}
從上面程序中可以看出,使用ListIterator迭代List集合時,開始也需要采用正向迭代,即先使用next()方法進行迭代,在迭代過程中可以使用add()方法向上一次迭代元素的后面添加一個新元素。運行結果:
瘋狂Java講義
瘋狂ios講義
輕量級Java EE企業應用實戰
===========下面開始反向迭代=====
---------分隔符---------
輕量級Java EE企業應用實戰
---------分隔符---------
瘋狂ios講義
---------分隔符---------
瘋狂Java講義
Java8為Map新增的方法
Java8除了為Map增加remove(Object key,Object value)默認方法外,還增加了如下方法:
public class MapTest {
public static void main(String[] args) {
Map map = new HashMap();
//存放多個鍵值對
map.put("瘋狂Java講義",109);
map.put("瘋狂IOS講義",99);
map.put("瘋狂Ajax講義",79);
//嘗試替換key為"瘋狂xml講義"的value,由於原map中沒有對應的key
//因此map沒有改變,不會添加新的鍵值對
map.replace("瘋狂xml講義",66);
System.out.println(map);
//使用原value與傳入參數計算出來的結果覆蓋原有的value
map.merge("瘋狂IOS講義",10,(oldVal,param)->(Integer)oldVal+(Integer)param);
System.out.println(map);
//當key為"java"對應的value為null時,使用計算的結果作為新value
map.computeIfAbsent("java",(key)->((String)key).length());
System.out.println(map);
//當key為"java"對應的value存在時,使用計算的結果作為新value
map.computeIfPresent("java",(key,value)->(Integer)value*(Integer)value);
System.out.println(map);
}
}
運行結果:
{瘋狂Ajax講義=79, 瘋狂IOS講義=99, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, 瘋狂IOS講義=109, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, java=4, 瘋狂IOS講義=109, 瘋狂Java講義=109}
{瘋狂Ajax講義=79, java=16, 瘋狂IOS講義=109, 瘋狂Java講義=109}
Java8改進的HashMap和Hashtable實現類
HashMap和Hashtable都是Map接口的典型實現類,它們之間的關系完全類似於ArrayList和Vector的關系:Hashtable是一個古老的Map實現類,它從JDK1.0就有了,當它出現時,Java還沒有提供Map接口。
Java8改進了HashMap的實現,使用HashMap存在key沖突時依然具有較好的性能。
除此之外,Hashtable與HashMap存在兩點典型區別。
- Hashtable是一個線程安全的Map實現,但HashMap是線程不安全的實現,所以HashMap比HashTable的性能高一點;但如果有多個線程訪問同一個Map對象時,使用HashTable實現類會更好
- Hashtable不允許使用null作為key和value,如果視圖把null值放進Hashtable中,會出現空指針異常;但HashMap可以使用null作為key或value
由於HashMap里的key不能重復,所以HashMap里最多只有一個key-value對的key為null,但可以有無數多個key-value對的value為null。下面程序示范了用null值作為HashMap的key和value的情形。
public class NullInHashMap {
public static void main(String[] args) {
HashMap hm = new HashMap();
//試圖將兩個可以為null值的key-value對放入HashMap中
hm.put(null,null);
hm.put(null,null);
//將一個value為null值的key-value對放入HashMap中
hm.put("a",null);
//輸出map
System.out.println(hm);
}
}
執行結果:
{null=null, a=null}
上面程序試圖向HashMap中放入三個鍵值對,其中第二個hm.put(null,null);
無法放入,因為map中已經有一個鍵值對的key為null值,所以無法再放入key為null的鍵值對。hm.put("a",null);
可以放入鍵值對,因為一個HashMap中可以有多個value為null值。
為了成功地在HashMap、Hashtable中存儲、獲取對象,用作key的對象必須實現hashCode()方法和equals()方法。
與HashSet集合不能保證元素的順序一樣,HashMap、Hashtable也不能保證其中key-value對的順序。類似於HashSet,HashMap、Hashtable判斷兩個key相等的標准也是:兩個key通過equals()方法比較返回true,兩個key的hashCode值也相等。
除此之外,HashMap、Hashtable中還包含一個containsValue()方法,用於判斷是否包含指定的value。那么HashMap、Hashtable如何判斷兩個value相等呢?HashMap、Hashtable判斷兩個value相等的標准更簡單:只要兩個對象通過equals()方法比較返回true即可。下面程序示范了Hashtable判斷兩個可以相等的標准和兩個value相等的標准。
public class HashtableTest {
public static void main(String[] args) {
Hashtable ht = new Hashtable();
ht.put(new A(60000),"瘋狂Java講義");
ht.put(new A(89898),"輕量級Java EE企業應用實戰");
ht.put(new A(1234),new B());
System.out.println(ht);
//只要兩個對象通過equals()方法比較返回true
//Hashtable就認為它們是相等的value
//由於Hashtable中有一個B對象
//它與任何對象通過equals()方法比較都相等,所以下面輸出true
System.out.println(ht.containsValue("測試字符串"));
//只要兩個A對象的count相等,它們通過equals()方法比較返回true,且hashCode值相等
//Hashtable即認為它們是相同的key,所以下面輸出true
System.out.println(ht.containsKey(new A(89898)));
//下面語句可以刪除最后一個鍵值對
ht.remove(new A(1234));
System.out.println(ht);
}
}
class A{
int count;
public A(int count){
this.count=count;
}
//根據count的值來判斷兩個對象是否相等
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj!=null && obj.getClass() ==A.class){
A a = (A)obj;
return this.count == a.count;
}
return false;
}
//根據count來計算hashCode值
@Override
public int hashCode() {
return this.count;
}
}
class B {
//重寫equals()方法,B對象與任何對象通過equals()方法比較都返回true
@Override
public boolean equals(Object obj) {
return true;
}
}
運行結果:
{com.tianhao.luo.collection.map.A@15f2a=輕量級Java EE企業應用實戰, com.tianhao.luo.collection.map.A@ea60=瘋狂Java講義, com.tianhao.luo.collection.map.A@4d2=com.tianhao.luo.collection.map.B@4554617c}
true
true
{com.tianhao.luo.collection.map.A@15f2a=輕量級Java EE企業應用實戰, com.tianhao.luo.collection.map.A@ea60=瘋狂Java講義}
上面程序定義了A類和B類,其中A類判斷兩個A對象相等的標准是count實例變量:只要兩個A對象的count變量相等,則通過equals()方法比較它們返回true,它們的hashCode值也相等;而B對象則可以與任何對象相等。
Hashtable判斷value相等的標准是:value與另外一個對象通過equals()方法比較返回true即可。上面程序中的ht對象包含了一個B對象,它與任何對象通過equals()方法比較總是返回true,所以第一個輸出判斷為true。在這種情況下,不管傳給ht對象的containsValue()方法參數是什么,程序總是返回true。
根據Hashtable判斷兩個key相等的標准,程序在第二個輸出判斷也是true,因為兩個A對象雖然不是同一個對象,但它們通過equals()方法比較返回true,且hashCode值相等,Hashtable即認為它們是同一個key。類似的是,程序在最后ht.remove(new A(1234));
可以刪除對應的鍵值對。
與HashSet類似的是,如果使用可變對象作為HashMap、Hashtable的key,並且程序修改了作為key的可變對象,則也可能出現與HashSet類似的情形:程序再也無法准確訪問到map中被修改過的key。
public class HashMapErrorTest {
public static void main(String[] args) {
HashMap ht = new HashMap();
//此處的A類與前一個程序的A類是同一個類
ht.put(new A(60000),"瘋狂Java講義");
ht.put(new A(87563),"輕量級Java EE企業應用實戰");
//獲得Hashtable的key Set集合對應的Iterator迭代器
Iterator it = ht.keySet().iterator();
//取出Map中第一個key,並修改它的count值
A first = (A)it.next();
first.count=87563;
System.out.println(ht);
ht.remove(new A(87563));
System.out.println(ht);
//無法獲取剩下的value,下面兩行代碼都將輸出null
System.out.println(ht.get(new A(87563)));
System.out.println(ht.get(new A(60000)));
}
}
該程序使用的還是上一個程序定義的A類實例作為key,而A對象是可變對象。當用first.count=87563;
修改了A對象之后,實際上修改了HashMap集合中元素的key,這就導致該key不能被准確訪問。當程序試圖刪除count為87653的A對象時,只能刪除沒有被修改的key所對應的key-value對。程序最后都不能訪問"瘋狂Java講義"字符串,這都是因為它對應的key被修改過的原因。