1. 簡介
毫無疑問,Java 8是Java自Java 5(發佈於2004年)之後的最重要的版本。這個版本包含語言、編譯器、庫、工具和JVM等方面的十多個新特性。
在本文中我們將學習這些新特性,並用實際的例子說明在什麼場景下適合使用。
這個教程包含Java開發者經常面對的幾類問題:
- 語言
- 編譯器
- 庫
- 工具
- 運行時(JVM)
2. Java語言的新特性
Java 8是Java的一個重大版本,有人認爲,雖然這些新特性領Java開發人員十分期待,但同時也需要花不少精力去學習。在這一小節中,我們將介紹Java 8的大部分新特性。
2.1 Lambda表達式和函數式接口
Lambda表達式(也稱爲閉包)是Java 8中最大和最令人期待的語言改變。它允許我們將函數當成參數傳遞給某個方法,或者把代碼本身當作數據處理:函數式開發者非常熟悉這些概念。
(parameters參數) -> expression表達式或方法體
paramaters:類似方法中的形參列表,這裏的參數是函數式接口裏的參數 ->:可理解爲“被用於”的意思
Lambda的設計耗費了很多時間和很大的社區力量,最終找到一種折中的實現方案,可以實現簡潔而緊湊的語言結構。
Lambda表達式可以引用類成員和局部變量(會將這些變量隱式得轉換成final的),例如下列兩個代碼塊的效果完全相同:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
和
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
匿名傳統內部類與使用lambad比較
// 使用 java 7 排序(傳統的使用匿名內部類)
private void sortUsingJava7(List<String> names) {
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
}
// 使用 java 8 排序(使用lambad)
private void sortUsingJava8(List<String> names) {
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
}
公共定義的函數式接口
a.功能性接口:Function<T, R>,有輸入參數,有返回值,是對接收一個T類型參數,返回R類型的結果的方法的抽象,通過調用apply方法執行內容
需求:給定一個字符串,返回字符串長度
public class Main {
public static void main(String[] args) {
String str = "helloworld";
int length = testFun(str, (s) -> s.length());
System.out.println(length);
}
public static int testFun(String str, Function<String, Integer> fun) {
// 執行
Integer length = fun.apply(str);
return length;
}
}
實例方法引用
排序後輸出
b.消費型接口:Consumer<T>
有輸入參數,沒返回值
對應的方法類型爲接收一個參數,沒有返回值
一般來說使用Consumer接口往往伴隨着一些期望狀態的改變
或者事件的發生,典型的forEach就是使用的Consumer接口
雖然沒有任何的返回值,但是向控制檯輸出結果
Consumer 使用accept對參數執行行爲
public class Main {
public static void main(String[] args) {
String str = "helloworld";
testCon(str,(s)->System.out.println(s));
}
public static void testCon(String str, Consumer<String> con) {
// 執行
con.accept(str);
}
}
c.供給型接口:Supplier<T>,無傳入參數,有返回值,該接口對應的方法類型不接受參數,但是提供一個返回值,使用get()方法獲得這個返回值
public class Main {
public static void main(String[] args) {
String str = "helloworld";
String resultStr = testSup(()->str);
System.out.println(resultStr);
}
public static String testSup(Supplier<String> sup) {
// 執行
String s = sup.get();
return s;
}
}
d.斷言型接口:Predicate<T>
有傳入參數,有返回值Boolean
該接口對應的方法爲接收一個參數,返回一個Boolean類型值
多用於判斷與過濾,使用test()方法執行這段行爲
需求:輸入字符串,判斷長度是否大於0
public class Main {
public static void main(String[] args) {
String str = "helloworld";
Boolean flg = testSup(str,(s)->str.length()>0);
if (flg){
System.out.println("字符串大於0");
}else {
System.out.println("字符串小於0");
}
}
public static Boolean testSup(String str,Predicate<String> pre) {
// 執行
Boolean b = pre.test(str);
return b;
}
}
2.2 lambad表達式優點
a.極大的減少代碼冗餘,同時可讀性也好過冗長的匿名內部類
b.與集合類批處理操作結合,實現內部迭代,並充分利用現代多核CPU進行並行計算。之前集合類的迭代都是外部的,即客戶代碼。而內部迭代意味着由Java類庫來進行迭代,而不是客戶代碼
流的性能測試:https://mp.weixin.qq.com/s/-p-N-K_oY-vGtvBIKUqShA
2. Java官方庫的新特性
Java 8增加了很多新的工具類(date/time類),並擴展了現存的工具類,以支持現代的併發編程、函數式編程等。
2.1 Optional
Java應用中最常見的bug就是空值異常。在Java 8之前,Google Guava引入了Optionals類來解決NullPointerException,從而避免源碼被各種null檢查污染,以便開發者寫出更加整潔的代碼。Java 8也將Optional加入了官方庫。
接下來看一點使用Optional的例子:可能爲空的值或者某個類型的值:
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;orElseGet()方法,Optional實例持有null,則可以接受一個lambda表達式生成的默認值;map()方法可以將現有的Opetional實例的值轉換成新的值;orElse()方法與orElseGet()方法類似,但是在持有null的時候返回傳入的默認值。
上述代碼的輸出結果如下:
Full Name is set? false
Full Name: [none]
Hey Stranger!
2.2 Streams
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
strings.forEach(s -> System.out.println(s));//全部輸出
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());//經過過濾後輸出
filtered.forEach(s -> System.out.println(s));
A.流中使用forEach來迭代流中的每個數據
Random random = new Random();
random.ints().limit(10).forEach(System.out :: println);
List<String> strings1 = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
long count = strings1.stream().filter(s -> s.isEmpty()).count();
B.Filter方法用於通過設置的條件過濾出元素(見上面程序)
C.limit 方法用於獲取指定數量的流見下面程序(獲取10個數)
Random random2 = new Random();
random2.ints().limit(10).sorted().forEach(System.out::println);
D.sorted 方法用於對流進行排序,見上面程序
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
Long count2 = strings.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println("並行(parallel)程序空字符串的數量" + count2);
E.並行處理字符串(與fork/join原理一樣,利用多核CPU)
List<String> strings3 = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered1 = strings3.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList());
System.out.println("篩選列表: " + filtered1);
//將流中的元素放置到一個列表集合中去。這個列表默認爲ArrayList
String mergedString = strings3.stream().filter(s -> !s.isEmpty()).collect(Collectors.joining(","));
System.out.println("合併字符串: " + mergedString);
//joining的目的是將流中的元素全部以字符序列的方式連接到一起,可以指定連接符,甚至是結果的前後綴。
2.3 Nashorn JavaScript引擎
Java 8提供了新的Nashorn JavaScript引擎,使得我們可以在JVM上開發和運行JS應用。Nashorn JavaScript引擎是javax.script.ScriptEngine的另一個實現版本,這類Script引擎遵循相同的規則,允許Java和JavaScript交互使用,例子代碼如下:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
這個代碼的輸出結果如下:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
2.4 Base64
對Base64編碼的支持已經被加入到Java 8官方庫中,這樣不需要使用第三方庫就可以進行Base64編碼,例子代碼如下:
package com.javacodegeeks.java8.base64;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
這個例子的輸出結果如下:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
新的Base64API也支持URL和MINE的編碼解碼。
(Base64.getUrlEncoder() / Base64.getUrlDecoder(), Base64.getMimeEncoder() / Base64.getMimeDecoder())。
BASE64不是用來加密的,是BASE64編碼後的字符串,例如:在上傳圖片時候就會將圖片轉成base64,之前用過的freemark生成world文檔就是。全部都是由標準鍵盤上面的常規字符組成,這樣編碼後的字符串在網關之間傳遞不會產生UNICODE字符串不能識別或者丟失的現象。你再仔細研究下EMAIL就會發現其實EMAIL就是用base64編碼過後再發送的。然後接收的時候再還原。
2.5 並行數組
Java8版本新增了很多新的方法,用於支持並行數組處理。最重要的方法是parallelSort(),可以顯著加快多核機器上的數組排序。下面的例子論證了parallexXxx系列的方法:
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );//取前10位數
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );//排序後取前10位數
System.out.println();
}
}
上述這些代碼使用parallelSetAll()方法生成20000個隨機數,然後使用parallelSort()方法進行排序。這個程序會輸出亂序數組和排序數組的前10個元素。上述例子的代碼輸出的結果是:
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
3. 新的Java工具
Java 8提供了一些新的命令行工具,這部分會講解一些對開發者最有用的工具。
3.1 Nashorn引擎:jjs
jjs是一個基於標準Nashorn引擎的命令行工具,可以接受js源碼並執行。例如,我們寫一個func.js文件,內容如下:
function f() {
return 1;
};
print( f() + 1 );
可以在命令行中執行這個命令:jjs func.js
,控制檯輸出結果是:
2
如果需要了解細節,可以參考官方文檔。
3.2 類依賴分析器:jdeps
jdeps是一個相當棒的命令行工具,它可以展示包層級和類層級的Java類依賴關係,它以.class文件、目錄或者Jar文件爲輸入,然後會把依賴關係輸出到控制檯。
我們可以利用jedps分析下Spring Framework庫,爲了讓結果少一點,僅僅分析一個JAR文件:org.springframework.core-3.0.5.RELEASE.jar。
jdeps org.springframework.core-3.0.5.RELEASE.jar
這個命令會輸出很多結果,我們僅看下其中的一部分:依賴關係按照包分組,如果在classpath上找不到依賴,則顯示"not found".
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
-> java.io
-> java.lang
-> java.lang.annotation
-> java.lang.ref
-> java.lang.reflect
-> java.util
-> java.util.concurrent
-> org.apache.commons.logging not found
-> org.springframework.asm not found
-> org.springframework.asm.commons not found
org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
-> java.lang
-> java.lang.annotation
-> java.lang.reflect
-> java.util
其他補充
Lambda的設計者們爲了讓現有的功能與Lambda表達式良好兼容,考慮了很多方法,於是產生了函數接口這個概念。函數接口指的是隻有一個函數的接口,這樣的接口可以隱式轉換爲Lambda表達式。java.lang.Runnable和java.util.concurrent.Callable是函數式接口的最佳例子。在實踐中,函數式接口非常脆弱:只要某個開發者在該接口中添加一個函數,則該接口就不再是函數式接口進而導致編譯失敗。爲了克服這種代碼層面的脆弱性,並顯式說明某個接口是函數式接口,Java 8 提供了一個特殊的註解@FunctionalInterface(Java 庫中的所有相關接口都已經帶有這個註解了),舉個簡單的函數式接口的定義:
@FunctionalInterface
public interface Functional {
void method();
}
不過有一點需要注意,默認方法和靜態方法不會破壞函數式接口的定義,因此如下的代碼是合法的。
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
Lambda表達式作爲Java 8的最大賣點,它有潛力吸引更多的開發者加入到JVM平臺,並在純Java編程中使用函數式編程的概念。如果你需要了解更多Lambda表達式的細節,可以參考官方文檔。
Date/Time API(JSR 310)
Java 8引入了新的Date-Time API(JSR 310)來改進時間、日期的處理。時間和日期的管理一直是最令Java開發者痛苦的問題。java.util.Date和後來的java.util.Calendar一直沒有解決這個問題(甚至令開發者更加迷茫)。
1、本地化日期時間 API
public class Main {
public static void main(String[] args) {
Main java8tester = new Main();
java8tester.testLocalDateTime();
}
public void testLocalDateTime(){
// 獲取當前的日期時間
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("當前時間: " + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);
// 22 小時 15 分鐘
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);
}
}
輸出結果
2、使用時區的日期時間API
public class Main {
public static void main(String[] args) {
Main java8tester = new Main();
java8tester.testZonedDateTime();
}
public void testZonedDateTime(){
// 獲取當前時間日期
ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date1: " + date1);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("當期時區: " + currentZone);
}
}
待分析: