Jdk1.8新特性實戰篇(41個案例)

前言

一直想把jdk1.8的新特性整理下,恰好看到老外的git(文後有鏈接),在這個結構上繼續完善了說明和功能,做了41個單元測試案例,方便新人學習。以下內容很乾,對於一個萌新小白來說,學習jdk1.8的新特性,基本看一遍就知道個7788了,在熟讀兩遍最後跟着寫一遍,那麼在實際項目中就可以運用了。不過!新特性,雖然很好。但如果想用,那麼自己一定要看看相對應的源碼並多練習,否則真的容易給自己搞暈,又很難閱讀。

零、回顧一個抽象類

在jdk1.8之前,因爲接口裏只能做方法定義不能有方法的實現,因此我們通常會在抽象類裏面實現默認的方法{一般這個默認的方法是抽象後公用的方法,不需要每一個繼承者都去實現,只需調用即可}。就像下面這樣;

在定義的時候;

 1public abstract class AFormula {
 2
 3    abstract double calculate(int a);
 4
 5    // 平方
 6    double sqrt(int a) {
 7        return Math.sqrt(a);
 8    }
 9
10}

在使用的時候;

 1@Test
 2public void test_00() {
 3    AFormula aFormula = new AFormula() {
 4        @Override
 5        double calculate(int a) {
 6            return a * a;
 7        }
 8    };
 9    System.out.println(aFormula.calculate(2)); //求平方:4
10    System.out.println(aFormula.sqrt(2));     //求開方:1.4142135623730951
11}

一、在接口中提供默認的方法實現(有點像抽象類)

在jdk1.8裏面,不僅可以定義接口,還可以在接口中提供默認的實現。這一個小小的改變卻讓整個抽象設計都隨着改變了!

在定義的時候;{default 關鍵字必須}

 1public interface IFormula {
 2
 3    double calculate(int a);
 4
 5    // 平方
 6    default double sqrt(int a) {
 7        return Math.sqrt(a);
 8    }
 9
10}

在使用的時候(一);

 1@Test
 2public void test_01() {
 3    IFormula formula = new IFormula() {
 4        @Override
 5        public double calculate(int a) {
 6            return a * a;
 7        }
 8    };
 9    System.out.println(formula.calculate(2));
10    System.out.println(formula.sqrt(2));
11}

在使用的時候(二);如果只是一里面方式這麼使用,那麼就沒多大意思了。我一直說過;好的代碼都很騷!

  1. a; a是一個入參名稱,可以其他任何名字

  2. ->a*a;箭頭指向是具體的實現

  3. 但是,這樣其實不太適合加日誌了

1@Test
2public void test_02() {
3    // 入參a 和 實現
4    IFormula formula = a -> a * a;
5    System.out.println(formula.calculate(2));
6    System.out.println(formula.sqrt(2));
7}

二、Lambda 表達式

因爲有接口中可以增加默認的方法實現,那麼Java肯定是因爲要簡化開發纔出現的這麼個設計。所以你會從各個我們以前的List、Set等等所有接口中看到默認的方法實現。

從一段熟悉的排序列子入手

1List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
2
3Collections.sort(names, new Comparator<String>() {
4    @Override
5    public int compare(String a, String b) {
6        return b.compareTo(a);
7    }
8});

Collections 工具類提供了靜態方法 sort 方法,入參是一個 List 集合,和一個 Comparator 比較器,以便對給定的 List 集合進行排序。上面的示例代碼創建了一個匿名內部類作爲入參,這種類似的操作在我們日常的工作中隨處可見。

Java 8 中不再推薦這種寫法,而是推薦使用 Lambda 表達:

1Collections.sort(names, (String a, String b) -> {
2    return b.compareTo(a);
3});

上面的這段同樣功能的代碼塊,簡短乾淨了許多。就像婆媳一樣可能剛開始看不習慣,但是接觸接觸就喜歡了。因爲,它還可以更加簡短優秀;

1Collections.sort(names, (String a, String b) -> b.compareTo(a));

爲了追求極致,我們還可以讓它再短點:{當然過你的實現不是一行代碼,那麼不能這麼幹}

1names.sort((a, b) -> b.compareTo(a));

java.util.List 集合現在已經添加了 sort 方法。而且 Java 編譯器能夠根據類型推斷機制判斷出參數類型,這樣,你連入參的類型都可以省略啦,怎麼樣,是不是感覺很騷氣呢!

java.util.List.sort

1default void sort(Comparator<? super E> c) {
2    Object[] a = this.toArray();
3    Arrays.sort(a, (Comparator) c);
4    ListIterator<E> i = this.listIterator();
5    for (Object e : a) {
6        i.next();
7        i.set((E) e);
8    }
9}

好了!你以爲這就結束了嗎,不!它還可以更短!(得益於Comparator接口中還提供了stack默認方法,也就是說接口中不是隻可有default默認實現,還可以有靜態方法)

1names.sort(Comparator.reverseOrder());

三、函數式接口 Functional Interfaces

How does lambda expressions fit into Java's type system? Each lambda corresponds to a given type, specified by an interface. A so called functional interface must contain exactly one abstract method declaration. Each lambda expression of that type will be matched to this abstract method. Since default methods are not abstract you're free to add default methods to your functional interface.

通過上面的例子我們可以看到通過Lambda可以開發出同樣功能的邏輯但是代碼卻很簡單,那麼Jvm是如何進行類型推斷,並且找到對應的方法呢?

通過官文介紹以及我們使用發現,並不是每個接口都可以縮寫成Lambda表達式的開發方式。其實是隻有那些函數式接口(Functional Interface)才能縮寫成 Lambda 表示式。

所謂函數式接口(Functional Interface)就是隻包含一個抽象方法的聲明。針對該接口類型的所有 Lambda 表達式都會與這個抽象方法匹配。{另外,只是在接口上添加default並不算抽象方法}

總結:爲了保證一個接口明確的被定義爲一個函數式接口(Functional Interface),我們需要爲該接口添加註解:@FunctionalInterface。這樣,一旦你添加了第二個抽象方法,編譯器會立刻拋出錯誤提示。{不填寫,但是隻寫一個default也可以}

定義含有註解@FunctionalInterface的接口

1@FunctionalInterface
2public interface IConverter<F, T> {
3
4    T convert(F from);
5
6}
  1. 先來一段傳統方式 & 簡單易懂哈,因爲看習慣了

1IConverter<String, Integer> converter01 = new IConverter<String, Integer>() {
2@Override
3public Integer convert(String from) {
4    return Integer.valueOf(from);
5}
  1. 稍微簡化下,化個妝 & (form),只有一個參數括號可以不要

1IConverter<String, Integer> converter02 = (from) -> {
2    return Integer.valueOf(from);
3};
  1. 繼續簡化,因爲他的實現只有一行代碼,可以更加簡短

1IConverter<String, Integer> converter03 = from -> Integer.valueOf(from);
  1. 還能短點,其實這個另類屬於下一段的內容了,先放這有個印象

1IConverter<Integer, String> converter04 = String::valueOf;

四、方法和構造函數的便捷應用

在上面我們先加了印象片段 XX::xx,它也是Java8的新特性便捷式引用,這四個點可能你在其他語言裏也見過。

1IConverter<Integer, String> converter04 = String::valueOf;
2String converted04 = converter04.convert(11);
3System.out.println(converted04);

這四個點::的關鍵字,不只是可以引用方法和構造函數,還可以引用普通方法。

1public class Something{
2    public String startsWith(String s) {
3        return String.valueOf(s.charAt(0));
4    }
5}
1IConverter<String, String> converter01 = s -> String.valueOf(s.charAt(0)); //[參照物]直接把邏輯放到這調用
2IConverter<String, String> converter02 = something::startsWith;            //引用的方法體裏面邏輯可以更多,否則只是一句代碼並不能適合所有的情況
3System.out.println(converter01.convert("Java"));
4System.out.println(converter02.convert("Java"));

接下來我們在使用這四個點,來看下如何引用類的構造器。首先我們創建一個這樣的類;

 1public class Person {
 2    String firstName;
 3    String lastName;
 4
 5    Person() {}
 6
 7    Person(String firstName, String lastName) {
 8        this.firstName = firstName;
 9        this.lastName = lastName;
10    }
11}

然後我還需要頂一個工廠類,用於生成Person對象;

1@FunctionalInterface
2public interface IPersonFactory<P extends Person> {
3
4    P create(String firstName, String lastName);
5
6}

現在就到了用四餅::的時候了;

1IPersonFactory<Person> personFactory = Person::new;  //[參照物]:(firstName, lastName) -> new Person(firstName, lastName);
2Person person = personFactory.create("Peter", "Parker");

提醒;工廠函數中依然只能有一個函數,否則會報錯

四餅::,可以讓我們直接引用到Person類的構造函數,然後 Java 編譯器能夠根據類的簽名選中正確的構造器去實現 PersonFactory.create 方法。

五、Lambda作用範圍

Accessing outer scope variables from lambda expressions is very similar to anonymous objects. You can access final variables from the local outer scope as well as instance fields and static variables.

Lambda表達式訪問外部的變量(局部變量,成員變量,靜態變量,接口的默認方法),它與匿名內部類訪問外部變量非常相似。

1. 訪問局部變量

我們可以從lambda表達式的外部範圍讀取最終局部變量num;

1int num = 1;
2IConverter<Integer, String> stringConverter = from -> String.valueOf(from + num);
3String convert = stringConverter.convert(2);
4System.out.println(convert); // 3

但是這個num是不可變值,這樣改變值會報錯;

1int num = 1;
2IConverter<Integer, String> stringConverter =
3        (from) -> String.valueOf(from + num);
4num = 3;

Variable used in lambda expression should be final or effectively final

另外在lambda表達式內部修改也是不允許的;

1int num = 1;
2IConverter<Integer, String> converter = (from) -> {
3    String value = String.valueOf(from + num);
4    num = 3;
5    return value;
6};

Variable used in lambda expression should be final or effectively final

2. 訪問成員變量和靜態變量

在 Lambda 表達式中訪問局部變量。與局部變量相比,在 Lambda 表達式中對成員變量和靜態變量擁有讀寫權限:

 1public class Lambda4 {
 2
 3    // 靜態變量
 4    static int outerStaticNum;
 5    // 成員變量
 6    int outerNum;
 7
 8    void testScopes() {
 9        IConverter<Integer, String> stringConverter1 = (from) -> {
10            // 對成員變量賦值
11            outerNum = 23;
12            return String.valueOf(from);
13        };
14
15        IConverter<Integer, String> stringConverter2 = (from) -> {
16            // 對靜態變量賦值
17            outerStaticNum = 72;
18            return String.valueOf(from);
19        };
20    }
21
22}

3. 訪問默認接口方法 

還記得第一節的IFormula示例嗎? 

 1public interface IFormula {
 2
 3    double calculate(int a);
 4
 5    // 平方
 6    default double sqrt(int a) {
 7        return Math.sqrt(a);
 8    }
 9
10}

當時,我們在接口中定義了一個帶有默認實現的 sqrt 求平方根方法,在匿名內部類中我們可以很方便的訪問此方法:

1IFormula formula = new IFormula() {
2    @Override
3    public double calculate(int a) {
4        return a * a;
5    }
6};

但是不能通過lambda表達式訪問默認方法,這樣的代碼沒法通過編譯;

1IFormula formula = (a) -> sqrt(a * a);

帶有默認實現的接口方法,是不能在 lambda 表達式中訪問的,上面這段代碼將無法被編譯通過。

六、內置的函數式接口

JDK 1.8 API 包含了很多內置的函數式接口。其中就包括我們在老版本中經常見到的 Comparator 和 Runnable,Java 8 爲他們都添加了 @FunctionalInterface 註解,以用來支持 Lambda 表達式。

例如我們舊版本的Jdk中常用的 Comparator 和 Runnable 外,還有一些新的函數式接口,可以通過函數註解實現Lamdba支持,它們很多都借鑑於知名的 Google Guava 庫。

即使你已經熟悉這個類庫,也應該密切關注那些接口是如何通過一些有用的方法擴展來擴展的:

1. Predicate 斷言

Predicate 是一個可以指定入參類型,並返回 boolean 值的函數式接口。它內部提供了一些帶有默認實現的方法,可以 被用來組合一個複雜的邏輯判斷(and, or, negate):

 1@Test
 2public void test11() {
 3    Predicate<String> predicate = (s) -> s.length() > 0;
 4
 5    boolean foo0 = predicate.test("foo");           // true
 6    boolean foo1 = predicate.negate().test("foo");  // negate否定相當於!true
 7
 8    Predicate<Boolean> nonNull = Objects::nonNull;
 9    Predicate<Boolean> isNull = Objects::isNull;
10
11    Predicate<String> isEmpty = String::isEmpty;
12    Predicate<String> isNotEmpty = isEmpty.negate();
13}

2. Functions

Function 函數式接口的作用是,我們可以爲其提供一個原料,他給生產一個最終的產品。通過它提供的默認方法,組合,鏈行處理(compose, andThen):

1@Test
2public void test12() {
3    Function<String, Integer> toInteger = Integer::valueOf;                                         //轉Integer
4    Function<String, String> backToString = toInteger.andThen(String::valueOf);                     //轉String
5    Function<String, String> afterToStartsWith = backToString.andThen(new Something()::startsWith); //截取第一位 
6    String apply = afterToStartsWith.apply("123");// "123"
7    System.out.println(apply);
8}

3. Suppliers

Supplier 與 Function 不同,它不接受入參,直接爲我們生產一個指定的結果,有點像生產者模式:

1@Test
2public void test13() {
3    Supplier<Person> personSupplier0 = Person::new;
4    personSupplier0.get();   // new Person
5    Supplier<String> personSupplier1 = Something::test01;  //這個test方法是靜態的,且無入參
6    personSupplier1.get();   // hi
7
8    Supplier<String> personSupplier2 = new Something()::test02;
9}

4. Consumers

對於 Consumer,我們需要提供入參,用來被消費,如下面這段示例代碼:

 1@Test
 2public void test14() {
 3    // 參照物,方便知道下面的Lamdba表達式寫法
 4    Consumer<Person> greeter01 = new Consumer<Person>() {
 5        @Override
 6        public void accept(Person p) {
 7            System.out.println("Hello, " + p.firstName);
 8        }
 9    };
10    Consumer<Person> greeter02 = (p) -> System.out.println("Hello, " + p.firstName);
11    greeter02.accept(new Person("Luke", "Skywalker"));  //Hello, Luke
12    Consumer<Person> greeter03 = new MyConsumer<Person>()::accept;    // 也可以通過定義類和方法的方式去調用,這樣纔是實際開發的姿勢
13    greeter03.accept(new Person("Luke", "Skywalker"));  //Hello, Luke
14}

5. Comparators

Comparator 在 Java 8 之前是使用比較普遍的。Java 8 中除了將其升級成了函數式接口,還爲它拓展了一些默認方法:

1@Test
2public void test15(){
3    Comparator<Person> comparator01 = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
4    Comparator<Person> comparator02 = Comparator.comparing(p -> p.firstName);           //等同於上面的方式
5    Person p1 = new Person("John", "Doe");
6    Person p2 = new Person("Alice", "Wonderland");
7    comparator01.compare(p1, p2);             // > 0
8    comparator02.reversed().compare(p1, p2);  // < 0
9}

七、Optionals

首先,Optional 它不是一個函數式接口,設計它的目的是爲了防止空指針異常(NullPointerException),要知道在 Java 編程中,空指針異常可是臭名昭著的。

讓我們來快速瞭解一下 Optional 要如何使用!你可以將 Optional 看做是包裝對象(可能是 null, 也有可能非 null)的容器。當你定義了

一個方法,這個方法返回的對象可能是空,也有可能非空的時候,你就可以考慮用 Optional 來包裝它,這也是在 Java 8 被推薦使用的做法。

 1@Test
 2public void test16(){
 3    Optional<String> optional = Optional.of("bam");
 4    optional.isPresent();                  // true
 5    optional.get();                        // "bam"
 6    optional.orElse("fallback");    // "bam"
 7    optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"
 8    Optional<Person> optionalPerson = Optional.of(new Person());
 9    optionalPerson.ifPresent(s -> System.out.println(s.firstName));
10}

八、Stream 流

什麼是 Stream 流?

簡單來說,我們可以使用 java.util.Stream 對一個包含一個或多個元素的集合做各種操作。這些操作可能是 中間操作 亦或是 終端操作。
終端操作會返回一個結果,而中間操作會返回一個 Stream 流。

需要注意的是,你只能對實現了 java.util.Collection 接口的類做流的操作。

Stream 流支持同步執行,也支持併發執行。

注意:Map不支持Stream流,但是他的key和value是支持的!

讓我們先看看Stream流是如何工作的。首先,我們以字符串列表的形式創建一個示例;

1List<String> stringCollection = new ArrayList<>();
2stringCollection.add("ddd2");
3stringCollection.add("aaa2");
4stringCollection.add("bbb1");
5stringCollection.add("aaa1");
6stringCollection.add("bbb3");
7stringCollection.add("ccc");
8stringCollection.add("bbb2");
9stringCollection.add("ddd1");

1. Filter 過濾

Filter 的入參是一個 Predicate, 上面已經說到,Predicate 是一個斷言的中間操作,它能夠幫我們篩選出我們需要的集合元素。它的返參同樣 是一個 Stream 流,我們可以通過 foreach 終端操作,來打印被篩選的元素:

1@Test
2public void test17(){
3    stringCollection
4            .stream()
5            .filter((s) -> s.startsWith("a"))
6            .forEach(System.out::println);
7}

2. Sorted 排序

Sorted 同樣是一箇中間操作,它的返參是一個 Stream 流。另外,我們可以傳入一個 Comparator 用來自定義排序,如果不傳,則使用默認的排序規則。

1@Test
2public void test18() {
3    stringCollection
4            .stream()
5            .sorted()
6            .filter((s) -> s.startsWith("a"))
7            .forEach(System.out::println);
8}

注意;這個sorted 只是做了一個排序的視圖進行輸出,實際沒有將List內的數據進行排序

1System.out.println(stringCollection);
2// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

3. Map 轉換

中間操作映射通過給定的函數將每個元素轉換爲另一個對象。例如下面的示例,通過 map 我們將每一個 string 轉成大寫:

1@Test
2public void test19(){
3    stringCollection
4            .stream()
5            .map(String::toUpperCase)
6            .sorted(Comparator.reverseOrder())  //等同於(a, b) -> b.compareTo(a)
7            .forEach(System.out::println);
8}

這個可以用做DTO數據對象轉換,領域驅動設計開發中將DTO轉爲DO向後臺傳輸。

4. Match 匹配

顧名思義,match 用來做匹配操作,它的返回值是一個 boolean 類型。通過 match, 我們可以方便的驗證一個 list 中是否存在某個類型的元素。

 1@Test
 2public void test20(){
 3    // anyMatch:驗證 list 中 string 是否有以 a 開頭的, 匹配到第一個,即返回 true
 4    boolean anyStartsWithA =
 5            stringCollection
 6                    .stream()
 7                    .anyMatch((s) -> s.startsWith("a"));
 8    System.out.println(anyStartsWithA);      // true
 9    // allMatch:驗證 list 中 string 是否都是以 a 開頭的
10    boolean allStartsWithA =
11            stringCollection
12                    .stream()
13                    .allMatch((s) -> s.startsWith("a"));
14    System.out.println(allStartsWithA);      // false
15    // noneMatch:驗證 list 中 string 是否都不是以 z 開頭的
16    boolean noneStartsWithZ =
17            stringCollection
18                    .stream()
19                    .noneMatch((s) -> s.startsWith("z"));
20    System.out.println(noneStartsWithZ);      // true
21}

5. Count 計數

count 是一個終端操作,它能夠統計 stream 流中的元素總數,返回值是 long 類型。

 1@Test
 2public void test21() {
 3    // count:先對 list 中字符串開頭爲 b 進行過濾,讓後統計數量
 4    long startsWithB =
 5            stringCollection
 6                    .stream()
 7                    .filter((s) -> s.startsWith("b"))
 8                    .count();
 9    System.out.println(startsWithB);    // 3
10}

6. Reduce

Reduce 中文翻譯爲:減少、縮小。通過入參的 Function,我們能夠將 list 歸約成一個值。它的返回類型是 Optional 類型。

 1@Test
 2public void test22() {
 3    Optional<String> reduced =
 4            stringCollection
 5                    .stream()
 6                    .sorted()
 7                    .reduce((s1, s2) -> s1 + "#" + s2);
 8    reduced.ifPresent(System.out::println);
 9    // aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2
10}

九、Parallel-Streams 並行流

如上所述,流可以是順序的,也可以是並行的。順序流上的操作在單個線程上執行,而並行流上的操作在多個線程上併發執行。

下面的示例演示了使用並行流來提高性能是多麼的容易。親測提升了1倍性能!

首先,我們創建一個較大的List:

1int max = 1000000;
2List<String> values = new ArrayList<>(max);
3for (int i = 0; i < max; i++) {
4    UUID uuid = UUID.randomUUID();
5    values.add(uuid.toString());
6}

1. Sequential Sort 順序流排序

 1@Test
 2public void test23() {
 3    int max = 1000000;
 4    List<String> values = new ArrayList<>(max);
 5    for (int i = 0; i < max; i++) {
 6        UUID uuid = UUID.randomUUID();
 7        values.add(uuid.toString());
 8    }
 9    // 納秒
10    long t0 = System.nanoTime();
11    long count = values.stream().sorted().count();
12    System.out.println(count);
13    long t1 = System.nanoTime();
14    // 納秒轉微秒
15    long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
16    System.out.println(String.format("順序流排序耗時: %d ms", millis));
17    //順序流排序耗時: 712 ms
18}

2. Parallel Sort 並行流排序

 1@Test
 2public void test24(){
 3    int max = 1000000;
 4    List<String> values = new ArrayList<>(max);
 5    for (int i = 0; i < max; i++) {
 6        UUID uuid = UUID.randomUUID();
 7        values.add(uuid.toString());
 8    }
 9    long t0 = System.nanoTime();
10    long count = values.parallelStream().sorted().count();
11    System.out.println(count);
12    long t1 = System.nanoTime();
13    long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
14    System.out.println(String.format("parallel sort took: %d ms", millis));
15    //parallel sort took: 385 ms
16}

如您所見,這兩個代碼片段幾乎相同,但並行排序大約快50%。您只需將stream()更改爲parallelStream()。

十、Map 集合

如前所講,Map是不支持 Stream 流的,因爲 Map 接口並沒有像 Collection 接口那樣,定義了 stream() 方法。但是,我們可以對其 key, values, entry 使用 流操作,如 map.keySet().stream(), map.values().stream() 和 map.entrySet().stream().

另外, JDK 8 中對 map 提供了一些其他新特性:

 1@Test
 2public void test25() {
 3    Map<Integer, String> map = new HashMap<>();
 4    for (int i = 0; i < 10; i++) {
 5        // 與老版不同的是,putIfAbent() 方法在 put 之前,  不用在寫if null continue了
 6        // 會判斷 key 是否已經存在,存在則直接返回 value, 否則 put, 再返回 value
 7        map.putIfAbsent(i, "val" + i);
 8    }
 9    // forEach 可以很方便地對 map 進行遍歷操作
10    map.forEach((key, value) -> System.out.println(value));
11}

之後我們做一個Map對象的轉換輸出;(定義兩個類BeanA、BeanB)

 1@Test
 2public void test26() {
 3    Map<Integer, BeanA> map = new HashMap<>();
 4    for (int i = 0; i < 10; i++) {
 5        // 與老版不同的是,putIfAbent() 方法在 put 之前,  不用在寫if null continue了
 6        // 會判斷 key 是否已經存在,存在則直接返回 value, 否則 put, 再返回 value
 7        map.putIfAbsent(i, new BeanA(i, "明明" + i, i + 20, "89021839021830912809" + i));
 8    }
 9    Stream<BeanB> beanBStream00 = map.values().stream().map(new Function<BeanA, BeanB>() {
10        @Override
11        public BeanB apply(BeanA beanA) {
12            return new BeanB(beanA.getName(), beanA.getAge());
13        }
14    });
15    Stream<BeanB> beanBStream01 = map.values().stream().map(beanA -> new BeanB(beanA.getName(), beanA.getAge()));
16    beanBStream01.forEach(System.out::println);
17}

除了上面的 putIfAbsent() 和 forEach() 外,我們還可以很方便地對某個 key 的值做相關操作:

 1@Test
 2public void test27() {
 3    // 如下:對 key 爲 3 的值,內部會先判斷值是否存在,存在,則做 value + key 的拼接操作
 4    map.computeIfPresent(3, (num, val) -> val + num);
 5    map.get(3);             // val33
 6
 7    // 先判斷 key 爲 9 的元素是否存在,存在,則做刪除操作
 8    map.computeIfPresent(9, (num, val) -> null);
 9    map.containsKey(9);     // false
10
11    // computeIfAbsent(), 當 key 不存在時,纔會做相關處理
12    // 如下:先判斷 key 爲 23 的元素是否存在,不存在,則添加
13    map.computeIfAbsent(23, num -> "val" + num);
14    map.containsKey(23);    // true
15
16    // 先判斷 key 爲 3 的元素是否存在,存在,則不做任何處理
17    map.computeIfAbsent(3, num -> "bam");
18    map.get(3);             // val33
19}

關於刪除操作,JDK 8 中提供了能夠新的 remove() API:

1@Test
2public void test28() {
3    map.remove(3, "val3");
4    map.get(3);             // val33
5
6    map.remove(3, "val33");
7    map.get(3);             // null
8}

如上代碼,只有當給定的 key 和 value 完全匹配時,纔會執行刪除操作。

關於添加方法,JDK 8 中提供了帶有默認值的 getOrDefault() 方法:

1@Test
2public void test29() {
3    // 若 key 42 不存在,則返回 not found
4    map.getOrDefault(42, "not found");  // not found
5}

對於 value 的合併操作也變得更加簡單:

1@Test
2public void test30() {
3    // merge 方法,會先判斷進行合併的 key 是否存在,不存在,則會添加元素
4    map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
5    map.get(9);             // val9
6    // 若 key 的元素存在,則對 value 執行拼接操作
7    map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
8    map.get(9);             // val9concat
9}

十一、日期 Date API

Java 8 中在包 java.time 下添加了新的日期 API. 它和 Joda-Time 庫相似,但又不完全相同。接下來,我會通過一些示例代碼介紹一下新 API 中 最關鍵的特性:

1. Clock

Clock 提供對當前日期和時間的訪問。我們可以利用它來替代 System.currentTimeMillis() 方法。另外,通過 clock.instant() 能夠獲取一個 instant 實例,
此實例能夠方便地轉換成老版本中的 java.util.Date 對象。

1@Test
2public void test31(){
3    Clock clock = Clock.systemDefaultZone();
4    long millis = clock.millis();
5    Instant instant = clock.instant();
6    Date legacyDate = Date.from(instant);   // 老版本 java.util.Date
7}

2. Timezones 時區

ZoneId 代表時區類。通過靜態工廠方法方便地獲取它,入參我們可以傳入某個時區編碼。另外,時區類還定義了一個偏移量,用來在當前時刻或某時間 與目標時區時間之間進行轉換。

 1@Test
 2public void test32() {
 3    System.out.println(ZoneId.getAvailableZoneIds());
 4    // prints all available timezone ids
 5
 6    ZoneId zone1 = ZoneId.of("Europe/Berlin");
 7    ZoneId zone2 = ZoneId.of("Brazil/East");
 8    System.out.println(zone1.getRules());
 9    System.out.println(zone2.getRules());
10
11    //[Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/Gada/Atlantic, Atlantic/St_Helena, Australia/Tasmania, Libya, Europe/Guernsey, America/Grand_Turk, US/Pacific-New, Asia/Samarkand, America/Argentina/Cordoba, Asia/Phnom_Penh, Africa/Kigali, Asia/Almaty, US/Alaska, Asi...
12    // ZoneRules[currentStandardOffset=+01:00]
13    // ZoneRules[currentStandardOffset=-03:00]
14}

3. LocalTime

LocalTime 表示一個沒有指定時區的時間類,例如,10 p.m.或者 17:30:15,下面示例代碼中,將會使用上面創建的 時區對象創建兩個 LocalTime。然後我們會比較兩個時間,並計算它們之間的小時和分鐘的不同。

 1@Test
 2public void test33(){
 3    ZoneId zone1 = ZoneId.of("Europe/Berlin");
 4    ZoneId zone2 = ZoneId.of("Brazil/East");
 5    LocalTime now1 = LocalTime.now(zone1);
 6    LocalTime now2 = LocalTime.now(zone2);
 7    System.out.println(now1.isBefore(now2));  // false
 8    long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
 9    long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
10    System.out.println(hoursBetween);       // -3
11    System.out.println(minutesBetween);     // -239
12}

LocalTime 提供多個靜態工廠方法,目的是爲了簡化對時間對象實例的創建和操作,包括對時間字符串進行解析的操作等。

 1@Test
 2public void test34(){
 3    LocalTime late = LocalTime.of(23, 59, 59);
 4    System.out.println(late);       // 23:59:59
 5    DateTimeFormatter germanFormatter =
 6            DateTimeFormatter
 7                    .ofLocalizedTime(FormatStyle.SHORT)
 8                    .withLocale(Locale.GERMAN);
 9    LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
10    System.out.println(leetTime);   // 13:37
11}

4. LocalDate

LocalDate 是一個日期對象,例如:2014-03-11。它和 LocalTime 一樣是個 final 類型對象。下面的例子演示瞭如何通過加減日,月,年等來計算一個新的日期。

 1@Test
 2public void test35(){
 3    LocalDate today = LocalDate.now();
 4    // 今天加一天
 5    LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
 6    // 明天減兩天
 7    LocalDate yesterday = tomorrow.minusDays(2);
 8    // 2014 年七月的第四天
 9    LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
10    DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
11    System.out.println(dayOfWeek);    // 星期五
12}

也可以直接解析日期字符串,生成 LocalDate 實例。(和 LocalTime 操作一樣簡單)

1@Test
2public void test36(){
3    DateTimeFormatter germanFormatter =
4            DateTimeFormatter
5                    .ofLocalizedDate(FormatStyle.MEDIUM)
6                    .withLocale(Locale.GERMAN);
7    LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
8    System.out.println(xmas);   // 2014-12-24
9}

5. LocalDateTime

LocalDateTime 是一個日期-時間對象。你也可以將其看成是 LocalDate 和 LocalTime 的結合體。操作上,也大致相同。

 1@Test
 2public void test37(){
 3    LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
 4    DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
 5    System.out.println(dayOfWeek);      // 星期三
 6    Month month = sylvester.getMonth();
 7    System.out.println(month);          // 十二月
 8    // 獲取改時間是該天中的第幾分鐘
 9    long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
10    System.out.println(minuteOfDay);    // 1439
11}

如果再加上的時區信息,LocalDateTime 還能夠被轉換成 Instance 實例。Instance 能夠被轉換成老版本中 java.util.Date 對象。

1@Test
2public void test38(){
3    LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
4    Instant instant = sylvester
5            .atZone(ZoneId.systemDefault())
6            .toInstant();
7    Date legacyDate = Date.from(instant);
8    System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014
9}

格式化 LocalDateTime 對象就和格式化 LocalDate 或者 LocalTime 一樣。除了使用預定義的格式以外,也可以自定義格式化輸出。

1@Test
2public void test39(){
3    DateTimeFormatter formatter =
4            DateTimeFormatter
5                    .ofPattern("MMM dd, yyyy - HH:mm");
6    LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
7    String string = formatter.format(parsed);
8    System.out.println(string);     // Nov 03, 2014 - 07:13
9}

Unlike java.text.NumberFormat the new DateTimeFormatter is immutable and thread-safe.

For details on the pattern syntax read here.

十二、Annotations 註解

Java8中的註釋是可重複的。讓我們直接深入到一個例子中來解決這個問題。{在SpringBoot的啓動類中就可以看到這中類型的註解}

首先,我們定義一個包裝器註釋,它包含一個實際註釋數組:

1@Repeatable(Hints.class)
2public @interface Hint {
3    String value();
4}
5
6public @interface Hints {
7    Hint[] value();
8}

Java 8通過聲明註釋@Repeatable,使我們能夠使用同一類型的多個註釋。

第一種形態:使用註解容器(老方法)

1 @Test
2 public void test40() {
3     @Hints({@Hint("hint1"), @Hint("hint2")})
4     class Person {
5     }
6 }

第二種形態:使用可重複註解(新方法)

1@Test
2public void test41() {
3    @Hint("hint1")
4    @Hint("hint2")
5    class Person {
6    }
7}

java編譯器使用變量2隱式地在引擎蓋下設置@Hints註釋。這對於通過反射讀取註釋信息很重要。

 1@Test
 2public void test41() {
 3    @Hint("hint1")
 4    @Hint("hint2")
 5    class Person {
 6    }
 7    Hint hint = Person.class.getAnnotation(Hint.class);
 8    System.out.println(hint);                   // null
 9    Hints hints1 = Person.class.getAnnotation(Hints.class);
10    System.out.println(hints1.value().length);  // 2
11    Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class
12    System.out.println(hints2.length);          // 2
13}

儘管我們絕對不會在 Person 類上聲明 @Hints 註解,但是它的信息仍然是可以通過 getAnnotation(Hints.class) 來讀取的。
並且,getAnnotationsByType 方法會更方便,因爲它賦予了所有 @Hints 註解標註的方法直接的訪問權限。

1@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
2@interface MyAnnotation {}

綜上總結

  • jdk8的新特性包括了;Lambda、函數式接口、四餅調用::、內置函數(斷言、Function、生產者、消費者)、Stream流、Map集合特性、日期、註解等

  • 合理的組合運行新的特性可以減少很多的編碼量,同時讓代碼更加整潔

  • 在一些新的框架中SpringBoot裏如果翻看源碼可以看到很多的新特性使用

  • 案例來源;https://github.com/winterbe/java8-tutorial {英文}

  • 源碼貢獻;https://github.com/fuzhengwei/itstack-demo-jdk8

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章