JDK13快來了,JDK8的這幾點應該再看看!

  • 2019 年 10 月 6 日
  • 筆記

說明

jdk8雖然出現很久了,但是可能我們還是有很多人並不太熟悉,本文主要就是介紹說明一些jdk8相關的內容。

主要會講解:

  • lambda表達式
  • 方法引用
  • 默認方法
  • Stream
  • 用Optional取代null
  • 新的日誌和時間
  • CompletableFuture
  • 去除了永久代(PermGen) 被元空間(Metaspace)代替

jdk8開篇

https://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html

主要有:

1:lambda表達式:一種新的語言特性,能夠把函數作為方法的參數或將程式碼作為數據。lambda表達式使你在表示函數介面(具有單個方法的介面)的實例更加緊湊。

2:方法引用 是lambda表達式的一個簡化寫法,所引用的方法其實是lambda表達式的方法體實現,這樣使程式碼更容易閱讀

3:默認方法:Java 8引入default method,或者叫virtual extension method,目的是為了讓介面可以事後添加新方法而無需強迫所有實現該介面的類都提供新方法的實現。也就是說它的主要使用場景可能會涉及程式碼演進。

4:Stream 不是 集合元素,也不是數據結構,它相當於一個 高級版本的 Iterator,不可以重複遍歷裡面的數據,像水一樣,流過了就一去不復返。它和普通的 Iterator 不同的是,它可以並行遍歷,普通的 Iterator 只能是串列,在一個執行緒中執行。操作包括:中間操作 和 最終操作(只能操作一遍) 串列流操作在一個執行緒中依次完成。並行流在多個執行緒中完成,主要利用了 JDK7 的 Fork/Join 框架來拆分任務和加速處理。相比串列流,並行流可以很大程度提高程式的效率

5:用Optional取代null

6:新的日誌和時間,可以使用Instant代替Date LocalDateTime代替Calendar DateTimeFormatter代替SimpleDateFormat

7:CompletableFuture:CompletableFuture提供了非常強大的Future的擴展功能,可以幫助我們簡化非同步編程的複雜性,並且提供了函數式編程的能力,可以通過回調的方式處理計算結果,也提供了轉換和組合 CompletableFuture 的方法。

8:去除了永久代(PermGen) 被元空間(Metaspace)代替 配置:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m 代替 -XX:PermSize=10m -XX:MaxPermSize=10m

lambda

JDK8最大的特性應該非lambda莫屬!

IDEA工具自動提示:

lambda語法結構 : 完整的Lambda表達式由三部分組成:參數列表、箭頭、聲明語句;

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {  statment1;  statment2;  //.............  return statmentM;}  

絕大多數情況,編譯器都可以從上下文環境中推斷出lambda表達式的參數類型,所以參數可以省略:

(param1,param2, ..., paramN) -> {  statment1;  statment2;  //.............  return statmentM;}  

當lambda表達式的參數個數只有一個,可以省略小括弧:

param1 -> {  statment1;  statment2;  //.............  return statmentM;}  

當lambda表達式只包含一條語句時,可以省略大括弧、return和語句結尾的分號:

param1 -> statment  

在那裡以及如何使用Lambda????

你可以在函數式介面上面使用Lambda表達式。

備註: JDK定義了很多現在的函數介面,實際自己也可以定義介面去做為表達式的返回,只是大多數情況下JDK定義的直接拿來就可以用了。

Java SE 7中已經存在的函數式介面:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.util.concurrent.Callable
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

除此之外,Java SE 8中增加了一個新的包:java.util.function,它裡面包含了常用的函數式介面,例如:

  • Predicate——接收T對象並返回boolean
  • Consumer——接收T對象,不返回值
  • Function——接收T對象,返回R對象
  • Supplier——提供T對象(例如工廠),不接收值

隨便看幾個:

默認方法

Java 8 引入了新的語言特性——默認方法(Default Methods)。

Default methods enable new functionality to be added to the interfaces of libraries and ensure binary compatibility with code written for older versions of those interfaces. 默認方法允許您添加新的功能到現有庫的介面中,並能確保與採用舊版本介面編寫的程式碼的二進位兼容性。

默認方法是在介面中的方法簽名前加上了 default 關鍵字的實現方法。

為什麼要有默認方法

在 java 8 之前,介面與其實現類之間的 耦合度 太高了(tightly coupled),當需要為一個介面添加方法時,所有的實現類都必須隨之修改。默認方法解決了這個問題,它可以為介面添加新的方法,而不會破壞已有的介面的實現。這在 lambda 表達式作為 java 8 語言的重要特性而出現之際,為升級舊介面且保持向後兼容(backward compatibility)提供了途徑。

這個 forEach 方法是 jdk 1.8 新增的介面默認方法,正是因為有了默認方法的引入,才不會因為 Iterable介面中添加了 forEach 方法就需要修改所有 Iterable 介面的實現類。

方法引用(Method references)

如果一個Lambda表達式僅僅是調用方法的情況,那麼就可以用方法引用來完成,這種情況下使用方法引用程式碼更易讀。

方法引用語法:

目標引用放在分隔符::前,方法的名稱放在後面。

names2.forEach(System.out::println);//1  names2.forEach(s->System.out.println(s));//2  

第二行程式碼的lambda表達式僅僅就是調用方法,調用的System.out的println方法,所以可以用方法引用寫成System.out::println即可。

方法引用的種類(Kinds of method references)

方法引用有很多種,它們的語法如下:

  • 靜態方法引用:ClassName::methodName
  • 實例上的實例方法引用:instanceReference::methodName
  • 父類的實例方法引用:super::methodName
  • 類型上的實例方法引用:ClassName::methodName

備註:String::toString 等價於lambda表達式 (s) -> s.toString() 這裡不太容易理解,實例方法要通過對象來調用,方法引用對應Lambda,Lambda的第一個參數會成為調用實例方法的對象。

  • 構造方法引用:Class::new
  • 數組構造方法引用:TypeName[]::new

個人理解:方法引用,說白了,用更好,不用也可以,如果可以盡量用!!!

Stream

Java 8 中的 Stream 是對集合(Collection)對象功能的增強,它專註於對集合對象進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量數據操作 (bulk data operation)。Stream API 藉助於同樣新出現的 Lambda 表達式,極大的提高編程效率和程式可讀性。同時它提供串列和並行兩種模式進行匯聚操作,併發模式能夠充分利用多核處理器的優勢,使用 fork/join 並行方式來拆分任務和加速處理過程。通常編寫並行程式碼很難而且容易出錯, 但使用 Stream API 無需編寫一行多執行緒的程式碼,就可以很方便地寫出高性能的並發程式。

  • Stream 不是集合元素,它不是數據結構並不保存數據,它是有關演算法和計算的,它更像一個高級版本的 Iterator。
  • Stream 就如同一個迭代器(Iterator),單向,不可往複,數據只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。
  • 和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、串列化操作。

對stream的操作分為三類。

  1. 創建stream
  2. 中間操作(intermediate operations)【沒有終止操作是不會執行的】
  3. 終止操作(terminal operations):

中間操作會返回另一個流。可以用鏈式編程.的形式繼續調用。在沒有終止操作的時候,中間操作是不會執行的。

終止操作不會返迴流了,而是返回結果(比如返回void-僅僅System.out輸出,比如返回總數 int,返回一個集合list等等)

例如:

流的創建

3種方式創建流,普通流調用

  • 通過Stream介面的靜態工廠方法
  • 通過Arrays方法
  • 通過Collection介面的默認方法
//通過Stream介面的靜態工廠方法  Stream stream = Stream.of("hello", "world", "hello world");    String[] strArray = new String[]{"hello", "world", "hello world"};  //通過Stream介面的靜態工廠方法  Stream stream1 = Stream.of(strArray);    //通過Arrays方法  Stream stream2 = Arrays.stream(strArray);    List<String> list = Arrays.asList(strArray);  //通過Collection介面的默認方法  Stream stream3 = list.stream();  

本質都是StreamSupport.stream。

通過Collection介面的默認方法獲取並行流。

或者通過stream流調用parallel獲取並行流

只需要對並行流調用sequential方法就可以把它變成順序流

中間操作

終止操作

並行流

可以通過對收集源調用parallelStream方法來把集合轉換為並行流。並行流就是一個把內容分成多個數據 塊,並用不同的執行緒分別處理每個數據塊的流。這樣一來,你就可以自動把給定操作的工作負荷分配給多核處理器的所有內核,讓它們都忙起來。

並行流用的執行緒是從哪兒來的?有多少個?怎麼自定義這個過程呢?

並行流內部使用了默認的ForkJoinPool,它默認的執行緒數量就是你的處理器數量,這個值是由Runtime.getRuntime().availableProcessors()得到的。但是你可以通過系統屬性 java.util.concurrent.ForkJoinPool.common. parallelism來改變執行緒池大小,如下所示: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12"); 這是一個全局設置,因此它將影響程式碼中所有的並行流。反過來說,目前還無法專門為某個 並行流指定這個值。一般而言,讓ForkJoinPool的大小等於處理器數量是個不錯的默認值, 除非你有很好的理由,否則我們強烈建議你不要修改它

測試並行流和順序流速度

//Sequential Sort, 採用順序流進行排序      @Test      public void sequentialSort(){          long t0 = System.nanoTime();            long count = values.stream().sorted().count();          System.err.println("count = " + count);            long t1 = System.nanoTime();            long millis  = TimeUnit.NANOSECONDS.toMillis(t1 - t0);          System.out.println(String.format("sequential sort took: %d ms", millis));          //sequential sort took: 1932 ms        }        //parallel Sort, 採用並行流進行排序      @Test      public void parallelSort(){          long t0 = System.nanoTime();            long count = values.parallelStream().sorted().count();          System.err.println("count = " + count);            long t1 = System.nanoTime();            long millis  = TimeUnit.NANOSECONDS.toMillis(t1 - t0);          System.out.println(String.format("parallel sort took: %d ms", millis));          //parallel sort took: 1373 ms 並行排序所花費的時間大約是順序排序的一半。      }  

錯誤使用流

class Accumlator{      public long total = 0;        public void add(long value) {          total += value;      }  }      public class ParallelTest {      public static void main(String[] args) {          //錯誤使用並行流示例          System.out.println("SideEffect parallel sum done in :" + measureSumPerf(ParallelTest::sideEffectParallelSum, 1_000_000_0) + "mesecs");          System.out.println("=================");          //正確應該這樣的          System.out.println("SideEffect  sum done in :" + measureSumPerf(ParallelTest::sideEffectSum, 1_000_000_0) + "mesecs");      }        //錯誤使用並行流      public static long sideEffectParallelSum(long n) {          Accumlator accumlator = new Accumlator();          LongStream.rangeClosed(1, n).parallel().forEach(accumlator::add);          return accumlator.total;      }        //正確使用流      public static long sideEffectSum(long n) {          Accumlator accumlator = new Accumlator();          LongStream.rangeClosed(1, n).forEach(accumlator::add);          return accumlator.total;      }        //定義測試函數      public static long measureSumPerf(Function<Long, Long> adder, long n) {          long fastest = Long.MAX_VALUE;          //迭代10次          for (int i = 0; i < 2; i++) {              long start=System.nanoTime();              long sum = adder.apply(n);              long duration=(System.nanoTime()-start)/1_000_000;              System.out.println("Result: " + sum);              //取最小值              if (duration < fastest) {                  fastest = duration;              }          }          return fastest;      }    }  

本質問題在於total += value;它不是原子操作,並行調用的時候它會改變多個執行緒共享的對象的可變狀態,從而導致錯誤,在使用並行流需要避免這類問題發生!

思考: 什麼情況結果正常,但是並行流比順序流慢的情況呢???

並行流中更新共享變數,如果你加入了同步,很可能會發現執行緒競爭抵消了並行帶來的性能提升!

特別是limit和findFirst等依賴於元素順序的操作,它們在並行流上執行的代價非常大

對於較小的數據量,選擇並行流幾乎從來都不是一個好的決定。並行處理少數幾個元素的好處還抵不上並行化造成的額外開銷。

備註:sort或distinct等操作接受一個流,再生成一個流(中間操作),從流中排序和刪除重複項時都需要知道所有集合數據,如果集合數據很大可能會有問題(如果數據大,都放記憶體,記憶體不夠就會OOM了)。

使用並行流還是順序流都應該應該測試,以及壓測,如果在並行流正常的情況下,效率有提升就選擇並行流,如果順序流快就選擇順序流。

`CompletableFuture`非同步函數式編程

引入CompletableFuture原因

Future模式的缺點
  • Future雖然可以實現獲取非同步執行結果的需求,但是它沒有提供通知的機制,我們無法得知Future什麼時候完成
  • 要麼使用阻塞,在future.get()的地方等待future返回的結果,這時又變成同步操作。要麼使用isDone()輪詢地判斷Future是否完成,這樣會耗費CPU的資源。
Future 介面的局限性

future介面可以構建非同步應用,但依然有其局限性。它很難直接表述多個Future 結果之間的依賴性。實際開發中,我們經常需要達成以下目的:

  • 將兩個非同步計算合併為一個——這兩個非同步計算之間相互獨立,同時第二個又依賴於第 一個的結果。
  • 等待 Future 集合中的所有任務都完成。
  • 僅等待 Future 集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同 一個值),並返回它的結果。
  • 通過編程方式完成一個 Future 任務的執行(即以手工設定非同步操作結果的方式)。
  • 應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,並能使用 Future 計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)

新的CompletableFuture將使得這些成為可能。

CompletableFuture提供了四個靜態方法用來創建CompletableFuture對象:

方法入參和返回值有所區別。

裡面有非常多的方法,返回為CompletableFuture之後可以用鏈式編程.的形式繼續調用,最後調用一個不是返回CompletableFuture的介紹,和流式操作裡面的中間操作-終止操作。

日期

/**       * 可以使用Instant代替Date       * LocalDateTime代替Calendar       * DateTimeFormatter代替SimpleDateFormat       */        public static void main(String args[]) {          DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");          LocalDateTime now = LocalDateTime.now();          System.out.println(now.format(formatter));            //10分鐘前          String d1 = now.minusMinutes(10).format(formatter);          //10分鐘後          String d2 = now.plusMinutes(10).format(formatter);            System.out.println(d1);          System.out.println(d2);              LocalDateTime t5 = LocalDateTime.parse("2019-01-01 00:00:00", formatter);            System.out.println(t5.format(formatter));          }  

JVM方面改變

去除了永久代(PermGen) 被元空間(Metaspace)代替 配置:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m 代替 -XX:PermSize=10m -XX:MaxPermSize=10m

用Optional取代null

Optional對象創建

1、 創建空對象

Optional<String> optStr = Optional.empty();  

上面的示例程式碼調用empty()方法創建了一個空的Optional對象型。

2、 創建對象:不允許為空 Optional提供了方法of()用於創建非空對象,該方法要求傳入的參數不能為空,否則拋NullPointException,示例如下:

Optional<String> optStr = Optional.of(str);  // 當str為null的時候,將拋出NullPointException  

3、創建對象:允許為空 如果不能確定傳入的參數是否存在null值的可能性,則可以用Optional的ofNullable()方法創建對象,如果入參為null,則創建一個空對象。示例如下:

Optional<String> optStr = Optional.ofNullable(str);  // 如果str是null,則創建一個空對象  

常用方法

String str = null;    len = Optional.ofNullable(str).map(String::length).orElse(0); //不會報NullPointerException