Java 8 (又稱為 jdk 1.8) 是 Java 語言開發的一個主要版本。 Oracle 公司于 2014 年 3 月 18 日釋出 Java 8 ,它支援函數式程式設計,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。(文章很長,建議點贊收藏)
新特性
以下是Java 8 新增的部分特性,更多新特性了解請詳細參考:What's New in JDK 8
• Lambda 表達式
• 方法引用
• 函數式接口
• 預設方法
• Stream
• Optional 類
• Nashorn, JavaScript 引擎
• Date/Time API 新的日期時間 API
• Base64
• 新工具 − 新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器jdeps。
一、Lambda 表達式
Lambda 表達式(也可稱為閉包),它是推動 Java 8 釋出的最重要新特性。
Lambda 表達式允許把
函數作為一個方法的參數
,可以取代大部分的匿名内部類,寫出更優雅的 Java 代碼,尤其在集合的周遊和其他集合操作中,可以極大地優化代碼結構。
什麼是 Lambda 表達式
先來看看 Lambda 表達式的官方解釋:
Lambda 表達式(lambda expression)是一個匿名函數,Lambda表達式基于數學中的λ演算得名,直接對應于其中的lambda抽象(lambda abstraction),是一個
匿名函數
,即沒有函數名的函數。
這樣的解釋還是讓人摸不着頭腦,那我們接着往下看。
首先介紹, Lambda 表達式的文法格式:
() -> {}
其中 () 用來描述參數清單,{} 用來描述方法體,-> 為 lambda運算符 ,讀作(goes to)。
簡單例子:
(int x, int y) -> x + y //接收2個int型整數,傳回他們的和
(String s) -> System.out.print(s) // 接受一個 string 對象,并在控制台列印,不傳回任何值(看起來像是傳回void)
前面我們說了 Lambda 表達式可以取代大部分的匿名内部類,舉個例子:
@FunctionalInterface
public interface NoReturnMultiParam {
void method(int a, int b);
}
public class Test {
public void sayHello() {
// 匿名類實作NoReturnMultiParam接口
NoReturnMultiParam noReturnMultiParam = new NoReturnMultiParam() {
@Override
public void method(int a, int b) {
System.out.println("param:{a=" + a + ",b=" + b + "}");
}
};
// 調用接口
noReturnMultiParam.method(1,2);
}
public static void main(String[] args) {
Test test = new Test();
test.sayHello();
}
}
運作結果:
param:{a=1,b=2}
接着,我們将匿名類實作替換為 Lambda表達式
public class Test {
public void sayHello() {
// Lambda實作NoReturnMultiParam接口
NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "}");
// 調用接口
lambda.method(1,2);
}
public static void main(String[] args) {
Test test = new Test();
test.sayHello();
}
}
運作結果與之前相同,可以看到,Lambda 表達式可以來定義行内執行的方法類型接口,免去了使用匿名方法的麻煩。
簡單來說,在 Java 中可以将 Lambda 表達式看成一個接口的實作,但并不是所有的接口都可以使用 Lambda 表達式來實作。
Lambda 規定接口中隻能有一個需要被實作的方法,即
函數式接口
,不是規定接口中隻能有一個方法。
閉包問題
lambda 表達式隻能引用标記了
final
的外層局部變量,這就是說不能在 lambda 内部修改定義在域外的局部變量,否則會編譯錯誤。
public static void main(String[] args) {
int c = 10;
NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "},c="+c);
lambda.method(1,2);
}
這裡c沒有辨別為
final
,但是沒有被後續代碼修改,是以在編譯期間虛拟機會幫我們加上 final 修飾關鍵字(即隐性的具有 final 的語義)
修改代碼,出現錯誤
java: 從lambda 表達式引用的本地變量必須是最終變量或實際上的最終變量
public static void main(String[] args) {
int c = 10;
NoReturnMultiParam lambda = (a, b) -> System.out.println("param:{a=" + a + ",b=" + b + "},c="+c);
c = c + 2;
lambda.method(1,2);
}
二、方法引用
方法引用通過方法的名字來指向一個方法,在使用 Lambda 表達式時,有時候我們不是必須要自己重寫某個匿名内部類的方法,而是可以利用 Lambda 表達式的接口快速指向一個已經被實作的方法。
文法: 方法歸屬者::方法名 靜态方法的歸屬者為類名,普通方法歸屬者為對象
Java 中 4 種不同方法的引用:
• 構造器引用 ClassName::new
• 靜态方法引用 Class::static_method
• 特定類的任意對象的方法引用 Class::method
• 特定對象的方法引用 instance::method
代碼示例:
package com.local.springboot.springbootservice.sysuser;
interface InstanceCreate {
Test get();
}
@FunctionalInterface
interface ReturnMultiParam {
int method(int a, int b);
}
public class Test {
public static int addNum(int a, int b) {
return a + b;
}
public int deleteNum(int a, int b) {
return a - b;
}
public void sayHello() {
System.out.println("Hello World");
}
public static void main(String[] args) {
ReturnMultiParam lambda = (a, b) -> addNum(a, b);
System.out.println(lambda.method(1, 2));
// 構造器引用
InstanceCreate create = Test::new;
System.out.println(create.get());
// 靜态方法引用
ReturnMultiParam result = Test::addNum;
System.out.println(result.method(1, 2));
// 特定對象的方法引用
Test test = new Test();
ReturnMultiParam result2 = test::deleteNum;
System.out.println(result2.method(2, 2));
}
}
運作結果:
3
com.local.springboot.springbootservice.sysuser.Test@179d3b25
3
0
三、函數式接口
上面提到接口中隻有一個需要被實作的方法的接口,叫做函數式接口。
函數式接口需要用
@FunctionalInterface
注解修飾,要求接口中的抽象方法隻有一個,但可以有多個非抽象方法。
函數式接口執行個體
函數式接口可以對現有的函數友好地支援 lambda。比如常用的
Comparator
或者
Consumer
接口。
比如常見的集合内元素的排序
List<Cat> list = new ArrayList<>();
list.add(new Cat(5,"Tom"));
list.add(new Cat(2,"Aimi"));
list.add(new Cat(3,"Doe"));
list.sort(new Comparator<Cat>() {
@Override
public int compare(Cat o1, Cat o2) {
return o1.getIndex()- o2.getIndex();
}
});
list.forEach(item -> {
System.out.println(item.toString());
});
在以前我們若要為集合内的元素排序,就必須調用 sort 方法,傳入比較器匿名内部類重寫 compare 方法,我們現在可以使用 lambda 表達式來簡化代碼。
list.sort((o1,o2)->o1.getIndex()-o2.getIndex());
//list.sort((Comparator.comparing(Cat::getIndex)));
list.forEach(item -> {
System.out.println(item.toString());
});
更多函數式接口
JDK 1.8 之前已有的函數式接口:
•
java.lang.Runnable
•
java.util.concurrent.Callable
•
java.security.PrivilegedAction
•
java.util.Comparator
•
java.io.FileFilter
•
java.nio.file.PathMatcher
•
java.lang.reflect.InvocationHandler
•
java.beans.PropertyChangeListener
•
java.awt.event.ActionListener
•
javax.swing.event.ChangeListener
JDK 1.8 新增加的函數接口:
•
java.util.function
java.util.function 它包含了很多類,用來支援 Java的 函數式程式設計,詳細的函數式接口請檢視源碼。
四、預設方法
簡單說,預設方法就是接口可以有實作方法,而且不需要實作類去實作其方法。
用法:隻需在方法名前面加個
default
關鍵字即可實作預設方法。
被 default 修飾的方法會有預設實作,不是必須被實作的方法,是以不影響 Lambda 表達式的使用
相同的預設方法
當一個類實作多個接口,而且這些接口存在相同的預設方法,會發生什麼情況呢?
interface Interface1{
default void print() {
System.out.println("我是接口1預設方法實作!");
}
}
interface Interface2{
default void print() {
System.out.println("我是接口2預設方法實作!");
}
}
public class InterfaceImpl implements Interface1, Interface2{
@Override
public void print() {
// 可以使用 super 來調用指定接口的預設方法
// Interface1.super.print();
System.out.println("我是接口實作類!");
}
public static void main(String[] args) {
InterfaceImpl interfaceImpl = new InterfaceImpl();
interfaceImpl.print();
}
}
運作結果:
我是接口實作類!
當出現這種接口沖突,一般有兩種解決方案
覆寫重寫接口的預設方法
、
使用 super 來調用指定接口的預設方法
靜态預設方法
Java 8 中接口是可以聲明靜态方法(并且可以提供實作)的。例如:
interface Interface2{
default void print() {
System.out.println("我是接口2預設方法實作!");
}
// 靜态方法
static void doSomething(){
System.out.println("我是接口2中的靜态方法");
}
}
為什麼要有這個特性
說了這麼多,那麼問題來了,為什麼要新增這個特性?
新增預設方法是為了解決接口的修改與現有的實作不相容的問題。
我們都知道接口是面向抽象而不是面向具體程式設計的,是以當需要修改接口時,就需要修改全部實作改接口的類。
是以對于以前釋出的版本,是做不到在修改接口的同時不影響已有的實作。
五、Stream
Java 8 API 添加了一個新的抽象稱為流 Stream,可以讓你以一種
聲明的方式
處理資料。
文法糖 Stream 以一種類似用 SQL 語句從資料庫查詢資料的直覺方式來提供一種對
Java 集合運算和表達的高階抽象
。簡單來說流是Java 8 中對Collection對象功能的加強。
流操作
【資料集合】 ->【資料源】 - > 【轉換(聚合)操作】 ->【終點操作】
• Intermediate(轉換操作):中間操作都會傳回流對象本身。就是說,僅僅調用到這類方法,并沒有真正開始流的周遊。多次的轉換操作隻會在遇到終點操作之後,才會依次執行。
轉換操作:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel...
• Terminal(終點操作):一個流隻能有一個 terminal 操作,當這個操作執行後,流就被使用“光”了,無法再被操作。是以這必定是流的最後一個操作。Terminal 操作的執行,才會真正開始流的周遊,并且會生成一個結果,或者一個 side effect。
終點操作:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny...
• Short-Circuiting(短路操作):短路操作其實和終點操作也是一樣的,可能不再傳回一個流,或是傳回一個被截取過的流。比如anyMatch方法,通過Predicate<T>接口傳回了一個真值。由于流Stream在理論上是無限大的,短路操作被用以對流進行截取,把無限的變成有限的流,比如limit方法,可以限制擷取資料源的數量。
短路操作:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit...
外部疊代與内部疊代
這裡值得一提的是以前對集合周遊都是通過Iterator或者for-each的方式,顯式的在集合外部進行疊代, 這叫做
外部疊代
。與以前的Collection操作不同,流操作提供了
内部疊代
的方式, 通過通路者模式(Visitor)實作。
那什麼是外部疊代和内部疊代呢?舉個簡單的列子:
比如你請人打掃房間,但有覺得不放心,于是你覺得現場訓示勞工先擦桌子,再拖地,最後洗碗...直到打掃完畢,這就是所謂的外部疊代,即顯示地取出元素進行處理。
後來你和清潔勞工熟悉之後,你隻需要和她說把房間打掃幹淨,清潔勞工自己選擇先做什麼,再做什麼,你等着接收成果就行了。這就是内部疊代。
生成流
頂層集合類Collection添加了兩個方法:
stream()
、
parallelStream()
。
• stream() − 為集合建立串行流。
• parallelStream() − 為集合建立并行流。
開啟流計算時根據操作的資料量選擇調用stream()或者parallelStream()
使用執行個體
forEach
Stream 提供了新的方法 'forEach' 來疊代流中的每個資料。
List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.forEach(System.out::println);
map
map 方法用于映射每個元素到對應的結,以下代碼片段使用 map 輸出了元素對應的大寫
List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().map(String::toUpperCase).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println);
filter
filter 方法用于通過設定的條件過濾出元素。把除了
abc
的字元串過濾出來
List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().filter(string -> !"abc".equals(string)).forEach(System.out::println);
limit
limit 方法用于擷取指定數量的流。 列印3條資料
List<String> strings = Arrays.asList("abc", "bc", "efg", "abd","jkl");
strings.stream().limit(3).forEach(System.out::println);
流操作簡單說明
•
filter
:用于過濾出滿足條件的元素
•
distinct
:去重,需要重寫equals()和hashCode()
•
sorted
:對元素進行排序
•
limit
:傳回前n個元素
•
skip
:去掉前n個元素
•
map
:方法用于映射每個元素對應的結果
•
flapMap
:将流中的每一個元素T映射成為一個流,再把每一個流連接配接成一個流
•
anyMatch
:是否存在任意一個元素滿足條件(傳回布爾值)
•
allMatch
:是否所有元素都滿足條件(傳回布爾值)
•
noneMatch
:是否所有元素都不滿足條件(傳回布爾值)
•
findAny
:找到其中一個元素 (使用 stream() 時找到的是第一個元素;使用 parallelStream() 并行時找到的是其中一個元素)
•
findFirst
:找到第一個元素
•
reduce
:用于組合流中的元素,如求和,求積,求最大值等
•
count
:傳回流中元素個數,結果為 long 類型
•
collect
:收集方法,我們很常用的是 collect(toList()),當然還有 collect(toSet()) 等,參數是一個收集器接口
Stream流式計算的使用
說了這麼多,那麼流使用好處以及對性能的影響如何呢?
Stream API 可以極大提高Java程式員的生産力,讓程式員寫出高效率、幹淨、簡潔的代碼。
通過實際示例對比了正常的集合類的過濾、封裝、統計操作,幾百的小資料量操作,正常外部疊代更快;資料量再大一點,stream()串行的流式計算會更快;上萬級别的資料量後,parallelStream()并行流式計算會更快。
六、Optional 類
相信大家在編碼中最常遇見的就是空指針異常,而Optional 類的引入就是為了很好地解決空指針異常。
Optional
是個容器:它可以儲存類型T的值,或者僅僅儲存null。Optional提供很多有用的方法,這樣我們就不用顯式進行空值檢測。
常用類方法
方法 | 描述 |
T get() | 如果在這個Optional中包含這個值,傳回值,否則抛出異常:NoSuchElementException |
void ifPresent(Consumer<? super T> consumer) | 如果值存在則使用該值調用 consumer , 否則不做任何事情。 |
boolean isPresent() | 如果值存在則方法會傳回true,否則傳回 false。 |
<U>Optional<U> map(Function<? super T,? extends U> mapper) | 如果有值,則對其執行調用映射函數得到傳回值。如果傳回值不為 null,則建立包含映射傳回值的Optional作為map方法傳回值,否則傳回空Optional。 |
T orElse(T other) | 如果存在該值,傳回值, 否則傳回 other。 |
T orElseGet(Supplier<? extends T> other) | 如果存在該值,傳回值, 否則觸發 other,并傳回 other 調用的結果。 |
簡單舉例
orElse
存在則傳回
aa
,不存在則傳回
bb
Optional<String> string= Optional.of("aa");
string.orElse("bb");
七、日期時間 API
舊版日期時間API問題:
•
非線程安全
: java.util.Date 是非線程安全的,所有的日期類都是可變的,這是Java日期類最大的問題之一。
•
日期/時間類的定義并不一緻
:在java.util和java.sql的包中都有日期類
•
時區處理麻煩
:日期類并不提供國際化,沒有時區支援
Java 8 在
java.time
包下提供了很多新的 API:
•
Local(本地)
:簡化了日期時間的處理,沒有時區的問題。
•
Zoned(時區
:通過制定的時區處理日期時間。
新的java.time包涵蓋了所有處理日期,時間,日期/時間,時區,時刻(instants),過程(during)與時鐘(clock)的操作。
使用時區的日期時間API
時區使用
ZoneId
來表示,使用靜态方法
of
來擷取時區
// 擷取目前時間日期
ZonedDateTime date = ZonedDateTime.parse("2021-11-13T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date: " + date);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("當期時區: " + currentZone);
本地化日期時間API
LocalDate、LocalTime 、LocalDateTime 都是用于處理日期時間的 API,在處理日期時間時可以不用強制性指定時區
// 擷取目前的日期時間
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("目前時間: " + currentTime);//目前時間: 2016-04-15T16:55:48.668
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);//date1: 2016-04-15
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);//月: APRIL, 日: 15, 秒: 48
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);//date2: 2012-04-10T16:55:48.668
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);//date3: 2014-12-12
// 22 小時 15 分鐘
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);//date4: 22:15
// 解析字元串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);//date5: 20:15:30
自定義格式使用
DateTimeFormatter
,它是不可變的(線程安全)
LocalDateTime currentTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(formatter.format(currentTime));// 2021-11-13 10:34:55.731
八、Base64
在Java 8中,Base64編碼已經成為Java類庫的标準。
至于它的使用則十分簡單,來看個例子:
// 編碼
String base64encodedString = Base64.getEncoder().encodeToString("Java8-Base64".getBytes("utf-8"));
System.out.println(base64encodedString);
// 解碼
String base64decodedString = new String(Base64.getDecoder().decode(base64encodedString), "utf-8");
System.out.println(base64decodedString);
輸出結果為:
SmF2YTgtQmFzZTY0
Java8-Base64
此外,Base64工具類還提供了URL、MIME
方法 | 描述 |
static Base64.Decoder getMimeDecoder() | 傳回一個 Base64.Decoder ,解碼使用 MIME 型 base64 編碼方案。 |
static Base64.Encoder getMimeEncoder() | 傳回一個 Base64.Encoder ,編碼使用 MIME 型 base64 編碼方案。 |
static Base64.Decoder getUrlDecoder() | 傳回一個 Base64.Decoder ,解碼使用 URL 和檔案名安全型 base64 編碼方案。 |
static Base64.Encoder getUrlEncoder() | 傳回一個 Base64.Encoder ,編碼使用 URL 和檔案名安全型 base64 編碼方案。 |
九、Nashorn JavaScript引擎
Nashorn 是一個 javascript 引擎,使得JavaScript 代碼可以在 Java 中執行,如下:
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine nashorn = scriptEngineManager.getEngineByName("JavaScript");
System.out.println(engine.eval("function f(){return 10;}; f() + 1;"));//12
jjs
jjs是個基于Nashorn引擎的指令行工具。它接受一些JavaScript源代碼為參數,并且執行這些源代碼。
使用Nashorn運作腳本的示例
jjs script.js
在互動模式下運作Nashorn的示例
jjs
jjs> println("Hello, World!")
Hello, World!
jjs> quit()
将參數傳遞給Nashorn的示例
$ jjs -- a b c
jjs> arguments.join(", ")
a, b, c
jjs>
值得注意的是:
随着ECMAScript語言标準的快速發展,維護Nashorn引擎變得越發挑戰,是以該引擎将在Java中廢棄。Java11将聲明棄用Nashorn JavaScript腳本引擎,被标注為 @Deprecated(forRemoval=true)
。
十、類依賴分析器jdeps
jdeps是一個相當棒的指令行工具,它可以展示包層級和類層級的Java類依賴關系,它以.class檔案、目錄或者Jar檔案為輸入,然後會把依賴關系輸出到控制台。
我們可以利用jedps分析下
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