JDK8到底有啥特性

大feiyu發表於2020-09-24

前言

自 JDK9 之後,oracle 每年 3 月與 9 月 JDK 都會釋出一個新的版本,而2020 年 9 月即將迎來 JDK15。
然而令人悲傷地是我卻還在用 JDK 8 !!!
這真是個悲傷地故事

然而JDK8的特性除了聽到的lambda表示式,我還會啥。

在這裡插入圖片描述
然而還有的人連lambda都不知道怎麼用,這就更慘了

lambda表示式

先看下lambda 表示式是怎麼定義的:

lambda 表示式是一個匿名函式。 lambda 表示式允許把一個函式作為引數進行傳遞

可能剛看到這兩句話時,不知道是什麼意思。那麼,對比一下 js 中的 setInterval 函式的用法,你就能找到一些感覺了

//每一秒執行一次匿名函式。(模擬時鐘)
setInterval(function() {
    console.log("當前時間為:" + new Date());
}, 1000);

如上,function(){}這段,就是一個匿名函式,並且可以把它作為引數傳遞給 setInterval 函式。
這是因為,在 js 中,函式是一等公民。
然而,在 Java 中,物件才是一等公民。但是,到了 JDK8 我們也可以通過 lambda 表示式表示同樣的效果。
lambda 表示式語法如下

(引數1,引數2) ->  { 方法體 }

左邊指定了 lambda 表示式所需要的所有引數,右邊用來描述方法體。-> 即為 lambda 運算子。
想一下,在之前我們通過匿名內部類的方式來啟動一個執行緒,是怎麼做的?

public class LambdaTest {
    @Test
    public void test(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("執行緒執行...");
            }
        }).start();
    }
}

現在,若把它改為用 lambda 表示式,則為,

public class LambdaTest {
    @Test
    public void test(){
     // 一行搞定
        new Thread(()->System.out.println("執行緒執行...")).start();
    }
}

可以發現,明顯用 lambda 表示式,寫法更簡潔了。
其實,Lambda 表示式就是: [^1] 函數語言程式設計的體現。
注意事項:

  • 引數列表的資料型別會自動推斷。也就是說,如果匿名函式有引數列表的話,只需要寫引數名即可,不需要寫引數的型別。
  • 如果引數列表為空,則左邊只需要寫小括號即可。
  • 如果引數只有一個,則可以省略小括號,只寫引數的名稱即可。
  • 如果方法體中只有一條執行語句,則可以省略右邊的大括號。若有返回值,則可以把 return 和大括號同時省略。

介面方法

介面預設方法

在 Java 的介面中,只能定義方法名,不能實現方法體的,具體的實現需要子類去做。

但是,到了 JDK8 就不一樣了。在介面中,也可以通過 default關鍵字來實現方法體。

那麼,問題就來了。好端端的,為什麼要加入這個奇怪的功能呢,它有什麼用?

當然是為了提高程式碼的重用性了。此外,介面的預設方法可以在不影響原來的繼承體系的情況下,進行功能的擴充,實現介面的向下相容。

這句話很抽象。那,就用程式碼來說明一下吧。

// 假設各種動物的繼承體系如下

public interface Animal {
    //所有動物都需要吃東西,具體吃什麼,讓子類去實現
    void eat();
}
public class Bird implements Animal {
    @Override
    public void eat() {
        System.out.println("早起的鳥兒有蟲吃!");
    }
}
public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("小貓愛吃魚!");
    }
}

現在,需要對 Animal介面擴充功能了。動物不能只會吃東西吧,它也許會奔跑,也許會飛行。那麼,我在介面中新增兩個方法, run 和 fly 就可以了吧。

這樣定義方法雖然是可以的,但是,問題就來了。介面中定義了方法,實現類就要實現它的所有方法。小貓會奔跑,但是不會飛啊。而小鳥會飛,你讓它在地上跑不是委屈人家嘛。

所以,這個設計不是太合理。

此時,就可以在介面中定義預設方法。子類不需要實現所有方法,可以按需實現,或者直接使用介面的預設方法。

因此,修改 Animal 介面如下,把 run 和 fly 定義為預設方法,

public interface Animal {
    //所有動物都需要吃東西,具體吃什麼,讓子類去實現
    void eat();

    default void run(){
        System.out.println("我跑");
    }

    default void fly(){
        System.out.println("我飛");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly();

        Cat cat = new Cat();
        cat.run();
    }
}

在 JDK8 的集合中,就對 Collection 介面進行了擴充,如增加預設方法 stream() 等。既增強了集合的一些功能,而且也能向下相容,不會對集合現有的繼承體系產生影響。
在這裡插入圖片描述

介面靜態方法

在介面中也可以定義靜態方法。這樣,就可以直接通過介面名呼叫靜態方法。(這也很正常,介面本來就不能例項化)

需要注意的是,不能通過實現類的物件去呼叫介面的靜態方法

public interface MyStaticInterface {
    static void method(){
        System.out.println("這是介面的靜態方法");
    }
}

public class MyStaticInterfaceImpl implements MyStaticInterface {

    public static void main(String[] args) {
        //直接通過介面名呼叫靜態方法,不能通過實現類的物件呼叫
        MyStaticInterface.method();
    }
}

函式式介面

如果一個介面中只有一個抽象方法,則稱其為函式式介面。可以使用 @FunctionalInterface 註解來檢測一個介面是否為函式式介面。

JDK提供了常見的最簡單的四種函式式介面:(必須掌握)

  • Consumer,消費型介面。接收一個引數,沒有返回值。其方法有:void accept(T t);
  • Supplier,供給型介面。沒有引數,帶返回值。其方法:T get();
  • Function<T, R>,函式型介面。接收一個引數,返回一個結果。其方法:R apply(T t);
  • Predicate,斷言型介面。接收一個引數,返回boolean值。其方法:boolean test(T t);
    我這裡舉例了它們的使用方法,
public class LambdaTest {
    @Test
    public void test2(){
        //列印傳入的 msg
        printMsg((s)-> System.out.println(s),"JDK8函式介面");
    }

    public void printMsg(Consumer<String> consumer,String msg){
        //消費型,只有傳入引數,沒有返回值
        consumer.accept(msg);
    }

    @Test
    public void test3(){
        //返回一個 0~99 的隨機數
        Integer content = getContent(() -> new Random().nextInt(100));
        System.out.println(content);
    }

    public Integer getContent(Supplier<Integer> supplier){
        //供給型,傳入引數為空,帶返回值
        return supplier.get();
    }

    @Test
    public void test4(){
        //傳入一個字串,然後把它都轉換成大寫字母。
        System.out.println(transfer((str) -> str.toUpperCase(), "My wechat : mistyskys"));
    }

    public String transfer(Function<String,String> func,String str){
        // 函式型,傳入一個引數,對其進行處理之後,返回一個結果
        return func.apply(str);
    }

    @Test
    public void test5(){
        //定義一個list,用來做篩選
        ArrayList<String> list = new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("jerry");
        list.add("tom");
        //篩選出集合中,字串長度大於 3 的,並加入到結果集。
        List<String> filterResult = filter((str) -> str.length() > 3, list);
        System.out.println(filterResult.toString());
    }

    public List<String> filter(Predicate<String> predicate, List<String> list){
        List<String> result = new ArrayList<>();
        for (String str : list) {
            //斷言型,傳入一個引數,並返回true或者false。
            //這裡的邏輯是,若斷言為真,則把當前的字串加入到結果集中
            if(predicate.test(str)){
                result.add(str);
            }
        }
        return result;
    }
}

還有一些其他函式式介面,都在java.util.function包下,可以自行檢視。使用方法都是一樣的,不再贅述。

除此之外,JDK 中還有很多函式式介面,例如Comparator.java。只要類上邊看到了 @FunctionalInterface 這個註解,你都可以使用 lambda 表示式來簡化寫法。

方法引用

定義:方法引用是用來直接訪問類或者例項的已經存在的方法或者構造方法。

這裡強調一下已經存在的含義。因為,lambda表示式本質上就是一個匿名函式。我們知道,函式就是做邏輯處理的:拿一些資料,去做一些操作。

如果,我們發現有其他地方(類或者物件)已經存在了相同的邏輯處理方案,那麼就可以引用它的方案,而不必重複寫邏輯。這就是方法引用。

其實方法引用就是一個lambda表示式的另外一種更簡潔的表達方式。也可以說是語法糖。

只不過,這裡要求 lambda 表示式需要符合一定的要求。首先,方法體只有一行程式碼。其次,方法的實現已經存在。此時,就可以用方法引用替換 lambda 表示式。

方法引用的操作符為雙冒號::。

下邊就以最簡單的一個我們非常常見的列印語句為例。

//遍歷陣列裡邊的元素,並列印,用lambda表示式
String[] arr = new String[]{"zhangsan","lisi"};
Arrays.asList(arr).forEach((s)-> System.out.println(s));

可以發現,lambda 表示式只有一行程式碼,且方法體邏輯為列印字串。而列印字串的方案,在 System.out 物件中已經存在方法 println() 了。

所以,此處 lambda 表示式可以用方法引用替換。

// 注意:方法引用中的方法名不可帶括號。
Arrays.asList(arr).forEach(System.out::println);

方法引用有以下四種形式:

  • 物件 :: 例項方法
  • 類 :: 靜態方法
  • 類 :: 例項方法
  • 類 :: new

下邊舉例說明:

public class ReferTest {
    public static void main(String[] args) {
        //函式式介面的抽象方法的引數列表和返回值型別,必須與方法引用對應的方法引數列表和返回值型別保持一致(情況3除外,比較特殊)。
        //======= 1.物件::例項方法 =========
        // lambda 表示式
        Consumer consumer1 = (s) -> System.out.println(s);
        consumer1.accept("hello world");
        //方法引用。Consumer的accept方法,和System.out的println方法結構一樣,
        //都是傳入一個引數,無返回值。故可以用方法引用。
        Consumer consumer2 = System.out::println;
        consumer2.accept("hello java");

        //======= 2.類::靜態方法 =========
        Integer[] arr = new Integer[]{12,20,15};
        List<Integer> list = Arrays.asList(arr);
        // lambda 表示式
        Comparator<Integer> com1 = (o1, o2) -> Integer.compare(o1, o2);
        Collections.sort(list,com1);
        //方法引用。Comparator的compare方法,和Integer的compare靜態方法結構一樣,
        //都是傳入兩個引數,返回一個int值,故可以用方法引用。
        Comparator<Integer> com2 = Integer::compare;
        Collections.sort(list,com2);

        //======= 3.類::例項方法 =========
        // lambda表示式
        Comparator<Integer> com3 = (o1, o2) -> o1.compareTo(o2);
        //方法引用。這種形式比較特殊,(o1, o2) -> o1.compareTo(o2) ,
        //當第一個引數o1為呼叫物件,且第二個引數o2為需要引用方法的引數時,才可用這種方式。
        Comparator<Integer> com4 = Integer::compareTo;

        //======= 4.類::new =========
        // lambda表示式
        Supplier<String> supplier1 = () -> new String();
        //方法引用。這個就比較簡單了,就是類的構造器引用,一般用於建立物件。
        Supplier<String> supplier2 = String::new;
    }
}

題外話:方法引用,有時候不太好理解,讓人感覺莫名其妙。所以,如果不熟悉的話,用 lambda 表示式完全沒有問題。就是習慣的問題,多寫就有感覺了。

Optional類

Optional 類是一個容器類。在之前我們通常用 null 來表達一個值不存在,現在可以用 Optional 更好的表達值存在或者不存在。

這樣的目的,主要就是為了防止出現空指標異常 NullPointerException 。

我們知道,像層級關係比較深的物件,中間的呼叫過程很容易出現空指標,如下程式碼。

User user = new User()//中間過程,user物件或者address物件都有可能為空,從而產生空指標異常
String details = user.getAddress().getDetails();

其中,物件的關係如下,

// 地址資訊類
public class Address {
    private String province; //省
    private String city; //市
    private String county; //縣
    private String details; //詳細地址

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCounty() {
        return county;
    }

    public void setCounty(String county) {
        this.county = county;
    }

    public String getDetails() {
        return details;
    }

    public void setDetails(String details) {
        this.details = details;
    }
}

//使用者類
public class User {
    private String name;
    private Address address;

    public String getName() {
        return name;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}
在 Optional 類出現之前,為了防止空指標異常,可以這樣做。(每一層都新增判空處理)

private static String getUserAddr(User user){
    if(user != null){
        Address address = user.getAddress();
        if(address != null){
            return address.getDetails();
        }else {
            return "地址資訊未填寫";
        }
    }else {
        return "地址資訊未填寫";
    }
}

可以發現,程式碼冗長,還不利於維護,隨著層級關係更深,將會變成災難。

那麼,有了 Optional 類,我們就可以寫出更優雅的程式碼,並且防止空指標異常。(後邊就填坑)

實際上,Optional 是對原值(物件)的一層包裝,我們看下 Optional 的原始碼就知道了。

它把真正需要操作的物件 T 封裝成 value 屬性。構造器私有化,並提供三種靜態的建立 Optional 物件的方法。

public final class Optional<T> {
    //EMPTY 代表一個值為空的 Optional 物件
    private static final Optional<?> EMPTY = new Optional<>();

    //用 value 來代表包裝的實際值
    private final T value;

    //值為null的建構函式
    private Optional() {
        this.value = null;
    }

    //要求值不為null的建構函式,否則丟擲空指標異常,見requireNonNull方法
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }
    
    /** 此為Objects類的requireNonNull方法
    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }
    */

    // 1. 建立一個值為空的 Optional 物件
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    
    // 2. 建立一個值不為空的 Optional 物件
    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    // 3. 建立一個值可為空的 Optional 物件
    // 如果值 value 為空,則同1,若不為空,則同2
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }
}

因此,當我們十分確定傳入的user物件不為空時,可以用 Optional.of(user)方法。若不確定,則用 Optional.ofNullable(user),這樣在後續的操作中可以避免空指標異常(後續map說明)。

Optional類常用方法

1. get方法

public T get() {
    //如果值為null,則丟擲異常,否則返回非空值value
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

2.isPresent方法

//判斷值是否存在,若值不為空,則認為存在
public boolean isPresent() {
    return value != null;
}

看到這,不知道有沒有小夥伴和我當初有一樣的疑惑。既然有判空方法 isPresent,還有獲取物件的 get 方法。那開頭的那個坑,是不是就可以改寫為如下,

//注意此時user型別為Optional<User>
private static String getUserAddr(Optional<User> user){
    //如果user存在,則取address物件
    if(user.isPresent()){
        Address address = user.get().getAddress();
        //把address包裝成Optional物件
        Optional<Address> addressOptional = Optional.ofNullable(address);
        //如果address存在,則取details地址資訊
        if(addressOptional.isPresent()){
            return addressOptional.get().getDetails();
        }else {
            return "地址資訊未填寫";
        }
    }else{
        return "地址資訊未填寫";
    }
}

這樣看起來,好像功能也實現了。但是,我們先不說程式碼並沒有簡潔(反而更復雜了),其實是陷入了一個怪圈了。

因為,if(user.isPresent()){}和手動判空處理 if(user!=null){}實質上是沒有區別的。這就是受之前一直以來的程式碼思維限制了。

所以,我們不要手動呼叫 isPresent 方法 。

不要奇怪,isPresent 方法,其實是為了 Optional 中的其他方法服務的(如map方法),本意並不是為了讓我們手動呼叫。你會在後續多個方法中,見到 isPresent 的身影。

3. ifPresent

//傳入一個消費型介面,當值存在時,才消費。
public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

與 isPresent 方法不同, ifPresent 方法是我們推薦使用的。

如可以這樣判空,

Optional<User> user = Optional.ofNullable(new User());
user.ifPresent(System.out::println);
//不要用下邊這種
if (user.isPresent()) {
  System.out.println(user.get());
}

4. orElse


public T orElse(T other) {
    return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

這兩個方法都是當值不存在時,用於返回一個預設值。如user物件為null時,返回預設值。

@Test
public void test1(){
    User user = null;
    System.out.println("orElse呼叫");
    User user1 = Optional.ofNullable(user).orElse(createUser());
    System.out.println("orElseGet呼叫");
    User user2 = Optional.ofNullable(user).orElseGet(() -> createUser());
}

private User createUser() {
    //此處列印,是為了檢視orElse和orElseGet的區別
    System.out.println("createUser...");
    return new User();
}

//列印結果
orElse呼叫
createUser...
orElseGet呼叫
createUser...

以上是user為null時,兩個方法是沒有區別的。因為都需要建立user物件作為預設值返回。

但是,當user物件不為null時,我們看下對比結果,

@Test
public void test2(){
    User user = new User();
    System.out.println("orElse呼叫");
    User user1 = Optional.ofNullable(user).orElse(createUser());
    System.out.println("orElseGet呼叫");
    User user2 = Optional.ofNullable(user).orElseGet(() -> createUser());
}  
//列印結果
orElse呼叫
createUser...
orElseGet呼叫

可以發現,當user物件不為null時,orElse依然會建立User物件,而orElseGet不會建立。

所以,當 orElse() 方法傳入的引數需要建立物件或者比較耗時的操作時,建議用 orElseGet()

5. orElseThrow

當值為null,可以返回自定義異常。

User user = null;
Optional.ofNullable(user).orElseThrow(IllegalAccessError::new);

若user物件為null,則丟擲非法訪問。

這樣,可以有針對的對特定異常做一些其他處理。因為,會丟擲哪些異常的情況,是我們可控的。

6. map

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    //看到沒,map內部會先呼叫isPresent方法來做判空處理。
    //所以我們不要自己去呼叫isPresent方法
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

map類似 Stream 的 map方法。處理完之後,返回的還是一個 Optional 物件,所以可以做鏈式呼叫。

User user = new User();
String name = Optional.of(user).map(User::getName).orElse("佚名");
System.out.println(name);

如上,取出user物件的name值,若name為空,返回一個預設值“佚名”(神奇的名字)。

這裡,直接呼叫map方法,就不需要對user物件進行預先判空了。因為在map方法裡邊,會呼叫isPresent方法幫我們處理user為null的情況。

到這裡,腦袋轉圈快的小夥伴,是不是對開頭的坑已經有啟發了。

沒錯,我們可以通過 Optional 的鏈式呼叫,通過 map,orElse 等操作改寫。如下,

private static String getUserAddr1(Optional<User> user){
    //先獲取address物件
    return user.map((u)->u.getAddress())
            //再獲取details值,
            .map(e -> e.getDetails())
            //若detail為null,則返回一個預設值
            .orElse("地址資訊未填寫");
}

中間所有可能出現空指標的情況,Optional都會規避。因為 value!=null這個操作已經被封裝了。而且在不同的處理階段,Optional 會自動幫我們包裝不同型別的值。

就像上邊的操作,第一個map方法包裝了User型別的user物件值,第二個map包裝了String型別的details值,orElse 返回最終需要的字串。

7. flatMap

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}

乍看這個方法和 map 沒什麼區別。其實,它們的區別就在於傳入的 mapper引數的第二個泛型。
map

flatMap

map第二個泛型為? extends U,flatMap第二個泛型為Optional<U>

所以,map方法在最後,用方法Optional.ofNullable 包裝成了 Optional 。但是,flatMap就需要我們自己去包裝 Optional 了。

下邊就看下怎麼操作 flatMap。

@Test
public void test3(){
    User user = new User();
    String name = Optional.of(user).flatMap((u) -> this.getUserName(u))
        .orElse("佚名");
    System.out.println(name);
}

//把使用者名稱包裝成Optional<String>,作為 Function 介面的返回值,以適配flatMap
private Optional<String> getUserName(User user){
    return Optional.ofNullable(user.getName());
}

8.filter

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
} 

見名知意,filter 是用來根據條件過濾的,如果符合條件,就返回當前 Optional 物件本身,否則返回一個值為 null的 Optional 物件。

如下,過濾姓名為空的 user。

User user = new User();
//由於user沒有設定 name,所以返回一個值為 null 的 optionalUser
Optional<User> optionalUser = Optional.of(user).filter((u) -> this.getUserName(u).isPresent());
//由於值為 null,所以get方法丟擲異常 NoSuchElementException
optionalUser.get();

Stream API

流 (Stream) 和 Java 中的集合類似。但是集合中儲存的資料,而流中儲存的是,對集合或者陣列中資料的操作。

之所以叫流,是因為它就像一個流水線一樣。從原料經過 n 道加工程式之後,變成可用的成品。

原料
加工廠1
加工廠2
產品

如果,你有了解過 Spark 裡邊的 Streaming,就會有一種特別熟悉的感覺。因為它們的思想和用法如此相似。

包括 lazy 思想,都是在需要計算結果的時候,才真正執行。類似 Spark Streaming 對 RDD 的操作,分為轉換(transformation)和行動(action)。轉換隻是記錄這些操作邏輯,只有行動的時候才會開始計算。

對應的,Stream API 對資料的操作,有中間操作和終止操作,只有在終止操作的時候才會執行計算。

所以,Stream 有如下特點,

  • Stream 自己不儲存資料。
  • Stream 不會改變源物件,每次中間操作後都會產生一個新的 Stream。
  • Stream 的操作是延遲的,中間操作只儲存操作,不做計算。只有終止操作時才會計算結果。
    那麼問題來了,既然 Stream 是用來運算元據的。沒有資料來源,你怎麼操作,因此還要有一個資料來源。

於是,stream運算元據的三大步驟為:資料來源,中間操作,終止操作。

資料來源

流的源可以是一個陣列,一個集合,一個生成器方法等等。

1. 使用 Collection 介面中的 default 方法。

default Stream<E> stream()  //返回一個順序流
default Stream<E> parallelStream() //返回一個並行流

此處,我們也就明白了,為什麼 JDK8 要引入預設方法了吧。

由於 Collection 集合父介面定義了這些預設方法,所以像 List,Set 這些子介面下的實現類都可以用這種方式生成一個 Stream

public class StreamTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("zhangzan");
        list.add("lisi");
        list.add("wangwu");
        //順序流
        Stream<String> stream = list.stream();
        //並行流
        Stream<String> parallelStream = list.parallelStream();
        //遍歷元素
        stream.forEach(System.out::println);
    }
}

3. Stream介面的 of() ,generate(),iterate()方法

注意,of() 方法返回的是有限流,即元素個數是有限的,就是你傳入的元素個數。

而 generate(),iterate() 這兩個方法,是無限流,即元素個數是無限個。

使用方法如下,

//of
Stream<Integer> stream2 = Stream.of(10, 20, 30, 40, 50);
stream.forEach(System.out::println);
//generate,每個元素都是0~99的隨機數
Stream<Integer> generate = Stream.generate(() -> new Random().nextInt(100));
//iterate,從0開始迭代,每個元素依次增加2
Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);

4. IntStream,LongStream,DoubleStream 的 of、range、rangeClosed 方法

它們的用法都是一樣,不過是直接包裝了一層。

實際,of()方法底層用的也是 Arrays.stream()方法。

以 IntStream 類為例,其他類似,

IntStream intStream = IntStream.of(10, 20, 30);
//從0每次遞增1,到10,包括0,但不包括10
IntStream rangeStream = IntStream.range(0, 10);
//從0每次遞增1,到10,包括0和10
IntStream rangeClosed = IntStream.rangeClosed(0, 10);

中間操作

一個流可以有零個或者多箇中間操作,每一箇中間操作都會返回一個新的流,供下一個操作使用。

1.篩選與切片

常見的包括:

  • filter
  • limit
  • skip
  • distinct
    用法如下:
@Test
public void test1(){
    ArrayList<Employee> list = new ArrayList<>();
    list.add(new Employee("張三",3000));
    list.add(new Employee("李四",5000));
    list.add(new Employee("王五",4000));
    list.add(new Employee("趙六",4500));
    list.add(new Employee("趙六",4500));

    // filter,過濾出工資大於4000的員工
    list.stream()
        .filter((e) -> e.getSalary() > 4000)
        .forEach(System.out::println);

    System.out.println("===============");
    // limit,限定指定個數的元素
    list.stream()
        .limit(3)
        .forEach(System.out::println);

    System.out.println("===============");
    // skip,和 limit 正好相反,跳過前面指定個數的元素
    list.stream()
        .skip(3)
        .forEach(System.out::println);

    System.out.println("===============");
    // distinct,去重元素。注意自定義物件需要重寫 equals 和 hashCode方法
    list.stream()
        .distinct()
        .forEach(System.out::println);
}
// 列印結果:
Employee{name='李四', salary=5000}
Employee{name='趙六', salary=4500}
Employee{name='趙六', salary=4500}
===============
Employee{name='張三', salary=3000}
Employee{name='李四', salary=5000}
Employee{name='王五', salary=4000}
===============
Employee{name='趙六', salary=4500}
Employee{name='趙六', salary=4500}
===============
Employee{name='張三', salary=3000}
Employee{name='李四', salary=5000}
Employee{name='王五', salary=4000}
Employee{name='趙六', salary=4500}

2. 對映

主要是map,包括:

  • map
  • mapToInt
  • mapToLong
  • mapToDouble
  • flatMap
    用法如下:
@Test
public void test2(){
    int[] arr = {10,20,30,40,50};
    // map,對映。每個元素都乘以2
    Arrays.stream(arr)
          .map(e -> e * 2)
          .forEach(System.out::println);

    System.out.println("===============");
    //mapToInt,mapToDouble,mapToLong 用法都一樣,不同的是返回型別分別是
    //IntStream,DoubleStream,LongStream.
    Arrays.stream(arr)
          .mapToDouble(e -> e * 2 )
          .forEach(System.out::println);

    System.out.println("===============");
    Arrays.stream(arr)
          .flatMap(e -> IntStream.of(e * 2))
          .forEach(System.out::println);
}
//列印結果:
20
40
60
80
100
===============
20.0
40.0
60.0
80.0
100.0
===============
20
40
60
80
100

這裡需要說明一下 map 和 flatMap。上邊的例子看不出來它們的區別。因為測試資料比較簡單,都是一維的。

其實,flatMap 可以把二維的集合對映成一維的。看起來,就像把二維集合壓平似的。( flat 的英文意思就是壓平)

現在給出這樣的資料,若想返回所有水果單詞的所有字母(“appleorangebanana”),應該怎麼做?

String[] fruits = {"apple","orange","banana"};

先遍歷 fruits 陣列拿到每個單詞;然後,對每個單詞切分,切分後還是一個陣列 。

注意,此時的陣列是一個二維陣列,形如 [[“a”,“p”,“p”,“l”,“e”] , [],[]]。

所以需要進一步遍歷,再遍歷(遍歷兩次),如下

String[] fruits = {"apple","orange","banana"};
Stream.of(fruits).map((s) -> Stream.of(s.split("")))
        .forEach(e -> e.forEach(System.out::print));      

雖然也實現了需求,但是整個流程太複雜了,單 forEach 遍歷就兩次。

用 flatMap 可以簡化這個過程,如下。其實,就是把中間的二維陣列直接壓平成一維的單個元素,減少遍歷次數。

Stream.of(fruits).map(s -> s.split(""))
                 .flatMap(e -> Stream.of(e))
                 .forEach(System.out::print);
                 
還有一種寫法,不用 map,直接 flatMap。
Stream.of(fruits).flatMap(s -> Stream.of(s.split("")))
         .collect(Collectors.toList())
         .forEach(System.out::print)

3、排序

  • sorted()
  • sorted(Comparator<? super T> comparator)
    排序有兩個方法,一個是無參的,預設按照自然順序。一個是帶參的,可以指定比較器。
@Test
public void test4(){
    String[] arr = {"abc","aa","ef"};
    //預設升序(字典升序)
    Stream.of(arr).sorted().forEach(System.out::println);
    System.out.println("=====");
    //自定義排序,字典降序
    Stream.of(arr).sorted((s1,s2) -> s2.compareTo(s1)).forEach(System.out::println);
} 

終止操作

一個流只會有一個終止操作。Stream只有遇到終止操作,它的源才開始執行遍歷操作。注意,在這之後,這個流就不能再使用了。

1.查詢與匹配

  • allMatch(Predicate p),傳入一個斷言型函式,檢查是否匹配所有元素
  • anyMatch( (Predicate p) ),檢查是否匹配任意一個元素
  • noneMatch(Predicate p),檢查是否沒有匹配的元素,如果都不匹配,則返回 true
  • findFirst(),返回第一個元素
  • findAny(),返回任意一個元素
  • count(),返回流中的元素總個數
  • max(Comparator c),按給定的規則排序後,返回最大的元素
  • min(Comparator c),按給定的規則排序後,返回最小的元素
  • forEach(Consumer c),迭代遍歷元素(內部迭代)
    由於上邊 API 過於簡單,不再做例子。

2.規約

規約就是 reduce ,把資料集合到一起。相信你肯定聽說過 hadoop 的 map-reduce ,思想是一樣的。

這個方法著重說一下,比較常用,有三個過載方法。

2.1 一個引數

Optional<T> reduce(BinaryOperator<T> accumulator);

傳入的是一個二元運算子,返回一個 Optional 物件。

我們需要看下 BinaryOperator 這個函式式介面的結構,不然後邊就不懂了,也不知道怎麼用。

//BinaryOperator繼承自 BiFunction<T,T,T>,我們發現它們的泛型型別都是T,完全相同
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}

public interface BiFunction<T, U, R> {
    //傳入 T 和 U,返回型別 R ,這就說明它們的引數型別可以完全不相同,當然也可以完全相同
    //對應的它的子類 BinaryOperator 就是完全相同的
    R apply(T t, U u);
}

使用方式如下,

Integer[] arr = {1,2,3,4,5,6};
Integer res1 = Stream.of(arr).reduce((x, y) -> x + y).get();
System.out.println(res1);
// 結果:21

它表達的意思是,反覆合併計算。如上,就是先計算1和2的和,然後計算結果3再和下一個元素3求和,依次反覆計算,直到最後一個元素。

2.2兩個引數

T reduce(T identity, BinaryOperator<T> accumulator);

傳入兩個引數,第一個引數代表初始值,第二個引數是二元運算子。返回的型別是 T ,而不是 Optional。

如下,給一個 10 的初始值,依次累加,

Integer res2 = Stream.of(arr).reduce(10, (x, y) -> x + y);
System.out.println(res2);
// 結果:31

注意:accumulator 累加器函式需要滿足結合律。如上,加法就滿足結合律。

在這裡插入圖片描述
identity 先和 T1 做計算,返回值作為中間結果,參與下一次和 T2 計算,如此反覆。

另外需要注意的時,原始碼中說明了一句,並不強制要求一定按順序計算。

but is not constrained to execute sequentially.

也就是說,實際計算時有可能會和圖中表示的計算順序不太一樣。比如 T1 先和 T3 運算,然後結果再和 T2 運算。

這也是為什麼它要求函式符合結合律,因為交換元素順序不能影響到最終的計算結果。

2.3、三個引數

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

這個引數有三個,比較複雜。我們分析一下。

  • U identity,這個是初始值。(但是,在平行計算中,和兩個引數的 reduce 初始值含義不一樣,一會兒說)x需要注意,初始值和規約函式的返回值型別一致都是 U。而 Stream 流中的元素型別是 T ,所以可以和 U 相同,也可以不相同。

  • BiFunction<U, ? super T, U> accumulator,這是一個累加器。其型別是BiFunction,需要注意這個輸入 U 於 T 型別的兩個引數,返回型別是 U 。也就是說,輸入的第一個引數和返回值型別一樣,輸入的第二個引數和 Stream 流中的元素型別一樣。

  • BinaryOperator combiner,這是一個組合器。其型別是 BinaryOperator ,前面說過這個函式式介面,它是傳入兩個相同型別的引數,返回值型別也相同,都是 U 。需要注意的是,這個引數只有在 reduce 平行計算中才會生效。

因此,我們可以把 reduce 分為非並行和並行兩種情況。

2.3.1非並行規約

非並行情況下,第三個引數不起作用,identity 代表的是初始值。

以下的計算,是初始化一個 list,並向其中新增流中的元素。

Integer[] arr = {1,2,3,4,5,6};
ArrayList<Integer> res = Stream.of(arr).reduce(Lists.newArrayList(0),
                                               (l, e) -> {
                                                   l.add(e);
                                                   return l;
                                               },
                                               (l, c) -> {
                                                   //結果不會列印這句話,說明第三個引數沒有起作用
                                                   System.out.println("combiner");
                                                   l.addAll(c);
                                                   return l;
                                               });
System.out.println(res);
// [0, 1, 2, 3, 4, 5, 6]

2.3.2並行規約

並行規約,用的是 fork-join 框架思想,分而治之。把一個大任務分成若干個子任務,然後再合併。

不瞭解 fork-join 的,可以看這篇文章介紹:fork-join框架分析

所以,這裡的累加器 accumulator 是用來計算每個子任務的。組合器 combiner 是用來把若干個子任務合併計算的。

下邊用例子說明:

Integer res4 = Stream.of(1,2,3,4).parallel().reduce(1,
                (s,e) -> s + e,
                (sum, s) -> sum + s);
System.out.println(res4); // 結果:14

奇了怪了,計算結果應該是 10 的,為什麼是 14 呢。

這裡就要說明,這個 identity 初始值了。它是在每次執行 combiner 的時候,都會把 identity 累加上。

具體執行幾次 combiner ,可以通過以下方式計算出來 。( c 並不能代表有幾個執行子任務)

AtomicInteger c = new AtomicInteger(0);
Integer res4 = Stream.of(1,2,3,4).parallel().reduce(1,
        (s,e) -> s + e,
        (sum, s) -> {c.getAndIncrement(); return sum + s;});
System.out.println(c); //3
System.out.println(res4); //14

c 為 3 代表執行了 3 次 combiner ,最後計算總結果時,還會再加一次初始值,所以結果為:

(1+2+3+4) + (3+1) * 1 = 14
// 1+2+3+4 為正常非並行結算的和,3+1 為總共計算了幾次初始值。

我們可以通過加大stream的資料量來驗證猜想。從1 加到 100 。初始值為 2 。

AtomicInteger count = new AtomicInteger(0);
int length = 100;
Integer[] arr1 = new Integer[length];
for (int i = 0; i < length; i++) {
    arr1[i] = i + 1;
}
Integer res5 = Stream.of(arr1).parallel().reduce(2,
                         (s,e) -> s + e,
                         (sum, s) -> {count.getAndIncrement(); return sum + s;});
System.out.println(count.get()); //15
System.out.println(res5); //5082 

即: (1+...+100) + (15+1) * 2 = 5082
怎麼正常使用?

那麼,問題就來了。這個平行計算不靠譜啊,都把計算結果計算錯了。

這是為什麼呢,是它的演算法有問題麼?

非也,其實是我們的用法姿勢錯了。可以看下原始碼中對 identity 的說明。

This means that for all u, combiner(identity, u) is equal to u.

在這裡插入圖片描述

意思是,需要每次 combiner 運算時,identity 的值保證 u == combiner(identity,u) 是一個恆等式。

那麼,為了滿足這個要求,此種情況只能讓 identity = 0 。

故,改寫程式如下,

//其他都不變,只有 identity 由 2 改為 0
AtomicInteger count = new AtomicInteger(0);
int length = 100;
Integer[] arr1 = new Integer[length];
for (int i = 0; i < length; i++) {
    arr1[i] = i + 1;
}
Integer res5 = Stream.of(arr1).parallel().reduce(0,
                         (s,e) -> s + e,
                         (sum, s) -> {count.getAndIncrement(); return sum + s;});
System.out.println(count.get()); //15
System.out.println(res5); //5050 

當然,只要保證 identity 不影響這個恆等式就行。

比如,對於 set 集合會自動去重,這種情況下,也可以使用平行計算,

//初始化一個set,然後把stream流的元素新增到set中,
//需要注意:用並行的方式,這個set集合必須是執行緒安全的。否則會報錯ConcurrentModificationException
Set<Integer> res3 = Stream.of(1, 2, 3, 4).parallel().reduce(Collections.synchronizedSet(Sets.newHashSet(10),
                (l, e) -> {
                    l.add(e);
                    return l;
                },
                (l, c) -> {
                    l.addAll(c);
                    return l;
                });
System.out.println(res3);

3. 收集

收集操作,可以把流收集到 List,Set,Map等中。而且,Collectors 類中提供了很多靜態方法,方便的建立收集器供我們使用。

這裡舉幾個常用的即可。具體的 API 可以去看 Collectors 原始碼(基本涵蓋了各種,最大值,最小值,計數,分組等功能。)。

 @Test
public void test6() {
    ArrayList<Employee> list = new ArrayList<>();
    list.add(new Employee("張三", 3000));
    list.add(new Employee("李四", 5000));
    list.add(new Employee("王五", 4000));
    list.add(new Employee("趙六", 4500));

    //把所有員工的姓名收集到list中
    list.stream()
        .map(Employee::getName)
        .collect(Collectors.toList())
        .forEach(System.out::println);

    //求出所有員工的薪資平均值
    Double average = list.stream()
        .collect(Collectors.averagingDouble(Employee::getSalary));
    System.out.println(average);

}

日期時間新 API

JDK8 之前的時間 API 存線上程安全問題,並且設計混亂。因此,在 JDK8 就重新設計了一套 API。

如下,執行緒不安全的例子。

@Test
public void test1() throws Exception{
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    List<Future<Date>> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        Future<Date> future = executorService.submit(() -> sdf.parse("20200905"));
        list.add(future);
    }
    for (Future<Date> future : list) {
        System.out.println(future.get());
    }

}

多次執行,就會報錯 java.lang.NumberFormatException 。
接下來,我們就看新的時間 API ,然後改寫上邊的程式。

LocalDate,LocalTime,LocalDateTime

這三個都是不可變類,用法差不多。以 LocalDate 為例。

1. 建立時間物件

  • now ,靜態方法,根據當前時間建立物件
  • of,靜態方法,根據指定日期、時間建立物件
  • parse,靜態方法,通過字串指定日期
LocalDate localDate1 = LocalDate.now();
System.out.println(localDate1);  //2020-09-05
LocalDate localDate2 = LocalDate.of(2020, 9, 5);
System.out.println(localDate2); //2020-09-05
LocalDate localDate3 = LocalDate.parse("2020-09-05");
System.out.println(localDate3); //2020-09-05

2. 獲取年月日周

  • getYear,獲取年
  • getMonth ,獲取月份,返回的是月份的列舉值
  • getMonthValue,獲取月份的數字(1-12)
  • getDayOfYear,獲取一年中的第幾天(1-366)
  • getDayOfMonth,獲取一個月中的第幾天(1-31)
  • getDayOfWeek,獲取一週的第幾天,返回的是列舉值
localDate currentDate = LocalDate.now();
System.out.println(currentDate.getYear()); //2020
System.out.println(currentDate.getMonth()); // SEPTEMBER
System.out.println(currentDate.getMonthValue()); //9
System.out.println(currentDate.getDayOfYear()); //249
System.out.println(currentDate.getDayOfMonth()); //5
System.out.println(currentDate.getDayOfWeek()); // SATURDAY
  1. 日期比較,前後或者相等
  • isBefore ,第一個日期是否在第二個日期之前
  • isAfter,是否在之後
  • equals,日期是否相同
  • isLeapYear,是否是閏年
    它們都返回的是布林值。
LocalDate date1 = LocalDate.of(2020, 9, 5);
LocalDate date2 = LocalDate.of(2020, 9, 6);
System.out.println(date1.isBefore(date2)); //true
System.out.println(date1.isAfter(date2)); //false
System.out.println(date1.equals(date2)); //false
System.out.println(date1.isLeapYear()); //true
  1. 日期加減
  • plusDays, 加幾天
  • plusWeeks, 加幾周
  • plusMonths, 加幾個月
  • plusYears,加幾年
    減法同理,
LocalDate nowDate = LocalDate.now();
System.out.println(nowDate);  //2020-09-05
System.out.println(nowDate.plusDays(1)); //2020-09-06
System.out.println(nowDate.plusWeeks(1)); //2020-09-12
System.out.println(nowDate.plusMonths(1)); //2020-10-05
System.out.println(nowDate.plusYears(1)); //2021-09-05
  1. 時間戳 Instant
    Instant 代表的是到從 UTC 時區 1970年1月1日0時0分0秒開始計算的時間戳。
Instant now = Instant.now();
System.out.println(now.toString()); // 2020-09-05T14:11:07.074Z
System.out.println(now.toEpochMilli()); // 毫秒數, 1599315067074 
  1. 時間段 Duration
    用於表示時間段 ,可以表示 LocalDateTime 和 Instant 之間的時間段,用 between 建立。
LocalDateTime today = LocalDateTime.now(); //今天的日期時間
LocalDateTime tomorrow = today.plusDays(1); //明天
Duration duration = Duration.between(today, tomorrow); //第二個引數減去第一個引數的時間差
System.out.println(duration.toDays()); //總天數,1
System.out.println(duration.toHours()); //小時,24
System.out.println(duration.toMinutes()); //分鐘,1440
System.out.println(duration.getSeconds()); //秒,86400
System.out.println(duration.toMillis()); //毫秒,86400000
System.out.println(duration.toNanos()); // 納秒,86400000000000

7. 日期段 Period
和時間段 Duration,但是 Period 只能精確到年月日。

有兩種方式建立 Duration 。

LocalDate today = LocalDate.now(); //今天
LocalDate date = LocalDate.of(2020,10,1); //國慶節
//1. 用 between 建立 Period 物件
Period period = Period.between(today, date);
System.out.println(period); // P26D
//2. 用 of 建立 Period 物件
Period of = Period.of(2020, 9, 6);
System.out.println(of); // P2020Y9M6D
// 距離國慶節還有 0 年 0 月 26 天 
System.out.printf("距離國慶節還有 %d 年 %d 月 %d 天" , period.getYears(),period.getMonths(),period.getDays());

8. 時區 ZoneId

ZoneId 表示不同的時區。

  • getAvailableZoneIds() ,獲取所有時區資訊,大概40多個時區
  • of(id),根據時區id獲得對應的 ZoneId 物件
  • systemDefault,獲取當前時區
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(System.out::println); //列印所有時區
ZoneId of = ZoneId.of("Asia/Shanghai");   //獲取亞洲上海的時區物件
System.out.println(of);  
System.out.println(ZoneId.systemDefault()); //當前時區為:Asia/Shanghai

9. 日期時間格式化
JDK1.8 提供了執行緒安全的日期格式化類 DateTimeFormatter。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 1. 日期時間轉化為字串。有兩種方式
String format = dtf.format(LocalDateTime.now());
System.out.println(format); // 2020-09-05 23:02:02
String format1 = LocalDateTime.now().format(dtf); //實際上呼叫的也是 DateTimeFormatter 類的format方法
System.out.println(format1); // 2020-09-05 23:02:02

// 2. 字串轉化為日期。有兩種方式,需要注意,月和日位數要補全兩位
//第一種方式用的是,DateTimeFormatter.ISO_LOCAL_DATE_TIME ,格式如下
LocalDateTime parse = LocalDateTime.parse("2020-09-05T00:00:00");
System.out.println(parse); // 2020-09-05T00:00
//第二種方式可以自定義格式
LocalDateTime parse1 = LocalDateTime.parse("2020-09-05 00:00:00", dtf);
System.out.println(parse1); // 2020-09-05T00:00

改為執行緒安全類
接下來,就可以把上邊執行緒不安全的類改寫為新的時間 API 。

@Test
public void test8() throws Exception{
    // SimpleDateFormat 改為 DateTimeFormatter
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    // Date 改為 LocalDate
    List<Future<LocalDate>> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        //日期解析改為 LocalDate.parse("20200905",dtf)
        Future<LocalDate> future = executorService.submit(() -> LocalDate.parse("20200905",dtf));
        list.add(future);
    }
    for (Future<LocalDate> future : list) {
        System.out.println(future.get());
    }

}

相關文章