Java8 通關攻略

點贊+收藏 就學會系列,文章收錄在 GitHub JavaEgg ,N線互聯網開發必備技能兵器譜

Java8早在2014年3月就發佈了,6年了,你有對它做個全面的瞭解嗎

本文是用我拙劣的英文和不要臉的這抄抄那抄抄,熬出來的,沒有深究源碼,只是對 Java8 有一個整體的認知,可以上手用起來,示例代碼也都在github上

JDK 8 有什麼新功能

  • Java 編程語言( Java Programming Language)

    • Lambda表達式:一個新的語言特性, 它們使您能夠將函數視爲方法參數,或將代碼視爲數據
    • 方法引用: 方法引用爲已經有名稱的方法提供易於閱讀的lambda表達式
    • 默認方法:使用 default 關鍵字爲接口定義默認方法(有實現的方法)
    • 重複註解提供了將同一註解多次應用於同一聲明或類型使用的能力
    • 類型註解提供了在使用類型的任何地方應用註解的能力,而不僅僅是在聲明上
    • Java8 增強了類型推斷
    • 方法參數反射
    • java.util.function: 一個新的包,它包含爲lambda表達式和方法引用提供目標類型的通用功能接口
  • 集合(Collections)

    • java.util.stream包中新增了 Stream API ,用來支持對元素流的函數式操作
    • 改進了有鍵衝突問題的 HashMap
  • 精簡運行時(Compact Profiles)

  • 安全性(Security)

  • JavaFX

  • Tools(包含一些調用Nashorn引擎、 啓動JavaFX應用程序等等 )

  • 國際化(Internationalization)

    • Unicode增強,包括對Unicode 6.2.0的支持
    • 提供了新的 Calendar 和 Locale API
  • 部署(Deployment)

  • 日期-時間 包(Date-Time Package):提供了更全面的時間和日期操作

  • 腳本(Scripting):Java 8提供了一個新的 Nashorn javascript 引擎(取代了Nashorn javascript引擎),它允許我們在JVM上運行特定的 javascript 應用

  • 改進 IO 和 NIO

  • 改進 java.langjava.util

    • 支持數組並行排序
    • 支持Base64 的編碼和解碼
    • 支持 無符號運算
    • Optional 類 :最大化減少空指針異常
  • JDBC

    • JDBC-ODBC橋已被移除
    • JDBC 4.2引入了新的特性
  • Java DB(一個Java數據庫)

  • 網絡(Networking)

    • 新增了 java.net.URLPermission
  • 併發(Concurrency

    • CompletableFuture 增強了之前的Future
    • java.util.concurrent.ConcurrentHashMap 支持基於新添加的streams功能和lambda表達式的聚合操作
    • java.util.concurrent.atomic 提供了一組原子變量類, 對於單個變量支持無鎖、線程安全操作的工具類
    • java.util.concurrent.ForkJoinPool 用於補充ExecutorService
    • java.util.concurrent.locks.StampedLock 提供了基於功能的鎖,有三種模式用於控制讀/寫訪問
  • JVM: 移除了 PermGen ,取而代之的是Metaspace

Java8 特別強大的是Lambda 表達式和Stream,通過它兩新增和增強了很多包

新增: java.lang.invokejava.util.functionjava.util.stream

修改:

modify-class.png


一、Lambda表達式

可以把 Lambda 表達式理解爲簡潔的表示可傳遞的匿名函數的一種方式,Lambda表達式基於數學中的λ演算得名:它沒有名稱,但有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。

  • 匿名——匿名函數(即沒有函數名的函數),不像普通的方法有一個明確的名稱,“寫得少,想得多”
  • 函數——Lambda函數不像方法那樣屬於某個特定的類,但一樣有參數列表、函數主體和返回類型
  • 傳遞——Lambda表達式可以作爲參數傳遞給方法或者存儲在變量中
  • 簡潔——無需像匿名類那樣寫很多模板代碼

Lambda表達式使您能夠封裝單個行爲單元並將其傳遞給其他代碼。如果希望對集合的每個元素、流程完成時或流程遇到錯誤時執行某個操作,可以使用lambda表達式。

1. 爲什麼要使用Lambda表達式

Lambda 是一個匿名函數,我們可以把 Lambda表達式理解爲是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞——行爲參數化)。可以寫出更簡潔、更靈活的代碼。作爲一種更緊湊的代碼風格,使Java的語言表達能力得到了提升。

匿名類的一個問題是,如果您的匿名類的實現非常簡單,例如一個接口只包含一個方法,那麼匿名類的語法可能看起來很笨拙和不清楚。在這些情況下,您通常試圖將功能作爲參數傳遞給另一個方法,例如當有人單擊按鈕時應該採取什麼操作。Lambda表達式允許您這樣做,將功能視爲方法參數,或將代碼視爲數據。

hello-lambda

2. Lambda 表達式語法

(parameters) -> expression(parameters) ->{ statements; }

Lambda 表達式在 Java 語言中引入了一個新的語法元素和操作符。這個操作符爲 -> , 該操作符被稱爲 Lambda 操作符或剪頭操作符。它將 Lambda 分爲兩個部分:

  • 左側:指定了 Lambda 表達式需要的所有參數

  • 右側:指定了 Lambda 體,即 Lambda 表達式要執行的功能

eg(錯誤示範):

(Integer i) -> return "hello"+i;   //錯誤的Lambda,return是一個控制流語句,需要{}
(String s) -> {"hello";}    //“hello”是一個表達式,不是語句,不需要{},可以寫成{return “hello”;}

eg(正確示範):

  1. 無參,無返回值,Lambda 體只需一條語句

    Runnable runnable = () -> System.out.println("hello lambda");
    
  2. Lambda 需要一個參數

    Consumer<String> consumer = (args) -> System.out.println(args);
    

    Lambda 只需要一個參數時,參數的小括號可以省略

    Consumer<String> consumer = args -> System.out.println(args);
    
  3. Lambda 需要兩個參數,並且有返回值

    BinaryOperator<Long> binaryOperator = (Long x,Long y) -> {
    	System.out.println("實現函數接口方法");
    	return x +y;
    };
    

    參數的數據類型可省略,Java8增強了類型推斷,且當 Lambda 體只有一條語句時,return 與大括號可以省略

    BinaryOperator<Long> binaryOperator = (x, y) -> x + y;
    

類型推斷

上述 Lambda 表達式中的參數類型都是由編譯器推斷得出的。Lambda 表達式中無需指定類型,程序依然可以編譯,這是因爲 javac 根據程序的上下文,在後臺推斷出了參數的類型。Lambda 表達式的類型依賴於上下文環境,是由編譯器推斷出來的。這就是所謂的“類型推斷”。Java7中引入的菱形運算符<>),就是利用泛型從上下文推斷類型。

List<String> list = new ArrayList<>();

3. Lambda表達式實例

官方提供的示例,假設你要開發一個社交軟件,那個缺打的PM成天改需求,今天要查詢出成年用戶的信息,明天又要查詢成年女性的用戶信息,後天又要按各種奇怪的搜索條件查詢。

這時的程序員:從簡單的用戶遍歷比較方法改爲通用的搜索方法到後來都用上了工廠模式,等到第7天的時候,你不耐煩了,瑪德,每個條件就一句話,我寫了7個類,我可不想做CtrlCV工程師,這時候Lambda表達式是你的不二之選。

行爲參數化就是可以幫助你處理頻繁變更的需求的一種軟件開發模式。

官方提供的demo,一步步告訴你使用Java8的好處(從值參數化到行爲參數化)。代碼

import java.util.List;
import java.util.ArrayList;
import java.time.chrono.IsoChronology;
import java.time.LocalDate;
public class Person {
    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    Person(String nameArg, LocalDate birthdayArg,
           Sex genderArg, String emailArg) {
        name = nameArg;
        birthday = birthdayArg;
        gender = genderArg;
        emailAddress = emailArg;
    }

    public int getAge() {
        return birthday
                .until(IsoChronology.INSTANCE.dateNow())
                .getYears();
    }

    public void printPerson() {
        System.out.println(name + ", " + this.getAge());
    }

    public Sex getGender() {
        return gender;
    }

    public String getName() {
        return name;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public LocalDate getBirthday() {
        return birthday;
    }

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    public static List<Person> createRoster() {
        List<Person> roster = new ArrayList<>();
        roster.add(new Person(
                        "Fred",
                        IsoChronology.INSTANCE.date(1980, 6, 20),
                        Person.Sex.MALE,
                        "[email protected]"));
        roster.add(new Person(
                        "Jane",
                        IsoChronology.INSTANCE.date(1990, 7, 15),
                        Person.Sex.FEMALE, "[email protected]"));
        roster.add(new Person(
                        "George",
                        IsoChronology.INSTANCE.date(1991, 8, 13),
                        Person.Sex.MALE, "[email protected]"));
        roster.add(new Person(
                        "Bob",
                        IsoChronology.INSTANCE.date(2000, 9, 12),
                        Person.Sex.MALE, "[email protected]"));
        return roster;
    }
}

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class RosterTest {
    interface CheckPerson {
        boolean test(Person p);
    }

    /**
     * 1. eg:輸出年齡大於20歲的成員
     *   匹配符合某一特徵的成員的方法
     *   如果老闆要年齡在某一區間的成員呢?接着換方法
     */
    public static void printPersonsOlderThan(List<Person> roster, int age) {
        for (Person p : roster) {
            if (p.getAge() >= age) {
                p.printPerson();
            }
        }
    }

    /**
     * 2. eg:輸出年齡在14到30歲之間的成員
     * 		更全面的匹配方法
     * 		如果老闆只要男性成員呢?
     */
    public static void printPersonsWithinAgeRange(
            List<Person> roster, int low, int high) {
        for (Person p : roster) {
            if (low <= p.getAge() && p.getAge() < high) {
                p.printPerson();
            }
        }
    }

    /**
     * 3. eg:老闆又提出了各種複雜的需求,不要處女座的、只要郵箱是163的,怎麼搞?
     * 方法1:在本地類中指定搜索條件代碼,通過接口方式,不同的需求對應不同的實現類,
     *		 每次都要新建實現類,寫大量的代碼
     * 方法2:在匿名類中指定搜索條件代碼,不需要寫各種實現,但是還要寫個interface CheckPerson,
     *       而且匿名類寫起來也挺麻煩
     * 方法3:Lambda表達式是懶人的不二之選,CheckPerson是一個只包含一個抽象方法的接口,
     *	     比較簡單,Lambda可以省略其實現
     */
    public static void printPersons(
            List<Person> roster, CheckPerson tester) {
        for (Person p : roster) {
            if (tester.test(p)) {
                p.printPerson();
            }
        }
    }

    /**
     * 4. eg: 搞這麼久,還得寫一個接口,而且是隻有一個抽象方法,還是不爽?
     * 			你也可以使用標準的函數接口來代替接口CheckPerson,從而進一步減少所需的代碼量
     * 			java.util.function包中定義了標準的函數接口
     * 			我們可以使用JDK8提供的 Predicate<T>接口來代替CheckPerson。
     *			該接口包含方法boolean test(T t)
     */
    public static void printPersonsWithPredicate(
            List<Person> roster, Predicate<Person> tester) {
        for (Person p : roster) {
            if (tester.test(p)) {
                p.printPerson();
            }
        }
    }

    /**
     * 5. Lambda表達式可不只是能夠簡化匿名類
     * 		簡化 p.printPerson(), 
     * 		使用Consumer<T>接口的void accept(T t)方法,相當於入參的操作
     */
    public static void processPersons(
            List<Person> roster,
            Predicate<Person> tester,
            Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
    }

    /**
     * 6. eg: 老闆說了只想看到郵箱
     * Function<T,R>接口,相當於輸入類型,mapper定義參數,block負責方對給定的參數進行執行
     */
    public static void processPersonsWithFunction(
            List<Person> roster,
            Predicate<Person> tester,
            Function<Person, String> mapper,
            Consumer<String> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                String data = mapper.apply(p);
                block.accept(data);
            }
        }
    }

    // 7. 使用泛型
    public static <X, Y> void processElements(
            Iterable<X> source,
            Predicate<X> tester,
            Function<X, Y> mapper,
            Consumer<Y> block) {
        for (X p : source) {
            if (tester.test(p)) {
                Y data = mapper.apply(p);
                block.accept(data);
            }
        }
    }

    public static void main(String[] args) {
        List<Person> roster = Person.createRoster();

        /**
         * 1. 輸出年齡大於20歲的成員
         */
        System.out.println("Persons older than 20:");
        printPersonsOlderThan(roster, 20);
        System.out.println();

        /**
         * 2. 輸出年齡在14到30歲之間的成員
         */
        System.out.println("Persons between the ages of 14 and 30:");
        printPersonsWithinAgeRange(roster, 14, 30);
        System.out.println();

        /**
         * 3. 輸出年齡在18到25歲的男性成員
         * (在本地類中指定搜索條件)
         *   您可以使用一個匿名類而不是一個本地類,並且不必爲每個搜索聲明一個新類
         */
        System.out.println("Persons who are eligible for Selective Service:");
        class CheckPersonEligibleForSelectiveService implements CheckPerson {
            public boolean test(Person p) {
                return p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25;
            }
        }

        // 這個其實就是通過行爲參數化傳遞代碼
        printPersons(
                roster, new CheckPersonEligibleForSelectiveService());

        System.out.println();

        // 3. 在匿名類中指定搜索條件代碼
        System.out.println("Persons who are eligible for Selective Service " +
                "(anonymous class):");
        printPersons(
                roster,
                new CheckPerson() {
                    public boolean test(Person p) {
                        return p.getGender() == Person.Sex.MALE
                                && p.getAge() >= 18
                                && p.getAge() <= 25;
                    }
                }
        );

        System.out.println();

        // 3: 使用Lambda表達式簡化代碼,一個箭頭
        System.out.println("Persons who are eligible for Selective Service " +
                "(lambda expression):");

        printPersons(
                roster,
                (Person p) -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25
        );

        System.out.println();

        // 4. 使用Lambda的標準功能接口
        System.out.println("Persons who are eligible for Selective Service " +
                "(with Predicate parameter):");

        printPersonsWithPredicate(
                roster,
                p -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25
        );

        System.out.println();

        //5.使用Predicate和Consumer參數
        System.out.println("5. Persons who are eligible for Selective Service " +
                "(with Predicate and Consumer parameters):");

        processPersons(
                roster,
                p -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25,
                p -> p.printPerson()
        );

        System.out.println();

        // 6. 通過Function<T,R> 指定輸出類型
        System.out.println("Persons who are eligible for Selective Service " +
                "(with Predicate, Function, and Consumer parameters):");

        processPersonsWithFunction(
                roster,
                p -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25,
                p -> p.getEmailAddress(),
                email -> System.out.println(email)
        );

        System.out.println();

        // 7. 使用泛型
        System.out.println("Persons who are eligible for Selective Service " +
                "(generic version):");

        processElements(
                roster,
                p -> p.getGender() == Person.Sex.MALE
                        && p.getAge() >= 18
                        && p.getAge() <= 25,
                p -> p.getEmailAddress(),
                email -> System.out.println(email)
        );

        System.out.println();

        // 8: 使用接受Lambda表達式的批量數據操作
        System.out.println("Persons who are eligible for Selective Service " +
                "(with bulk data operations):");

        roster.stream()
                .filter(
                        p -> p.getGender() == Person.Sex.MALE
                                && p.getAge() >= 18
                                && p.getAge() <= 25)
                .map(p -> p.getEmailAddress())
                .forEach(email -> System.out.println(email));
        System.out.println();

        /**
         *  9. 按年齡排序。Java 8 之前需要實現 Comparator 接口
         *  接口比較器是一個功能接口。因此,
         *  可以使用lambda表達式來代替定義並創建一個實現了Comparator的類的新實例:
         */
        Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

        Arrays.sort(rosterAsArray,
                (a, b) -> Person.compareByAge(a, b)
        );

        for (Person person : roster) {
            person.printPerson();
        }

        /**
         *  這種比較兩個Person實例的出生日期的方法已經作爲Person. 
         *	comparebyage存在。你可以在lambda表達式中調用這個方法
         */

        Arrays.sort(rosterAsArray,
                (a, b) -> Person.compareByAge(a, b)
        );
}

二、函數式接口

1. 什麼是函數式接口

  • 只包含一個抽象方法的接口,稱爲函數式接口,該抽象方法也被稱爲函數方法。 我們熟知的Comparator和Runnable、Callable就屬於函數式接口。
  • 這樣的接口這麼簡單,都不值得在程序中定義,所以,JDK8在 java.util.function 中定義了幾個標準的函數式接口,供我們使用。Package java.util.function
  • 可以通過 Lambda 表達式來創建該接口的對象。(若 Lambda 表達式拋出一個受檢異常,那麼該異常需要在目標接口的抽象方法上進行聲明)。
  • 我們可以在任意函數式接口上使用 @FunctionalInterface 註解, 這樣做可以檢查它是否是一個函數式接口,同時 javadoc 也會包含一條聲明,說明這個接口是一個函數式接口。

2. 自定義函數式接口

@FunctionalInterface    //@FunctionalInterface標註該接口會被設計成一個函數式接口,否則會編譯錯誤
public interface MyFunc<T> {
    T getValue(T t);
}
public static String toUpperString(MyFunc<String> myFunc, String str) {
    return myFunc.getValue(str);
}

public static void main(String[] args) {
    String newStr = toUpperString((str) -> str.toUpperCase(), "abc");
    System.out.println(newStr);
}

作爲參數傳遞 Lambda 表達式:爲了將 Lambda 表達式作爲參數傳遞,接收Lambda 表達式的參數類型必須是與該 Lambda 表達式兼容的函數式接口的類型

函數接口爲lambda表達式和方法引用提供目標類型

3. Java 內置四大核心函數式接口

函數式接口 參數類型 返回類型 用途
Consumer<T> T void 對類型爲T的對象應用操作,包含方法:void accept(T t)
Supplier<T> T 返回類型爲T的對象,包 含方法:T get();
Function<T,R> T R 對類型爲T的對象應用操作,並返回結果。結果是R類型的對象。包含方法:R apply(T t);
Predicate<T> T boolean 確定類型爲T的對象是否滿足某約束,並返回 boolean 值。包含方法 boolean test(T t);
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/*
 * Java8 內置的四大核心函數式接口
 * Consumer<T> : 消費型接口  void accept(T t);
 * Supplier<T> : 供給型接口   T get();
 * Function<T, R> : 函數型接口  R apply(T t);
 * Predicate<T> : 斷言型接口   boolean test(T t);
 */
public class FunctionalInterfaceTest {

    //Predicate<T> 斷言型接口:將滿足條件的字符串放入集合
    public List<String> filterStr(List<String> list, Predicate<String> predicate) {
        List<String> newList = new ArrayList<>();
        for (String s : list) {
            if (predicate.test(s)) {
                newList.add(s);
            }
        }
        return newList;
    }

    @Test
    public void testPredicate() {
        List<String> list = Arrays.asList("hello", "java8", "function", "predicate");
        List<String> newList = filterStr(list, s -> s.length() > 5);
        for (String s : newList) {
            System.out.println(s);
        }
    }

    // Function<T, R> 函數型接口:處理字符串
    public String strHandler(String str, Function<String, String> function) {
        return function.apply(str);
    }

    @Test
    public void testFunction() {
        String str1 = strHandler("測試內置函數式接口", s -> s.substring(2));
        System.out.println(str1);

        String str2 = strHandler("abcdefg", s -> s.toUpperCase());
        System.out.println(str2);
    }

    //Supplier<T> 供給型接口 :產生指定個數的整數,並放入集合
    public List<Integer> getNumList(int num, Supplier<Integer> supplier) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            Integer n = supplier.get();
            list.add(n);
        }
        return list;
    }

    @Test
    public void testSupplier() {
        List<Integer> numList = getNumList(10, () -> (int) (Math.random() * 100));

        for (Integer num : numList) {
            System.out.println(num);
        }
    }

    //Consumer<T> 消費型接口 :修改參數
    public void modifyValue(Integer value, Consumer<Integer> consumer) {
        consumer.accept(value);
    }

    @Test
    public void testConsumer() {
        modifyValue(3, s -> System.out.println(s * 3));
    }
}

Package java.util.function 包下還提供了很多其他的演變方法。

java8-function.png

?> Tip

Java類型要麼是引用類型(Byte、Integer、Objuct、List),要麼是原始類型(int、double、byte、char)。但是泛型只能綁定到引用類型。將原始類型轉換爲對應的引用類型,叫裝箱,相反,將引用類型轉換爲對應的原始類型,叫拆箱。當然Java提供了自動裝箱機制幫我們執行了這一操作。

List<Integer> list = new ArrayList();
	for (int i = 0; i < 10; i++) {
	list.add(i);    //int被裝箱爲Integer
}

但這在性能方面是要付出代價的。裝箱後的值本質上就是把原始類型包裹起來,並保存在堆裏。因此,裝箱後的值需要更多的內存,並需要額外的內存搜索來獲取被包裹的原始值。

以上funciton包中的IntPredicate、DoubleConsumer、LongBinaryOperator、ToDoubleFuncation等就是避免自動裝箱的操作。一般,針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴。


三、方法引用

  • 方法引用是指通過方法的名字來指向一個方法

  • 當要傳遞給 Lambda 體的操作,已經有實現的方法了,就可以使用方法引用(實現抽象方法的參數列表,必須與方法引用方法的參數列表保持一致!)

  • 方法引用的唯一用途是支持Lambda的簡寫(可以理解爲方法引用是lambda表達式的另一種表現形式,快捷寫法)

使用 :: 操作符將方法名對象或類的名字分隔開

1. eg

BinaryOperator<Double> binaryOperator = (x,y)->Math.pow(x,y);
//等價於
BinaryOperator<Double> binaryOperator1 = Math::pow;

2. 方法引用類型

Java 8 提供了4種方法引用

Kind Example
靜態方法引用 ContainingClass::staticMethodName
特定對象的實例方法引用 containingObject::instanceMethodName
特定類型的任意對象的實例方法引用 ContainingType::methodName
構造器引用 ClassName::new

1. 靜態方法引用

//比較年齡的方法在Person.compareByAge的已經存在,所以可以使用方法引用
Arrays.sort(rosterAsArray, Person::compareByAge);
//---------------------
@Test
public void test3(){
    BiFunction<Double,Double,Double> bif = (x,y)->Math.max(x,y);
    System.out.println(bif.apply(22.1,23.2));

    System.out.println("===等價於===");

    BiFunction<Double,Double,Double> bif1 = Math::max;
    System.out.println(bif1.apply(22.1,23.2));
}

@Test
public void test4(){
    Comparator<Integer> com = (x, y)->Integer.compare(x,y);
    System.out.println(com.compare(1,2));

    System.out.println("===等價於===");
    Comparator<Integer> com1 = Integer::compare;
    System.out.println(com1.compare(1,2));
}

2. 特定對象的實例方法引用

class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }
        
    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
//------------------------
@Test
public void test2() {
    Person person = new Person("Tom", IsoChronology.INSTANCE.date(1995, 6, 20), Person.Sex.MALE, "[email protected]");

    Supplier<String> sup = () -> person.getName();
    System.out.println(sup.get());

    System.out.println("===等價於===");

    Supplier<String> sup1 = person::getName;
    System.out.println(sup1.get());
}

3. 特定類型的任意對象的實例方法引用

String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
//-------------------
@Test
public void test5(){
    BiPredicate<String,String> bp = (x,y)->x.equals(y);
    System.out.println(bp.test("Java情報局","Java情報局1"));
    System.out.println("===等價於===");

    BiPredicate<String,String> bp1 = String::equals;
    System.out.println(bp.test("Java情報局","Java情報局"));
}

4. 構造器引用

將一個集合內元素複製到另一個集合中。

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {
        
        DEST result = collectionFactory.get();
        for (T t : sourceCollection) {
            result.add(t);
        }
        return result;
}

Supplier是一個函數式接口,您可以使用lambda表達式調用方法TransferElements

Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

使用構造器引用代替lambda表達式

Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
//Java編譯器可以推斷出要創建包含Person類型元素的HashSet集合,可簡寫
Set<Person> rosterSet = transferElements(roster, HashSet::new);
Function<Integer,MyClass> fun = (n) -> new MyClass(n);
//等價於
Function<Integer,Person> fun = MyClass::new;
// 帶兩個參數的構造器引用就要用BiFunction,多個參數的話,還可以自定義一個這樣的函數式接口
@Test
public void test6(){
    Supplier<Person> sup = ()->new Person("Tom", IsoChronology.INSTANCE.date(1995, 6, 20), Person.Sex.MALE, "[email protected]");
    System.out.println(sup.get());

構造器引用還可以創建數組

@Test
public void test7(){
    Function<Integer,String[]> fun = args -> new String[args];
    String[] strs = fun.apply(6);
    System.out.println(strs.length);
    
    System.out.println("===等價於===");
    
    Function<Integer,String[]> fun1 = String[]::new;
    String[] strs1 = fun1.apply(6);
    System.out.println(strs1.length);
}

四、Stream——函數式數據處理

Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射數據等操作。 使用Stream API 對集合數據進行操作,就類似於使用 SQL 執行的數據庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之, Stream API 提供了一種高效且易於使用的處理數據的方式

1. Stream是個啥

Stream(流) 是數據渠道,用於操作數據源(集合、數組等)所生成的元素序列。

集合講的是數據,流講的是計算!

?>tip

  • Stream 自己不會存儲元素

  • Stream 不會改變源對象。相反,他們會返回一個持有結果的新Stream

  • Stream 操作是延遲執行的。這意味着他們會等到需要結果的時候才執行

流操作有兩個重要特點

  • 流水線——很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線
  • 內部迭代——與迭代器顯示迭代集合不同,流的迭代操作都在背後進行

2. Stream 的操作三個步驟

  1. 創建 Stream 一個數據源(如:集合、數組),獲取一個流
  2. 中間操作(一箇中間操作鏈,對數據源的數據進行處理,形成一條流的流水線)
  3. 終止操作(一個終止操作,執行中間操作鏈,併產生結果)

2.1. 創建 Stream

Java8 中的 Collection 接口被擴展,提供了兩個獲取流的方法:

  • default Stream<E> stream() : 返回一個順序流

  • default Stream<E> parallelStream() : 返回一個並行流

由數組創建流

Java8 中的 Arrays 的靜態方法 stream() 可以獲取數組流:

  • static Stream stream(T[] array): 返回一個流

重載形式,能夠處理對應基本類型的數組:

  • public static IntStream stream(int[] array)

  • public static LongStream stream(long[] array)

  • public static DoubleStream stream(double[] array)

由值創建流

可以使用靜態方法 Stream.of(), 通過顯示值創建一個流。它可以接收任意數量的參數。

  • public static<T> Stream<T> of(T… values) : 返回一個流
由函數創建流:創建無限流

可以使用靜態方法 Stream.iterate() 和 Stream.generate(), 創建無限流。

  • 迭代

    • public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
  • 生成

    • public static<T> Stream<T> generate(Supplier<T> s) :
//創建 Stream
@Test
public void test1(){
  //1. Collection 提供了兩個方法  stream() 與 parallelStream()
  List<String> list = new ArrayList<>();
  Stream<String> stream = list.stream(); //獲取一個順序流
  Stream<String> parallelStream = list.parallelStream(); //獲取一個並行流

  //2. 通過 Arrays 中的 stream() 獲取一個數組流
  Integer[] nums = new Integer[10];
  Stream<Integer> stream1 = Arrays.stream(nums);

  //3. 通過 Stream 類中靜態方法 of()
  Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);

  //4. 創建無限流
  //迭代
  Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2).limit(10);
  stream3.forEach(System.out::println);

  //生成
  Stream<Double> stream4 = Stream.generate(Math::random).limit(2);
  stream4.forEach(System.out::println);
}

2.2. Stream 的中間操作

多箇中間操作可以連接起來形成一個流水線,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理! 而在終止操作時一次性全部處理,稱爲“惰性求值”

2.2.1 篩選與切片
方法 描述
filter(Predicate p) 接收 Lambda , 從流中排除某些元素
distinct() 篩選,通過流所生成元素的 hashCode() 和 equals() 去除重複元素
limit(long maxSize) 截斷流,使其元素不超過給定數量
skip(long n) 跳過元素,返回一個扔掉了前 n 個元素的流。若流中元素不足 n 個,則返回一個空流。與 limit(n) 互補
List<Person> persons = Person.createRoster();

//內部迭代:迭代操作 Stream API 內部完成
@Test
public void test2(){
  //所有的中間操作不會做任何的處理
  Stream<Person> stream = persons.stream()
    .filter((e) -> {
      System.out.println("測試中間操作");
      return e.getAge() <= 35;
    });

  //只有當做終止操作時,所有的中間操作會一次性的全部執行,稱爲“惰性求值”
  stream.forEach(System.out::println);
}

//外部迭代
@Test
public void test3(){
  Iterator<Person> it = persons.iterator();

  while(it.hasNext()){
    System.out.println(it.next());
  }
}

@Test
public void test4(){
  persons.stream()
    .filter((p) -> {
      System.out.println("大於25歲的成員:"); // &&  ||
      return (p.getAge()) >= 25;
    }).limit(3)
    .forEach(System.out::println);
}

@Test
public void test5(){
  persons.parallelStream()
    .filter((e) -> e.getAge() >= 20)
    .skip(2)
    .forEach(System.out::println);
}

@Test
public void test6(){
  persons.stream()
    .distinct()
    .forEach(System.out::println);
}
2.2.2 映射
方法 描述
map(Function f) 接收一個函數作爲參數,該函數會被應用到每個元素上,並將其映射成一個新的元素
mapToDouble(ToDoubleFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 DoubleStream
mapToInt(ToIntFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 IntStream。
mapToLong(ToLongFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 LongStream
flatMap(Function f) 接收一個函數作爲參數,將流中的每個值都換成另一個流,然後把所有流連接成一個流
//映射
@Test
public void test1(){
  Stream<String> str = persons.stream()
    .map((e) -> e.getName());
  System.out.println("-------------------------------------------");
  List<String> strList = Arrays.asList("aaa", "bbb", "ccc", "ddd", "eee");
  Stream<String> stream = strList.stream()
    .map(String::toUpperCase);
  stream.forEach(System.out::println);

  System.out.println("---------------------------------------------");

  Stream<Character> stream3 = strList.stream()
    .flatMap(TestStreamAPI::filterCharacter);
  stream3.forEach(System.out::println);
}

public static Stream<Character> filterCharacter(String str){
  List<Character> list = new ArrayList<>();
  for (Character ch : str.toCharArray()) {
    list.add(ch);
  }
  return list.stream();
}
2.2.3 排序
方法 描述
sorted() 產生一個新流,其中按自然順序排序
sorted(Comparator comp) 產生一個新流,其中按比較器順序排序
@Test
public void test(){
  persons.stream()
    .map(Person::getName)
    .sorted()
    .forEach(System.out::println);

  System.out.println("------------------------------------");

  persons.stream()
    .sorted((x, y) -> {
      if(x.getAge() == y.getAge()){
        return x.getName().compareTo(y.getName());
      }else{
        return Integer.compare(x.getAge(), y.getAge());
      }
    }).forEach(System.out::println);
}

2.3. Stream 的終止操作

終端操作會從流的流水線生成結果。其結果可以是任何不是流的值,例如:List、Integer,甚至是 void

2.3.1 查找與匹配
方法 描述
allMatch(Predicate p) 檢查是否匹配所有元素
anyMatch(Predicate p) 檢查是否至少匹配一個元素
noneMatch(Predicate p) 檢查是否沒有匹配所有元素
findFirst() 返回第一個元素
findAny() 返回當前流中的任意元素
count() 返回流中元素總數
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer c) 內部迭代(使用 Collection 接口需要用戶去做迭 代,稱爲外部迭代。相反,Stream API 使用內部 迭代——它幫你把迭代做了)
public class TestStreamAPI2 {

	List<Person> persons = Person.createRoster();	
	//3. 終止操作
	@Test
	public void test1(){
			boolean bl = persons.stream()
				.allMatch((e) -> e.getGender().equals(Person.Sex.FEMALE));
			
			System.out.println("所有成員都爲女性嗎?"+bl);
			
			boolean bl1 = persons.stream()
				.anyMatch((e) -> e.getGender().equals(Person.Sex.FEMALE));
			
			System.out.println("成員中有女性嗎?"+bl1);
			
			boolean bl2 = persons.stream()
				.noneMatch((e) -> e.getGender().equals(Person.Sex.FEMALE));
			
			System.out.println("成員中是不是沒有女性?"+bl2);
	}
	
	@Test
	public void test2(){
		Optional<Person> op = persons.stream()
			.sorted(Comparator.comparingInt(Person::getAge))
			.findFirst();
		System.out.println("年齡最小的:"+op.get());
		
		Optional<Person> op2 = persons.parallelStream()
			.filter((e) -> e.getGender().equals(Person.Sex.MALE))
			.findAny();
		
		System.out.println("隨便找個男的:"+op2.get());
	}
	
	@Test
	public void test3(){
		long count = persons.stream()
						 .filter((e) -> e.getGender().equals(Person.Sex.FEMALE))
						 .count();
		
		System.out.println("女生的人數:"+count);
		
		Optional<Integer> op = persons.stream()
			.map(Person::getAge)
			.max(Integer::compare);
		
		System.out.println("最大年齡:"+op.get());
		
		Optional<Person> op2 = persons.stream()
			.min((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge()));
		
		System.out.println("最小年齡成員:"+op2.get());
	}
	
	//注意:流進行了終止操作後,不能再次使用
	@Test
	public void test4(){
		Stream<Person> stream = persons.stream()
		 .filter((e) -> e.getGender().equals(Person.Sex.FEMALE));
		
		long count = stream.count();
		
		stream.map(Person::getAge)
			.max(Integer::compare);
	}
}
2.3.2 規約
方法 描述
reduce(T iden, BinaryOperator b) 可以將流中元素反覆結合起來,得到一個值。 返回 T
reduce(BinaryOperator b) 可以將流中元素反覆結合起來,得到一個值。 返回 Optional<T>

備註:map 和 reduce 的連接通常稱爲 map-reduce 模式,因 Google 用它來進行網絡搜索而出名。

List<Person> persons = Person.createRoster();

//3. 終止操作:歸約
@Test
public void test1(){
  List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
  Integer sum = list.stream()
    .reduce(0, (x, y) -> x + y);

  System.out.println(sum);
  System.out.println("----------------------------------------");

  Optional<Integer> op = persons.stream()
    .map(Person::getAge)
    .reduce(Integer::sum);
  System.out.println("所有成員的年齡和:"+op.get());
}

//需求:搜索名字中 “B” 出現的次數
@Test
public void test2(){
  Optional<Integer> sum = persons.stream()
    .map(Person::getName)
    .flatMap(TestStreamAPI1::filterCharacter)
    .map((ch) -> {
      if(ch.equals('B'))
        return 1;
      else 
        return 0;
    }).reduce(Integer::sum);

  System.out.println(sum.get());
}
2.3.3 收集
方法 描述
collect(Collector c) 將流轉換爲其他形式。接收一個 Collector接口的 實現,用於給Stream中元素做彙總的方法

Collectors

Collector接口中方法的實現決定了如何對流執行收集操作(如收集到 List、Set、Map)。但是 Collectors 實用類提供了很多靜態方法,可以方便地創建常見收集器實例,具體方法與實例如下表: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html

方法 返回類型 作用 示例
toList List 把流中元素收集到List List list= list.stream().collect(Collectors.toList());
toSet Set 把流中元素收集到Set Set set= list.stream().collect(Collectors.toSet());
toCollection Collection 把流中元素收集到創建的集合 Collectione mps=list.stream().collect(Collectors.toCollection(ArrayList::new));
counting Long 計算流中元素的個數 long count = list.stream().collect(Collectors.counting());
summingInt Integer 對流中元素的整數屬性求和 Integer sum = persons.stream() .collect(Collectors.summingInt(Person::getAge));
averagingInt Double 計算流中元素Integer屬性的平均值 double avg= list.stream().collect(Collectors.averagingInt(Person::getAge));
summarizingInt IntSummaryStatistics 收集流中Integer屬性的統計值。 如:平均值 IntSummaryStatistics iss= list.stream().collect(Collectors.summarizingInt(Person::getAge));
joining String 連接流中每個字符串 String str= list.stream().map(Person::getName).collect(Collectors.joining());
maxBy Optional<T> 根據比較器選擇最大值 Optionalmax= list.stream().collect(Collectors.maxBy(comparingInt(Person::getAge)));
minBy Optonal<T> 根據比較器選擇最小值 Optional min = list.stream().collect(Collectors.minBy(comparingInt(Person::getAge)));
reducing 歸約產生的類型 從一個作爲累加器的初始值開始,利用BinaryOperator與 流中元素逐個結合,從而歸 約成單個值 int total=list.stream().collect(Collectors.reducing(0, Person::getAge, Integer::sum));
collectingAndThen 轉換函數返回的類型 包裹另一個收集器,對其結果轉換函數 int how= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<K,List<T>> 根據某屬性值對流分組,屬性爲K,結果爲V Map<Person.Sex, List<Person>> map = persons.stream() .collect(Collectors.groupingBy(Person::getGender));
partitioningBy Map<Boolean,List<T>> 根據true或false進行分區 Map<Boolean, List<Person>> map = persons.stream() .collect(Collectors.partitioningBy((e) -> e.getAge() >= 50));
@Test
public void test3(){
  List<String> list = persons.stream()
    .map(Person::getName)
    .collect(Collectors.toList());
  list.forEach(System.out::println);
}

@Test
public void test4(){
  Optional<Integer> max = persons.stream()
    .map(Person::getAge)
    .collect(Collectors.maxBy(Integer::compare));

  System.out.println("最大年齡:"+max.get());

  Optional<Person> op = persons.stream().min(Comparator.comparingInt(Person::getAge));

  System.out.println("最小年齡的成員:"+op.get());

  Integer sum = persons.stream()
    .collect(Collectors.summingInt(Person::getAge));

  System.out.println("所有成員年齡和:"+sum);

  IntSummaryStatistics dss = persons.stream()
    .collect(Collectors.summarizingInt(Person::getAge));

  System.out.println("最大年齡:"+dss.getMax());
}

//分組
@Test
public void test5(){
  Map<Person.Sex, List<Person>> map = persons.stream()
    .collect(Collectors.groupingBy(Person::getGender));

  System.out.println("按性別分組:"+map);
}

//多級分組
@Test
public void test6(){
  Map<Person.Sex, Map<String, List<Person>>> map = persons.stream()
    .collect(Collectors.groupingBy(Person::getGender, Collectors.groupingBy((e) -> {
      if(e.getAge() >= 60)
        return "老年";
      else if(e.getAge() >= 35)
        return "中年";
      else
        return "成年";
    })));

  System.out.println(map);
}

//分區
@Test
public void test7(){
  Map<Boolean, List<Person>> map = persons.stream()
    .collect(Collectors.partitioningBy((e) -> e.getAge() >= 50));

  System.out.println(map);
}
@Test
public void test8(){
  String str = persons.stream()
    .map(Person::getName)
    .collect(Collectors.joining("," , "----", "----"));

  System.out.println(str);
}

@Test
public void test9(){
  Optional<Integer> sum = persons.stream()
    .map(Person::getAge)
    .collect(Collectors.reducing(Integer::sum));
  System.out.println(sum.get());
}

3. 並行流與串行流

先說說並行和併發

併發是兩個任務共享時間段,並行則是兩個任務在同一時間發生,比如運行在多核CPU上。

lbvsVU.png

並行流就是把一個內容分成多個數據塊,並用不同的線程分別處理每個數據塊的流

Java 8 中將並行進行了優化,我們可以很容易的對數據進行並行操作。Stream API 可以聲明性地通過 parallel()sequential() 在並行流與順序流之間進行切換。如果想從一個集合類創建一個流,調用parallerStream就可以獲取一個並行流。

public static long parallelSum(long n) {
    return Stream.iterate(1L, i -> i + 1)
        .limit(n)
        .parallel()    //將流轉化爲並行流
        .reduce(0L, Long::sum);
}

配置並行流使用的線程池

使用流的parallel方法,你可能會想到,並行流用的線程是從哪兒來的?有多少個?怎麼自定義?

並行流內部使用了默認的ForkJoinPool(分支/合併框架),它默認的線程數量就是你的處理器數量,這個值是由Runtime.getrRuntime().acailable-Processors()得到。

你可以通過系統屬性java.util.concurrent.ForkJoinPool.common.parallelism來改變線程池大小,如下

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");,這是一個全局設置,因此會影響代碼中所有的並行流(目前還無法專門爲某個並行流指定該值,一般而言,讓ForkJoinPool的大小等於處理器數量是個不錯的默認值)。

高效使用並行流

  • 並行流並不是總是比順序流快
  • 留意裝箱。自動裝箱和拆箱操作會大大降低性能,Java8 中有原始類型流(IntStream、LongStream…)來避免這種操作
  • 有些操作本身在並行流上的性能就比順序流差,特別是 limit 和 findFirst 等依賴元素順序的操作,他們在並行流上執行的代價就非常大
  • 還要考慮流的操作流水線的總計算成本
  • 對於較小的數據量,沒必要使用並行流
  • 要考慮流背後的數據結構是否易於分解,比如,ArrayList 的拆分效率比 LinkedList 高很多,前者無需遍歷
  • 還要考慮終端操作中合併步驟的代價是大是小(比如Collector中的combiner方法)

4. Fork/Join 框架

並行流背後使用的基礎框架就是 Java7 中引入的分支/合併框架

Fork/Join(分支/合併)框架的目的是以遞歸方式將可以並行的任務拆分(fork)成更小的任務,然後將每個任務的結果合併 (join)起來生成整體效果。它是ExectorService接口的一個實現,把子任務分配給線程池(稱爲ForkJoinPool)中的工作線程。

Fork/Join 框架:就是在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 彙總

fork-join.png

// 用分支/合併框架 並行求和
public class ForkJoinSumCalculator extends RecursiveTask<Long> {

    private final long[] numbers;
    private final int start;
    private final int end;

    //不再將任務分解爲子任務的數組大小
    public static long THRESHOLD = 100;

    //公共構造器用於創建主任務
    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    //私有構造器用於以遞歸方式爲主任務創建子任務
    private ForkJoinSumCalculator(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        //如果大小小於等於閾值,順序計算結果
        if (length <= THRESHOLD) {
            return computerSequntially();
        }

        ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);

        leftTask.fork();

        ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);

        Long rightResult = rightTask.compute();   //同步執行第二個任務,
        Long leftResult = leftTask.join(); // 讀取第一個子任務的結果,如果尚未完成就等待
        return rightResult + leftResult;
    }


    // 子任務不再可分時計算和
    private long computerSequntially() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            sum += numbers[i];
        }
        return sum;
    }

    public static long forkJoimSum(long n) {
        long[] numbers = LongStream.rangeClosed(1, n).toArray();
        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
        return new ForkJoinPool().invoke(task);
    }

    public static void main(String[] args) {
        System.out.println("sum:" + forkJoimSum(10000));
    }
}

Fork/Join 框架與傳統線程池的區別

採用 “工作竊取”模式(work-stealing): 當執行新的任務時它可以將其拆分成更小的任務執行,並將小任務加到線程隊列中,然後再從一個隨機線程的隊列中偷一個並把它放在自己的隊列中。

相對於一般的線程池實現,fork/join框架的優勢體現在對其中包含的任務的處理方式上,在一般的線程池中,如果一個線程正在執行的任務由於某些原因無法繼續運行,那麼該線程會處於等待狀態,而在fork/join框架實現中,如果某個子問題由於等待另外一個子問題的完成而無法繼續運行,那麼處理該子問題的線程會主動尋找其他尚未運行的子問題來執行,這種方式減少了線程的等待時間,提高了性能。

使用Fork/Join框架的最佳做法

  • 對一個任務調用join方法會阻塞調用方,直到該任務作出結果。因此,有必要在兩個子任務的計算都開始之後再調用它
  • 不應該在 RecursiveTask 內部使用 ForkJoinPool 的 invoke 方法。相反,你應該始終直接調用 compute 或fork 方法,只有順序代碼才應該用 invoke 來啓動並行計算

工作竊取

fork-join-steal.jpg

5. Spliterator

“可分迭代器”——spliterator,和Iterator一樣,也用於遍歷數據源中的元素,它是爲了並行執行而設計。

Java8 爲集合框架中包含的所有數據結構都提供了一個默認的 Spliterator 方法。集合實現了Spliterator接口,接口提供了一個Spliterator方法。

spliterator

五、接口中的默認方法與靜態方法

傳統上,Java中實現接口的類必須爲接口中定義的每個方法提供一個實現類,或者從父類中繼承它的實現。但如果類庫的設計者需要修改接口,加入新的方法,這種方式就會出現問題。所有使用該接口的實體類爲了適配新的接口約定都需要進行修改(要是這麼不兼容的話,遲早被淘汰)。所以,Java8爲了解決這一問題引入了一種新的機制。Java8中的接口支持在聲明方法的同時提供實現。其一,Java8允許在接口中聲明靜態方法。其二,Java8引入的新功能——默認方法,通過默認方法可以指定接口方法的默認實現(因此,實現接口的類如果不顯式的提供該方法的具體實現,就會自動繼承默認的實現,這種機制可以使你平滑的進行接口的優化和升級)。

默認方法

Java 8中允許接口中包含具有具體實現的方法,該方法稱爲 “默認方法”,默認方法使用 default 關鍵字修飾。

interface MyFunc<T>{
    T func(int a);

    default String getName(){
        return "hello java8";
    }
}
@Test
public void test1(){
    List<Integer> list = Arrays.asList(22,11,33,55,4);
    //sort是List接口中的默認方法,naturalOrder是Comparator的靜態方法
    list.sort(Comparator.naturalOrder());
    for (Integer integer : list) {
        System.out.println(integer);
    }
}

default-method.png

默認方法的”類優先”原則

若一個接口中定義了一個默認方法,而另外一個父類或接口中又定義了一個同名的方法時

  • 選擇父類中的方法。如果一個父類提供了具體的實現,那麼接口中具有相同名稱和參數的默認方法會被忽略。

  • 接口衝突。如果一個父接口提供一個默認方法,而另一個接口也提供了一個具有相同名稱和參數列表的方法(不管方法是否是默認方法),那麼必須覆蓋該方法來解決衝突

interface MyFunc<T> {
    default String getName() {
        return "hello java8";
    }
}

interface MyFunc1 {
    default String getName() {
        return "hello Java情報局";
    }
}

class MyClass implements MyFunc, MyFunc1 {

    @Override
    public String getName() {
        return MyFunc1.super.getName();
    }
}

JavaAPI的設計者們充分利用了默認方法,爲集合接口和類新增了很多新的方法。


六、Optional 類

1. 用 Optional 取代 null

當你碰到程序中有一個NullPointerException時的第一衝動是不就是趕緊找到代碼,添加一個if語句,檢查下??

NullPointerException是Java程序開發中典型的異常。爲了避免這種異常,我們的代碼有可能充斥着一層又一層的深度嵌套的null檢查,代碼可讀性極差。

Optional類(java.util.Optional) 是一個容器類,代表一個值存在或不存在, 原來用 null 表示一個值不存在,現在 Optional 可以更好的表達這個概念。並且可以避免空指針異常。

變量存在時,Optional類知識對類簡單封裝。變量不存在時,缺失的值就會被建模成一個“空”的Optional對象,由方法Optional.empty()返回。

常用方法:

  • Optional.of(T t) : 創建一個 Optional 實例
  • Optional.empty() : 創建一個空的 Optional 實例
  • Optional.ofNullable(T t):若 t 不爲 null,創建 Optional 實例,否則創建空實例
  • isPresent() : 判斷是否包含值 orElse(T t) : 如果調用對象包含值,返回該值,否則返回t
  • orElseGet(Supplier s) :如果調用對象包含值,返回該值,否則返回 s 獲取的值
  • map(Function f): 如果有值對其處理,並返回處理後的Optional,否則返回 Optional.empty()
  • flatMap(Function mapper):與 map 類似,要求返回值必須是Optional

2. Optional 實例

2.1 創建Optional對象

@Test
public void test(){

    Optional<Person> optional = Optional.empty();  //創建一個空Optional

    Optional<Person> op = Optional.of(new Person());
    Person p = op.get();
    System.out.println(p);   //Person{name='null', birthday=null, gender=null, emailAddress='null'}

    Person person = null;
    Optional<Person> op1 = Optional.of(person); //person爲null,拋出NullPointerException

    Optional<Person> op2 = Optional.ofNullable(person);   //創建允許null值得Optional對象

}

2.2 optional 對象操作

@Test
public void test4(){
    Person person = new Person("Tom",IsoChronology.INSTANCE.date(1999, 7, 15),Person.Sex.FEMALE, "[email protected]")
    Optional<Person> op = Optional.ofNullable(person);

    Optional<String> op1 = op.map(Person::getName);
    System.out.println(op1.get());
    
    /**
    * 使用 map 從 optional 對象中提取和轉換值
    * 如果想提取人員姓名,之前需要判斷persion !=null,Optional提供了一個map方法,對其處理
    **/
    Optional<String> op2 = op.map(Person::getName);
    System.out.println(op2.get());

    //使用 flatMap 鏈接 optional 對象
    Optional<String> op3 = op.flatMap((e) -> Optional.of(e.getName()));
    System.out.println(op3.get());
    
    //TODO
}

七、CompletableFuture —— 組合式異步編程

1. Future接口

Future接口在 Java 5 中被引入,設計初衷是對將來某個時刻會發生的結果進行建模。它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束後,這個引用被返回給調用方。在 Future中觸發那些潛在耗時的操作把調用線程解放出來,讓它能繼續執行其他有價值的工作, 不再需要等待耗時的操作完成。打個比方,你可以把它想象成這樣的場景:你拿了一袋衣服到你中意的乾洗店去洗衣服。乾洗店員工會給你張發票,告訴你什麼時候你的衣服會洗好(這就 是一個Future事件)。衣服乾洗的同時,你可以去做其他的事情。Future的另一個優點是它比 更底層的Thread更易用。要使用Future,通常你只需要將耗時的操作封裝在一個Callable對象中,再將它提交給ExecutorService,就可以了。下面這段代碼展示了Java 8之前使用 Future的一個例子。

ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> future = executor.submit(new Callable<Double>() {
    public Double call() {
        return doSomeThings();    //異步方式在新的線程中執行操作
    }
});
//doSomethingElse();    //異步操作進行的同時,可以做其他事情
try {
    //獲取異步操作的結果,如果阻塞,等1秒後退出
    Double result = future.get(1, TimeUnit.SECONDS);   
} catch (ExecutionException | InterruptedException | TimeoutException e) {
}

1.1 Future接口的侷限性

雖然Future以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞或者輪詢的方式得到任務的結果。阻塞的方式顯然和我們的異步編程的初衷相違背,輪詢的方式又會耗費無謂的CPU資源,而且也不能及時地得到計算結果,爲什麼不能用觀察者設計模式當計算結果完成及時通知監聽者呢?

Java的一些框架,比如Netty,自己擴展了Java的 Future接口,提供了addListener等多個擴展方法。Google guava也提供了通用的擴展Future:ListenableFuture、SettableFuture 以及輔助類Futures等,方便異步編程。

作爲正統的Java類庫,是不是應該做點什麼,加強一下自身庫的功能呢?

在Java 8中, 新增加了一個包含50個方法左右的類: CompletableFuture,提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的複雜性,提供了函數式編程的能力,可以通過回調的方式處理計算結果,並且提供了轉換和組合CompletableFuture的方法。

比如實現下面一些例子:

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

1.2 使用CompletableFuture 構建異步應用

public class TestCompletableFuture {
    public static CompletableFuture<Integer> compute() {
        final CompletableFuture<Integer> future = new CompletableFuture<>();
        return future;
    }
    public static void main(String[] args) throws Exception {
        final CompletableFuture<Integer> f = compute();
        class Client extends Thread {
            CompletableFuture<Integer> f;
            Client(String threadName, CompletableFuture<Integer> f) {
                super(threadName);
                this.f = f;
            }
            @Override
            public void run() {
                try {
                    System.out.println(this.getName() + ": " + f.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
        new Client("Client1", f).start();
        new Client("Client2", f).start();
        System.out.println("waiting");
        f.complete(100);
        System.in.read();
    }
}

八、新時間日期 API

1. 使用 LocalDate、LocalTime、LocalDateTime

  • LocalDate、LocalTime、LocalDateTime 類的實例是不可變的對象,分別表示使用 ISO-8601日曆系統的日期、時間、日期和時間。它們提供了簡單的日期或時間,並不包含當前的時間信息。也不包含與時區相關的信息。

    @Test
    public void test1(){
        LocalDate date = LocalDate.of(2020,01,03);
        Month month = date.getMonth();
        System.out.println(month);    //JANUARY
    
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        System.out.println(dayOfWeek);   //FRIDAY
    
        int len = date.lengthOfMonth();
        System.out.println(len);  //31
        //使用TemporalField(ChronoField枚舉實現了該接口)讀取LocalDate的值
        int year = date.get(ChronoField.YEAR);
        System.out.println(year);  //2020
    
        LocalDate ld = LocalDate.parse("2020-01-03");
        System.out.println(ld);   //2020-01-03
    
        LocalTime time = LocalTime.of(19,56,11);
        System.out.println(time);  //19:56:11
    
        LocalDateTime ldt = LocalDateTime.now();
        LocalDateTime l1 = LocalDateTime.of(2020,01,03,18,48);
        System.out.println(l1);  //2020-01-03T18:48
    
        LocalDateTime l2 = l1.plusYears(3);
        System.out.println(l2);     //2023-01-03T18:48
    
        LocalDateTime l3 = l1.minusMonths(1);
        System.out.println(l3);  //2019-12-03T18:48
        System.out.println(l3.getMinute()+","+l3.getYear());   //48,2019
    }
    

2. Instant 時間戳

  • 用於“時間戳”的運算。它是以Unix元年(傳統的設定爲UTC時區1970年1月1日午夜時分)開始所經歷的秒數進行計算

    @Test
    public void test2(){
        Instant ins = Instant.now();  //默認使用 UTC 時區
        OffsetDateTime odt = ins.atOffset(ZoneOffset.ofHours(8));
        System.out.println(odt);
        System.out.println(ins.getNano());
        Instant ins2 = Instant.ofEpochSecond(5);
        System.out.println(ins2);
    }
    

3. Duration 和 Period

  • Duration:用於計算兩個“時間”間隔

  • Period:用於計算兩個“日期”間隔

    @Test
    public void test3(){
        Instant ins1 = Instant.now();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        Instant ins2 = Instant.now();
        System.out.println("所耗費時間爲:" + Duration.between(ins1, ins2));   
    
        System.out.println("----------------------------------");
        LocalDate ld1 = LocalDate.now();
        LocalDate ld2 = LocalDate.of(2019, 1, 1);
    
        Period pe = Period.between(ld2, ld1);
        System.out.println(pe.getYears());  
        System.out.println(pe.getMonths());
        System.out.println(pe.getDays());
    }
    

4. 日期的操縱

  • 通過 withXXX 方法修改 LocalDate 的屬性

  • TemporalAdjuster : 時間校正器。有時我們可能需要獲取例如:將日期調整到“下個週日”等操作。

  • TemporalAdjusters : 該類通過靜態方法提供了大量的常 用 TemporalAdjuster 的實現。

    @Test
    public void test(){
        LocalDate date = LocalDate.now();
        //通過withAttributer方法修改LocalDate的屬性
        LocalDate date1 = date.with(ChronoField.ALIGNED_WEEK_OF_YEAR,9);
        LocalDate date2 = date.withYear(2019);
        LocalDate date3 = date.withDayOfMonth(11);  //修改爲11號
        System.out.println(date1);
        //下週日
        LocalDate nextSunday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
        System.out.println(nextSunday);
    }
    

5. 解析與格式化

java.time.format.DateTimeFormatter 類:該類提供了三種格式化方法:

  • 預定義的標準格式

  • 語言環境相關的格式

  • 自定義的格式

    @Test
    public void test(){
        //DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE;
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E");
    
        LocalDateTime ldt = LocalDateTime.now();
        String strDate = ldt.format(dtf);
    
        System.out.println(strDate); //2020年01月03日 20:32:14 星期五
    
        LocalDateTime newLdt = ldt.parse(strDate, dtf);
        System.out.println(newLdt);  //2020-01-03T20:32:14
    }
    

時區的處理

  • Java8 中加入了對時區的支持,帶時區的時間爲分別爲: ZonedDate、ZonedTime、ZonedDateTime

    其中每個時區都對應着 ID,地區ID都爲 “{區域}/{城市}”的格式 例如 :Asia/Shanghai 等

    ZoneId:該類中包含了所有的時區信息

    • getAvailableZoneIds() : 可以獲取所有時區時區信息
  • of(id) : 用指定的時區信息獲取 ZoneId 對象

    @Test
    public void test(){
        Set<String> set = ZoneId.getAvailableZoneIds();  //遍歷時區
        set.forEach(System.out::println);
        LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
        System.out.println(ldt);
    
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("US/Pacific"));
        System.out.println(zdt);
    }
    

九、重複註解與類型註解

註解

Java 8 對註解處理提供了兩點改進:可重複的註解可用於類型的註解

Java中註解是一種對程序元素進行配置,提供附加信息的機制(Java8之前,註解只能被用在聲明上)

重複註解

Java8 之前不允許上邊這樣的重複註解,所以一般會通過一些慣用手法繞過這一限制。可以聲明一個新的註解,它包含了你希望重複的註解數組。

創建一個重複註解

  1. 將註解標記爲**@Repeatable**
  2. 提供一個註解的容器
import java.lang.annotation.Repeatable;
@Repeatable(Authors.class)
public @interface Author {
    String name();
}
public @interface Authors {
    Author[] value();
}
@Author(name = "Java")
@Author(name = "Android")
public class Book {
    public static void main(String[] args) {
        Author[] authors = Book.class.getAnnotationsByType(Author.class);
        Arrays.asList(authors).forEach(s->{
            System.out.println(s.name());
        });
    }
}

類型註解

Java8 開始,註解可以應用於任何類型。包括new操作符、類型轉換、instanceof檢查、範型類型參數,以及implemtnts和throws子句。

@NotNull String name = person.getName();    //getName不返回空

List<@NotNull Person> persons = new ArrayList<>();  //persons總是非空

十、其他語言特性

原子操作

java.util.concurrent.atomic 包提供了多個對數字類型進行操作的類,比如AtomicInteger和AtomicLong,它們支持對單一變量的原子操作。這些類在Java 8中新增了更多的方法支持。

  • getAndUpdate——以原子方式用給定的方法更新當前值,並返回變更之前的值

  • updateAndGet——以原子方式用給定的方法更新當前值,並返回變更之後的值

  • getAndAccumulate——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之前的值

  • accumulateAndGet——以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之後的值

Adder和Accumulator

多線程的環境中,如果多個線程需要頻繁地進行更新操作,且很少有讀取的動作(比如,在統計計算的上下文中),Java API文檔中推薦大使用新的類LongAdder、LongAccumulator、Double-Adder以及DoubleAccumulator,儘量避免使用它們對應的原子類型。這些新的類在設計之初就考慮了動態增長的需求,可以有效地減少線程間的競爭。

LongAddr 和 DoubleAdder 類都支持加法操作 , 而 LongAccumulator 和 DoubleAccumulator可以使用給定的方法整合多個值。

ConcurrentHashMap

ConcurrentHashMap類的引入極大地提升了HashMap現代化的程度,新引入的ConcurrentHashMap對併發的支持非常友好。ConcurrentHashMap允許併發地進行新增和更新操作,因爲它僅對內部數據結構的某些部分上鎖。因此,和另一種選擇,即同步式的Hashtable比較起來,它具有更高的讀寫性能。

  1. 性能

    爲了改善性能,要對ConcurrentHashMap的內部數據結構進行調整。典型情況下,map的條目會被存儲在桶中,依據鍵生成哈希值進行訪問。但是,如果大量鍵返回相同的哈希值,由於桶是由List實現的,它的查詢複雜度爲O(n),這種情況下性能會惡化。在Java 8中,當桶過於臃腫時,它們會被動態地替換爲排序樹(sorted tree),新的數據結構具有更好的查詢性能(排序樹的查詢複雜度爲O(log(n)))。注意,這種優化只有當鍵是可以比較的(比如String或者Number類)時纔可能發生。

  2. 類流操作

    ConcurrentHashMap支持三種新的操作,這些操作和你之前在流中所見的很像:

  • forEach——對每個鍵值對進行特定的操作

  • reduce——使用給定的􏰤簡函數(reduction function),將所有的鍵值對整合出一個結果􏰝

  • search——對每一個鍵值對執行一個函數,直到函數的返回值爲一個非空值

    以上每一種操作都支持四種形式,接受使用鍵、值、Map.Entry以及鍵值對的函數:

  • 使用鍵和值的操作(forEach、reduce、search)

  • 使用鍵的操作(forEachKey、reduceKeys、searchKeys)

  • 使用值的操作 (forEachValue、reduceValues、searchValues)

  • 使用Map.Entry對象的操作(forEachEntry、reduceEntries、searchEntries)

注意,這些操作不會對ConcurrentHashMap的狀態上鎖。它們只會在運行過程中對元素進行操作。應用到這些操作上的函數不應該對任何的順序,或者其他對象,或在計算過程發生變化的值,有依賴。 除此之外,你需要爲這些操作指定一個併發閾值。如果經過預預估當前map的大小小於設定的閾值,操作會順序執行。使用值1開開啓基於通用線程池的最大並行。使用值Long.MAX_VALUE設定程序以單線程執行操作。下面這個例子中,我們使用reduceValues試圖找出map中的最大值:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); 
Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));

注意,對int、long和double,它們的reduce操作各有不同(比如reduceValuesToInt、reduceKeysToLong等)。

  1. 計數

    ConcurrentHashMap類提供了一個新的方法,名叫mappingCount,它以長整型long返回map中映射的數目。我們應該儘量使用這個新方法,而不是老的size方法,size方法返回的類型爲int。這是因爲映射的數量可能是int無法表示的。

  2. 集合視圖

    ConcurrentHashMap類還提供了一個名爲KeySet的新方法,該方法以Set的形式返回ConcurrentHashMap的一個視圖(對map的修改會反映在該Set中,反之亦然)。你也可以使用新的靜態方法newKeySet,由ConcurrentHashMap創建一個Set。

Arrays

Arrays類提供了不同的靜態方法對數組進行操作。現在,它又包括了四個新的方法(它們都有特別重載的變量)

  • parallelSort:parallelSort方法會以併發的方式對指定的數組進行排序,你可以使用自然順序,也可以

    爲數組對象定義特別的Comparator

  • setAll和parallelSetAll:setAll和parallelSetAll方法可以以順序的方式也可以用併發的方式,使用提供的函數 計算每一個元素的值,對指定數組中的所有元素進行設置

  • parallelPrefix:parallelPrefix方法以併發的方式,用用戶提供的二進制操作符對給定數組中的每個元素

    進行累積計算

Number

Number類中新增方法

  • Short、Integer、Long、Float和Double類提供了靜態方法sum、min和max
  • Integer和Long類提供了compareUnsigned、divideUnsigned、remainderUnsigned 和toUnsignedLong方法來處理無符號數。
  • Integer和Long類也分別提供了靜態方法parseUnsignedInt和parseUnsignedLong 將字符解析爲無符號int或者long類型。
  • Byte和Short類提供了toUnsignedInt和toUnsignedLong方法通過無符號轉換將參數轉化爲 int 或 者 long 類型。類似地, Integer 類現在也提供了靜態方法 toUnsignedLong。
  • Double和Float類提供了靜態方法isFinite,可以檢查參數是否爲有限浮點數。
  • Boolean類現在提供了靜態方法logicalAnd、logicalOr和logicalXor,可以在兩個 boolean之間執行and、or和xor操作。
  • BigInteger 類提供了 byteValueExact 、 shortValueExact 、 intValueExact 和 longValueExact,可以將BigInteger類型的值轉換爲對應的基礎類型。不過,如果在轉換過程中有信息的丟失,方法會拋出算術異常。

Math

如果Math中的方法在操作中出現ຼ出,Math類提供了新的方法可以拋出算術異常。支持這一異常的方法包括使用int和long參數的addExact、subtractExact、multipleExact、 incrementExact、decrementExact和negateExact。此外,Math類還新增了一個靜態方法 toIntExact,可以將long值轉換爲int值。其他的新增內容包括靜態方法floorMod、floorDiv 和nextDown。

Files

Files類最引人注目的改變是,你現在可以用文件直接產生流

  • Files.list——生成由指定目錄中所有條目構成的Stream<Path>。這個列表不是遞歸包含的。由於流是延遲消費的,處理包含內容非常龐大的目錄時,這個方法非常有用
  • Files.walk——和Files.list有些類似,它也生成包含給定目錄中所有條目的 Stream<Path>。不過這個列表是遞歸的,你可以設定遞歸的深度。注意,該遍歷是依照深度優先進行的
  • Files.find—— 通過遞歸地遍歷一個目錄找到符合條件的條目,並生成一個 Stream<Path> 對象

String

String類也新增􏱗了一個靜態方法,名叫join。它可以用一個分隔符將多個字符串􏶘接起來。和我們以前使用的apache提供的StringUtils.join一樣。

Reflection

Reflection API的變化就是爲了支持Java 8中註解機制的改變。 除此之外,Relection接口的另一個變化是新增了可以查詢方法參數信息的API,比如,你現在可以使用新的java.lang.reflect.Parameter類查詢方法參數的名稱和修飾符。


FAQ

  • 說說你知道的Java8 有哪寫新特性?

  • 什麼是lambda表達式?有啥優點?

  • ConcurrentHashMap 在Java8 和 Java7的實現區別?

  • 能說說 Java 8 改進的JVM 不?

  • hashMap原理,java8做了哪些改變?

  • 外部迭代和內部迭代,你曉得吧?


參考

《Java 8實戰》

《Java 8函數式編程》

Java 8官方文檔

某免費視頻學習網站

發佈了68 篇原創文章 · 獲贊 120 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章