JDK8新特性

函數(shù)式接口

解釋

有且只有一個(gè)抽象方法的接口

函數(shù)式編程的體現(xiàn)就是Lambda,所以函數(shù)式接口就是可以適用于Lambda使用的接口.只有確保接口中有且只有一個(gè)抽象方法,java中的Lambda才能順利的進(jìn)行推導(dǎo)

備注:“語法糖”是指使用更加方便统求,但是原理不變的代碼語法坡倔。例如在遍歷集合時(shí)使用的for-each語法,其實(shí) 底層的實(shí)現(xiàn)原理仍然是迭代器,這便是“語法糖”奏甫。從應(yīng)用層面來講,Java中的Lambda可以被當(dāng)做是匿名內(nèi)部 類的“語法糖”炎疆,但是二者在原理上是不同的考阱。

格式

修飾符 interface 接口名稱{
    public abstract 返回值類型 方法名稱(可選參數(shù)信息);
    //其他非抽象內(nèi)容
}

由于接口當(dāng)中抽象方法的 public abstract 是可以省略的,所以定義一個(gè)函數(shù)式接口很簡單:

//可以檢測接口是否是一個(gè)函數(shù)式接口
@FunctionalInterface
public interface MyFunctionInterface {
    //定義一個(gè)抽象方法,public abstract 可以省略
    public abstract void method();


}

與 @Override 注解的作用類似粘姜,Java 8中專門為函數(shù)式接口引入了一個(gè)新的注解: @FunctionalInterface 鬓照。該注 解可用于一個(gè)接口的定義上.
一旦使用該注解來定義接口,編譯器將會(huì)強(qiáng)制檢查該接口是否確實(shí)有且僅有一個(gè)抽象方法孤紧,否則將會(huì)報(bào)錯(cuò)豺裆。需要注意的是,即使不使用該注解坛芽,只要滿足函數(shù)式接口的定義留储,這仍然是一個(gè)函數(shù)式接口,使用起來都一樣咙轩。

函數(shù)式接口的使用:一般可以作為方法的參數(shù)和返回值類型

public class demo01 {
    //定義一個(gè)方法,參數(shù)使用函數(shù)式接口MyFunctionInterface
    public static void show(MyFunctionInterface myInterface){
        myInterface.method();

    }
    public static void main(String[] args) {
        //調(diào)用show方法,方法的參數(shù)是一個(gè)接口,所以可以傳遞接口的實(shí)現(xiàn)類對象
        show(new MyFunctionalInterfaceImpl());

        //調(diào)用show方法,方法的參數(shù)是一個(gè)借口,所以我們可以傳遞接口的匿名內(nèi)部類
        show(new MyFunctionInterface() {
            @Override
            public void method() {
                System.out.println("使用匿名內(nèi)部類重寫接口中的抽象方法");
            }
        });
        //調(diào)用show方法,方法的參數(shù)是一個(gè)函數(shù)式接口,所以我們可以Lambda表達(dá)式
        show(()->{
            System.out.println("使用Lambda表達(dá)式重寫接口中的方法");
        });
        //簡化Lambda表達(dá)式
        show(()-> System.out.println("使用簡化的Lambda表達(dá)式重寫接口中的方法"));
    }
}

函數(shù)式編程

Lambda的延遲執(zhí)行

有些場景的代碼執(zhí)行后获讳,結(jié)果不一定會(huì)被使用,從而造成性能浪費(fèi)活喊。而Lambda表達(dá)式是延遲執(zhí)行的丐膝,這正好可以 作為解決方案,提升性能钾菊。

性能浪費(fèi)的日志案例

注:日志可以幫助我們快速的定位問題帅矗,記錄程序運(yùn)行過程中的情況,以便項(xiàng)目的監(jiān)控和優(yōu)化煞烫。 一種典型的場景就是對參數(shù)進(jìn)行有條件使用浑此,例如對日志消息進(jìn)行拼接后,在滿足條件的情況下進(jìn)行打印輸出:

public class Demo01Loger {
    //定義一個(gè)根據(jù)日志的級別,顯示日志的信息的方法
    public static void showLog(int level, String message){
        if(level==1){
            System.out.println(message);
        }
    }
    public static void main(String[] args) {
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "Java";
        showLog(1,msg1+msg2+msg3);
    }
}

這段代碼存在問題:無論級別是否滿足要求滞详,作為 log 方法的第二個(gè)參數(shù)凛俱,三個(gè)字符串一定會(huì)首先被拼接并傳入方法內(nèi)紊馏,然后才會(huì)進(jìn)行級別判斷。如果級別不符合要求蒲犬,那么字符串的拼接操作就白做了朱监,存在性能浪費(fèi)。

備注:SLF4J是應(yīng)用非常廣泛的日志框架原叮,它在記錄日志時(shí)為了解決這種性能浪費(fèi)的問題赫编,并不推薦首先進(jìn)行 字符串的拼接,而是將字符串的若干部分作為可變參數(shù)傳入方法中奋隶,僅在日志級別滿足要求的情況下才會(huì)進(jìn) 行字符串拼接擂送。例如: LOGGER.debug("變量{}的取值為{}。", "os", "macOS") 达布,其中的大括號(hào) {} 為占位 符团甲。如果滿足日志級別要求,則會(huì)將“os”和“macOS”兩個(gè)字符串依次拼接到大括號(hào)的位置黍聂;否則不會(huì)進(jìn)行字 符串拼接躺苦。這也是一種可行解決方案,但Lambda可以做到更好产还。

Lambda更優(yōu)寫法

public class Demo02LogerLambda {
    public static void showLog(int level,MessageBuilder mb){
        if(level==1){
            System.out.println(mb.buildMessage());
        }
    }
    public static void main(String[] args) {
        String msg1 = "Hello";
        String msg2 = "World";
        String msg3 = "Java";
//        showLog(1,()->{
//            //返回一個(gè)拼接好的字符串
//            return msg1+msg2+msg3;
//        });
        showLog(1,()->msg1+msg2+msg3);
        /*
            使用Lambda表達(dá)式作為參數(shù)傳遞,僅僅是把參數(shù)傳遞到showLog方法中,只有滿足條件
                才會(huì)調(diào)用接口MessageBuilder中的方法builderMessage
                才會(huì)進(jìn)行字符串的拼接
            如果條件不滿足,MessageBuilder接口中的方法builderMessage不會(huì)執(zhí)行
         */
    }
}

使用Lambda作為參數(shù)和返回值

如果拋開實(shí)現(xiàn)原理不說匹厘,Java中的Lambda表達(dá)式可以被當(dāng)作是匿名內(nèi)部類的替代品。如果方法的參數(shù)是一個(gè)函數(shù)式接口類型脐区,那么就可以使用Lambda表達(dá)式進(jìn)行替代愈诚。使用Lambda表達(dá)式作為方法參數(shù),其實(shí)就是使用函數(shù)式接口作為方法參數(shù)牛隅。

例如 java.lang.Runnable 接口就是一個(gè)函數(shù)式接口炕柔,假設(shè)有一個(gè) startThread 方法使用該接口作為參數(shù),那么就可以使用Lambda進(jìn)行傳參媒佣。這種情況其實(shí)和 Thread 類的構(gòu)造方法參數(shù)為 Runnable 沒有本質(zhì)區(qū)別匕累。

public class Demo04Runnable {
    private static void startThread(Runnable task) {
        new Thread(task).start();
    }

    public static void main(String[] args) {
        startThread(()-> System.out.println("線程任務(wù)執(zhí)行!"));
    }
}

類似地默伍,如果一個(gè)方法的返回值類型是一個(gè)函數(shù)式接口欢嘿,那么就可以直接返回一個(gè)Lambda表達(dá)式。當(dāng)需要通過一 個(gè)方法來獲取一個(gè) java.util.Comparator 接口類型的對象作為排序器時(shí),就可以調(diào)該方法獲取也糊。

public class Demo05Comparator {
    public static Comparator<String> getComparator() {
        //方法的返回值類型是一個(gè)接口,那么我們可以返回這個(gè)接口的匿名內(nèi)部類
        /*
        return new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.length()-o1.length();
            }
        };
        */

        //方法的返回值類型是一個(gè)函數(shù)式接口,所以我們可以返回一個(gè)Lambda表達(dá)式
        return (String o1, String o2) -> o2.length() - o1.length();
    }

    public static void main(String[] args) {
        String[] array = {"abc", "ab", "abcd"};
        System.out.println(Arrays.toString(array));
        Arrays.sort(array, getComparator());
        System.out.println(Arrays.toString(array));
    }
}

常用函數(shù)式接口

JDK提供了大量常用的函數(shù)式接口以豐富Lambda的典型使用場景炼蹦,它們主要在 java.util.function 包中被提供。 下面是最簡單的幾個(gè)接口及使用示例狸剃。

java.util.function.Supplier<T> 接口僅包含一個(gè)無參的方法:T get()掐隐。用來獲取一個(gè)泛型參數(shù)指定類型的對 象數(shù)據(jù)。由于這是一個(gè)函數(shù)式接口钞馁,這也就意味著對應(yīng)的Lambda表達(dá)式需要“對外提供”一個(gè)符合泛型類型的對象數(shù)據(jù)瑟枫。

public class Demo01Supplier {
    //定義一個(gè)方法,方法的參數(shù)傳遞Supplier<T>接口,泛型執(zhí)行String,get方法就會(huì)返回一個(gè)String
    public static String getString(Supplier<String>sup){
        return sup.get();
    }

    public static void main(String[] args) {
        String s = getString(()-> "胡歌");
        System.out.println(s);
    }
}

Consumer接口

java.util.function.Consumer<T> 接口則正好與Supplier接口相反斗搞,它不是生產(chǎn)一個(gè)數(shù)據(jù),而是消費(fèi)一個(gè)數(shù)據(jù)慷妙, 其數(shù)據(jù)類型由泛型決定。

抽象方法:accept

Consumer 接口中包含抽象方法 void accept(T t) 允悦,意為消費(fèi)一個(gè)指定泛型的數(shù)據(jù)膝擂。基本使用如:

public class Demo {
    public static void method(String name,Consumer<String> con){
        con.accept(name);
    }

    public static void main(String[] args) {
        method("C羅",(String name)-> System.out.println(new StringBuilder(name).reverse().toString()));
    }
}
默認(rèn)方法:andThen

如果一個(gè)方法的參數(shù)和返回值全都是 Consumer 類型隙弛,那么就可以實(shí)現(xiàn)效果:消費(fèi)數(shù)據(jù)的時(shí)候架馋,首先做一個(gè)操作, 然后再做一個(gè)操作全闷,實(shí)現(xiàn)組合叉寂。而這個(gè)方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代碼:

default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

備注: java.util.Objects 的 requireNonNull 靜態(tài)方法將會(huì)在參數(shù)為null時(shí)主動(dòng)拋出 NullPointerException 異常总珠。這省去了重復(fù)編寫if語句和拋出空指針異常的麻煩屏鳍。

要想實(shí)現(xiàn)組合,需要兩個(gè)或多個(gè)Lambda表達(dá)式即可局服,而 andThen 的語義正是“一步接一步”操作钓瞭。例如兩個(gè)步驟組 合的情況

/**
 * Consumer<String> con1;
 * Consumer<String> con2;
 * String s = "Hello";
 * con1.accept(s);
 * con2.accept(s);
 * 連接兩個(gè)Consumer接口 再進(jìn)行消費(fèi)
 * con1.andThen(con2).accept(s) 誰寫前邊誰先消費(fèi)
 */
public class DemoConsumerAndThen {
    public static void method(String s, Consumer<String> con1, Consumer<String> con2) {
//        con1.accept(s);
//        con2.accept(s);
        con1.andThen(con2).accept(s);
    }

    public static void main(String[] args) {
        method("Hello",
                (t) -> {
                    //消費(fèi)方式:把字符串轉(zhuǎn)換為大寫輸出
                    System.out.println(t.toUpperCase());
                },
                (t) -> {
                    //消費(fèi)方式:把字符串轉(zhuǎn)換為小寫輸出
                    System.out.println(t.toLowerCase());
                }
        );
    }
}

Predicate接口

有時(shí)候我們需要對某種類型的數(shù)據(jù)進(jìn)行判斷,從而得到一個(gè)boolean值結(jié)果淫奔。這時(shí)可以使用 java.util.function.Predicate<T> 接口山涡。

抽象方法:test
public class DemoTest {
    public static boolean checkString(String s, Predicate<String> pre) {
        return pre.test(s);
    }

    public static void main(String[] args) {
        String s = "abdksabdk";
        //對參數(shù)傳遞的字符串進(jìn)行判斷.判斷字符串長度是否大于5,并把結(jié)果返回
        checkString(s, (String str) -> str.length() > 5
        );
    }
}

Predicate 接口中包含一個(gè)抽象方法: boolean test(T t) 。用于條件判斷的場景:

默認(rèn)方法:and

既然是條件判斷唆迁,就會(huì)存在與鸭丛、或、非三種常見的邏輯關(guān)系唐责。其中將兩個(gè) Predicate 條件使用“與”邏輯連接起來實(shí)現(xiàn)“并且”的效果時(shí)鳞溉,可以使用default方法 and。其JDK源碼為:

default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
import java.util.function.Predicate;

/**
 * 判斷一個(gè)字符串:
 * 1.長度是否大于5
 * 2.是否包含a
 */
public class DemoAnd {
    public static boolean method(String s, Predicate<String> one, Predicate<String> two) {
        return one.and(two).test(s);
    }

    public static void main(String[] args) {
        String s = "dadasbfuabfias";
        boolean b = method(s,
                (String str) -> {
            return str.length()>5;
                },
                (String str) -> {
            return str.contains("a");
                });
        System.out.println(b);
    }
}

默認(rèn)方法:or

與 and 的“與”類似妒蔚,默認(rèn)方法 or 實(shí)現(xiàn)邏輯關(guān)系中的“或”穿挨。JDK源碼為:

default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
/**
 * 判斷一個(gè)字符串:
 * 1.長度是否大于5
 * 2.是否包含a
 * 滿足一個(gè)條件即可
 */
public class DemoOr {
    public static boolean method(String s, Predicate<String> one, Predicate<String> two) {
        return one.or(two).test(s);
    }

    public static void main(String[] args) {
        String s = "dadasbfuabfias";
        boolean b = method(s,
                (String str) -> {
                    return str.length()>5;
                },
                (String str) -> {
                    return str.contains("a");
                });
        System.out.println(b);
    }
}
默認(rèn)方法:negate

“與”、“或”已經(jīng)了解了肴盏,剩下的“非”(取反)也會(huì)簡單科盛。默認(rèn)方法 negate 的JDK源代碼為:

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

從實(shí)現(xiàn)中很容易看出,它是執(zhí)行了test方法之后菜皂,對結(jié)果boolean值進(jìn)行“!”取反而已贞绵。一定要在 test 方法調(diào)用之前 調(diào)用 negate 方法,正如 and 和 or 方法一樣:

/**
 * 判斷一個(gè)字符串:
 * 1.長度是否大于5
 * 如果大于5返回false
 * 如果小于5,返回true
 */
public class DemoNegate {
    public static boolean method(String s, Predicate<String> one) {
        return one.negate().test(s);
    }

    public static void main(String[] args) {
        String s = "dadasbfuabfias";
        boolean b = method(s,
                (String str) -> {
                    return str.length()>5;
                }
                );
        System.out.println(b);
    }
}

Function接口

java.util.function.Function<T,R> 接口用來根據(jù)一個(gè)類型的數(shù)據(jù)得到另一個(gè)類型的數(shù)據(jù)恍飘,前者稱為前置條件榨崩, 后者稱為后置條件谴垫。
抽象方法:apply Function
接口中最主要的抽象方法為: R apply(T t) ,根據(jù)類型T的參數(shù)獲取類型R的結(jié)果母蛛。 使用的場景例如:將 String 類型轉(zhuǎn)換為 Integer 類型翩剪。
public class DemoApply {
/**
* 定義一個(gè)方法,方法的參數(shù)傳遞一個(gè)字符串類型的證書
* 方法的另一個(gè)參數(shù)傳遞一個(gè)Function接口,泛型使用<String,Integer>
* 使用Function接口中的方法apply,把字符串類型的整數(shù),轉(zhuǎn)換為Integer類型的整數(shù)
*/
public static void change(String s, Function<String, Integer> fun) {
// Integer in = fun.apply(s);
int in = fun.apply(s);//自動(dòng)拆箱
System.out.println(in);
}

public static void main(String[] args) {
    String s = "12345";
    change(s, (String str) -> Integer.parseInt(str));
}

}
當(dāng)然,最好是通過方法引用的寫法彩郊。

默認(rèn)方法:andThen Function

接口中有一個(gè)默認(rèn)的 andThen 方法前弯,用來進(jìn)行組合操作。JDK源代碼如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

該方法同樣用于“先做什么秫逝,再做什么”的場景恕出,和 Consumer 中的 andThen 差不多:

import java.util.function.Function;

/**
 * 需求:
 * 把String類型的"1234"轉(zhuǎn)化為Integer類型,把轉(zhuǎn)換后的結(jié)果加100
 * 把增加后的Integer類型的數(shù)據(jù)轉(zhuǎn)換為String
 */
public class Demo_andThen {
    public static void method(String s, Function<String, Integer> fun1, Function<Integer, String> fun2) {
        System.out.println(fun1.andThen(fun2).apply(s));
    }

    public static void main(String[] args) {
        String s = "1234";
        method(s,
                (String str) -> Integer.parseInt(str) + 100,
                (Integer i) -> i + ""
        );
    }

請注意,F(xiàn)unction的前置條件泛型和后置條件泛型可以相同违帆。

Stream流

說到Stream便容易想到I/O Stream浙巫,而實(shí)際上,誰規(guī)定“流”就一定是“IO流”呢刷后?在Java 8中的畴,得益于Lambda所帶 來的函數(shù)式編程,引入了一個(gè)全新的Stream概念惠险,用于解決已有集合類庫既有的弊端苗傅。

傳統(tǒng)集合的多步遍歷代碼

幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當(dāng)我們需要對集合中的元 素進(jìn)行操作的時(shí)候班巩,除了必需的添加渣慕、刪除、獲取外抱慌,最典型的就是集合遍歷逊桦。例如:

public class ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強(qiáng)");
        list.add("張三豐");
        for (String name : list) {
            System.out.println(name);
        }
    }
}

循環(huán)遍歷的弊端

Java 8的Lambda讓我們可以更加專注于做什么(What),而不是怎么做(How)抑进,這點(diǎn)此前已經(jīng)結(jié)合內(nèi)部類進(jìn)行 了對比說明∏烤現(xiàn)在,我們仔細(xì)體會(huì)一下上例代碼寺渗,可以發(fā)現(xiàn):

  • for循環(huán)的語法就是“怎么做”
  • for循環(huán)的循環(huán)體才是“做什么”

為什么使用循環(huán)匿情?因?yàn)橐M(jìn)行遍歷。但循環(huán)是遍歷的唯一方式嗎信殊?遍歷是指每一個(gè)元素逐一進(jìn)行處理炬称,而并不是從 第一個(gè)到最后一個(gè)順次處理的循環(huán)。前者是目的涡拘,后者是方式玲躯。

試想一下,如果希望對集合中的元素進(jìn)行篩選過濾:

  1. 將集合A根據(jù)條件一過濾為子集B;
  2. 然后再根據(jù)條件二過濾為子集C跷车。

Stream的更優(yōu)寫法

public class UseStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強(qiáng)");
        list.add("張三豐");
        list.stream()
                .filter(name->name.startsWith("張"))
                .filter(name->name.length()==3)
                .forEach(name-> System.out.println(name));

    }
}

直接閱讀代碼的字面意思即可完美展示無關(guān)邏輯方式的語義:獲取流棘利、過濾姓張、過濾長度為3朽缴、逐一打印善玫。代碼 中并沒有體現(xiàn)使用線性循環(huán)或是其他任何算法進(jìn)行遍歷,我們真正要做的事情內(nèi)容被更好地體現(xiàn)在代碼中不铆。

備注:“Stream流”其實(shí)是一個(gè)集合元素的函數(shù)模型蝌焚,它并不是集合,也不是數(shù)據(jù)結(jié)構(gòu)誓斥,其本身并不存儲(chǔ)任何 元素(或其地址值)。

Stream(流)是一個(gè)來自數(shù)據(jù)源的元素隊(duì)列 元素是特定類型的對象许帐,形成一個(gè)隊(duì)列劳坑。 Java中的Stream并不會(huì)存儲(chǔ)元素,而是按需計(jì)算成畦。

數(shù)據(jù)源 流的來源距芬。 可以是集合,數(shù)組 等循帐。 和以前的Collection操作不同框仔, Stream操作還有兩個(gè)基礎(chǔ)的特征:

  • Pipelining: 中間操作都會(huì)返回流對象本身。 這樣多個(gè)操作可以串聯(lián)成一個(gè)管道拄养, 如同流式風(fēng)格(fluent style)离斩。 這樣做可以對操作進(jìn)行優(yōu)化, 比如延遲執(zhí)行(laziness)和短路( short-circuiting)瘪匿。
  • 內(nèi)部迭代: 以前對集合遍歷都是通過Iterator或者增強(qiáng)for的方式, 顯式的在集合外部進(jìn)行迭代跛梗, 這叫做外部迭 代。 Stream提供了內(nèi)部迭代的方式棋弥,流可以直接調(diào)用遍歷方法核偿。

當(dāng)使用一個(gè)流的時(shí)候,通常包括三個(gè)基本步驟:獲取一個(gè)數(shù)據(jù)源(source)→ 數(shù)據(jù)轉(zhuǎn)換→執(zhí)行操作獲取想要的結(jié) 果顽染,每次轉(zhuǎn)換原有 Stream 對象不改變漾岳,返回一個(gè)新的 Stream 對象(可以有多次轉(zhuǎn)換),這就允許對其操作可以 像鏈條一樣排列粉寞,變成一個(gè)管道尼荆。

獲取流

java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。(這并不是一個(gè)函數(shù)式接口仁锯。) 獲取一個(gè)流非常簡單耀找,有以下幾種常用的方式:

  • 所有的 Collection 集合都可以通過 stream 默認(rèn)方法獲取流;
  • Stream 接口的靜態(tài)方法 of 可以獲取數(shù)組對應(yīng)的流。
public class GetStream {
    //集合轉(zhuǎn)換為Stream流
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();

    Set<String> set = new HashSet<>();
    Stream<String> stream2 = set.stream();

    Map<String,String>map = new HashMap<>();
    Set<String>keySet = map.keySet();
    Stream<String>stream3 = keySet.stream();

    Collection<String>values = map.values();
    Stream<String>stream4 =  values.stream();

    Set<Map.Entry<String,String>>entries = map.entrySet();
    Stream<Map.Entry<String,String>> stream5 = entries.stream();

    //把數(shù)組轉(zhuǎn)換為Stream流
    Stream<Integer>stream6 = Stream.of(1,2,3,4,5,6,7,8,9);
    //可變參數(shù)可以傳遞數(shù)組
    Integer[] arr = {1,2,3,4,6,7};
    Stream<Integer>stream7 = Stream.of(arr);
    String[]arr2={"a","bb","ccc"};
    Stream<String>stream8 = Stream.of(arr2);
}

常用方法

常用方法的操作過程

流模型的操作很豐富野芒,這里介紹一些常用的API蓄愁。這些方法可以被分成兩種:

  • 延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈?zhǔn)秸{(diào)用狞悲。(除了終結(jié)方法外撮抓,其余方 法均為延遲方法。)
  • 終結(jié)方法:返回值類型不再是 Stream 接口自身類型的方法摇锋,因此不再支持類似 StringBuilder 那樣的鏈?zhǔn)秸{(diào) 用丹拯。本小節(jié)中,終結(jié)方法包括 count 和 forEach 方法荸恕。
逐一處理:forEach

雖然方法名字叫 forEach 乖酬,但是與for循環(huán)中的“for-each”昵稱不同。
void forEach(Consumer<? super T> action);該方法接收一個(gè) Consumer 接口函數(shù)融求,會(huì)將每一個(gè)流元素交給該函數(shù)進(jìn)行處理咬像。

復(fù)習(xí)Consumer接口

java.util.function.Consumer<T>接口是一個(gè)消費(fèi)型接口。
Consumer接口中包含抽象方法void accept(T t)生宛,意為消費(fèi)一個(gè)指定泛型的數(shù)據(jù)县昂。 基本使用:

import java.util.stream.Stream; 
public class Demo12StreamForEach {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("張無忌", "張三豐", "周芷若");
        stream.forEach(name ‐> System.out.println(name));
    }
}

簡單記:
forEach方法,用來遍歷流中的數(shù)據(jù)
是一個(gè)終結(jié)方法,遍歷之后就不能繼續(xù)調(diào)用Stream流中的其他方法

過濾:filter

可以通過 filter 方法將一個(gè)流轉(zhuǎn)換成另一個(gè)子集流。
方法簽名: Stream<T> filter(Predicate<? super T> predicate); 該接口接收一個(gè) Predicate 函數(shù)式接口參數(shù)(可以是一個(gè)Lambda或方法引用)作為篩選條件陷舅。

復(fù)習(xí)Predicate接口

此前我們已經(jīng)學(xué)習(xí)過java.util.stream.Predicate函數(shù)式接口倒彰,其中唯一的抽象方法為: boolean test(T t);
該方法將會(huì)產(chǎn)生一個(gè)boolean值結(jié)果,代表指定的條件是否滿足莱睁。如果結(jié)果為true待讳,那么Stream流的 filter 方法 將會(huì)留用元素;如果結(jié)果為false缩赛,那么 filter 方法將會(huì)舍棄元素耙箍。 基本使用 Stream流中的 filter 方法基本使用的代碼如:

Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 
Stream<String> result = original.filter(s ‐> s.startsWith("張"));
映射:map
map

如果需要將流中的元素映射到另一個(gè)流中,可以使用 map 方法酥馍。方法簽名:

<R> Stream<R> map(Function<? super T, ? extends R> mapper); 

該接口需要一個(gè) Function 函數(shù)式接口參數(shù)辩昆,可以將當(dāng)前流中的T類型數(shù)據(jù)轉(zhuǎn)換為另一種R類型的流。

復(fù)習(xí)Function接口

此前我們已經(jīng)學(xué)習(xí)過 java.util.stream.Function 函數(shù)式接口旨袒,其中唯一的抽象方法為:R apply(T t);
這可以將一種T類型轉(zhuǎn)換成為R類型汁针,而這種轉(zhuǎn)換的動(dòng)作,就稱為“映射”砚尽。 基本使用 Stream流中的 map 方法基本使用的代碼如:

Stream<String> original = Stream.of("10", "12", "18"); 
Stream<Integer> result = original.map(str‐>Integer.parseInt(str));

這段代碼中施无, map 方法的參數(shù)通過方法引用,將字符串類型轉(zhuǎn)換成為了int類型(并自動(dòng)裝箱為 Integer 類對 象)必孤。

統(tǒng)計(jì)個(gè)數(shù):count

正如舊集合 Collection 當(dāng)中的 size 方法一樣猾骡,流提供 count 方法來數(shù)一數(shù)其中的元素個(gè)數(shù): long count();
也是一個(gè)終結(jié)方法

取用前幾個(gè):limit

是一個(gè)延遲方法

limit 方法可以對流進(jìn)行截取瑞躺,只取用前n個(gè)。方法簽名: Stream<T> limit(long maxSize); 參數(shù)是一個(gè)long型兴想,如果集合當(dāng)前長度大于參數(shù)則進(jìn)行截却鄙凇;否則不進(jìn)行操作嫂便±塘基本使用:

Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 
Stream<String> result = original.limit(2); 
System.out.println(result.count()); // 2
跳過前幾個(gè):skip

如果希望跳過前幾個(gè)元素,可以使用 skip 方法獲取一個(gè)截取之后的新流:
Stream<T> skip(long n); 如果流的當(dāng)前長度大于n毙替,則跳過前n個(gè)岸售;否則將會(huì)得到一個(gè)長度為0的空流〕Щ基本使用:

Stream<String> original = Stream.of("張無忌", "張三豐", "周芷若"); 
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
組合:concat

如果有兩個(gè)流凸丸,希望合并成為一個(gè)流,那么可以使用 Stream 接口的靜態(tài)方法 concat :
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
備注:這是一個(gè)靜態(tài)方法袱院,與 java.lang.String 當(dāng)中的 concat 方法是不同的甲雅。 該方法的基本使用代碼如:

Stream<String> streamA = Stream.of("張無忌"); 
Stream<String> streamB = Stream.of("張翠山"); 
Stream<String> result = Stream.concat(streamA, streamB);

方法引用

在使用Lambda表達(dá)式的時(shí)候,我們實(shí)際上傳遞進(jìn)去的代碼就是一種解決方案:拿什么參數(shù)做什么操作坑填。那么考慮 一種情況:如果我們在Lambda中所指定的操作方案,已經(jīng)有地方存在相同方案弛姜,那是否還有必要再寫重復(fù)邏輯脐瑰?

冗余的Lambda場景

來看一個(gè)簡單的函數(shù)式接口以應(yīng)用Lambda表達(dá)式:

@FunctionalInterface
public interface Printable {
    void print(String s);
}

在 Printable 接口當(dāng)中唯一的抽象方法 print 接收一個(gè)字符串參數(shù),目的就是為了打印顯示它廷臼。那么通過Lambda 來使用它的代碼很簡單:

public class Demo01Printable {
    public static void  printString(Printable data) {
        data.print("Hellow world");
    }

    public static void main(String[] args) {
        printString((s) -> {
            System.out.println(s);
        });
    }

分析:

Lambda表達(dá)式的目的:打印參數(shù)傳遞的字符串,把參數(shù)s傳遞給了System.out對象中的方法println對字符串進(jìn)行了輸出
注意:

  1. System,out對象是已經(jīng)存在的
  2. println方法是已經(jīng)存在的
    所以我們可以使用方法引用來優(yōu)化Lambda表達(dá)式
    可以使用System.out方法直接引語(調(diào)用)println方法
printString(System.out::println);

請注意其中的雙冒號(hào) :: 寫法苍在,這被稱為“方法引用”,而雙冒號(hào)是一種新的語法荠商。

語義分析

例如上例中寂恬, System.out 對象中有一個(gè)重載的 println(String) 方法恰好就是我們所需要的。那么對于 printString 方法的函數(shù)式接口參數(shù)莱没,對比下面兩種寫法初肉,完全等效:

  • Lambda表達(dá)式寫法: s -> System.out.println(s);
  • 方法引用寫法: System.out::println 第一種語義是指:拿到參數(shù)之后經(jīng)Lambda之手,繼而傳遞給 System.out.println 方法去處理饰躲。

第二種等效寫法的語義是指:直接讓 System.out 中的 println 方法來取代Lambda牙咏。兩種寫法的執(zhí)行效果完全一 樣,而第二種方法引用的寫法復(fù)用了已有方案嘹裂,更加簡潔妄壶。

注:Lambda 中 傳遞的參數(shù) 一定是方法引用中 的那個(gè)方法可以接收的類型,否則會(huì)拋出異常

推導(dǎo)與省略

如果使用Lambda,那么根據(jù)“可推導(dǎo)就是可省略”的原則寄狼,無需指定參數(shù)類型丁寄,也無需指定的重載形式——它們都 將被自動(dòng)推導(dǎo)。而如果使用方法引用,也是同樣可以根據(jù)上下文進(jìn)行推導(dǎo)伊磺。 函數(shù)式接口是Lambda的基礎(chǔ)盛正,而方法引用是Lambda的孿生兄弟。

下面這段代碼將會(huì)調(diào)用 println 方法的不同重載形式奢浑,將函數(shù)式接口改為int類型的參數(shù):

@FunctionalInterface 
public interface PrintableInteger { void print(int str); }

由于上下文變了之后可以自動(dòng)推導(dǎo)出唯一對應(yīng)的匹配重載蛮艰,所以方法引用沒有任何變化:

public class Demo01Printable {
    private static void printInteger(PrintableInteger data) {
        data.print(1024);
    }
    public static void main(String[] args) {
        printInteger(System.out::println);
    }
}

這次方法引用將會(huì)自動(dòng)匹配到 println(int) 的重載形式。

通過對象名引用成員方法

這是最常見的一種用法雀彼,與上例相同壤蚜。如果一個(gè)類中已經(jīng)存在了一個(gè)成員方法:

public class MethodRefObject {
    public void printUpperCase(String str){
        System.out.println(str.toUpperCase());
    }
}

函數(shù)式接口仍然定義為:

@FunctionalInterface
public interface Printable {
    void print(String s);
}

那么當(dāng)需要使用這個(gè) printUpperCase 成員方法來替代 Printable 接口的Lambda的時(shí)候,已經(jīng)具有了 MethodRefObject 類的對象實(shí)例徊哑,則可以通過對象名引用成員方法袜刷,代碼為:

public class Demo02ObjectMethodRef {
    // 定義一個(gè)方法,方法的參數(shù)傳遞Printable接口
    public static void printString(Printable p){
        p.print("Hello");
    }

    public static void main(String[] args) {
        // 調(diào)用printString方法,方法的參數(shù)Pirntable是一個(gè)函數(shù)式接口,所以可以傳遞Lambda表達(dá)式
        printString((s)->{
            MethodRefObject obj = new MethodRefObject();
            // 調(diào)用MethodRefObject對象中的成員方法將字符串按照大寫輸出
            obj.printUpperCase(s);
        });
        /*
            使用方法引用優(yōu)化Lambda
            對象是已經(jīng)存在的MethodRefObject,成員方法也是已經(jīng)存在的
            所以我們可以用對象名引用成員方法
         */
        MethodRefObject obj = new MethodRefObject();
        printString(obj::printUpperCase);
    }
}

通過類名稱引用靜態(tài)方法

由于在 java.lang.Math 類中已經(jīng)存在了靜態(tài)方法 abs ,所以當(dāng)我們需要通過Lambda來調(diào)用該方法時(shí)莺丑,有兩種寫法著蟹。首先是函數(shù)式接口:

@FunctionalInterface
public interface Calcable {
    //定義一個(gè)抽象方法,傳遞一個(gè)整數(shù),對整數(shù)進(jìn)行絕對值計(jì)算并返回
    int calsAbs(int number);
}

第一種寫法是使用Lambda表達(dá)式:

public class Demo01Lambda {
    public static int method(int number,Calcable c){
        return c.calsAbs(number);
    }
    public static void main(String[] args) {
        //調(diào)用method方法,傳遞計(jì)算絕對值的整數(shù)和Lambda表達(dá)式
        int num = method(-10,(n)->{
            return Math.abs(n);
        });
        System.out.println(num);
    }
}

但是使用方法引用的更好寫法是:

public class Demo02MethodRef {
    /*
        使用方法引用優(yōu)化Lambda表達(dá)式
        Math類是存在的,abs靜態(tài)方法也是存在的
        直接通過類名引用靜態(tài)方法
     */
    public static int method(int num, Calcable c) {
        return c.calsAbs(num);
    }

    public static void main(String[] args) {
        System.out.println(method(-10, Math::abs));
    }
}

在這個(gè)例子中,下面兩種寫法是等效的:

  • Lambda表達(dá)式: n -> Math.abs(n)
  • 方法引用: Math::abs

通過super引用成員方法

如果存在繼承關(guān)系梢莽,當(dāng)Lambda中需要出現(xiàn)super調(diào)用時(shí)萧豆,也可以使用方法引用進(jìn)行替代。首先是函數(shù)式接口:

@FunctionalInterface
public interface Greetable {
    void greet();
}

然后是父類 Human 的內(nèi)容:

public class Human {
    public void sayHello(){
        System.out.println("Hello,我是Human");
    }
}
最后是子類 Man 的內(nèi)容
public class Man extends Human {
    @Override
    public void sayHello() {
        System.out.println("Hello,我是Man");
    }

    //定義一個(gè)方法參數(shù)傳遞Greetable接口
    public void method(Greetable g) {
        g.greet();
    }

    public void show() {
        //調(diào)用Method方法,方法的參數(shù)Greetable是一個(gè)函數(shù)式接口
        /**
         *  method(()->{
         *  Human h = new Human();
         *  h.sayHello();
         });
         */
        /**
         * 因?yàn)橛凶痈割愱P(guān)系,所以存在的一個(gè)關(guān)鍵字super,我們可以直接通過super調(diào)用父類的成員方法
         */
        method(() -> {
            super.sayHello();
        });
        /**
         * 直接使用父類引用父類成員方法
         */
        method(super::sayHello);
    }

    public static void main(String[] args) {
        new Man().show();
    }
}

通過this引用成員方法

this代表當(dāng)前對象昏名,如果需要引用的方法就是當(dāng)前類中的成員方法涮雷,那么可以使用“this::成員方法”的格式來使用方 法引用。首先是簡單的函數(shù)式接口:

@FunctionalInterface
public interface Richable {
    void buy();
}

開心方法 beHappy 調(diào)用了結(jié)婚方法 marry 轻局,后者的參數(shù)為函數(shù)式接口 Richable 洪鸭,所以需要一個(gè)Lambda表達(dá)式忽媒。 但是如果這個(gè)Lambda表達(dá)式的內(nèi)容已經(jīng)在本類當(dāng)中存在了绍刮,則可以對 Husband 丈夫類進(jìn)行修改:

public class Husband {
    private void marry(Richable lambda) {
        lambda.buy();
    }
    private void buyHouse(){
        System.out.println("深圳南山一套房");
    }

    public void beHappy() {
        marry(this::buyHouse);
    }

    public static void main(String[] args) {
        new Husband().beHappy();
    }
}

在這個(gè)例子中,下面兩種寫法是等效的:

  • Lambda表達(dá)式: () -> this.buyHouse()
  • 方法引用: this::buyHouse

2.9 類的構(gòu)造器引用

由于構(gòu)造器的名稱與類名完全一樣段磨,并不固定镇饮。所以構(gòu)造器引用使用 類名稱::new 的格式表示蜓竹。首先是一個(gè)簡單 的 Person 類:

public class Person {
    private String name;
    public Person(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

然后是用來創(chuàng)建 Person 對象的函數(shù)式接口:

@FunctionalInterface
public interface PersonBuilder {
    Person buildPerson(String name);
}

通過構(gòu)造器引用,有更好的寫法:

public class Demo {
    public static void printName(String name,PersonBuilder builder){
        System.out.println(builder.buildPerson(name).getName());
    }

    public static void main(String[] args) {
        printName("趙麗穎",Person::new);
    }
}

在這個(gè)例子中盒让,下面兩種寫法是等效的:

  • Lambda表達(dá)式: name -> new Person(name)
  • 方法引用: Person::new

數(shù)組的構(gòu)造器引用

數(shù)組也是 Object 的子類對象梅肤,所以同樣具有構(gòu)造器,只是語法稍有不同邑茄。如果對應(yīng)到Lambda的使用場景中時(shí)姨蝴, 需要一個(gè)函數(shù)式接口:

@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}

在應(yīng)用該接口的時(shí)候,可以通過Lambda表達(dá)式,但是更好的寫法是使用數(shù)組的構(gòu)造器引用:

public class Demo {
    public static int[] initArray(int length,ArrayBuilder builder){
        return builder.buildArray(length);
    }

    public static void main(String[] args) {
        int[] array = initArray(10,length -> new int[length]);
        System.out.println(array.length);
    }
}

在這個(gè)例子中肺缕,下面兩種寫法是等效的:

  • Lambda表達(dá)式: length -> new int[length]
  • 方法引用: int[]::new
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末左医,一起剝皮案震驚了整個(gè)濱河市授帕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌浮梢,老刑警劉巖跛十,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異秕硝,居然都是意外死亡芥映,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門远豺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奈偏,“玉大人,你說我怎么就攤上這事躯护【矗” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵棺滞,是天一觀的道長裁蚁。 經(jīng)常有香客問我,道長继准,這世上最難降的妖魔是什么枉证? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮移必,結(jié)果婚禮上刽严,老公的妹妹穿的比我還像新娘。我一直安慰自己避凝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布眨补。 她就那樣靜靜地躺著管削,像睡著了一般。 火紅的嫁衣襯著肌膚如雪撑螺。 梳的紋絲不亂的頭發(fā)上含思,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機(jī)與錄音甘晤,去河邊找鬼含潘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛线婚,可吹牛的內(nèi)容都是我干的遏弱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼塞弊,長吁一口氣:“原來是場噩夢啊……” “哼漱逸!你這毒婦竟也來了泪姨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饰抒,失蹤者是張志新(化名)和其女友劉穎肮砾,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袋坑,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仗处,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了枣宫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婆誓。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖镶柱,靈堂內(nèi)的尸體忽然破棺而出旷档,到底是詐尸還是另有隱情,我是刑警寧澤歇拆,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布鞋屈,位于F島的核電站,受9級特大地震影響故觅,放射性物質(zhì)發(fā)生泄漏厂庇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一输吏、第九天 我趴在偏房一處隱蔽的房頂上張望权旷。 院中可真熱鬧,春花似錦贯溅、人聲如沸拄氯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽译柏。三九已至,卻和暖如春姐霍,著一層夾襖步出監(jiān)牢的瞬間鄙麦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工镊折, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胯府,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓恨胚,卻偏偏與公主長得像骂因,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子赃泡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355