天天看點

【都 Java19 了,還不了解 Java 8 ? 】一文帶你深入了解 Java 8 新特性

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