【轉】Java 8新特性終極指南

編者注:Java 8已經公佈有一段時間了,種種跡象表明Java 8是一個有重大改變的發行版。

在Java Code Geeks上已經有大量的關於Java 8 的教程了,像玩轉Java 8——lambda與併發Java 8 Date Time API 教程: LocalDateTime在Java 8中抽象類與接口的比較

我們也在其他地方引用了15個必讀的Java 8教程。當然,我們也探究了Java 8的一些不足之處,比如Java 8的「黑暗面」

現在,是時候把所有Java 8的重要特性收集整理成一篇單獨的文章了,希望這篇文章能給你帶來閱讀上的樂趣。開始吧!

目錄結構

  1. 介紹
  2. Java語言的新特性

    2.1 Lambdas表達式與Functional接口

    2.2 接口的默認與靜態方法

    2.3 方法引用

    2.4 重複註解

    2.5 更好的類型推測機制

    2.6 擴展註解的支持

  3. Java編譯器的新特性

    3.1 參數名字

  4. Java 類庫的新特性

    4.1 Optional

    4.2 Streams

    4.3 Date/Time API (JSR 310)

    4.4 JavaScript引擎Nashorn

    4.5 Base64

    4.6 並行(parallel)數組

    4.7 併發(Concurrency)

  5. 新增的Java工具

    5.1 Nashorn引擎: jjs

    5.2 類依賴分析器: jdeps

  6. Java虛擬機(JVM)的新特性

  7. 總結

  8. 更多資源

1.介紹

毫無疑問,Java 8發行版是自Java 5(發行於2004,已經過了相當一段時間了)以來最具革命性的版本。Java 8 爲Java語言、編譯器、類庫、開發工具與JVM(Java虛擬機)帶來了大量新特性。在這篇教程中,我們將一一探索這些變化,並用真實的例子說明它們適用的場景。

這篇教程由以下幾部分組成,它們分別涉及到Java平臺某一特定方面的內容:

  • Java語言
  • 編譯器
  • 類庫
  • 工具
  • Java運行時(JVM)

2.Java語言的新特性

不管怎麼說,Java 8都是一個變化巨大的版本。你可能認爲Java 8耗費了大量的時間才得以完成是爲了實現了每個Java程序員所期待的特性。在這個小節裏,我們將會涉及到這些特性的大部分。

2.1 Lambda表達式與Functional接口

Lambda表達式(也稱爲閉包)是整個Java 8發行版中最受期待的在Java語言層面上的改變,Lambda允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中),或者把代碼看成數據:函數式程序員對這一概念非常熟悉。在JVM平臺上的很多語言(Groovy,Scala,……)從一開始就有Lambda,但是Java程序員不得不使用毫無新意的匿名類來代替lambda。

關於Lambda設計的討論佔用了大量的時間與社區的努力。可喜的是,最終找到了一個平衡點,使得可以使用一種即簡潔又緊湊的新方式來構造Lambdas。在最簡單的形式中,一個lambda可以由用逗號分隔的參數列表、–>符號與函數體三部分表示。例如:

1
Arrays.asList( "a" , "b" , "d" ).forEach( e -> System.out.println( e ) );

請注意參數e的類型是由編譯器推測出來的。同時,你也可以通過把參數類型與參數包括在括號中的形式直接給出參數的類型:

1
Arrays.asList( "a" , "b" , "d" ).forEach( ( String e ) -> System.out.println( e ) );

在某些情況下lambda的函數體會更加複雜,這時可以把函數體放到在一對花括號中,就像在Java中定義普通函數一樣。例如:

1
2
3
4
Arrays.asList( "a" , "b" , "d" ).forEach( e -> {
     System.out.print( e );
     System.out.print( e );
} );

Lambda可以引用類的成員變量與局部變量(如果這些變量不是final的話,它們會被隱含的轉爲final,這樣效率更高)。例如,下面兩個代碼片段是等價的:

1
2
3
String separator = "," ;
Arrays.asList( "a" , "b" , "d" ).forEach(
     ( String e ) -> System.out.print( e + separator ) );

和:

1
2
3
final String separator = "," ;
Arrays.asList( "a" , "b" , "d" ).forEach(
     ( String e ) -> System.out.print( e + separator ) );

Lambda可能會返回一個值。返回值的類型也是由編譯器推測出來的。如果lambda的函數體只有一行的話,那麼沒有必要顯式使用return語句。下面兩個代碼片段是等價的:

1
Arrays.asList( "a" , "b" , "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );

和:

1
2
3
4
Arrays.asList( "a" , "b" , "d" ).sort( ( e1, e2 ) -> {
     int result = e1.compareTo( e2 );
     return result;
} );

語言設計者投入了大量精力來思考如何使現有的函數友好地支持lambda。最終採取的方法是:增加函數式接口的概念。函數式接口就是一個具有一個方法的普通接口。像這樣的接口,可以被隱式轉換爲lambda表達式。java.lang.Runnable與java.util.concurrent.Callable是函數式接口最典型的兩個例子。在實際使用過程中,函數式接口是容易出錯的:如有某個人在接口定義中增加了另一個方法,這時,這個接口就不再是函數式的了,並且編譯過程也會失敗。爲了克服函數式接口的這種脆弱性並且能夠明確聲明接口作爲函數式接口的意圖,Java 8增加了一種特殊的註解@FunctionalInterface(Java 8中所有類庫的已有接口都添加了@FunctionalInterface註解)。讓我們看一下這種函數式接口的定義:

1
2
3
4
@FunctionalInterface
public interface Functional {
     void method();
}

需要記住的一件事是:默認方法與靜態方法並不影響函數式接口的契約,可以任意使用:

1
2
3
4
5
6
7
@FunctionalInterface
public interface FunctionalDefaultMethods {
     void method();
         
     default void defaultMethod() {           
     }       
}

Lambda是Java 8最大的賣點。它具有吸引越來越多程序員到Java平臺上的潛力,並且能夠在純Java語言環境中提供一種優雅的方式來支持函數式編程。更多詳情可以參考官方文檔

2.2 接口的默認方法與靜態方法

Java 8用默認方法與靜態方法這兩個新概念來擴展接口的聲明。默認方法使接口有點像Traits(Scala中特徵(trait)類似於Java中的Interface,但它可以包含實現代碼,也就是目前Java8新增的功能),但與傳統的接口又有些不一樣,它允許在已有的接口中添加新方法,而同時又保持了與舊版本代碼的兼容性。

默認方法與抽象方法不同之處在於抽象方法必須要求實現,但是默認方法則沒有這個要求。相反,每個接口都必須提供一個所謂的默認實現,這樣所有的接口實現者將會默認繼承它(如果有必要的話,可以覆蓋這個默認實現)。讓我們看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private interface Defaulable {
     // Interfaces now allow default methods, the implementer may or
     // may not implement (override) them.
     default String notRequired() {
         return "Default implementation" ;
     }       
}
         
private static class DefaultableImpl implements Defaulable {
}
     
private static class OverridableImpl implements Defaulable {
     @Override
     public String notRequired() {
         return "Overridden implementation" ;
     }
}

Defaulable接口用關鍵字default聲明瞭一個默認方法notRequired(),Defaulable接口的實現者之一DefaultableImpl實現了這個接口,並且讓默認方法保持原樣。Defaulable接口的另一個實現者OverridableImpl用自己的方法覆蓋了默認方法。

Java 8帶來的另一個有趣的特性是接口可以聲明(並且可以提供實現)靜態方法。例如:

1
2
3
4
5
6
private interface DefaulableFactory {
     // Interfaces now allow static methods
     static Defaulable create( Supplier< Defaulable > supplier ) {
         return supplier.get();
     }
}

下面的一小段代碼片段把上面的默認方法與靜態方法黏合到一起。

1
2
3
4
5
6
7
public static void main( String[] args ) {
     Defaulable defaulable = DefaulableFactory.create( DefaultableImpl:: new );
     System.out.println( defaulable.notRequired() );
         
     defaulable = DefaulableFactory.create( OverridableImpl:: new );
     System.out.println( defaulable.notRequired() );
}

這個程序的控制檯輸出如下:

1
2
Default implementation
Overridden implementation

在JVM中,默認方法的實現是非常高效的,並且通過字節碼指令爲方法調用提供了支持。默認方法允許繼續使用現有的Java接口,而同時能夠保障正常的編譯過程。這方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……

儘管默認方法非常強大,但是在使用默認方法時我們需要小心注意一個地方:在聲明一個默認方法前,請仔細思考是不是真的有必要使用默認方法,因爲默認方法會帶給程序歧義,並且在複雜的繼承體系中容易產生編譯錯誤。更多詳情請參考官方文檔

2.3 方法引用

方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘代碼。

下面,我們以定義了4個方法的Car這個類作爲例子,區分Java中支持的4種不同的方法引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static class Car {
     public static Car create( final Supplier< Car > supplier ) {
         return supplier.get();
     }             
         
     public static void collide( final Car car ) {
         System.out.println( "Collided " + car.toString() );
     }
         
     public void follow( final Car another ) {
         System.out.println( "Following the " + another.toString() );
     }
         
     public void repair() {  
         System.out.println( "Repaired " + this .toString() );
     }
}

第一種方法引用是構造器引用,它的語法是Class::new,或者更一般的Class< T >::new。請注意構造器沒有參數。

1
2
final Car car = Car.create( Car:: new );
final List< Car > cars = Arrays.asList( car );

第二種方法引用是靜態方法引用,它的語法是Class::static_method。請注意這個方法接受一個Car類型的參數。

1
cars.forEach( Car::collide );

第三種方法引用是特定類的任意對象的方法引用,它的語法是Class::method。請注意,這個方法沒有參數。

1
cars.forEach( Car::repair );

最後,第四種方法引用是特定對象的方法引用,它的語法是instance::method。請注意,這個方法接受一個Car類型的參數

1
2
final Car police = Car.create( Car:: new );
cars.forEach( police::follow );

運行上面的Java程序在控制檯上會有下面的輸出(Car的實例可能不一樣):

1
2
3
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car @7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car @7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car @7a81197d

關於方法引用的更多詳情請參考官方文檔

2.4 重複註解

自從Java 5引入了註解機制,這一特性就變得非常流行並且廣爲使用。然而,使用註解的一個限制是相同的註解在同一位置只能聲明一次,不能聲明多次。Java 8打破了這條規則,引入了重複註解機制,這樣相同的註解可以在同一地方聲明多次。

重複註解機制本身必須用@Repeatable註解。事實上,這並不是語言層面上的改變,更多的是編譯器的技巧,底層的原理保持不變。讓我們看一個快速入門的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.javacodegeeks.java8.repeatable.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
public class RepeatingAnnotations {
     @Target ( ElementType.TYPE )
     @Retention ( RetentionPolicy.RUNTIME )
     public @interface Filters {
         Filter[] value();
     }
     
     @Target ( ElementType.TYPE )
     @Retention ( RetentionPolicy.RUNTIME )
     @Repeatable ( Filters. class )
     public @interface Filter {
         String value();
     };
     
     @Filter ( "filter1" )
     @Filter ( "filter2" )
     public interface Filterable {       
     }
     
     public static void main(String[] args) {
         for ( Filter filter: Filterable. class .getAnnotationsByType( Filter. class ) ) {
             System.out.println( filter.value() );
         }
     }
}

正如我們看到的,這裏有個使用@Repeatable( Filters.class )註解的註解類Filter,Filters僅僅是Filter註解的數組,但Java編譯器並不想讓程序員意識到Filters的存在。這樣,接口Filterable就擁有了兩次Filter(並沒有提到Filter)註解。

同時,反射相關的API提供了新的函數getAnnotationsByType()來返回重複註解的類型(請注意Filterable.class.getAnnotation( Filters.class )經編譯器處理後將會返回Filters的實例)。

程序輸出結果如下:

1
2
filter1
filter2

更多詳情請參考官方文檔

2.5 更好的類型推測機制

Java 8在類型推測方面有了很大的提高。在很多情況下,編譯器可以推測出確定的參數類型,這樣就能使代碼更整潔。讓我們看一個例子:

1
2
3
4
5
6
7
8
9
10
11
package com.javacodegeeks.java8.type.inference;
 
public class Value< T > {
     public static < T > T defaultValue() {
         return null ;
     }
     
     public T getOrDefault( T value, T defaultValue ) {
         return ( value != null ) ? value : defaultValue;
     }
}

這裏是Value< String >類型的用法。

1
2
3
4
5
6
7
8
package com.javacodegeeks.java8.type.inference;
 
public class TypeInference {
     public static void main(String[] args) {
         final Value< String > value = new Value<>();
         value.getOrDefault( "22" , Value.defaultValue() );
     }
}

Value.defaultValue()的參數類型可以被推測出,所以就不必明確給出。在Java 7中,相同的例子將不會通過編譯,正確的書寫方式是 Value.< String >defaultValue()。

2.6 擴展註解的支持

Java 8擴展了註解的上下文。現在幾乎可以爲任何東西添加註解:局部變量、泛型類、父類與接口的實現,就連方法的異常也能添加註解。下面演示幾個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.javacodegeeks.java8.annotations;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
 
public class Annotations {
     @Retention ( RetentionPolicy.RUNTIME )
     @Target ( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
     public @interface NonEmpty {       
     }
         
     public static class Holder< @NonEmpty T > extends @NonEmpty Object {
         public void method() throws @NonEmpty Exception {          
         }
     }
         
     @SuppressWarnings ( "unused" )
     public static void main(String[] args) {
         final Holder< String > holder = new @NonEmpty Holder< String >();      
         @NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();      
     }
}

ElementType.TYPE_USE和ElementType.TYPE_PARAMETER是兩個新添加的用於描述適當的註解上下文的元素類型。在Java語言中,註解處理API也有小的改動來識別新增的類型註解。

3. Java編譯器的新特性

3.1 參數名字

很長一段時間裏,Java程序員一直在發明不同的方式使得方法參數的名字能保留在Java字節碼中,並且能夠在運行時獲取它們(比如,Paranamer類庫)。最終,在Java 8中把這個強烈要求的功能添加到語言層面(通過反射API與Parameter.getName()方法)與字節碼文件(通過新版的javac的–parameters選項)中。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.javacodegeeks.java8.parameter.names;
 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
     public static void main(String[] args) throws Exception {
         Method method = ParameterNames. class .getMethod( "main" , String[]. class );
         for ( final Parameter parameter: method.getParameters() ) {
             System.out.println( "Parameter: " + parameter.getName() );
         }
     }
}

如果不使用–parameters參數來編譯這個類,然後運行這個類,會得到下面的輸出:

1
Parameter: arg0

如果使用–parameters參數來編譯這個類,程序的結構會有所不同(參數的真實名字將會顯示出來):

1
Parameter: args

對於有經驗的Maven用戶,通過maven-compiler-plugin的配置可以將-parameters參數添加到編譯器中去。

1
2
3
4
5
6
7
8
9
10
< plugin >
     < groupId >org.apache.maven.plugins</ groupId >
     < artifactId >maven-compiler-plugin</ artifactId >
     < version >3.1</ version >
     < configuration >
         < compilerArgument >-parameters</ compilerArgument >
         < source >1.8</ source >
         < target >1.8</ target >
     </ configuration >
</ plugin >

針對Java 8最新發布的Eclipse Kepler SR2(請檢查這裏的下載說明)提供了非常實用的配置選項,可以通過下圖的配置方式來控制編譯器行爲

圖1. 配置Eclipse工程使之支持Java 8編譯器的新特性——parameters參數

此外,Parameter類有一個很方便的方法isNamePresent()來驗證是否可以獲取參數的名字。

4. Java 類庫的新特性

Java 8 通過增加大量新類,擴展已有類的功能的方式來改善對併發編程、函數式編程、日期/時間相關操作以及其他更多方面的支持。

4.1 Optional

到目前爲止,臭名昭著的空指針異常是導致Java應用程序失敗的最常見原因。以前,爲了解決空指針異常,Google公司著名的Guava項目引入了Optional類,Guava通過使用檢查空值的方式來防止代碼污染,它鼓勵程序員寫更乾淨的代碼。受到Google Guava的啓發,Optional類已經成爲Java 8類庫的一部分。

Optional實際上是個容器:它可以保存類型T的值,或者僅僅保存null。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。更多詳情請參考官方文檔

我們下面用兩個小例子來演示如何使用Optional類:一個允許爲空值,一個不允許爲空值。

1
2
3
4
Optional< String > fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );       
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );

如果Optional類的實例爲非空值的話,isPresent()返回true,否從返回false。爲了防止Optional爲空值,orElseGet()方法通過回調函數來產生一個默認值。map()函數對當前Optional的值進行轉化,然後返回一個新的Optional實例。orElse()方法和orElseGet()方法類似,但是orElse接受一個默認值而不是一個回調函數。下面是這個程序的輸出:

1
2
3
Full Name is set ? false
Full Name: [none]
Hey Stranger!

讓我們來看看另一個例子:

1
2
3
4
5
Optional< String > firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );       
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();

下面是程序的輸出:

1
2
3
First Name is set ? true
First Name: Tom
Hey Tom!

更多詳情請參考官方文檔

4.2 Stream

最新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中。這是目前爲止對Java類庫最好的補充,因爲Stream API可以極大提供Java程序員的生產力,讓程序員寫出高效率、乾淨、簡潔的代碼。

Stream API極大簡化了集合框架的處理(但它的處理的範圍不僅僅限於集合框架的處理,這點後面我們會看到)。讓我們以一個簡單的Task類爲例進行介紹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Streams  {
     private enum Status {
         OPEN, CLOSED
     };
     
     private static final class Task {
         private final Status status;
         private final Integer points;
 
         Task( final Status status, final Integer points ) {
             this .status = status;
             this .points = points;
         }
         
         public Integer getPoints() {
             return points;
         }
         
         public Status getStatus() {
             return status;
         }
         
         @Override
         public String toString() {
             return String.format( "[%s, %d]" , status, points );
         }
     }
}

Task類有一個分數的概念(或者說是僞複雜度),其次是還有一個值可以爲OPEN或CLOSED的狀態.讓我們引入一個Task的小集合作爲演示例子:

1
2
3
4
5
final Collection< Task > tasks = Arrays.asList(
     new Task( Status.OPEN, 5 ),
     new Task( Status.OPEN, 13 ),
     new Task( Status.CLOSED, 8 )
);

我們下面要討論的第一個問題是所有狀態爲OPEN的任務一共有多少分數?在Java 8以前,一般的解決方式用foreach循環,但是在Java 8裏面我們可以使用stream:一串支持連續、並行聚集操作的元素。

1
2
3
4
5
6
7
8
// Calculate total points of all active tasks using sum()
final long totalPointsOfOpenTasks = tasks
     .stream()
     .filter( task -> task.getStatus() == Status.OPEN )
     .mapToInt( Task::getPoints )
     .sum();
         
System.out.println( "Total points: " + totalPointsOfOpenTasks );

程序在控制檯上的輸出如下:

1
Total points: 18

這裏有幾個注意事項。第一,task集合被轉換化爲其相應的stream表示。然後,filter操作過濾掉狀態爲CLOSED的task。下一步,mapToInt操作通過Task::getPoints這種方式調用每個task實例的getPoints方法把Task的stream轉化爲Integer的stream。最後,用sum函數把所有的分數加起來,得到最終的結果。

在繼續講解下面的例子之前,關於stream有一些需要注意的地方(詳情在這裏).stream操作被分成了中間操作與最終操作這兩種。

中間操作返回一個新的stream對象。中間操作總是採用惰性求值方式,運行一個像filter這樣的中間操作實際上沒有進行任何過濾,相反它在遍歷元素時會產生了一個新的stream對象,這個新的stream對象包含原始stream
中符合給定謂詞的所有元素。

像forEach、sum這樣的最終操作可能直接遍歷stream,產生一個結果或副作用。當最終操作執行結束之後,stream管道被認爲已經被消耗了,沒有可能再被使用了。在大多數情況下,最終操作都是採用及早求值方式,及早完成底層數據源的遍歷。

stream另一個有價值的地方是能夠原生支持並行處理。讓我們來看看這個算task分數和的例子。

1
2
3
4
5
6
7
8
// Calculate total points of all tasks
final double totalPoints = tasks
    .stream()
    .parallel()
    .map( task -> task.getPoints() ) // or map( Task::getPoints )
    .reduce( 0 , Integer::sum );
     
System.out.println( "Total points (all tasks): " + totalPoints );

這個例子和第一個例子很相似,但這個例子的不同之處在於這個程序是並行運行的,其次使用reduce方法來算最終的結果。
下面是這個例子在控制檯的輸出:

1
Total points (all tasks): 26.0

經常會有這個一個需求:我們需要按照某種準則來對集合中的元素進行分組。Stream也可以處理這樣的需求,下面是一個例子:

1
2
3
4
5
// Group tasks by their status
final Map< Status, List< Task > > map = tasks
     .stream()
     .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

這個例子的控制檯輸出如下:

1
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

讓我們來計算整個集合中每個task分數(或權重)的平均值來結束task的例子。

1
2
3
4
5
6
7
8
9
10
11
12
// Calculate the weight of each tasks (as percent of total points)
final Collection< String > result = tasks
     .stream()                                        // Stream< String >
     .mapToInt( Task::getPoints )                     // IntStream
     .asLongStream()                                  // LongStream
     .mapToDouble( points -> points / totalPoints )   // DoubleStream
     .boxed()                                         // Stream< Double >
     .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
     .mapToObj( percentage -> percentage + "%" )      // Stream< String>
     .collect( Collectors.toList() );                 // List< String >
         
System.out.println( result );

下面是這個例子的控制檯輸出:

1
[19%, 50%, 30%]

最後,就像前面提到的,Stream API不僅僅處理Java集合框架。像從文本文件中逐行讀取數據這樣典型的I/O操作也很適合用Stream API來處理。下面用一個例子來應證這一點。

1
2
3
4
final Path path = new File( filename ).toPath();
try ( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
     lines.onClose( () -> System.out.println( "Done!" ) ).forEach( System.out::println );
}

對一個stream對象調用onClose方法會返回一個在原有功能基礎上新增了關閉功能的stream對象,當對stream對象調用close()方法時,與關閉相關的處理器就會執行。

Stream API、Lambda表達式方法引用

相關文章
相關標籤/搜索