閱讀805 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java 8 特性 – 終極手冊(二)

4.2 Stream

新增加的Stream API (java.util.stream)引入了在Java裏可以工作的函數式編程。這是目前為止對java庫最大的一次功能添加,希望程序員通過編寫有效、整潔和簡明的代碼,能夠大大提高生產率。

Stream API讓集合處理簡化了很多(我們後麵會看到不僅限於Java集合類)。讓我們從一個簡單的類Task開始來看看Stream的用法。

01 public class Streams {
02 private enum Status {
03 OPEN, CLOSED
04 };
05  
06 private static final class Task {
07 private final Status status;
08 private final Integer points;
09  
10 Task( final Status status, final Integer points ) {
11 this.status = status;
12 this.points = points;
13 }
14  
15 public Integer getPoints() {
16 return points;
17 }
18  
19 public Status getStatus() {
20 return status;
21 }
22  
23 @Override
24 public String toString() {
25 return String.format( "[%s, %d]", status, points );
26 }
27 }
28 }

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

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

第一個問題是所有的開放的Task的點數是多少?在java 8 之前,通常的做法是用foreach迭代。但是Java8裏頭我們會用Stream。Stream是多個元素的序列,支持串行和並行操作。

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

控製台的輸出將會是:

Total points: 18
上麵代碼執行的流程是這樣的,首先Task集合會被轉化為Stream表示,然後filter操作會過濾掉所有關閉的Task,接下來使用Task::getPoints 方法取得每個Task實例的點數,mapToInt方法會把Task Stream轉換成Integer Stream,最後使用Sum方法將所有的點數加起來得到最終的結果。

在我們看下一個例子之前,我們要記住一些關於Stream的說明。Stream操作被分為中間操作和終點操作。

中間操作返回一個新的Stream。這些中間操作是延遲的,執行一個中間操作比如filter實際上不會真的做過濾操作,而是創建一個新的Stream,當這個新的Stream被遍曆的時候,它裏頭會包含有原來Stream裏符合過濾條件的元素。

終點操作比如說forEach或者sum會遍曆Stream從而產生最終結果或附帶結果。終點操作執行完之後,Stream管道就被消費完了,不再可用。在幾乎所有的情況下,終點操作都是即時完成對數據的遍曆操作。

Stream的另外一個價值是Stream創造性地支持並行處理。讓我們看看下麵這個例子,這個例子把所有task的點數加起來。

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

這個例子跟上麵那個非常像,除了這個例子裏使用了parallel()方法       並且計算最終結果的時候使用了reduce方法。

輸出如下:

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

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

控製台的輸出如下:

{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
讓我們來計算整個集合中每個task分數(或權重)的平均值來結束task的例子。

01 // Calculate the weight of each tasks (as percent of total points)
02 final Collection< String > result = tasks
03     .stream()                                        // Stream< String >
04     .mapToInt( Task::getPoints )                     // IntStream
05     .asLongStream()                                  // LongStream
06     .mapToDouble( points -> points / totalPoints )   // DoubleStream
07     .boxed()                                         // Stream< Double >
08     .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
09     .mapToObj( percentage -> percentage + "%" )      // Stream< String>
10     .collect( Collectors.toList() );                 // List< String >
11           
12 System.out.println( result );

控製台輸出如下:

[19%, 50%, 30%]

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

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

Stream的方法onClose 返回一個等價的有額外句柄的Stream,當Stream的close()方法被調用的時候這個句柄會被執行。

Stream API、Lambda表達式還有接口默認方法和靜態方法支持的方法引用,是Java 8對軟件開發的現代範式的響應。

 

4.3日期時間API(JSR310

 Java 8引入了新的日期時間API(JSR 310)改進了日期時間的管理。日期和時間管理一直是Java開發人員最痛苦的問題。java.util.Date和後來的java.util.Calendar一點也沒有改變這個情況(甚至讓人們更加迷茫)。

因為上麵這些原因,產生了Joda-Time ,可以替換Java的日期時間API。Joda-Time深刻影響了 Java 8新的日期時間API,Java 8吸收了Joda-Time 的精華。新的java.time包包含了所有關於日期、時間、日期時間、時區、Instant(跟日期類似但精確到納秒)、duration(持續時間)和時鍾操作的類。設計這些API的時候很認真地考慮了這些類的不變性(從java.util.Calendar吸取的痛苦教訓)。如果需要修改時間對象,會返回一個新的實例。

讓我們看看一些關鍵的類和用法示例。第一個類是Clock,Clock使用時區來訪問當前的instant, date和time。Clock類可以替換 System.currentTimeMillis() 和 TimeZone.getDefault().

1 // Get the system clock as UTC offset
2 final Clock clock = Clock.systemUTC();
3 System.out.println( clock.instant() );
4 System.out.println( clock.millis() );

控製台輸出如下:

2014-04-12T15:19:29.282Z
1397315969360

其他類我們看看LocalTime和LocalDate。LocalDate隻保存有ISO-8601日期係統的日期部分,有時區信息,相應地,LocalTime隻保存ISO-8601日期係統的時間部分,沒有時區信息。LocalDate和LocalTime都可以從Clock對象創建。

01 // Get the local date and local time
02 final LocalDate date = LocalDate.now();
03 final LocalDate dateFromClock = LocalDate.now( clock );
04  
05 System.out.println( date );
06 System.out.println( dateFromClock );
07  
08 // Get the local date and local time
09 final LocalTime time = LocalTime.now();
10 final LocalTime timeFromClock = LocalTime.now( clock );
11  
12 System.out.println( time );
13 System.out.println( timeFromClock );

控製台輸出如下:

2014-04-12
2014-04-12
11:25:54.568
15:25:54.568

LocalDateTime類合並了LocalDate和LocalTime,它保存有ISO-8601日期係統的日期和時間,但是沒有時區信息。讓我們看一個簡單的例子。

1 // Get the local date/time
2 final LocalDateTime datetime = LocalDateTime.now();
3 final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
4  
5 System.out.println( datetime );
6 System.out.println( datetimeFromClock );

輸出如下:

2014-04-12T11:37:52.309
2014-04-12T15:37:52.309

如果您需要一個類持有日期時間和時區信息,可以使用ZonedDateTime,它保存有ISO-8601日期係統的日期和時間,而且有時區信息。讓我們看一些例子:

1 // Get the zoned date/time
2 final ZonedDateTime zonedDatetime = ZonedDateTime.now();
3 final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
4 final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
5  
6 System.out.println( zonedDatetime );
7 System.out.println( zonedDatetimeFromClock );
8 System.out.println( zonedDatetimeFromZone );

輸出如下:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]

最後讓我們看看Duration類,Duration持有的時間精確到納秒。它讓我們很容易計算兩個日期中間的差異。讓我們來看一下:

1 // Get duration between two dates
2 final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16000 );
3 final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16235959 );
4  
5 final Duration duration = Duration.between( from, to );
6 System.out.println( "Duration in days: " + duration.toDays() );
7 System.out.println( "Duration in hours: " + duration.toHours() );

上麵的例子計算了兩個日期(2014年4月16日和2014年5月16日)之間的持續時間(基於天數和小時)輸出如下:

Duration in days: 365
Duration in hours: 8783

對於Java 8的新日期時間的總體印象還是比較積極的。一部分是因為有經曆實戰的Joda-Time的基礎,還有一部分是因為日期時間終於被認真對待而且聽取了開發人員的聲音。關於更多的詳細信息,請參考官方文檔

 

4.4   Nashorn javascript引擎

Java 8提供了一個新的Nashorn javascript引擎,它允許我們在JVM上運行特定的javascript應用。Nashorn javascript引擎隻是javax.script.ScriptEngine另一個實現,而且規則也一樣,允許Java和JavaScript互相操作。這裏有個小例子:

1 ScriptEngineManager manager = new ScriptEngineManager();
2 ScriptEngine engine = manager.getEngineByName( "JavaScript" );
3  
4 System.out.println( engine.getClass().getName() );
5 System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );

輸出如下:

jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
4.5   Base64

對Base64的支持最終成了Java 8標準庫的一部分,非常簡單易用:

01 package com.javacodegeeks.java8.base64;
02  
03 import java.nio.charset.StandardCharsets;
04 import java.util.Base64;
05  
06 public class Base64s {
07 public static void main(String[] args) {
08 final String text = "Base64 finally in Java 8!";
09  
10 final String encoded = Base64
11 .getEncoder()
12 .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
13 System.out.println( encoded );
14  
15 final String decoded = new String(
16 Base64.getDecoder().decode( encoded ),
17 StandardCharsets.UTF_8 );
18 System.out.println( decoded );
19 }
20 }

控製台輸出的編碼和解碼的字符串

QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!

新的Base64API也支持URL和MINE的編碼解碼。

(Base64.getUrlEncoder() / Base64.getUrlDecoder()Base64.getMimeEncoder() / Base64.getMimeDecoder()).

 

4.6   並行數組

Java 8新增加了很多方法支持並行的數組處理。最重要的大概是parallelSort()這個方法顯著地使排序在多核計算機上速度加快。下麵的小例子演示了這個新的方法(parallelXXX)的行為。

01 </pre>
02 <pre class="brush:java">package com.javacodegeeks.java8.parallel.arrays;
03  
04 import java.util.Arrays;
05 import java.util.concurrent.ThreadLocalRandom;
06  
07 public class ParallelArrays {
08     public static void main( String[] args ) {
09         long[] arrayOfLong = new long 20000 ];       
10  
11         Arrays.parallelSetAll( arrayOfLong,
12             index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
13         Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
14             i -> System.out.print( i + " " ) );
15         System.out.println();
16  
17         Arrays.parallelSort( arrayOfLong );
18         Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
19             i -> System.out.print( i + " " ) );
20         System.out.println();
21     }
22 }</pre>
23 <pre>

這一小段代碼使用parallelSetAll() t方法填充這個長度是2000的數組,然後使用parallelSort() 排序。這個程序輸出了排序前和排序後的10個數字來驗證數組真的已經被排序了。示例可能的輸出如下(請注意這些數字是隨機產生的)

Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793

4.7   並發

在新增Stream機製與lambda的基礎之上,在java.util.concurrent.ConcurrentHashMap中加入了一些新方法來支持聚集操作。同時也在java.util.concurrent.ForkJoinPool類中加入了一些新方法來支持共有資源池(common pool)(請查看我們關於Java 並發的免費課程)。

新增的java.util.concurrent.locks.StampedLock類提供一直基於容量的鎖,這種鎖有三個模型來控製讀寫操作(它被認為是不太有名的java.util.concurrent.locks.ReadWriteLock類的替代者)。

在java.util.concurrent.atomic包中還增加了下麵這些類:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

5.  新的工具

Java 8 提供了一些新的命令行工具,在這節裏我們將會介紹它們中最有趣的部分。

5.1  Nashorn引擎:jjs

jjs是個基於Nashorn引擎的命令行工具。它接受一些JavaScript源代碼為參數,並且執行這些源代碼。例如,我們創建一個具有如下內容的func.js文件:

 

1 </span>
2 <pre>
3 function f() {
4 return 1;
5 };
6  
7 print( f() + 1 );

我們可以把這個文件作為參數傳遞給jjs使得這個文件可以在命令行中執行

1 <span style="font-size: 13px;">jjs func.js</span>

輸出結果如下

2

更多的詳細信息請參考官方文檔

 

5.2 類依賴分析工具:jdeps

Jdeps是一個功能強大的命令行工具,它可以幫我們顯示出包層級或者類層級java類文件的依賴關係。它接受class文件、目錄、jar文件作為輸入,默認情況下,jdeps會輸出到控製台。

作為例子,讓我們看看現在很流行的Spring框架的庫的依賴關係報告。為了讓報告短一些,我們隻分析一個jar:org.springframework.core-3.0.5.RELEASE.jar.

jdeps org.springframework.core-3.0.5.RELEASE.jar 這個命令輸出內容很多,我們隻看其中的一部分,這些依賴關係根絕包來分組,如果依賴關係在classpath裏找不到,就會顯示not found.

01 </pre>
02 <pre class="brush:java">org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
03    org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
04       -> java.io
05       -> java.lang
06       -> java.lang.annotation
07       -> java.lang.ref
08       -> java.lang.reflect
09       -> java.util
10       -> java.util.concurrent
11       -> org.apache.commons.logging                         not found
12       -> org.springframework.asm                            not found
13       -> org.springframework.asm.commons                    not found
14    org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
15       -> java.lang
16       -> java.lang.annotation
17       -> java.lang.reflect
18       -> java.util</pre>
19 <pre>

更多的詳細信息請參考官方文檔

6. JVM的新特性

JVM內存永久區已經被metaspace替換(JEP 122)。JVM參數 -XX:PermSize 和 –XX:MaxPermSizeXX:MetaSpaceSize 和 -XX:MaxMetaspaceSize代替

7. 結論

更多展望:Java 8通過發布一些可以增加程序員生產力的特性來推進這個偉大的平台的進步。現在把生產環境遷移到Java 8還為時尚早,但是在接下來的幾個月裏,它會被大眾慢慢的接受。毫無疑問,現在是時候讓你的代碼與Java 8兼容,並且在Java 8足夠安全穩定的時候遷移到Java 8。

作為社區對Java 8的認可,最近Pivotal發布了可在生產環境下支持Java 8的Spring Framework 4.0.3

如果你喜歡這篇文章,請訂閱我們的郵件列表來查看每周的更新以及免費贈送的白皮書。對於更高級的教程,請查看我們的[JCG學院][JCG]。

我們歡迎你對Java 8中激動人心的特性進行評論!

 8. 資源

下麵一些文章從不同層麵上深度討論了Java 8的特性

最後更新:2017-05-24 09:31:43

  上一篇:go  免費OA係統有真正意義上的永久免費嗎?
  下一篇:go  Java 8 特性 – 終極手冊(一)