JDK15就要來了,你卻還不知道JDK8的新特性!

微信搜「煙雨星空」,白嫖更多好文。

如今 Oracle 官方每隔半年就會出一個 JDK 新版本。按時間來算的話,這個月就要出 JDK15 了。然而,大部分公司仍是在使用 JDK7 和 8 。javascript

以前去我朋友家,居然被嘲笑不會用 JDK8 。 不服氣的我,回來以後,固然是重點學習之啊。html

話很少說,本文目錄以下:java

目錄:程序員

  • lambda 表達式
  • 接口默認方法和靜態方法
  • 函數式接口
  • 方法引用
  • Optional
  • Stream API
  • 日期時間新 API

1、lambda表達式

先看下 lambda 表達式是怎麼定義的:算法

lambda 表達式是一個匿名函數。 lambda 表達式容許把一個函數做爲參數進行傳遞。apache

可能剛看到這兩句話時,不知道是什麼意思。那麼,對比一下 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 表達式就是函數式編程的體現。(什麼,你還不知道什麼是函數式編程? 那還不趕快百度去。)

注意事項:

  • 參數列表的數據類型會自動推斷。也就是說,若是匿名函數有參數列表的話,只須要寫參數名便可,不須要寫參數的類型。
  • 若是參數列表爲空,則左邊只須要寫小括號便可。
  • 若是參數只有一個,則能夠省略小括號,只寫參數的名稱便可。
  • 若是方法體中只有一條執行語句,則能夠省略右邊的大括號。如有返回值,則能夠把 return 和大括號同時省略。

2、接口默認方法和靜態方法

接口默認方法

咱們知道,在 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();
    }
}

3、函數式接口

若是一個接口中只有一個抽象方法,則稱其爲函數式接口。可使用 @FunctionalInterface 註解來檢測一個接口是否爲函數式接口。

JDK提供了常見的最簡單的四種函數式接口:(必須掌握哦)

  • Consumer<T>,消費型接口。接收一個參數,沒有返回值。其方法有:void accept(T t);
  • Supplier<T>,供給型接口。沒有參數,帶返回值。 其方法:T get();
  • Function<T, R>,函數型接口。接收一個參數,返回一個結果。其方法:R apply(T t);
  • Predicate<T>,斷言型接口。接收一個參數,返回boolean值。其方法:boolean test(T t);

我這裏舉例了它們的使用方法,

public class LambdaTest {
    @Test
    public void test2(){
        //打印傳入的 msg
        printMsg((s)-> System.out.println(s),"聽朋友說「煙雨星空」公衆號不只文章好看,還免費送程序員福利,我心動了");
    }

    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 表達式來簡化寫法。

4、方法引用

概念:方法引用是用來直接訪問類或者實例的已經存在的方法或者構造方法。

這裏強調一下已經存在的含義。由於,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 表達式徹底沒有問題。就是習慣的問題,多寫就有感受了。

5、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 "地址信息未填寫";
    }
}

能夠發現,代碼冗長,還不利於維護,隨着層級關係更深,將會變成災難(是否依稀記得js的回調地獄)。

那麼,有了 Optional 類,咱們就能夠寫出更優雅的代碼,而且防止空指針異常。(後邊就填坑)

下面,就一塊兒領略一下 Optional 的魅力吧!

建立 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說明)。

經常使用方法

一、get方法

public T get() {
    //若是值爲null,則拋出異常,不然返回非空值value
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

二、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 的身影。

三、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());
}

四、orElse 和 orElseGet

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()

五、orElseThrow

當值爲null,能夠返回自定義異常。

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

若user對象爲null,則拋出非法訪問。

這樣,能夠有針對的對特定異常作一些其餘處理。由於,會拋出哪些異常的狀況,是咱們可控的。

六、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 返回最終須要的字符串。

七、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());
}

八、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();

6、Stream API

首先,什麼是 Stream 流?

流 (Stream) 和 Java 中的集合相似。可是集合中保存的數據,而流中保存的是,對集合或者數組中數據的操做。

之因此叫流,是由於它就像一個流水線同樣。從原料通過 n 道加工程序以後,變成可用的成品。

若是,你有了解過 Spark 裏邊的 Streaming,就會有一種特別熟悉的感受。由於它們的思想和用法如此類似。

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

轉換介紹:http://spark.apache.org/docs/...

對應的,Stream API 對數據的操做,有中間操做和終止操做,只有在終止操做的時候纔會執行計算。

因此,Stream 有以下特色,

  • Stream 本身不保存數據。
  • Stream 不會改變源對象,每次中間操做後都會產生一個新的 Stream。
  • Stream 的操做是延遲的,中間操做只保存操做,不作計算。只有終止操做時纔會計算結果。

那麼問題來了,既然 Stream 是用來操做數據的。沒有數據源,你怎麼操做,所以還要有一個數據源。

因而,stream操做數據的三大步驟爲:數據源,中間操做,終止操做。

數據源

流的源能夠是一個數組,一個集合,一個生成器方法等等。

一、使用 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);
    }
}

二、 Arrays 的靜態方法 stream()

static <T> Stream<T> stream(T[] array)

能夠傳入各類類型的數組,把它轉化爲流。以下,傳入一個字符串數組。

String[] arr = {"abc","aa","ef"};
Stream<String> stream1 = Arrays.stream(arr);

三、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);

四、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);

中間操做

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

一、篩選與切片

常見的包括:

  • 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}

二、映射

主要是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);

三、排序

  • 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只有遇到終止操做,它的源纔開始執行遍歷操做。注意,在這以後,這個流就不能再使用了。

一、查找與匹配

  • allMatch(Predicate p),傳入一個斷言型函數,檢查是否匹配全部元素
  • anyMatch( (Predicate p) ),檢查是否匹配任意一個元素
  • noneMatch(Predicate p),檢查是否沒有匹配的元素,若是都不匹配,則返回 true
  • findFirst(),返回第一個元素
  • findAny(),返回任意一個元素
  • count(),返回流中的元素總個數
  • max(Comparator c),按給定的規則排序後,返回最大的元素
  • min(Comparator c),按給定的規則排序後,返回最小的元素
  • forEach(Consumer c),迭代遍歷元素(內部迭代)

因爲上邊 API 過於簡單,再也不作例子。

二、規約

規約就是 reduce ,把數據集合到一塊兒。相信你確定據說過 hadoop 的 map-reduce ,思想是同樣的。

這個方法着重說一下,比較經常使用,有三個重載方法。

2.一、一個參數

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.二、兩個參數

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.三、三個參數

<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<U> combiner,這是一個組合器。其類型是 BinaryOperator ,前面說過這個函數式接口,它是傳入兩個相同類型的參數,返回值類型也相同,都是 U 。須要注意的是,這個參數只有在 reduce 並行計算中才會生效。

所以,咱們能夠把 reduce 分爲非並行和並行兩種狀況。

2.3.一、 非並行規約

非並行狀況下,第三個參數不起做用,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.二、並行規約

並行規約,用的是 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);

三、收集

收集操做,能夠把流收集到 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);

}

7、日期時間新 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 爲例。

一、建立時間對象

  • 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

二、獲取年月日周

  • 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

三、日期比較,先後或者相等

  • 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

四、日期加減

  • 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

時間戳 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

時間段 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

日期段 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());

時區 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

日期時間格式化

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());
    }

}

PS:若是本文對你有用,就請關注我,給我點贊吧。你的支持是我寫做最大的動力 ~

首發於:JDK8新特性最全總結

相關文章
相關標籤/搜索