Java語法新特性_java5到java11

1、前言

萬萬沒想到,都0202年了,Sun都亡了,老夫還要從Java5的新特性開始寫,還要重點寫Java8的新特性。。。html

其實網上這種玩意一大堆,爲啥老夫還要寫呢?java

  1. 由於領導們以爲你們平時太(真)忙(懶),沒有時間去學習,因此集中講一下,來個速(應)成(付)。
  2. 網上的資料講的都挺好,都很全面,但對於咱們外(死)包(跑)開(龍)發(套)來講,應該重點關注那些能提升生(搬)產(磚)效率的新特性。
  3. 之因此仍是從Java5的新特性開始講起,表面的緣由是更能清楚地看到java發展的脈絡。真實緣由你們本身慢慢品。。。

這個速成版資料特色有:node

  1. 主要講對開發影響比較大的新特性,好比Java類庫API的新增或加強,編譯(javac)層面的語法糖等等。其餘的在字節碼層面,Java總體架構層面,以及虛擬機內部的改進,由於對開發幾乎沒有影響,這個速成版資料就略過不講了。(老夫也講很差。。。)
  2. Java5到Java7的新特性只是簡單過一遍,重點講Java8的新特性。
  3. Java9,Java10是過分版本,所以它們的新特性會跟Java11放到一塊兒講。
  4. 相關代碼均在JDK11 + IDEA 2019.1 環境上運行經過。
固然,光說不練假把式,這個速成版資料全部示例代碼位於下面的github或gitee倉庫,請各位自行下載,在本地準備好Java與IDE後,自行參考並練習:(代碼位於 src/test/java下)

https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test
或 : https://gitee.com/XiaTangShaoBing/study/tree/master/jdk11-testpython

2、Java5到Java7的新特性

這一章主要快速地講一下Java5到Java7在語法上的一些重要的新特性,以及一些重要的新的類庫API。react

2.1 Java5的新特性

Java5新特性比較多,但大部分咱們都已經很熟悉了,簡單過一下:git

  • 泛型 Generics :
泛型即參數化類型(Parameterized Type)。引入泛型以後,容許指定集合裏元素的類型,免去了強制類型轉換,而且能在編譯時刻進行類型檢查。泛型是長度可變的參數列表(vararg)、註解(annotation)、枚舉(enumeration)、集合(collection)的基石。
List<String> lst01 = new ArrayList<String>();

// 用 ? 表示接受任何類型,能夠避免調用方法時類型檢查警告。
private void test01(List<?> list) {
    for (Iterator<?> i = list.iterator(); i.hasNext(); ) {
        System.out.println((i.next().toString()));
    }
}

// 限制類型,此處表示參數類型必須繼承TestCase01Generic
private <T extends TestCase01Generic> void test02(T t) {
    t.doSomething();
}
  • 枚舉 Enumeration :
枚舉類是一種特殊的類,它和普通的類同樣,有本身的成員變量、成員方法、構造器 (只能使用 private 訪問修飾符,因此沒法從外部調用構造器,構造器只在構造枚舉值時被調用);enum 定義的枚舉類默認繼承了 java.lang.Enum 類,並實現了 java.lang.Seriablizable 和 java.lang.Comparable 兩個接口;全部的枚舉值默認都是 public static final 的(無需顯式添加),且非抽象的枚舉類不能再派生子類;枚舉類的全部實例(枚舉值)必須在枚舉類的第一行顯式地列出,不然這個枚舉類將永遠不能產生實例。列出這些實例(枚舉值)時,系統會自動添加 public static final 修飾,無需程序員顯式添加。
enum Color {
    black, white, red, yellow
}

// 枚舉常常用於switch語句
private void test01(Color color) {
    switch (color) {
        case red:
            System.out.println("霜葉紅於二月花");
            break;
        case black:
            System.out.println("黑雲壓城城欲摧");
            break;
        case white:
            System.out.println("一行白鷺上青天");
            break;
        case yellow:
            System.out.println("故人西辭黃鶴樓");
            break;
    }

    System.out.println(Color.black.compareTo(color));
    System.out.println(Color.white.compareTo(color));
    System.out.println(Color.red.compareTo(color));
    System.out.println(Color.yellow.compareTo(color));
}
  • 自動裝箱拆箱 autoboxing & unboxing :
八種primitive類型與其封裝引用類型的自動裝箱與拆箱:Boolean、Byte、Short、Character、Integer、Long、Float、Double
List<Integer> lstInt = new ArrayList<Integer>();
lstInt.add(1);
lstInt.add(2);
lstInt.add(3);

for (int i = 0; i < lstInt.size(); i++) {
    System.out.println(lstInt.get(i).toString());
    System.out.println(lstInt.get(i) + 1);
}
  • 長度可變的參數列表 varargs number of arguments : 參數類型相同時,把重載函數合併到一塊兒。
me.test01("One ring to rule them all,");
me.test01("one ring to find them,", "One ring to bring them all ", "and in the darkness bind them.");

private void test01(String ... args) {
    for (String s : args) {
        System.out.println(s);
    }
}
  • 註解 Annotations :

註解用於爲 Java 代碼提供元數據。通常來講註解不會直接影響代碼執行,不少註解的做用就是作數據約束和標準定義,能夠將其理解成代碼的規範標準(代碼的模板),但有一些註解能夠存活到JVM運行時,所以能夠結合其餘手段(如反射)來影響實際運行的代碼邏輯。因此註解的目的通常來講有二:一則規範代碼;二則動態注入(須要配合其餘手段實現)。程序員

一般註解能夠分爲四類:github

  1. Java自帶的標準註解,如@Override、@Deprecated、@SuppressWarnings等,一般編譯時就會依據這些註解對代碼進行檢查;
  2. 元註解,用於定義註解的註解,包括@Retention、@Target、@Inherited、@Documented等。
  3. 第三方註解,如spring,mybatis,lombok都提供了本身的註解。
  4. 自定義註解,使用@interface與元註解配合定義。
// 編譯器看到 @Override 註解,就知道這個方法須是重寫父類的方法
// 所以會嚴格檢查方法聲明信息是否與父類對應方法相同
// 如返回值類型,參數列表等等
@Override
public String toString() {
    return "解落三秋葉,能開二月花。";
}

// 一個自定義註解的例子,用於AOP中對方法參數進行非空檢查
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamNotEmpty {
}
  • foreach循環 : 迭代器循環的一個語法糖。
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 0; i < 10; i++) {
    numbers.add(i + 1);
}

for(Integer number : numbers) {
    System.out.println(number);
}
  • 靜態導入 import static : 沒啥好說的,直接看代碼,不建議使用。
package java5;

import static java5.TestCase07ImportStatic.TestInner.test;
import static java.lang.System.out;
import static java.lang.Integer.*;

/**
 * @author zhaochun
 */
public class TestCase07ImportStatic {
    public static void main(String[] args) {
        test();
        out.println(MIN_VALUE);
        out.println(toBinaryString(100));
    }

    static class TestInner {
        public static void test() {
            System.out.println("TestInner");
        }
    }
}
  • 格式化 : Java5中新增了printf-style格式化字符串的解釋器。
private void test01_formatter() {
    StringBuilder sb = new StringBuilder();
    Formatter formatter = new Formatter(sb);
    // "  前不見古人,  後不見來者。 念天地之悠悠, 獨愴然而涕下。"
    formatter.format("%4$7s,%3$7s。%2$7s,%1$7s。%n", "獨愴然而涕下", "念天地之悠悠", "後不見來者", "前不見古人");
    // "祖沖之的迷之數字 : +3.1415927 "
    formatter.format("祖沖之的迷之數字 : %+5.7f %n", Math.PI);
    // "某款手機價格 : ¥ 5,988.00"
    formatter.format("某款手機價格 : ¥ %(,.2f", 5988.0);
    System.out.println(formatter.toString());
    formatter.close();
}

private void test02_printf() {
    List<String> lines = new ArrayList<>();
    lines.add("人閒桂花落,");
    lines.add("夜靜春山空。");
    lines.add("月出驚山鳥,");
    lines.add("時鳴春澗中。");
    for (int i = 0; i < lines.size(); i++) {
        System.out.printf("Line %d: %s%n", i + 1, lines.get(i));
    }
}

private void test03_stringFormat() {
    Calendar c = new GregorianCalendar(2020, Calendar.MAY, 28);
    System.out.println(String.format("今天是個好日子: %1$tY-%1$tm-%1$te", c));
}

private void test04_messageFormat() {
    String msg = "您好,{0}!有您的快遞哦!請到{1}號櫃拿取您的快遞,每超時{2}小時要收費{3}元哦~~~";
    MessageFormat mf = new MessageFormat(msg);
    String fmsg = mf.format(new Object[]{"張三", 3, 8, 2});
    System.out.println(fmsg);
}

private void test05_dateFormat() {
    String str = "2020-05-28 14:55:21";
    SimpleDateFormat format1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    SimpleDateFormat format2 = new SimpleDateFormat("yyyyMMddHHmmss");
    try {
        System.out.println(format2.format(format1.parse(str)));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

其餘諸如ProcessBuilder,Scanner,加強反射,加強集合框架,StringBuilder,concurrent併發工具包,等等,由於要麼用得少,要麼你們已經很熟悉了,這裏就再也不一一介紹了。web

2.2 Java6的新特性

Java6的新特性不多,對開發幾乎沒有影響,簡單看一下。算法

  • WebService註解支持
  • 引入了一個能夠運行Javascript,python等腳本語言的引擎
  • Compiler API,運行期動態編譯java源碼
  • Http Server API
  • 通用的Annotations支持
  • JDBC 4.0
  • 集合框架加強,增長了一些不經常使用的接口,類和方法。

還有一些其餘的,不列了。

2.3 Java7的新特性

Java7的新特性也很少,但相比Java6,仍是有幾個新語法或新類庫API能改善開發效率的,咱們來看一下。

  • switch支持String
private String test01_switch(String title) {
    switch (title) {
        case "鹿柴":
            return "空山不見人,但聞人語響。返景入深林,復照青苔上。";
        case "山中送別":
            return "山中相送罷,日暮掩柴扉。春草明年綠,王孫歸不歸。";
        case "渭城曲":
            return "渭城朝雨浥輕塵,客舍青青柳色新。勸君更盡一杯酒,西出陽關無端人。";
        default:
            return "";
    }
}
  • 實例化時自動推斷泛型類型
List<String> tempList = new ArrayList<>();
  • autoclose接口 : 部分資源管理類,如文件IO,JDBC的Conection等,實現了AutoCloseable接口,它們能夠使用try-with-resources新語法。
String filePath = "/home/work/sources/jdk11-test/src/test/java/java7/TestCaseForJava7.java";
try (FileInputStream fis = new FileInputStream(filePath);
        InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
        BufferedReader br = new BufferedReader(isr)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
  • 捕獲多個異常
try {
    if (n < 0) {
        throw new FileNotFoundException();
    }
    if (n > 0) {
        throw new SQLException();
    }
    System.out.println("No Exceptions.");
} catch (FileNotFoundException | SQLException e) {
    e.printStackTrace();
}
  • 數字加強 : java7支持用下劃線分割較長的數字,支持用0b開頭直接寫二進制數字。
int num1 = 1_000_000;
System.out.println(num1);

int num2 = 0b11;
System.out.println(num2);
  • New IO 2.0 : Java7提供了一些新的文件操做API,如Path,而且提供了對指定目錄進行監視的WatchService,可以監聽指定目錄下文件的增刪改事件。(但並不能直接監聽文件變化內容)
private void test06_newIO2() {
    Path path = Paths.get("/home/zhaochun/test");
    System.out.printf("Number of nodes: %s %n", path.getNameCount());
    System.out.printf("File name: %s %n", path.getFileName());
    System.out.printf("File root: %s %n", path.getRoot());
    System.out.printf("File parent: %s %n", path.getParent());

    try {
        Files.deleteIfExists(path);
        Files.createDirectory(path);
        watchFile(path);
    } catch (IOException | InterruptedException e) {
        e.printStackTrace();
    }
}

private void watchFile(Path path) throws IOException, InterruptedException {
    WatchService service = FileSystems.getDefault().newWatchService();
    Path pathAbs = path.toAbsolutePath();
    pathAbs.register(service,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);
    while (true) {
        WatchKey key = service.take();
        for (WatchEvent<?> event : key.pollEvents()) {
            String fileName = event.context().toString();
            String kind = event.kind().name();

            System.out.println(String.format("%s : %s", fileName, kind));
            if ("end".equals(fileName) && "ENTRY_DELETE".equals(kind)) {
                return;
            }
        }
        key.reset();
    }
}
  • JDBC 4.1 : Connection接口增長了一些方法,若是之前有對JDBC Connection的實現或封裝,升級到Java7後會編譯不過。若是一直使用各個數據庫提供的JDBC驅動包,只須要確認版本支持 JDBC 4.1 以上便可。
  • Fork/Join 框架 : Java7增長了一個新的多線程編程框架,fork/join。不多直接用,並且Java8之後基於這個並行編程框架增長了一種集合操做的並行模式,因此咱們後面學習Java8的新特性時會簡單講一下這個fork/join的機制,這裏就不說了。

Java7還有一些其餘的新特性,但對開發影響不大,這裏就不一一講述了。

3、Java8的新特性

Java8是Java繼Java5以後又一個具備里程碑意義的大版本,有不少革命性的新特性。

固然,Java8新特性雖多,但咱們主要講那些對開發影響比較大的,語法上的新特性:

  • lambda表達式
  • Stream API
  • 接口默認方法
  • Optional
  • Map操做及HashMap性能優化
  • Date API
  • CompletableFuture

3.1 lambda表達式

Java8最重要的新特性就是添加了對lambda表達式的支持,使得Java能夠進行函數式編程(functional programming)。

3.1.1 什麼是lambda表達式

Lambda表達式就是可按引用傳遞的代碼塊,相似於其餘語言的閉包的概念:它們是實現某項功能的代碼,可接受一個或多個輸入參數,並且可返回一個結果值。閉包是在一個上下文中定義的,可訪問來自上下文的值。

而在Java8中,lambda表達式能夠被具體地表述爲函數式接口的一個具體實現。所謂函數式接口,就是隻定義了一個抽象方法的interface。(函數式接口能夠經過添加註解@FunctionalInterface,從而在編譯時強制檢查該接口是否只有一個抽象方法。但這個註解不是必須的。)

咱們先看一個具體的例子:

假設咱們有這樣一個接口,它只有一個抽象方法,是一個函數式接口:

@FunctionalInterface
interface TestLambda {
    String join(String a, String b);
}

以及一個使用它的方法:(顯然這個方法並不須要知道具體實現TestLambda接口的類是誰)

private String joinStr(TestLambda testLambda, String a, String b) {
    return testLambda.join(a, b);
}

接下來,咱們嘗試使用joinStr方法來鏈接兩個字符串。在Java8以前,咱們每每使用匿名內部類在須要的地方直接實現TestLambda接口:

String s1 = joinStr(new TestLambda() {
    @Override
    public String join(String a, String b) {
        return a + ", " + b;
    }
}, "問君能有幾多愁", "恰似一江春水向東流");
System.out.println(s1);

很顯然,匿名內部類很臃腫,語義上也不夠直觀,你們受夠了沒有?

從Java8開始,你能夠使用lambda表達式代替匿名內部類,就是下面代碼中的(a, b) -> a + ", " + b;這種寫法,很簡潔,語義直觀,更接近天然語言:

TestLambda simpleJoin = (a, b) -> a + ", " + b;
String s2 = joinStr(simpleJoin, "高堂明鏡悲白髮", "朝如青絲暮成雪");
System.out.println(s2);

或直接寫爲:

String s3 = joinStr((a, b) -> a + ", " + b, "高堂明鏡悲白髮", "朝如青絲暮成雪");
System.out.println(s3);

當你要實現的接口邏輯比較複雜時,你能夠用{}把代碼塊包起來;你還能夠給每一個入參聲明類型:

TestLambda joinWithCheck = (String a, String b) -> {
    if (a != null && b != null) {
        return a + ", " + b;
    } else {
        return "空空如也";
    }
};
String s4 = joinStr(joinWithCheck, null, null);
System.out.println(s4);

至此咱們能夠知道:

  • 對於那些參數爲函數式接口的方法,能夠在調用時傳入一個lambda表達式,這個lambda表達式就是接口的一個具體實現。
  • lambda表達式在形式上表現爲(函數的參數列表) -> {函數實現}
  • 用來包裹函數實現的{}在僅有一行時能夠省略。
  • 僅有一行實現沒有{}時,默認返回這一行代碼的計算結果(函數有返回值時)。
  • 多行實現有{}時,須要顯式返回對應類型的計算結果(函數有返回值時)。
  • lambda表達式在效果上,能夠認爲等同於以前的匿名內部類。(但二者實現機制並不相同,lambda表達式並不能簡單視爲匿名內部類的高級語法糖。)
  • lambda表達式能夠內聯,也能夠提爲單獨的變量或方法引用。
  • lambda表達式實現的函數式接口爲啥只能定義一個抽象方法呢?由於lambda表達式不使用方法名啊。。。方法弄多了不曉得該調用哪一個方法了。。。

3.1.2 lambda表達式對上下文的訪問限制

lambda表達式內部是能夠訪問外部變量的。但要注意的是,若是這個外部變量是局部變量,那麼這個局部變量必須是final的(能夠不聲明爲final,但不能對其二次賦值,即,須要隱式final)。

private void test02_finalVars() {
    String a = "王維";
    new Thread(() -> {
        // lambda表達式裏能夠使用外部的final局部變量(不用顯式聲明final)
        System.out.println(a);
        // 下面這句編譯不過,不能對"lambda表達式裏使用的外部局部變量"從新賦值。
        // 即lambda內部使用的外部局部變量是隱式final的。
//            a = "李白";
    }).start();
    // 在lambda外面也不能對a從新賦值,由於須要在lambda表達式裏使用,所以a是隱式final的。
//        a = "李白";
}
注意是局部變量不能從新賦值。對於實例變量,靜態變量來講,能夠在lambda表達式裏隨意訪問,包括從新賦值。

3.1.3 方法引用

Java8除了提供比較標準(對比其餘語言)的lambda表達式之外,還提供了一種叫作方法引用的簡便形式。

  • 對象實例的方法引用 instance::method
new Thread(this::test02_finalVars).start();
// 上面這句等價於下面這句:
new Thread(() -> this.test02_finalVars()).start();

test02_finalVars是前面例子裏的一個實例方法。

  • 類的靜態方法引用 Class::static_method
new Thread(TestCase01Lambda::printSomething).start();
// 等價於:
new Thread(() -> TestCase01Lambda.printSomething()).start();
...
private static void printSomething() {
    System.out.println("大漠孤煙直,長河落日圓。");
}
  • 類的實例方法引用 Class::method
List<String> lines = new ArrayList<>();
lines.add("a005");
lines.add("a001");
lines.add("a003");
Collections.sort(lines, String::compareTo);
// 等價於:
Collections.sort(lines, (o1, o2) -> o1.compareTo(o2));
System.out.println(lines);
  • 構造器引用 Class<T>::new
Set<String> lineSet = transferElements(lines, HashSet::new);
// 等價於
lineSet = transferElements(lines, () -> new HashSet<>());
System.out.println(lineSet);
...
private static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {

    DEST result = collectionFactory.get();
    result.addAll(sourceCollection);
    return result;
}

3.1.4 標準函數式接口

以前咱們說過了,lambda表達式實現的只能是函數式接口,即,只有一個抽象方法定義的接口。Java8還爲了lambda接口的普遍使用,增長了新的java.util.function包,定義了一些能夠普遍使用lambda的函數式接口。

  • Function:接受一個參數,基於參數值返回結果
  • Predicate:接受一個參數,基於參數值返回一個布爾值
  • BiFunction:接受兩個參數,基於參數值返回結果
  • Supplier:不接受參數,返回一個結果
  • Consumer:接受一個參數,無結果 (void)

這些標準函數式接口在Stream的操做中獲得了普遍的應用,咱們後面講到Stream的時候會到處看到它們的身影。

若是你如今就去看這些接口的源碼,你會發現它們雖然都只定義了一個抽象方法,但內部每每還有一些default的實例方法。是否是有點懵逼,接口不是沒有實例方法的嗎?這個咱們後面講到Java8的另外一個新特性(接口默認方法)的時候再具體說。

3.2 Stream API

Java8中新增的Stream API是對集合(Collection)對象功能的加強,它專一於對集合對象進行各類很是便利、高效的聚合操做(aggregate operation),或者大批量數據操做 (bulk data operation)。Stream API 藉助於一樣新出現的 Lambda 表達式,極大的提升了編程效率和程序可讀性。同時它提供串行和並行兩種模式進行匯聚操做,併發模式可以充分利用多核處理器的優點,使用fork/join並行方式(Java7的新特性,由於不多直接用,咱們沒有講這個) 來拆分任務和加速處理過程。一般編寫並行代碼很難並且容易出錯, 但使用 Stream API 無需編寫一行多線程的代碼,就能夠很方便地寫出高性能的併發程序。因此說,Java 8 中首次出現的 java.util.stream 是一個函數式語言+多核時代綜合影響的產物。

所謂聚合操做,就是對數據集的各類統計操做,好比:平均值,總和,最小,最大,計數等等。在咱們開發的信息系統中,這些聚合操做每每是經過關係型數據庫的各類查詢SQL完成的。若是想在Java應用中完成這些操做,那麼咱們須要本身開發集合操做,經過不停的顯式地對集合進行遍歷,循環執行運算邏輯來達成。這些程序不但開發繁瑣,也不易維護,同時不當心就會出現性能問題。

而Java8提供的Stream API,則讓聚合操做的開發變得很是簡單;代碼可讀性更高;在多核機器上面對耗時的可併發聚合操做時,使用並行模式的性能表現也會更好。

3.2.1 Stream整體介紹

如今咱們有了個初步的概念,就是Stream是在對數據集作聚合操做。咱們先看一個典型的Stream完成聚合操做的例子:

int sum = Stream.of("", "1", null, "2", " ", "3")
        .filter(s -> s != null && s.trim().length() > 0)
        .map(s -> Integer.parseInt(s))
        .reduce((left, right) -> right += left)
        .orElse(0);
這個例子是在計算一個集合中全部數字的合計值。

先簡單講解一下上面這個Stream操做的過程:

  1. Stream.of("", "1", null, "2", " ", "3") : 獲取數據源的Stream對象;
  2. .filter(s -> s != null && s.trim().length() > 0) : 過濾前面返回的Stream對象,並返回過濾後的新的Stream對象;
  3. .map(s -> Integer.parseInt(s)) : 將前面返回的Stream對象中的字符串轉換爲數字,並返回新的Stream對象;
  4. .reduce((left, right) -> right += left) : 對前面返回的Stream對象執行合計操做,並返回合計值(Optional對象,包括最後的orElse,是Java8另外的新特性。後面再講,這裏先無視)。

先講Stream操做的基本流程

從上述經典示例中,咱們能夠看到,一個Stream操做能夠分爲三個基本步驟:

1.獲取數據源 Source --> 2.數據轉換 Transform --> 3.執行操做 Operation

再細緻一點,能夠將其視爲一個管道流操做:

數據集:Stream | filter:Stream | map:Stream | reduce

其中,filter與map屬於數據轉換 Transform,而reduce屬於執行操做 Operation。每次Transform的時候,不會改變原有的Stream對象,而是返回一個新的Stream對象,所以容許對其進行鏈式操做,從而造成一個管道。

獲取數據源的方式主要有:

1.從 Collection 和數組

Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()

2.從 BufferedReader

java.io.BufferedReader.lines()

3.靜態工廠

java.util.stream.IntStream.range()
java.nio.file.Files.walk()

4.本身構建

java.util.Spliterator

5.其它

Random.ints()
BitSet.stream()
Pattern.splitAsStream(java.lang.CharSequence)
JarFile.stream()

稍後講Stream操做示例的時候會有所介紹,沒必要着急。

Stream操做類型

Stream操做類型:

  • Intermediate: 中間操做,對應前面的Transform,其目的是打開前一個Stream對象,定義要執行的數據映射或過濾等轉換處理(Transform),而後返回一個新的Stream對象,交給下一個操做使用。語法上,多個Intermediate操做能夠鏈式鏈接在一塊兒。但這類操做都是惰性化的(lazy),就是說,僅僅調用到這類方法時,並無真正開始Stream的遍歷。
常見的Intermediate操做:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • Terminal: 終點站操做,對應前面的Operation,一次對數據集的Stream操做只能有一次terminal操做。當這個操做執行時,前面的鏈式的中間操做返回的最後一個Stream對象(也可能沒有中間操做,直接是數據源的Stream對象)就真正開始遍歷數據集,以後就沒法再操做這個Stream對象了。因此這一定是最後一個操做。Terminal 操做執行時,纔會真正開始數據集遍歷,併產生結果。
常見的Terminal操做:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
  • Short-circuiting: 短路操做,並不與前二者衝突,一個短路操做同時也是Intermediate或Terminal。它是在處理無限大的Stream時,須要返回一個有限的Stream對象(Intermediate),或有限的計算結果(Terminal)。但短路操做用在有限Stream對象也是徹底沒問題的。
常見的Short-circuiting操做:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

屢次的Intermediate操做並不會致使屢次的數據集遍歷,由於這些Intermediate是惰性的,這些轉換操做只會在 Terminal 操做的時候融合起來,一次遍歷完成。

至於Stream的哪些操做是Intermediate,哪些是Terminal,一個簡單的標準就是看方法返回值是否是Stream。

3.2.2 Stream常見操做的使用

若是你沒有用過Stream,那麼你看完前面對Stream的介紹估計就只能是霧裏看花了。來,騷年,跟老夫一塊兒動手把代碼擼起來。

先準備一個數據集,它的元素以下(Poet,詩人):

class Poet {
        private String name;
        private int age;
        private int evaluation;

        public Poet() {
        }

        public Poet(String name, int age, int evaluation) {
            this.name = name;
            this.age = age;
            this.evaluation = evaluation;
        }

        @Override
        public String toString() {
            return "Poet{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", evaluation=" + evaluation +
                    '}';
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public int getEvaluation() {
            return evaluation;
        }

        public void setEvaluation(int evaluation) {
            this.evaluation = evaluation;
        }
    }

而後準備一個唐代著名詩人的集合:

List<Poet> poets = preparePoets();
...
private List<Poet> preparePoets() {
    List<Poet> poets = new ArrayList<>();
    // 年齡未必準確,評價不能當真
    poets.add(new Poet("王維", 61, 4));
    poets.add(new Poet("李白", 61, 5));
    poets.add(new Poet("杜甫", 58, 5));
    poets.add(new Poet("白居易", 74, 4));
    poets.add(new Poet("李商隱", 45, 4));
    poets.add(new Poet("杜牧", 50, 4));
    poets.add(new Poet("李賀", 26, 4));
    return poets;
}
  • foreach:
// foreach 等價於 poets.stream().forEach(System.out::println);
poets.forEach(System.out::println);

注意,不能對同一個Stream反覆操做,演示以下:

Stream<Poet> poetStream = poets.stream();
poetStream.forEach(System.out::println);
try {
    // 不能對同一個stream對象作兩次操做,stream是流,不能回頭,操做過一次以後就不能再操做了。
    poetStream.forEach(System.out::println);
} catch (Throwable t) {
    System.out.println("stream has already been operated upon or closed. 別人嚼過的甘蔗你就別嚼了。。。");
}
// 可是從新從集合獲取stream是能夠重複操做的,由於是一個新的stream對象。
poets.stream().forEach(System.out::println);
  • map -> Collectors
String strPoets = poets.stream()
        .map(poet -> poet.getName() + " 唐代大詩人")
        .collect(Collectors.joining(","));
System.out.println(strPoets);
Collectors提供了不少操做,能夠對各個元素進行鏈接操做,能夠將元素導入其餘Collection(List或Set),等等。
  • filter + map + collect 倒入set集合中
Set<String> poetsLi = poets.stream()
        .filter(poet -> poet.getName().startsWith("李"))
        .map(poet -> "唐詩三李 之 " + poet.getName())
        .collect(Collectors.toSet());
System.out.println(poetsLi);
以前說對同一個stream對象只能操做一次,爲什麼這裏鏈式屢次操做?
由於 map, filter這些方法是Intermediate操做,返回了一個新的stream對象。
  • filter + findAny/findFirst 查找一個知足條件的數據
Poet topPoet = poets.stream()
        .filter(poet -> poet.getEvaluation() > 4)
        .findAny()
//      .findFirst()
        // 關於 orElse, 後面講 Optional 的時候再解釋
        .orElse(new Poet("杜甫", 58, 5));
System.out.println("最牛的詩人之一:" + topPoet.getName());
  • allMatch 和 anyMatch
boolean all50plus = poets.stream()
        .allMatch(poet -> poet.getAge() > 50);
System.out.println("大詩人們都活了50歲以上嗎?" + (all50plus ? "是的" : "並無"));

boolean any50plus = poets.stream()
        .anyMatch(poet -> poet.getAge() > 50);
System.out.println("大詩人們有活到50歲以上的嗎?" + (any50plus ? "有的有的" : "竟然沒有");
  • count max min sum
// 5星詩人數量 count
System.out.println("5星詩人數量:" + poets.stream()
        .filter(poet -> poet.getEvaluation() == 5)
        .count());
// 年齡最大的詩人
System.out.println("年齡最大的詩人:" + poets.stream()
        .max(Comparator.comparingInt(Poet::getAge))
        .orElse(null));
// 年齡最小的詩人
System.out.println("年齡最小的詩人:" + poets.stream()
        .min(Comparator.comparingInt(Poet::getAge))
        .orElse(null));
// 年齡合計
System.out.println("詩人們年齡合計:" + poets.stream()
        .mapToInt(Poet::getAge)
        .sum());
Java8的Stream API爲int,long,double專門提供了 mapToInt(), mapToLong(), mapToDouble()三個方法。從語義上來講,你本身寫map操做獲得一個泛型爲Integer/Long/Double的Stream對象,而後作後續操做固然能夠。但直接使用 mapToInt()能夠提升性能表現,由於會省去後續操做的循環中的自動裝箱解箱處理。
  • reduce 一個專門作統計的操做,好比這裏咱們也能夠用reduce計算合計
int sumAge = poets.stream()
        .mapToInt(Poet::getAge)
        .reduce((age, sum) -> sum += age)
//      .reduce(Integer::sum)
        .orElse(0);
System.out.println("reduce計算出的年齡合計:" + sumAge);

注意,reduce作統計是能夠有起始值的,例如:

// 假設唐代其餘詩人們的評價合計已經有了,假設是 100,但還未包括前面的7位,這裏從 100 開始繼續統計評價總值
int sumEvaluation = poets.stream()
        .mapToInt(Poet::getEvaluation)
        .reduce(100, (left, right) -> right += left);
//      .reduce(100, Integer::sum);
System.out.println("reduce計算出的有起始值的評價合計:" + sumEvaluation);
  • limit
System.out.println("生成一個等差數組,限制長度爲10:");
Stream.iterate(1, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));
  • distinct
String distinctEvaluation = poets.stream()
        .map(poet -> String.valueOf(poet.getEvaluation()))
        .distinct()
        .collect(Collectors.joining(","));
System.out.println("詩人們的評價分數(去重):" + distinctEvaluation);
  • sorted
System.out.println("詩人們按年齡排序:");
poets.stream()
        .sorted(Comparator.comparingInt(Poet::getAge))
        .forEach(System.out::println);
  • group
Map<String, List<Poet>> poetsByAge = poets.stream()
        .collect(Collectors.groupingBy(poet -> {
            int age = poet.getAge();
            if (age < 20) {
                return "1~19";
            } else if (age < 30) {
                return "20~29";
            } else if (age < 40) {
                return "30~39";
            } else if (age < 50) {
                return "40~49";
            } else if (age < 60) {
                return "50~59";
            } else if (age < 70) {
                return "60~69";
            } else {
                return "70~";
            }
        }));
System.out.println("將詩人們按年齡分組:");
poetsByAge.keySet().stream()
        .sorted(String::compareTo)
        .forEach(s -> System.out.println(
                String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(",")))));
  • flatmap [(poet1, poet2, poet3),(poet4,poet5)] --> [poet1, poet2, poet3, poet4, poet5]
System.out.println("經過flatmap將分組後的詩人集合扁平化:");
List<Poet> lstFromGroup = poetsByAge.values().stream()
        .flatMap(poets1 -> poets1.stream())
        .collect(Collectors.toList());
lstFromGroup.forEach(System.out::println);

3.2.3 Stream的並行模式

剛剛的例子,都是Stream的串行模式,如今咱們經過parallelStream獲取Stream的並行模式。要注意並行模式與串行模式有時執行相同操做會獲得不一樣的結果:

System.out.println("findAny:");
for (int i = 0; i < 10; i++) {
    Poet topPoet1 = poets.parallelStream()
            .filter(poet -> poet.getEvaluation() > 4)
            .findAny()
            .orElse(new Poet("XX", 50, 5));
    System.out.println("最牛的詩人之一:" + topPoet1.getName());
}

System.out.println("findFirst:");
for (int i = 0; i < 10; i++) {
    Poet topPoet2 = poets.parallelStream()
            .filter(poet -> poet.getEvaluation() > 4)
            .findFirst()
            .orElse(new Poet("XX", 50, 5));
    System.out.println("最牛的詩人之一:" + topPoet2.getName());
}
上述代碼的執行結果中,findFirst與串行並沒有不一樣,但findAny有時與串行結果不同。想一想爲何。

parallelStream使用要謹慎,並非全部的運算均可以並行執行的。

int sumEvaluation = poets.parallelStream()
        .mapToInt(Poet::getEvaluation)
        .reduce(100, Integer::sum);
System.out.println("reduce計算有初始值時,不該該用並行運算:" + sumEvaluation);
並行模式很吸引人,但前提是你要清楚何時才能使用。這個例子很好的說明了帶起始值的reduce操做並不適合用並行模式。
  • parallelStream的機制是基於Java7引入的Fork/Join框架。瞭解便可。

Fork/Join的本質和Hadoop的MapReduce同樣,都是基於分而治之的思想,將一個任務拆成多個能夠並行的小任務執行(Map、fork),最後集中到一塊兒(Reduce、join)。固然Hadoop更復雜,處理的是在不一樣節點上的分佈式進程,而Fork/Join是一個進程(JVM)裏的多個線程。

爲何咱們不多直接用Fork/Join呢?由於用起來麻煩。。。仍是簡單說一下吧。。。

  1. 首先你須要像線程池那樣定義一個ForkJoinPool,而後定義一個執行任務的ForkJoinTask,在ForkJoinPool中提交這個ForkJoinTask;
  2. 而後你的ForkJoinTask須要本身實現什麼樣的條件或閾值下,把你要處理的數據集拆開,對應new 幾個新的ForkJoinTask,而後調用這些子task的fork方法,再調用它們的join方法(即分而治之);
  3. Fork/Join中關鍵的機制叫作Work-stealing策略,它將子任務放到不一樣的雙端隊列中,每一個隊列對應一個線程去獲取並執行隊列中的子任務。所謂雙端隊列,就是正常來講線程從隊列的一端獲取接下來要執行的子任務,而當某個線程空閒時,它會從其餘線程的隊列的另外一端偷子任務來執行。。。Work-stealing的優點是能充分利用線程進行並行計算;缺點是隊列中任務較少時,爲了不線程對子任務的競爭,須要同步機制,此時會產生額外的性能損耗。(因此後面咱們驗證Stream的性能時,會發現,數據量較少時,parallelStream有時會更慢,就有這裏所說的緣由。)

3.2.4 將lambda表達式重構出來

在Stream操做中,有時咱們須要寫很長的lambda函數,這時咱們能夠靈活運用IDE的重構功能,將較長的lambda表達式重構爲變量或方法。

Predicate<Poet> poetPredicate = poet -> poet.getEvaluation() < 5;
Consumer<Poet> poetConsumer = poet -> System.out.println(poet.getName());
poets.stream()
        .filter(poetPredicate)
        .forEach(poetConsumer);

Function<Poet, String> poetStringFunction = poet -> {
    int age = poet.getAge();
    if (age < 20) {
        return "1~19";
    } else if (age < 30) {
        return "20~29";
    } else if (age < 40) {
        return "30~39";
    } else if (age < 50) {
        return "40~49";
    } else if (age < 60) {
        return "50~59";
    } else if (age < 70) {
        return "60~69";
    } else {
        return "70~";
    }
};
Map<String, List<Poet>> poetsByAge = poets.stream()
        .collect(Collectors.groupingBy(poetStringFunction));
System.out.println("將詩人們按年齡分組:");
Consumer<String> stringConsumer = s -> System.out.println(
        String.format("%s : %s", s, poetsByAge.get(s).stream().map(Poet::getName).collect(Collectors.joining(","))));
poetsByAge.keySet().stream()
        .sorted(String::compareTo)
        .forEach(stringConsumer);

3.2.5 Stream的性能

Stream的性能不能簡單地表述爲比之前的集合遍歷操做快或者慢,而是要根據具體場景的不一樣的性能約束條件去確認。

這裏簡單考慮三種場景:

  1. 單個數據集的簡單遍歷操做;
  2. 兩個數據集的join操做;
  3. 單個數據集的複雜轉換操做。

下面的代碼使用的硬件環境:

老夫本地可供程序使用的CPU資源:6 core (i7 4核8線程,但有兩個core常年被虛擬機佔用,所以共 6 個core能夠使用。)

單個數據集簡單遍歷

對於單個數據集的簡單遍從來說,整體上講,Stream的串行操做的性能表現大約介於fori循環與迭代器循環之間;而Stream的並行模式,在運行平臺具備多核,且循環中的單次操做比較耗時的前提下,確實能夠有效提升性能表現(比fori,迭代器,Stream串行都要更好)。

對於單個數據集的遍從來說,從下面的示例代碼中,咱們能夠發現會影響性能表現的約束條件最少包括如下幾點:

  1. 機器硬件條件,好比是否是多核,核數有多少。(兩核未必能保證並行比串行效率高,由於要考慮線程上下文切換的損耗。)
  2. 數據集件數,數據集的件數在不一樣量級(百件,千件,萬件,十萬,百萬,千萬。。。)下,不一樣的遍歷方式的性能表現的差異是顯著的。
  3. 單次循環的耗時,好比納秒級別的耗時,那麼Stream的並行模式並沒有優點(一樣由於線程上下文切換的損耗),但耗時達到數百毫秒的級別時,並行模式的優點就至關明顯了。(固然要在多核機器上運行)

對於下面的代碼,建議你們多嘗試一下不一樣的約束條件,好比:

  1. sleep時間調整,例如,從沒有sleep,到sleep 500毫秒;
  2. 數據集件數調整,例如,從1百件,到1千,1萬,10萬,百萬,千萬。。。(固然,件數大的時候適當減小sleep甚至去除sleep,省得跑過久)
  3. 不一樣硬件條件的機器,這個有條件的能夠試試CPU核數差距較大的機器上運行並行模式的結果,沒有條件就算了。

另外,代碼中的LocalDateTimeDuration是Java8的又一個新特性,後面會介紹,如今不用在乎。

List<String> numbers = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    numbers.add("a" + i);
}

System.out.println("=== loop with fori ===");
LocalDateTime startTime = LocalDateTime.now();
for (int i = 0; i < numbers.size(); i++) {
    String whatever = numbers.get(i) + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("loop with fori time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with Iterator ===");
startTime = LocalDateTime.now();
for (String num : numbers) {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
stopTime = LocalDateTime.now();
System.out.println("loop with Iterator time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with stream ===");
startTime = LocalDateTime.now();
numbers.stream().forEach(num -> {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
stopTime = LocalDateTime.now();
System.out.println("loop with stream time(millis):" + Duration.between(startTime, stopTime).toMillis());

System.out.println("=== loop with parallelStream ===");
startTime = LocalDateTime.now();
numbers.parallelStream().forEach(num -> {
    String whatever = num + "b";
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
stopTime = LocalDateTime.now();
System.out.println("loop with parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis());
上面的代碼在本地運行的時候,切記件數大的時候把sleep調小甚至註釋掉,省的跑半天出不來結果。。。

兩個數據集的join

上面的例子僅僅是單個數據集的遍歷,但在實際開發當中,咱們每每會更多地遇到更復雜的數據集操做。好比最典型的,兩個數據集的join操做。

首先咱們在Poet以外,再定義兩個Class:EvaluationPoetExt

class Evaluation {
    private int evaluation;
    private String description;

    public Evaluation() {
    }

    public Evaluation(int evaluation, String description) {
        this.evaluation = evaluation;
        this.description = description;
    }

    public int getEvaluation() {
        return evaluation;
    }

    public void setEvaluation(int evaluation) {
        this.evaluation = evaluation;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

class PoetExt extends Poet {
    private String description;

    public PoetExt(String name, int age, int evaluation, String description) {
        super(name, age, evaluation);
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "PoetExt{" +
                "name='" + this.getName() + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

很顯然,Poet對應詩人的定義數據,Evaluation對應評價的定義數據。咱們須要實現的需求是,poets 與 evaluations 作 join 得到 PoetExt集合。這個用關係型數據庫的SQL來講,就是主表爲Poet,副表爲Evaluation,以Poet.evaluation = Evaluation.evaluation爲條件鏈接查詢數據。

Java8以前,若是咱們須要在Java應用中實現這樣的兩個數據集的join操做,那麼咱們每每採用的是顯式的雙層迭代器循環嵌套的寫法,而Java8開始,咱們能夠利用Stream操做實現兩個數據集的join操做。而根據該場景的需求,咱們還能夠使用Stream的並行模式。

代碼以下所示,分別比較了三種寫法的性能表現(顯式雙層迭代器遍歷,Stream,並行Stream):

// poets件數
int n = 100000;
// evaluations件數
int m = 100000;
List<Poet> poets = new ArrayList<>();
for (int i = 0; i < n; i++) {
    String name = String.format("詩人%010d", i + 1);
    poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1));
}
List<Evaluation> evaluations = new ArrayList<>();
for (int i = 0; i < m; i++) {
    evaluations.add(new Evaluation(i + 1, (i + 1) + "星"));
}

// 要實現的邏輯是,poets 與 evaluations 作 join 得到 PoetExt集合

// 顯式雙層迭代器循環嵌套的寫法:
List<PoetExt> poetExts = new ArrayList<>();
System.out.println("=== 顯式雙層迭代器循環 ===");
LocalDateTime startTime = LocalDateTime.now();
for(Poet poet : poets) {
    int eva = poet.getEvaluation();
    for(Evaluation evaluation : evaluations) {
        if (eva == evaluation.getEvaluation()) {
            PoetExt poetExt = new PoetExt(poet.getName(), poet.getAge(), eva, evaluation.getDescription());
            poetExts.add(poetExt);
            break;
        }
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("顯式雙層迭代器循環 time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件數: %d 與第一件結果: %s %n", "顯式雙層迭代器循環", poetExts.size(), poetExts.get(0).toString());

// Stream寫法:
System.out.println("=== Stream ===");
startTime = LocalDateTime.now();
poetExts = poets.stream()
        .map(poet -> {
            Evaluation eva = evaluations.stream()
                    .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation())
                    .findAny()
                    .orElseThrow();
            return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription());
        })
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("Stream time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件數: %d 與第一件結果: %s %n", "Stream", poetExts.size(), poetExts.get(0).toString());

// parallelStream
System.out.println("=== parallelStream ===");
startTime = LocalDateTime.now();
poetExts = poets.parallelStream()
        .map(poet -> {
            Evaluation eva = evaluations.parallelStream()
                    .filter(evaluation -> evaluation.getEvaluation() == poet.getEvaluation())
                    .findAny()
                    .orElseThrow();
            return new PoetExt(poet.getName(), poet.getAge(), poet.getEvaluation(), eva.getDescription());
        })
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("parallelStream time(millis):" + Duration.between(startTime, stopTime).toMillis());
System.out.printf("%s 的件數: %d 與第一件結果: %s %n", "parallelStream", poetExts.size(), poetExts.get(0).toString());

老夫本地不一樣約束條件下的運行結果:時間單位:毫秒

poets件數 evaluations件數 顯式雙層迭代器循環 Stream parallelStream
1000 1000 53 44 145
10000 10000 772 603 520
100000 100000 27500 48351 11958
10000 100000 4375 4965 1510
100000 10000 3078 5053 1915
100000 1000000 421999 787188 186758
1000000 100000 278927 497239 122923
100000 100 140 306 895
100 100000 111 110 111

因而可知,在老夫本地硬件環境下(6個core可用),數據量較小時(join雙方數據集件數都在1萬如下),三者區別不大,顯式雙層迭代器循環與Stream接近,而parallelStream在1千的數據量時甚至會慢一點;而當數據量來到10萬件以上的規模時,三者性能表現出現較爲明顯的差距,parallelStream優點明顯,顯式雙層迭代器循環次之,Stream串行最慢。

  • 兩個數據集數據量都較小時,Stream不管串行模式仍是並行模式,與顯式雙層迭代器循環的性能表現差距不大,都在一個數量級。
  • 兩個數據集數據量都較大時,parallelStream > 顯式雙層迭代器循環 > Stream
  • 主數據集數據量較大而副數據集數據量較小時,顯式雙層迭代器循環 > Stream > parallelStream
  • 副數據集數據量較大而主數據集數據量較小時,三者接近

但要注意:

  1. 上述三個join操做並無考慮空間換時間的算法優化,好比將evaluations先轉換到HashMap中,以後在遍歷poets時,經過HashMap直接獲取目標evaluation。沒有考慮這個優化,是由於這裏相比較的就是Stream的隱式雙層遍歷與之前的顯式雙層遍歷之間的性能表現。利用HashMap的優化方法,這三者均可以使用。。。
  2. 顯式雙層遍歷就沒有考慮fori循環了,由於fori的性能原本就比不上迭代器循環,不必在這裏丟人現眼了。。。
  3. 數據集數量較大仍是較小的判斷標準取決於硬件環境,不能一律而論。
  4. 上述測試比較簡陋,每種case都只測了一次。若是你們有時間,建議每種數據量的case都測試10次以上取平均值。

單個數據集的複雜轉換操做

其實比較完上述兩個場景的性能表現以後,咱們大約已經能夠獲得一個粗略的印象:

  1. 數據量小的時候性能其實都差很少;
  2. 數據量較大時,只要業務容許,硬件足夠就儘可能並行;
  3. 只能串行時,又對性能有必定要求,那仍是顯式迭代器循環快一點。

但這裏老夫仍然要說,沒有極致的性能要求的話,優先用Stream操做。

咱們看這樣的一個例子:單個數據集的屢次數據轉換操做。

首先仍然是詩人集合與評價集合:

// poets件數
int n = 100000;
// evaluations件數
int m = 1000;
List<Poet> poets = new ArrayList<>();
for (int i = 0; i < n; i++) {
    String name = String.format("詩人%010d", i + 1);
    poets.add(new Poet(name, (int) (80 * Math.random()) + 10, (int) (m * Math.random()) + 1));
}
List<Evaluation> evaluations = new ArrayList<>();
for (int i = 0; i < m; i++) {
    evaluations.add(new Evaluation(i + 1, (i + 1) + "星"));
}

爲了不雙層遍歷,咱們把評價集合轉換爲HashMap:

Map<Integer, String> evaluationMap = evaluations.stream()
        .collect(Collectors.toMap(Evaluation::getEvaluation, Evaluation::getDescription));

下面咱們模擬這樣一段邏輯:從 poets 中找到全部評價 > m/2 的詩人,把它們拼接爲"詩人名:評價描述"的字段,而後再過濾掉"詩人名:評價描述"中不包含0的記錄。

雖然上述邏輯能夠在一次循環中實現,但在實際開發中,每每有更復雜的邏輯致使咱們常常按業務邏輯把它拆成數個循環處理。所以下面咱們的模擬代碼並未作一次循環搞定的優化。
System.out.println("=== 屢次循環實現數據轉換邏輯 ===");
LocalDateTime startTime = LocalDateTime.now();
List<Poet> betterPoets = new ArrayList<>();
for(Poet poet : poets) {
    if (poet.getEvaluation() > m / 2) {
        betterPoets.add(poet);
    }
}
List<String> poetWithEva2 = new ArrayList<>();
for(Poet poet : betterPoets) {
    poetWithEva2.add(poet.getName() + ":" + evaluationMap.get(poet.getEvaluation()));
}
List<String> poetWithEva3 = new ArrayList<>();
for(String s : poetWithEva2) {
    if (s != null && s.contains("0")) {
        poetWithEva3.add(s);
    }
}
LocalDateTime stopTime = LocalDateTime.now();
System.out.println("屢次循環實現數據轉換邏輯 time(millis):" + Duration.between(startTime, stopTime).toMillis());

而後咱們用Stream實現相同的邏輯:

System.out.println("=== Stream實現數據轉換邏輯 ===");
startTime = LocalDateTime.now();
List<String> poetWithEva = poets.stream()
        .filter(poet -> poet.getEvaluation() > m / 2)
        .map(poet -> poet.getName() + ":" + evaluationMap.get(poet.getEvaluation()))
        .filter(s -> s.contains("0"))
        .collect(Collectors.toList());
stopTime = LocalDateTime.now();
System.out.println("Stream實現數據轉換邏輯 time(millis):" + Duration.between(startTime, stopTime).toMillis());

再將三次顯式迭代器循環優化爲一次循環:

System.out.println("=== 一次循環實現數據轉換邏輯 ===");
startTime = LocalDateTime.now();
List<String> lastLst = new ArrayList<>();
for(Poet poet : poets) {
    if (poet.getEvaluation() > m / 2) {
        String tmp = poet.getName() + ":" + evaluationMap.get(poet.getEvaluation());
        if (tmp.contains("0")) {
            lastLst.add(tmp);
        }
    }
}
stopTime = LocalDateTime.now();
System.out.println("一次循環實現數據轉換邏輯 time(millis):" + Duration.between(startTime, stopTime).toMillis());

從運行結果上看,Stream與一次循環(迭代器)的差距微乎其微,但都比屢次循環優點明顯。緣由固然很淺顯,由於Stream也是最後一次遍歷。

但Stream在開發效率上具備巨大的優點:語義簡單明瞭,不須要開發人員先按邏輯寫多個循環,而後再優化成一次循環。

固然了,水平高點的程序員也是能夠一次寫出優化後的一次循環的,但你看二者的代碼,就問你哪一個優雅?哪一個更容易讀懂代碼的目的?結果是顯而易見的,Stream在易讀性和可維護性上,遠比顯式循環的寫法更有優點。

所以再強調一遍:沒有極致的性能要求的話,優先用Stream操做。

Stream與parallelStream的使用建議

直接給結論:

  1. 能用Stream的地方,儘可能用Strem(開發效率高,代碼易讀易維護,性能接近迭代器循環);
  2. 只要沒有用Stream達不到的性能需求,就不要用parallelStream。一是由於並不是全部的數據集操做均可以並行操做,二是並行操做嚴重依賴硬件特別是CPU核數,在一個複雜的有併發請求的應用中可能會致使其餘業務的請求搶不到足夠的資源。。。
關於並行模式的CPU消耗,各位在本地運行前面的性能測試代碼時,能夠打開本地的資源監視器,看看Stream串行與並行模式下的CPU使用率。你會發現,Stream串行與顯式迭代器循環在運行時,基本上只有一個core的使用率達到100%,而並行模式時,全部core的使用率都會達到100%。若是這時你的應用有其餘併發的,也比較消耗CPU的請求過來,你猜它會比平時慢呢,仍是慢呢,仍是慢呢?若是你的應用仍是個高併發的系統,那你可否保證對CPU產生大量消耗的並行操做只發生在併發低的時間段呢?(固然是假設的你高併發系統是有高併發峯值時間段的,峯值時間段之外不存在高併發場景。。。)

3.2.6 強行總結一波Stream

  • Stream究竟是啥?
Stream 其實並非集合或集合的元素,它自己不是數據結構,不保存數據,它實際上是對集合的一種運算框架。它更像一個高級版本的迭代器 Iterator。但不一樣於Iterator只能顯式地一個一個遍歷元素,Stream 只要開發者給出操做意圖及其函數實現(即作什麼和怎麼作),好比 "過濾掉小於0的數字"、"給每一個字符串從左補足10位"等,Stream 就會隱式地在內部進行遍歷,並作出相應的數據轉換。

作什麼就是你須要調用Stream的哪一個方法,而怎麼作就是你須要給Stream的方法傳入什麼樣的函數,即,lambda表達式!

  • 因此爲啥叫Stream呢?
首先,Stream是管道流操做。從前面的Stream操做的代碼示例中咱們能夠看到,整個Stream操做就是一個管道流操做,開始和中間操做老是返回一個新的Stream對象,後面繼續對這個Stream對象進行操做,猶如接力,直到最後執行操做得到結果。

其次,Stream就如同一個迭代器(Iterator)那樣,最後的Terminal對數據集的遍歷是單向的,不可往復的。數據只能遍歷一次,遍歷過一次後就結束了,不可逆轉,恰似黃河之水天上來,奔流到海不復回。

故名Stream

  • Stream與之前的集合操做相比,有哪些特色呢?
Stream與之前的集合操做相比,不一樣的地方在於,之前的集合操做(包括Iterator)只能命令式的,串行的操做。而Stream具備以下特色:
  1. 經過lambda表達式實現了對函數式編程的支持,語義更接近天然語言,代碼更易讀;
  2. 支持管道流的鏈式操做,能夠將大量遍歷邏輯更加簡潔地統一在一塊兒;
  3. 支持並行模式,能夠將數據分紅多個片斷,在不一樣的線程中執行,最後再合併輸出,並且不用顯式寫多線程操做。
這麼多好處,就問你爽不爽,漫卷詩書喜欲狂了沒?
  • 關於Stream的並行模式,它是有發展軌跡的。

從Java的並行編程API(或者說多線程編程)的角度來看,咱們能夠看到其在Java各個大版本中的發展壯大過程大體以下:

  1. Java1到Java4 中的 java.lang.Thread
  2. Java5開始提供,Java6繼續加強的 java.util.concurrent
  3. Java7引入的 Fork/Join 框架
  4. Java8新增的Stream並行模式

3.3 接口默認方法

前面講Lamdba表達式的標準函數式接口的時候,各位敏銳的小夥伴們應該就發現了,這些接口裏面竟然有已經實現了的方法。。。這是怎麼回事呢?豈不是違反了Java本身關於接口沒有實現方法的規定?

emmm,確實違反了,固然這是有緣由的,後面咱們再說。。。先看看接口裏的方法實現是怎麼回事。

3.3.1 給接口添加default方法

Java8開始,你能夠給接口添加default方法。以下所示:

public interface Printer {
    default void print() {
        System.out.println("衆鳥高飛盡");
    }

    default void printAnathor() {
        System.out.println("孤雲獨去閒");
    }
}

這些默認的實現不要求implements該接口的Class重寫就能夠直接使用,以下所示:

PrintClass printClass = new PrintClass();
printClass.print();
printClass.printAnathor();
...
class PrintClass implements Printer {
}

固然你偏要重寫接口的default方法也是沒有問題的。

3.3.2 如何避免defaut方法衝突

接口不一樣於抽象類,抽象類使用繼承,而Java是單繼承的,所以不會出現繼承的方法衝突問題。但接口能夠寫default方法後,就有了方法衝突的可能。由於Java中一個類能夠實現多個接口,那麼當這些接口中有相同的default方法時,就會出現default方法衝突。

例如接口Printer2中也實現了方法print

public interface Printer2 {
    default void print() {
        System.out.println("只有敬亭山");
    }
}

此時若是一個類同時實現接口PrinterPrinter2

class PrintClass2 implements Printer, Printer2 {
}

此時就會由於default方法衝突而編譯錯誤。

如何解決呢?咱們能夠在PrintClass2中重寫print方法:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看兩不厭");
    }
}

但若是想要調用某個接口中的default方法怎麼辦呢?這時能夠經過Printer2.super.print();這種特殊寫法實現:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看兩不厭");
        Printer2.super.print();
    }
}

總的規則以下:

  1. 類優先級高於接口。若是在繼承鏈中有方法體或抽象的方法聲明,那麼就能夠忽略接口中定義的方法。
  2. 子優先級高於父。若是一個接口繼承了另外一個接口,且兩個接口都定義了一個默認方法,那麼子接口中定義的方法勝出。
  3. 若是上面兩條規則都不適用,子類要麼須要實現該方法,要麼將該方法聲明爲抽象方法。

3.3.3 靜態方法實現

Java8的接口中不只能夠寫default方法,還能夠寫static方法:

public interface Printer2 {
    default void print() {
        System.out.println("只有敬亭山");
    }

    static void printHello(String name) {
        System.out.println("Hello " + name);
    }

    static void printBye(String name) {
        System.out.println("Goodbye " + name);
    }
}

調用時使用接口.靜態方法便可:

class PrintClass2 implements Printer, Printer2 {
    @Override
    public void print() {
        System.out.println("相看兩不厭");
        Printer2.super.print();
    }

    public void helloAndBye() {
        Printer2.printHello("Java8");
        Printer2.printBye("Java8");
    }
}

3.3.4 接口默認方法的討論

Java8給接口增長默認方法是引發不一樣意見比較多的一個新特性。不喜歡的認爲這一點破壞了Java做爲面嚮對象語言的規範性,容易引發方法引用混亂不易維護,好比之前的老夫;喜歡的以爲增長了Java的靈活性,只要能控制住範圍還挺好用的,好比如今以爲真香的老夫。。。

爲何Java要增長接口的默認方法?

  • Java8之因此給接口加上default方法,一方面是爲了配合Stream API與Lambda表達式,好比Collection的stream()方法,試想若是接口不能提供默認方法,那麼就須要爲全部的Collection類中實現stream()方法。。。
  • 另外一方面,default方法帶來了一個好處,就是在擴展一個新的簡單功能時,能夠直接在相關接口中加一個新的默認方法,而不用加一個新的實現類。增長新實現類可能會破壞現有的代碼繼承體系,常常增長新的實現類甚至可能會引發類爆炸。

但無論是之前添加新的實現類,仍是如今能夠直接在接口中添加默認方法,都是不能夠濫用的。前者會破壞代碼的類繼承體系甚至引發類爆炸,致使代碼難以維護;後者可能致使方法引用混亂,進而一樣致使代碼難以維護。運用之妙存乎一心,能夠用,但不能濫用。

目前的話,老夫有個小小的建議:

  1. 廣大應用層開發者,由於代碼的變更頻率比較高,且人員流動快,所謂鐵打的項目流水的程序員,這個時候仍是不要使用接口默認方法了。不挖坑從我作起!
  2. 可是,然而,若是英雄您已經能夠作共通或者框架底層開發了,那麼在須要的時候不妨一試。畢竟能作共通或框架的程序員,其基本素質咱們仍是要相信一下的。

3.4 Optional

前面講Stream的時候,看到有的Terminal操做會返回一個Optional對象,咱們對其進行orElse之類的操做。

Optional是Java8新增的用來解決NullPointerException的一個容器類,其中包含對其餘對象的引用。

這貨其實有點高冷,你對它不夠熟悉的話,它其實不是很好用。。。熟了之後你纔會以爲真香。。。

不逼逼,直接看代碼。

在Java8以前,咱們的代碼中總須要大量的非空判斷:

private void printLineOld(String line) {
    if (line != null) {
        System.out.println(line.trim());
    }
}

Java8以後,你能夠使用Optional優雅的完成不夠優雅的非空判斷。。。

首先,你須要用Optional把不知道是否是null的對象包起來:

// 若是肯定line不是null
Optional<String> line1 = Optional.of(line);
// 若是line是null,須要使用ofNullable
Optional<String> empty = Optional.ofNullable(line);

還有其餘的一些建立Optional對象的方法,這裏再也不一一介紹。面對未知是不是null的變量,老夫建議使用Optional.ofNullable將其封裝起來。

而後,在使用變量的地方,改成使用Optional對象:

// 假設 line 是一個 Optional<String> 類型的對象
try {
    System.out.println(line.get().trim());
} catch (NoSuchElementException e) {
    System.out.println("Optional.get 若是line是null,get會拋NoSuchElementException異常!");
}
// 僅在原來對象非null時執行傳入的lambda表達式
line.ifPresent(s -> System.out.println(s.trim()));
// 利用orElse,當原來對象是null時,使用orElse傳入的默認值
System.out.println(line.orElse(""));
// 利用orElseGet,當原來對象是null時,使用orElseGet傳入的lambda表達式
System.out.println(line.orElseGet(() -> "天生我材必有用," + "千金散盡還復來。"));
// 利用orElseThrow,當原來對象是null時,拋出本身定義的異常
System.out.println(line.orElseThrow(() -> new RuntimeException("也能夠拋出本身定義的異常!")));

其中:

  • ifPresent: 只有對象非null時纔會執行後面的lambda表達式;
  • orElse: 若是對象是null就返回後面傳入的默認值;
  • orElseGet:若是對象是null就執行後面傳入的lambda表達式來獲取返回值;
  • orElseThrow:若是對象是null就拋出本身定義的異常。

但要注意的是,使用Optional須要一個正確的打開姿式。。。

先看一個不正確的姿式:

// 不推薦將參數類型設計爲Optional,Optional適合用於返回值類型
public void printLine(Optional<String> line) {
    ...
}
這裏直接將方法參數設計爲Optional,這是不推薦的設計方式。由於做爲方法的提供者,你怎麼保證智商無下限的調用者必定會用 Optional.ofNullable顯式傳入參數呢?你擋不住人家放飛自我直接傳 null的。。。

除此之外,Optional也儘可能不要用於實例變量,由於它不能被序列化,當作字段屬性時可能會出問題。

來,看看正確的打開姿式:

private void test02_returnOptional(String line) {
    Optional<String> lineOpt = createLineOptional(line);

    // 僅在原來對象非null時執行傳入的lambda表達式
    lineOpt.ifPresent(s -> System.out.println(s.trim()));
    // 利用orElse,當原來對象是null時,使用orElse傳入的默認值
    System.out.println(lineOpt.orElse(""));
    // 利用orElseGet,當原來對象是null時,使用orElseGet傳入的lambda表達式
    System.out.println(lineOpt.orElseGet(() -> "天生我材必有用," + "千金散盡還復來。"));
    // 利用orElseThrow,當原來對象是null時,拋出本身定義的異常
    System.out.println(lineOpt.orElseThrow(() -> new RuntimeException("也能夠拋出本身定義的異常!")));
}

private Optional<String> createLineOptional(String line) {
    // 實際開發中,這裏也許會有比較複雜的邏輯,用於返回一個對象,而該方法不保證返回對象不爲null;
    // 所以使用該方法的地方必須判斷返回值是否爲null。。。
    // 但若是咱們將返回值用Optional包起來,那麼對於調用該方法的地方而言,非空判斷就能夠很優雅了。
    return Optional.ofNullable(line);
}

3.5 Map操做及HashMap性能優化

以前講Stream的時候,細心的小夥伴會發現,沒有從Map生成Stream的操做。是的,Map沒有stream()方法,無法直接獲取Map的Stream對象,由於Java到如今也不支持元組甚至二維元組,所以Map的元素鍵值對(key, value)無法做爲Stream<T>的泛型來使用。。。

固然,Java8的Map提供了一些新的方法來知足咱們平常操做的須要。

3.5.1 加強的Map操做

咱們先看看一個集合(List或Set)如何轉換爲Map。

// 仍是以前的詩人集合
List<Poet> poets = Poet.preparePoets();
// 利用 Collectors.toMap 將Stream中的數據集轉換爲Map
Map<String, Poet> poetMap = poets.stream().collect(Collectors.toMap(Poet::getName, poet -> poet));
以前Stream的示例代碼中也有相似的例子。。。強大的Collectors你們要多多親近。。。

接下來,讓咱們看看Map都有哪些好用的新方法:

  • foreach
poetMap.forEach((s, poet) -> {
    System.out.printf("%s 活了 %s 歲。 %n", s, poet.getAge());
    System.out.printf("%s 評價 : %s 。 %n", s, poet.getEvaluation());
});
這個看起來已經很像一個二維元組了。。。
  • putIfAbsent : 判斷map中是否已經存在目標key,沒有或爲null的話put一個value進去。
Poet censhen = poetMap.get("岑參");
if (censhen == null) {
    censhen = new Poet("岑參", 51, 4);
    poetMap.put("岑參", censhen);
}
System.out.println(censhen);
// 上面的代碼如今能夠直接使用 putIfAbsent 了。
poetMap.putIfAbsent("岑參", new Poet("岑參", 51, 5));
// 結果 "岑參" 的評價依舊是 4 而不是 5,由於 putIfAbsent 不會替換已經存在的value。
System.out.println(poetMap.get("岑參"));
比較一下之前的寫法和如今的寫法,是否是優雅了不少?優雅就是戰鬥力,優雅即正義。。。
  • computeIfPresent : 若是指定鍵的值存在且非空,則嘗試在給定鍵及其當前映射值的狀況下計算新映射。
// "岑參"已經加入了poetMap
poetMap.computeIfPresent("岑參", (s, poet) -> new Poet(s, 51,4));
// computeIfPresent會替換已經存在的value
System.out.println(poetMap.get("岑參"));
// "孟浩然"還沒有加入poetMap
poetMap.computeIfPresent("孟浩然", (s, poet) -> new Poet(s, 51,3));
// computeIfPresent只在key已經存在時替換value
System.out.println(poetMap.containsKey("孟浩然"));
  • computeIfAbsent : 只在key不存在時put一個非空的value
poetMap.computeIfAbsent("孟浩然", s -> new Poet(s, 51,3));
System.out.println(poetMap.get("孟浩然"));
computeIfAbsent 與 putIfAbsent 區別在於傳入參數不一樣,一個是lambda表達式,一個是具體的value。
  • remove(key, value) : key , value 都匹配時刪除
poetMap.remove("孟浩然", new Poet("孟浩然", 51,3));
// 刪除失敗,由於value不是一個對象
System.out.println(poetMap.containsKey("孟浩然"));
poetMap.remove("孟浩然", poetMap.get("孟浩然"));
// 刪除成功
System.out.println(poetMap.containsKey("孟浩然"));
  • getOrDefault
System.out.println(poetMap.getOrDefault("孟浩然", new Poet("XX", 20, 1)));
  • merge : key不存在時添加新value,key存在時根據lambda表達式merge value
Map<String, String> lines = new HashMap<>();
lines.merge("杜甫名句", "星垂平野闊,", (value, newValue) -> value.concat(newValue));
System.out.println(lines.get("杜甫名句"));
lines.merge("杜甫名句", "月涌大江流。", String::concat);
System.out.println(lines.get("杜甫名句"));

3.5.2 HashMap的性能優化

Java8對HashMap的性能也作了必定的優化。

這節都是理論,對HashMap機制不熟悉的小夥伴要回頭本身補補課了。。。

無論咱們知道仍是僞裝知道hashmap的機制,這裏都簡單回顧一下(Java8以前):

  1. Java中的hashmap存儲是一個node數組,經過key的hash值數組長度-1與運算獲得每一個key在數組中的存儲下標;
  2. 當不一樣的key計算獲得的存儲下標衝突時(不少資料叫hash衝突,其實大部分狀況並非hash值衝突),就把對應的不一樣的key-value都放到這個下標對應的node鏈表裏(對,node是鏈表結構)。
  3. 數組長度不是不變的,初始容量是2的4次方,負載係數默認0.75,數組長度超過容量×負載係數時,HashMap就會乘2擴容,即2的指數加1,而後你們從新排排坐(從新算下標)。
  4. 從HashMap查找一個key對應的value時,是先根據key的hash值計算下標,再遍歷下標對應的node鏈表,找到該key對應的value。

在Java8之前,HashMap的性能瓶頸主要有兩個地方:

  1. 下標衝突較多時,從HashMap取值要先根據key計算下標位置,而後遍歷該位置的node鏈表,直到找到key對應的value;
  2. HashMap每次擴容時,要從新計算全部元素的下標。

在Java8中,對這兩點作了必定的優化:

  1. node再也不老是鏈表,當鏈表長度超過8且node數組容量超過64時,就將鏈表改成紅黑樹。紅黑樹是一種讀寫性能比較均衡的特殊的平衡二叉樹。關於二叉樹,平衡二叉樹,紅黑樹,有興趣的小夥伴本身去了解吧。。。爲何還要指定node數組容量超過64呢?由於容量比較小時,下標衝突可能性比較大,這時應該優先擴容。
  2. 擴容時,再也不從新一個一個計算元素的hash值,而是直接對原來的下標值作位移(由於容量老是以乘2的規律擴張)。
擴容時,可能會致使紅黑樹又被拆分爲兩個鏈表。

3.6 Date API

Java 8 在包java.time下包含了一組全新的時間日期API,功能更強大,也更安全。

3.6.1 Clock與時區

  1. Clock類提供了訪問當前日期和時間的方法,Clock是時區敏感的,能夠用來取代 System.currentTimeMillis() 來獲取當前的微秒數。某一個特定的時間點也能夠使用Instant類來表示,Instant類也能夠用來建立老的java.util.Date對象。
  2. 在新API中時區使用ZoneId來表示。時區能夠很方便的使用靜態方法of來獲取到。 時區定義了到UTS時間的時間差,在Instant時間點對象到本地日期對象之間轉換的時候是極其重要的。
// 系統Clock對象 採用系統默認時區
Clock clock = Clock.systemDefaultZone();
System.out.println(clock);

// 系統當前微妙數
long millis = clock.millis();
System.out.println(millis);

// 獲取之前的Date對象
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);

// 獲取可用時區
System.out.println(ZoneId.getAvailableZoneIds());
// 獲取指定時區
ZoneId zoneSh = ZoneId.of("Asia/Shanghai");
System.out.println(zoneSh.getRules());
ZoneId zoneTk = ZoneId.of("Asia/Tokyo");
System.out.println(zoneTk.getRules());
ZoneId zoneNy = ZoneId.of("America/New_York");
System.out.println(zoneNy.getRules());

3.6.2 LocalTime、LocalDate與LocalDateTime

LocalTime、LocalDate與LocalDateTime都是Java8提供的新的日期API,它們具備以下特色:

  1. 不可變的,所以線程安全;
  2. 配合DateTimeFormatter作格式化,線程安全;
  3. 配合Duration,ChronoUnit等作時間差運算更方便;
  4. 獲取系統當前時間更方便;
  5. 等等。。。
  • LocalTime 定義了一個沒有時區信息的時間,例如 晚上10點,或者 17:30:15。
// LocalTime 沒有年月日和時區信息,只有時分秒及如下
LocalTime localTimeNowDefault = LocalTime.now(ZoneId.systemDefault());
System.out.println(localTimeNowDefault);
LocalTime localTimeNowTk = LocalTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(localTimeNowTk);
// 計算時間差
long hoursBetween = ChronoUnit.HOURS.between(localTimeNowDefault, localTimeNowTk);
System.out.println(hoursBetween);
long minutesBetween = ChronoUnit.MINUTES.between(localTimeNowDefault, localTimeNowTk);
System.out.println(minutesBetween);
// 獲取一個任意時間的 LocalTime
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);
// 根據格式轉換字符串爲 LocalTime (由於LocalTime只有小時如下,所以格式有限制,只能用FormatStyle.SHORT)
DateTimeFormatter dtf_localtime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", dtf_localtime);
System.out.println(leetTime);
  • LocalDate 表示了一個確切的日期,好比 2014-03-11。
// LocalDate 年月日
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate new_year_day = LocalDate.of(2020, Month.JANUARY, 1);
DayOfWeek dayOfWeek = new_year_day.getDayOfWeek();
System.out.printf("今天是%s,明天是%s,昨天是%s,元旦是%s,%s。 %n", today, tomorrow, yesterday, new_year_day, dayOfWeek);
// 格式化
DateTimeFormatter dtf_localdate = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.GERMAN);
LocalDate children_day = LocalDate.parse("01.06.2020", dtf_localdate);
System.out.println(children_day);
  • LocalDateTime 同時表示了時間和日期
// LocalDateTime 日期加時間
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime laborDay = LocalDateTime.of(2020, Month.MAY, 1, 14, 41, 3);
System.out.println(laborDay);
System.out.println(laborDay.getDayOfWeek());
System.out.println(laborDay.getMonth());
System.out.println(laborDay.getLong(ChronoField.MINUTE_OF_DAY));
// 經過時間點Instance對象轉換爲Date
Instant laborInstant = laborDay.atZone(ZoneId.systemDefault()).toInstant();
Date laborDate = Date.from(laborInstant);
System.out.println(laborDate);

// 自定義格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
String strNow = formatter.format(LocalDateTime.now());
System.out.println(strNow);
LocalDateTime ldtNow = LocalDateTime.parse(strNow, formatter);
System.out.println(ldtNow);

// 計算時間差
System.out.println(ChronoUnit.DAYS.between(ldtNow, laborDay));
System.out.println(Duration.between(ldtNow, laborDay).toDays());

3.7 CompletableFuture

Java8以前,多線程開發中,若是主線程須要子線程結束後再進行下一步的處理,那麼只能同步阻塞的等待,不管你是在主線程中調用子線程的join方法,仍是用Future的get方法。

Java8增長了新的CompletableFuture類,能夠配合lamda表達式,給子線程傳入函數用於子線程執行結束後的回調。

看個簡單的例子:

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "明月出天山,蒼茫雲海間。";
});
completableFuture.thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("長風幾萬裏,吹度玉門關。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("漢下白登道,胡窺青海灣。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("由來征戰地,不見有人還。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("戍客望邊邑,思歸多苦顏。");
}).thenApply(s -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return s.concat("\n").concat("高樓當此夜,嘆息未應閒。");
}).thenAccept(System.out::println);

System.out.println("關山月 唐 李白");
try {
    Thread.sleep(8000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("==================");
這個例子中, CompletableFuture.supplyAsync定義了一個子線程,異步執行傳入的lamda表達式,它返回一個CompletableFuture對象。 supplyAsync方法被重載爲兩個方法,一個如上面示例,只有一個參數。另外一個重載的方法有兩個參數,一個是傳入的子線程處理邏輯(lambda表達式),另外一個是線程池對象。不傳入線程池對象時,使用默認線程池(對於多核機器來講是一個forkjoin線程池)。

CompletableFuture對象的thenApply方法傳入了一個回調函數,這個回調函數會在子線程執行結束後被子線程回調,且回調函數以子線程的執行返回爲入參,並返回本次回調處理的結果。能夠看到,當連續用thenApply方法傳入多個回調函數時,這些回調函數會被串行回調。

而CompletableFuture對象的thenAccept傳入的回調函數只接收子線程的執行結果,自己沒有返回值。

一串的thenApply最後接一個thenAccept是一種常見用法。

再看一個例子:

CompletableFuture<Double> futurePrice = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep((long) (Math.random() * 1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    double price = Math.random() * 100;
    System.out.println("Price is " + price);
    return price;
});
CompletableFuture<Integer> futureCount = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep((long) (Math.random() * 1000));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    int count = (int) (Math.random() * 100);
    System.out.println("Count is " + count);
    return count;
});
CompletableFuture<Double> futureTotal = futurePrice.thenCombine(futureCount, (price, count) -> price * count);
futureTotal.thenAccept(total -> System.out.println("Total is " + total));

System.out.println("鬼知道要多久。。。該幹嗎幹嗎去。。。");
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
這個例子中,咱們須要先計算出price和count,而後相乘獲得總價。假設求取price和count的處理不知什麼時候方能完成。

因此咱們先分別異步執行price和count的子線程,而後經過thenCombine方法,執行這樣一個邏輯:等兩個子線程都結束之後,將它們的返回值做爲參數執行回調函數。

這樣咱們就達成了等兩個子線程都結束後再回調的邏輯,同時主線程依然該幹嗎幹嗎,不會阻塞。

CompletableFuture提供了不少方法,上面咱們的例子理解以後,就能夠自行去看看這些方法都是什麼功能,適用於什麼場景了:

  • 用於建立CompletableFuture的靜態方法:supplyAsync, runAsync。前面咱們用了supplyAsync,這是有返回值的子線程,runAsync是沒有返回值的子線程。它們都有帶線程池參數的重載方法。
  • CompletableFuture對象提供的用於指定回調函數的實例方法:thenAccept,thenApply,thenRun;thenCombine,thenAcceptBoth,runAfterBoth;applyToEither,acceptEither,runAfterEither;exceptionally;whenComplete,handle 等等
  • 獲取CompletableFuture對象的靜態方法:allOf,anyOf

3.8 其餘新特性

Java8還有不少新特性,好比多重註解,Arrays.parallelSort,StampedLock等等,這裏再也不一一介紹,有須要的小夥伴能夠自行學習。

4、Java9~Java11的新特性

由於java9和Java10都是過渡版本,咱們直接以Java11(Java8以後第一個LTS版本)爲邊界來說講9到11有哪些比較影響咱們開發的新特性。

Java11相對Java8,在語法上的新特性並很少。主要有:

  • 本地變量類型推斷
  • HttpClient
  • Collection加強
  • Stream加強
  • Optional加強
  • String加強
  • InputStream加強

4.1 本地變量類型推斷

Java10之後能夠用var定義一個局部變量,不用顯式寫出它的類型。但要注意,被var定義的變量仍然是靜態類型,編譯器會試圖去推斷其類型。

String strBeforeJava10 = "strBeforeJava10";
var strFromJava10 = "strFromJava10";
System.out.println(strBeforeJava10);
System.out.println(strFromJava10);

所以,要注意:

  • 不兼容的類型是不能從新賦值的!
// 例以下面的語句編譯會失敗,"InCompatible types."
strFromJava10 = 10;
  • 只要編譯器沒法推斷出變量類型,就會編譯錯誤!
// 例以下面這些都沒法經過編譯:
var testVarWithoutInitial;
var testNull = null;
var testLamda = () -> System.out.println("test");
var testMethodByLamda = () -> giveMeString();
var testMethod2 = this::giveMeString;

而推薦使用類型推斷的場景有:

  • 簡化泛型聲明
// 以下所示,Map <String,List <Integer >>類型,能夠被簡化爲單個var關鍵字
var testList = new ArrayList<Map<String, List<Integer>>>();
for (var curEle : testList) {
    // curEle可以被推斷出類型是 Map<String, List<Integer>>
    if (curEle != null) {
        curEle.put("test", new ArrayList<>());
    }
}
  • lambda參數
// 從Java 11開始,lambda參數也容許使用var關鍵字:
Predicate<String> predNotNull = (var a) -> a != null && a.trim().length() > 0;
String strAfterFilter = Arrays.stream((new String[]{"a", "", null, "x"}))
        .filter(predNotNull)
        .collect(Collectors.joining(","));
System.out.println(strAfterFilter);

4.2 HttpClient

Java 9開始引入HttpClient API來處理HTTP請求。 從Java 11開始,這個API正式進入標準庫包。參考網址:http://openjdk.java.net/groups/net/httpclient/intro.html

HttpClient具備如下特性:

  1. 同時支持 HTTP1.1 和 HTTP2 協議,並支持 websocket
  2. 同時支持同步和異步編程模型
  3. 將請求和響應主體做爲響應式流(reactive-streams)處理,並使用構建器模式

HttpClient

要發送http請求,首先要使用其構建器建立一個HttpClient。這個構建器可以配置每一個客戶端的狀態:

  • 首選協議版本 ( HTTP/1.1 或 HTTP/2 )
  • 是否跟隨重定向
  • 代理
  • 身份驗證

一旦構建完成,就能夠使用HttpClient發送多個請求。

HttpRequest

HttpRequest是由它的構建器建立的。請求的構建器可用於設置:

  • 請求URI
  • 請求Method ( GET, PUT, POST )
  • 請求主體(若是有)
  • 超時時間
  • 請求頭

HttpRequest構建以後是不可變的,但能夠發送屢次。

Synchronous or Asynchronous

請求既能夠同步發送,也能夠異步發送。固然同步的API會致使線程阻塞直到HttpResponse可用。異步API當即返回一個CompletableFuture,當HttpResponse可用時,它將獲取HttpResponse並執行後續處理。

CompletableFuture是Java 8添加的新特性,用於可組合的異步編程。

Data as reactive-streams

請求和響應的主體做爲響應式流(具備非阻塞背壓的異步數據流)供外部使用。HttpClient其實是請求正文的訂閱者和響應正文字節的發佈者。BodyHandler接口容許在接收實際響應體以前檢查響應代碼和報頭,並負責建立響應BodySubscriber。

HttpRequest和HttpResponse類型提供了許多便利的工廠方法,用於建立請求發佈者和響應訂閱者,以處理常見的主體類型,如文件、字符串和字節。這些便利的實現要麼累積數據,直到能夠建立更高級別的Java類型(如String),要麼就文件流傳輸數據。BodySubscriber和BodyPublisher接口能夠實現爲自定義反應流處理數據。

HttpRequest和HttpResponse還提供了轉換器,用於將 java.util.concurrent.Flow 的 Publisher/Subscriber 類型轉換爲 HTTP Client的 BodyPublisher/BodySubscriber 類型。

HTTP/2

Java HTTP Client支持 HTTP/1.1 和 HTTP/2。默認狀況下,客戶端將使用 HTTP/2 發送請求。發送到尚不支持 HTTP/2 的服務器的請求將自動降級爲 HTTP/1.1。如下是HTTP/2帶來的主要改進:

  • 標頭壓縮。 HTTP/2 使用 HPACK 壓縮,從而減小了開銷。
  • 與服務器的單一鏈接減小了創建多個TCP鏈接所需的往返次數。
  • 多路複用。 在同一鏈接上,同時容許多個請求。
  • 服務器推送。 能夠將其餘未來須要的資源發送給客戶端。
  • 二進制格式。 更緊湊。

因爲HTTP/2是默認的首選協議,而且在須要的地方無縫地實現回退到HTTP/1.1,那麼當HTTP/2被更普遍地部署時,Java HTTP客戶端就無需修正它的應用代碼。

API文檔

https://docs.oracle.com/en/ja...

演示代碼

代碼中請求的網址中,localhost:30001的相關uri來自工程https://github.com/zhaochuninhefei/study-czhao/tree/master/jdk11-test

package jdk11;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient
 *
 * @author zhaochun
 */
 public class TestCase02HttpClient {
    public static void main(String[] args) throws Exception {
        TestCase02HttpClient me = new TestCase02HttpClient();
        me.testHttpClientGetSync();
        me.testHttpClientGetAsync();
        me.testHttpClientPost();

        // 同一個HttpClient先登陸網站獲取token,再請求受限制資源,從而爬取須要認證的資源
        me.testLogin();

        // HttpClient支持websocket
        me.testWebsocket();
    }

    private void testHttpClientGetSync() {
        var url = "https://openjdk.java.net/";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();
        var client = HttpClient.newHttpClient();
        try {
            System.out.println(String.format("send begin at %s", LocalDateTime.now()));
            // 同步請求
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println(String.format("send end at %s", LocalDateTime.now()));
            System.out.println(String.format("receive response : %s", response.body().substring(0, 10)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testHttpClientGetAsync() {
        var url = "https://openjdk.java.net/";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();
        var client = HttpClient.newHttpClient();
        try {
            System.out.println(String.format("sendAsync begin at %s", LocalDateTime.now()));
            // 異步請求
            client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                    .thenApply(stringHttpResponse -> {
                        System.out.println(String.format("receive response at %s", LocalDateTime.now()));
                        return stringHttpResponse.body();
                    })
                    .thenAccept(s -> System.out.println(String.format("receive response : %s at %s", s.substring(0, 10), LocalDateTime.now())));
            System.out.println(String.format("sendAsync end at %s", LocalDateTime.now()));

            // 爲了防止異步請求還沒有返回主線程就結束(jvm會退出),這裏讓主線程sleep 10秒
            System.out.println("Main Thread sleep 10 seconds start...");
            Thread.sleep(10000);
            System.out.println("Main Thread sleep 10 seconds stop...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testHttpClientPost() {
        var url = "http://localhost:30001/jdk11/test/helloByPost";
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Content-Type", "text/plain")
                .POST(HttpRequest.BodyPublishers.ofString("zhangsan"))
                .build();
        var client = HttpClient.newHttpClient();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println(response.statusCode());
            System.out.println(response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void testLogin() throws Exception {
        var client = HttpClient.newHttpClient();
        // 某測試環境用戶登陸URL
        var urlLogin = "http://x.x.x.x:xxxx/xxx/login";
        var requestObj = new HashMap<String, Object>();
        requestObj.put("username", "xxxxxx");
        requestObj.put("password", "xxxxxxxxxxxxxxxx");
        var objectMapper = new ObjectMapper();
        var requestBodyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestObj);
        var requestLogin = HttpRequest.newBuilder()
                .uri(URI.create(urlLogin))
                .header("Content-Type", "application/json;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString(requestBodyJson))
                .build();
        HttpResponse<String> responseLogin = client.send(requestLogin, HttpResponse.BodyHandlers.ofString());
        // 這裏的登陸網站使用token,而沒有使用session,所以咱們須要從返回的報文主體中查找token信息;
        // 若是是使用session的網站,這裏須要從響應的headers中查找"set-cookie"從而獲取session id,並在後續請求中,將sid設置到header的Cookie中。
        // 如: responseLogin.headers().map().get("set-cookie")獲取cookies,再從中查找sid。
        var loginResponse = responseLogin.body();
        var mpLoginResponse = objectMapper.readValue(loginResponse, Map.class);
        var dataLogin = (Map<String, Object>) mpLoginResponse.get("data");
        var token = dataLogin.get("token").toString();
        // 測試環境獲取某資源的URL
        var urlGetResource = "http://xxxx:xxxx/xxx/resource";
        var requestRes = HttpRequest.newBuilder()
                .uri(URI.create(urlGetResource))
                .header("Content-Type", "application/json;charset=UTF-8")
                // 注意,token並不是必定設置到header的Authorization中,這取決於網站驗證的方式,也有可能token也放到cookie裏。
                // 但對於使用session的網站,sid都是設置在cookie裏的。如: .header("Cookie", "JSESSIONID=" + sid)
                .header("Authorization", token)
                .GET()
                .build();
        HttpResponse<String> responseResource = client.send(requestRes, HttpResponse.BodyHandlers.ofString());
        var response = responseResource.body();
        System.out.println(response);
    }

    private void testWebsocket() {
        var wsUrl = "ws://localhost:30001/ws/test";
        var httpClient = HttpClient.newHttpClient();
        WebSocket websocketClient = httpClient.newWebSocketBuilder()
                .buildAsync(URI.create(wsUrl), new WebSocket.Listener() {
                    @Override
                    public void onOpen(WebSocket webSocket) {
                        System.out.println("onOpen : webSocket opened.");
                        webSocket.request(1);
                    }

                    @Override
                    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                        System.out.println("onText");
                        webSocket.request(1);
                        return CompletableFuture.completedFuture(data)
                                .thenAccept(System.out::println);
                    }

                    @Override
                    public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
                        System.out.println("ws closed with status(" + statusCode + "). cause:" + reason);
                        webSocket.sendClose(statusCode, reason);
                        return null;
                    }

                    @Override
                    public void onError(WebSocket webSocket, Throwable error) {
                        System.out.println("error: " + error.getLocalizedMessage());
                        webSocket.abort();
                    }
                }).join();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // last參數用於指示websocketClient,本次發送的數據是不是完整消息的最後部分。
        // 若是是false,則websocketClient不會把消息發送給websocket後臺的listener,只會把數據緩存起來;
        // 當傳入true時,會將以前緩存的數據和此次的數據拼接起來一塊兒發送給websocket後臺的listener。
        websocketClient.sendText("test1", false);
        websocketClient.sendText("test2", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        websocketClient.sendText("org_all_request", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        websocketClient.sendText("employee_all_request", true);

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        websocketClient.sendClose(WebSocket.NORMAL_CLOSURE, "Happy ending.");
    }
}

4.3 Collection加強

List,Set,Map有了新的加強方法:ofcopyOf

List的of與copyOf

List.of根據傳入的參數列表建立一個新的不可變List集合;List.copyOf根據傳入的list對象建立一個不可變副本。

var listImmutable = List.of("a", "b", "c");
var listImmutableCopy = List.copyOf(listImmutable);

因爲拷貝的集合自己就是一個不可變對象,所以拷貝實際上並無建立新的對象,直接使用了原來的不可變對象。

// 結果爲true
System.out.println(listImmutable == listImmutableCopy);
// 不可變對象不能進行修改
try {
    listImmutable.add("d");
} catch (Throwable t) {
    System.out.println("listImmutable can not be modified!");
}
try {
    listImmutableCopy.add("d");
} catch (Throwable t) {
    System.out.println("listImmutableCopy can not be modified!");
}

若是想快速新建一個可變的集合對象,能夠直接使用以前的不可變集合做爲構造參數,建立一個新的可變集合。

var listVariable = new ArrayList<>(listImmutable);
var listVariableCopy = List.copyOf(listVariable);

新建立的可變集合固然是一個新的對象,從這個新對象拷貝出來的不可變副本也是一個新的對象,並非以前的不可變集合。

System.out.println(listVariable == listImmutable); // false
System.out.println(listVariable == listVariableCopy); // false
System.out.println(listImmutable == listVariableCopy); // false
// 新的可變集合固然是能夠修改的
try {
    listVariable.add("d");
} catch (Throwable t) {
    System.out.println("listVariable can not be modified!");
}
// 可變集合拷貝出來的副本依然是不可變的
try {
    listVariableCopy.add("d");
} catch (Throwable t) {
    System.out.println("listVariableCopy can not be modified!");
}

Set的of和copyOf

Set的of和copyOf與List相似。

var set = Set.of("a", "c", "r", "e");
var setCopy = Set.copyOf(set);
System.out.println(set == setCopy);

但要注意,用of建立不可變Set時,要確保元素不重複,不然運行時會拋出異常: "java.lang.IllegalArgumentException: duplicate element"

try {
    var setErr = Set.of("a", "b", "a");
} catch (Throwable t) {
    t.printStackTrace();
}

固然建立可變set後添加劇復元素不會拋出異常,但會被去重

var setNew = new HashSet<>(set);
setNew.add("c");
System.out.println(setNew.toString());

Map的of和copyOf

Map的of和copyOf與list,set相似,注意of方法的參數列表是依次傳入key和value:

var map = Map.of("a", 1, "b", 2);
var mapCopy = Map.copyOf(map);
System.out.println(map == mapCopy);

固然也要注意建立不可變Map時,key不能重複

try {
    var mapErr = Map.of("a", 1, "b", 2, "a", 3);
} catch (Throwable t) {
    t.printStackTrace();
}

4.4 Stream加強

Java8開始引入的stream,Java11提供了一些擴展:

  • 單個元素直接構造爲Stream對象
  • dropWhile與takeWhile
  • 重載iterate方法用於限制無限流範圍

單個元素直接構造爲Stream對象

注意null與""的區別:

long size1 = Stream.ofNullable(null).count();
System.out.println(size1); // 0
long size2 = Stream.ofNullable("").count();
System.out.println(size2); // 1

dropWhile與takeWhile

dropWhile,對於有序的stream,從頭開始去掉知足條件的元素,一旦遇到不知足元素的就結束

List lst1 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1)
        .dropWhile(e -> e < 3)
        .collect(Collectors.toList());
System.out.println(lst1); // [3, 4, 5, 4, 3, 2, 1]

takeWhile,對於有序的stream,從頭開始保留知足條件的元素,一旦遇到不知足的元素就結束

List lst2 = Stream.of(1, 2, 3, 4, 5, 4, 3, 2, 1)
        .takeWhile(e -> e < 3)
        .collect(Collectors.toList());
System.out.println(lst2); // [1, 2]

即便把剩下的元素都收集到了無序的set中,但在此以前,stream對象是有序的,所以結果包含了原來stream中最後的[a2]和[a1]:

Set set1 = Stream.of("a1", "a2", "a3", "a4", "a5", "a4", "a3", "a2", "a1")
        .dropWhile(e -> "a3".compareTo(e) > 0)
        .collect(Collectors.toSet());
System.out.println(set1); // [a1, a2, a3, a4, a5]

若是先建立一個無序不重複的set集合,set無序更準確的說法是不保證順序不變,事實上是有順序的。
所以這裏會發現,dropWhile仍是按set當前的元素順序斷定的,一旦不知足條件就結束。

Set<String> set = new HashSet<>();
for (int i = 1; i <= 100 ; i++) {
    set.add("test" + i);
}
System.out.println(set);
Set setNew = set.stream()
        .dropWhile(s -> "test60".compareTo(s) > 0)
        .collect(Collectors.toSet());
System.out.println(setNew);

重載iterate方法用於限制無限流範圍

java8裏能夠建立一個無限流,好比下面這個數列,起始值是1,後面每一項都在前一項的基礎上 * 2 + 1,經過limit限制這個流的長度:

Stream<Integer> streamInJava8 = Stream.iterate(1, t -> 2 * t + 1);
// 打印出該數列的前十個: 1,3,7,15,31,63,127,255,511,1023
System.out.println(streamInJava8.limit(10).map(Object::toString).collect(Collectors.joining(",")));

從Java9開始,iterate方法能夠添加一個斷定器,例如,限制數的大小不超過1000

Stream<Integer> streamFromJava9 = Stream.iterate(1, t -> t < 1000, t -> 2 * t + 1);
// 這裏打印的結果是 1,3,7,15,31,63,127,255,511
System.out.println(streamFromJava9.map(Objects::toString).collect(Collectors.joining(",")));

4.5 Optional加強

能夠將Optional對象直接轉爲stream

Optional.of("Hello openJDK11").stream()
        .flatMap(s -> Arrays.stream(s.split(" ")))
        .forEach(System.out::println);

能夠爲Optional對象提供一個默認的Optional對象

System.out.println(Optional.empty()
        .or(() -> Optional.of("default"))
        .get());

4.6 String加強

String方面,針對空白字符(空格,製表符,回車,換行等),提供了一些新的方法。

isBlank

判斷目標字符串是不是空白字符。如下結果所有爲true

// 半角空格
System.out.println(" ".isBlank());
// 全角空格
System.out.println(" ".isBlank());
// 半角空格的unicode字符值
System.out.println("\u0020".isBlank());
// 全角空格的unicode字符值
System.out.println("\u3000".isBlank());
// 製表符
System.out.println("\t".isBlank());
// 回車
System.out.println("\r".isBlank());
// 換行
System.out.println("\n".isBlank());
// 各類空白字符拼接
System.out.println(" \t\r\n ".isBlank());

strip,stripLeading與stripTrailing

去除首尾的空白字符:

// 全角空格 + 製表符 + 回車 + 換行 + 半角空格 + <內容> + 全角空格 + 製表符 + 回車 + 換行 + 半角空格
var strTest = " \t\r\n 你好 jdk11 \t\r\n ";

// strip 去除兩邊空白字符
System.out.println("[" + strTest.strip() + "]");
// stripLeading 去除開頭的空白字符
System.out.println("[" + strTest.stripLeading() + "]");
// stripTrailing 去除結尾的空白字符
System.out.println("[" + strTest.stripTrailing() + "]");

repeat

重複字符串內容,拼接新的字符串:

var strOri = "jdk11";
var str1 = strOri.repeat(1);
var str2 = strOri.repeat(3);
System.out.println(str1);
System.out.println(str2);
// repeat傳入參數爲1時,不會建立一個新的String對象,而是直接返回原來的String對象。
System.out.println(str1 == strOri);

lines

lines方法用 r 或 n 或 rn 對字符串切割並返回stream對象:

var strContent = "hello java\rhello jdk11\nhello world\r\nhello everyone";
// lines方法用 \r 或 \n 或 \r\n 對字符串切割並返回stream對象
strContent.lines().forEach(System.out::println);
System.out.println(strContent.lines().count());

4.7 InputStream加強

InputStream提供了一個新的方法transferTo,將輸入流直接傳輸到輸出流:

inputStream.transferTo(outputStream);

完整示例代碼

package jdk11;

import java.io.*;

/**
 * InputStream加強
 *
 * @author zhaochun
 */
public class TestCase07InputStream {
    public static void main(String[] args) {
        TestCase07InputStream me = new TestCase07InputStream();
        me.test01_transferTo();
    }

    private void test01_transferTo() {
        var filePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml";
        var tmpFilePath = "/home/work/sources/test/jdk11-test/src/main/resources/application.yml.bk";

        File tmpFile = new File(tmpFilePath);
        if (tmpFile.exists() && tmpFile.isFile()) {
            tmpFile.delete();
        }

        try(InputStream inputStream = new FileInputStream(filePath);
            OutputStream outputStream = new FileOutputStream(tmpFilePath)) {
            // transferTo將 InputStream 的數據直接傳輸給 OutputStream
            inputStream.transferTo(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.8 其餘新特性

Java9到Java11還有一些其餘的新特性,好比模塊化開發,REPL交互式編程,單文件源代碼程序的直接執行,新的垃圾回收器等等,對目前的開發來講,影響比較小,有興趣的小夥伴能夠查閱老夫另外一篇文章:

相關文章
相關標籤/搜索