函數(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)行篩選過濾:
- 將集合A根據(jù)條件一過濾為子集B;
- 然后再根據(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
如果需要將流中的元素映射到另一個(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)行了輸出
注意:
- System,out對象是已經(jīng)存在的
- 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