Java笔记12-函数式接口
主要內(nèi)容
- 自定義函數(shù)式接口
- 函數(shù)式編程
- 常用函數(shù)式接口
第一章 函數(shù)式接口
概念
函數(shù)式接口在java中指的是:有且只有一個(gè)抽象方法的接口
函數(shù)式接口,即適用于函數(shù)式編程場(chǎng)景的接口.而java中共的函數(shù)式編程體現(xiàn)就是Lambda,所以函數(shù)式接口就是可以適用于lambda使用的接口.只有確保接口中有且只有一個(gè)寵幸方法,java中的lambda才能順利地進(jìn)行推導(dǎo).
備注:語法糖是指使用更加方便,但是原理不變的代碼語法,例如在遍歷集合時(shí)使用for-each語法,其實(shí)底層的實(shí)現(xiàn)原理仍然是迭代器,這便是語法糖.從應(yīng)用應(yīng)用層面來京,java中的lambda可以被當(dāng)做是匿名內(nèi)部類了的語法糖,但是二者原理上是不同的.
格式
只要確保接口中有且僅有一個(gè)抽象方法即可:
修飾符 Interface 接口名稱{public abstract 返回值類型 方法名稱(可選參數(shù)信息);// 其他非抽象方法內(nèi)容 }由于接口當(dāng)中抽象方法的public abstract 是可以省略的,所以定義一函數(shù)式接口很簡(jiǎn)單:
public Interface MyFunctionalInterface{void myMethod(); }@FunctionalInterface注解
與@Override注解的作用類似,Java 8 中專門為函數(shù)式接口引入了一個(gè)新的注解:@FunctionInterface.該注解可用于一個(gè)接口的定義上:
@FunctionalInterface public interface MyFunctionalInterface{void myMethod(); }一旦使用該注解來定義接口,編譯期將會(huì)強(qiáng)制檢查該接口是否確實(shí)有且僅有一個(gè)抽象方法,否則將會(huì)報(bào)錯(cuò).需要注意的是,即使不使用該注解,主要滿足函數(shù)式接口的定義,這仍然是一個(gè)函數(shù)式接口,使用起來都一個(gè)B樣,只不過@FunctionalInterface用起來規(guī)范一點(diǎn),說白了逼格高那么一丟丟
自定義函數(shù)式接口
對(duì)于剛剛定義好的MyFunctionalInterface函數(shù)式接口,典型使用場(chǎng)景就是作為方法的參數(shù):
public class Demo09FunctionalInterface {// 使用自定義的函數(shù)式接口作為方法參數(shù)private static void doSomething(MyFunctionalInterface inter) {inter.myMethod(); // 調(diào)用自定義的函數(shù)式接口方法 }public static void main (String[]args){// 調(diào)用使用函數(shù)式接口的方法doSomething(() ‐ > System.out.println("Lambda執(zhí)行啦!"));}} }函數(shù)式編程
在兼顧面向?qū)ο筇匦缘幕A(chǔ)上,Java語言通過Lambda表達(dá)式與方法引用等,為開發(fā)者打開了函數(shù)式編程的大門
下面我們做一個(gè)初探
Lambda的延遲執(zhí)行
有些場(chǎng)景的代碼執(zhí)行后,結(jié)果不一定會(huì)被使用,從而造成性能浪費(fèi)。而Lambda表達(dá)式是延遲執(zhí)行的,這正好可以 作為解決方案,提升性能。
性能浪費(fèi)的日志案例
注:日志可以幫助我們快速的定位問題,記錄程序運(yùn)行過程中的情況,以便項(xiàng)目的監(jiān)控和優(yōu)化。 一種典型的場(chǎng)景就是對(duì)參數(shù)進(jìn)行有條件使用,例如對(duì)日志消息進(jìn)行拼接后,在滿足條件的情況下進(jìn)行打印輸出:
public class Demo01Logger {private static void log(int level, String msg) {if (level == 1) {System.out.println(msg); } }public static void main(String[] args) {String msgA = "Hello";String msgB = "World";String msgC = "Java";log(1, msgA + msgB + msgC);} }這段代碼存在問題:無論級(jí)別是否滿足要求,作為 log 方法的第二個(gè)參數(shù),三個(gè)字符串一定會(huì)首先被拼接并傳入方
法內(nèi),然后才會(huì)進(jìn)行級(jí)別判斷。如果級(jí)別不符合要求,那么字符串的拼接操作就白做了,存在性能浪費(fèi)。
備注:SLF4J是應(yīng)用非常廣泛的日志框架,它在記錄日志時(shí)為了解決這種性能浪費(fèi)的問題,并不推薦首先進(jìn)行 字符串的拼接,而是將字符串的若干部分作為可變參數(shù)傳入方法中,僅在日志級(jí)別滿足要求的情況下才會(huì)進(jìn) 行字符串拼接。例如: LOGGER.debug(“變量{}的取值為{}。”, “os”, “macOS”) ,其中的大括號(hào) {} 為占位 符。如果滿足日志級(jí)別要求,則會(huì)將“os”和“macOS”兩個(gè)字符串依次拼接到大括號(hào)的位置;否則不會(huì)進(jìn)行字 符串拼接。這也是一種可行解決方案,但Lambda可以做到更好。
體驗(yàn)lambda的更加優(yōu)化寫法
使用Lambda必然需要一個(gè)函數(shù)式接口:
@FunctionalInterfacepublic interface MessageBuilder {String buildMessage(); }然后對(duì) log 方法進(jìn)行改造:
public class Demo02LoggerLambda {private static void log(int level, MessageBuilder builder) {if (level == 1) {System.out.println(builder.buildMessage());} }public static void main(String[] args) {String msgA = "Hello";String msgB = "World";String msgC = "Java";log(1, () ‐> msgA + msgB + msgC );} }這樣一來,只有當(dāng)級(jí)別滿足要求的時(shí)候,才會(huì)進(jìn)行三個(gè)字符串的拼接;否則三個(gè)字符串將不會(huì)進(jìn)行拼接。
證明lambda的延遲
下面的代碼可以通過結(jié)果進(jìn)行驗(yàn)證:
public class Demo03LoggerDelay {private static void log(int level, MessageBuilder builder) {if (level == 1) {System.out.println(builder.buildMessage());} }public static void main(String[] args) {String msgA = "Hello";String msgB = "World";String msgC = "Java";log(2, () ‐> {System.out.println("Lambda執(zhí)行!");return msgA + msgB + msgC;}); } }從結(jié)果中可以看出,在不符合級(jí)別要求的情況下,Lambda將不會(huì)執(zhí)行。從而達(dá)到節(jié)省性能的效果。
擴(kuò)展:實(shí)際上使用內(nèi)部類也可以達(dá)到同樣的效果,只是將代碼操作延遲到了另外一個(gè)對(duì)象當(dāng)中通過調(diào)用方法來完成。而是否調(diào)用其所在方法是在條件判斷之后才執(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ū)別。
類似地,如果一個(gè)方法的返回值類型是一個(gè)函數(shù)式接口,那么就可以直接返回一個(gè)Lambda表達(dá)式。當(dāng)需要通過一個(gè)方法來獲取一個(gè) java.util.Comparator 接口類型的對(duì)象作為排序器時(shí),就可以調(diào)該方法獲取。
import java.util.Arrays; import java.util.Comparator; public class Demo06Comparator {private static Comparator<String> newComparator() {return (a, b) ‐> b.length() ‐ a.length();}public static void main(String[] args) {String[] array = { "abc", "ab", "abcd" };System.out.println(Arrays.toString(array));Arrays.sort(array, newComparator());System.out.println(Arrays.toString(array));} }其中直接return一個(gè)Lambda表達(dá)式即可
常用函數(shù)式接口
JDK提供了大量常用的函數(shù)式接口以豐富Lambda的典型使用場(chǎng)景,它們主要在 java.util.function 包中被提供。 下面是最簡(jiǎn)單的幾個(gè)接口及使用示例。
supplier接口
java.util.function.Supplier<T>接口僅包含一個(gè)無參的方法: T get() 。用來獲取一個(gè)泛型參數(shù)指定類型的對(duì) 象數(shù)據(jù)。由于這是一個(gè)函數(shù)式接口,這也就意味著對(duì)應(yīng)的Lambda表達(dá)式需要對(duì)外提供一個(gè)符合泛型類型的對(duì)象 數(shù)據(jù)。
import java.util.function.Supplier; public class Demo08Supplier {private static String getString(Supplier<String> function) {return function.get();}public static void main(String[] args) {String msgA = "Hello";String msgB = "World";System.out.println(getString(() ‐> msgA + msgB));} }練習(xí):求數(shù)組元素的最大值
題目:
使用 Supplier 接口作為方法參數(shù)類型,通過Lambda表達(dá)式求出int數(shù)組中的最大值。提示:接口的泛型請(qǐng)使用 java.lang.Integer 類。
解答:
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ù)。基本使用如:
import java.util.function.Consumer; public class Demo09Consumer {private static void consumeString(Consumer<String> function) {function.accept("Hello");}public static void main(String[] args) {consumeString(s ‐> System.out.println(s));} }當(dāng)然,更好的寫法是使用方法引用。
默認(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è)步驟組 合的情況:
運(yùn)行結(jié)果將會(huì)首先打印完全大寫的HELLO,然后打印完全小寫的hello。當(dāng)然,通過鏈?zhǔn)綄懛梢詫?shí)現(xiàn)更多步驟的 組合。
練習(xí):格式化打印信息
題目
下面的字符串?dāng)?shù)組當(dāng)中存有多條信息,請(qǐng)按照格式“ 姓名:XX。性別:XX。 ”的格式將信息打印出來。要求將打印姓 名的動(dòng)作作為第一個(gè) Consumer接口的Lambda實(shí)例,將打印性別的動(dòng)作作為第二個(gè) Consumer接口的Lambda實(shí) 例,將兩個(gè) Consumer接口按照順序“拼接”到一起。
解答
import java.util.function.Consumer; public class DemoConsumer {public static void main(String[] args) {String[] array = { "迪麗熱巴,女", "古力娜扎,女", "馬爾扎哈,男" };printInfo(s ‐> System.out.print("姓名:" + s.split(",")[0]),s ‐> System.out.println("。性別:" + s.split(",")[1] + "。"),array);}private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {for (String info : array) {one.andThen(two).accept(info); // 姓名:迪麗熱巴。性別:女。 }} }Predicate接口
有時(shí)候我們需要對(duì)某種類型的數(shù)據(jù)進(jìn)行判斷,從而得到一個(gè)boolean值結(jié)果。這時(shí)可以使用
java.util.function.Predicate<T> 接口。
抽象方法:test
Predicate 接口中包含一個(gè)抽象方法: boolean test(T t) 。用于條件判斷的場(chǎng)景:
import java.util.function.Predicate public class Demo15PredicateTest {private static void method(Predicate<String> predicate) {boolean veryLong = predicate.test("HelloWorld");System.out.println("字符串很長(zhǎng)嗎:" + veryLong); }public static void main(String[] args) {method(s ‐> s.length() > 5);} }條件判斷的標(biāo)準(zhǔn)是傳入的Lambda表達(dá)式邏輯,只要字符串長(zhǎng)度大于5則認(rèn)為很長(zhǎng)。
默認(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); }如果要判斷一個(gè)字符串既要包含大寫“H”,又要包含大寫“W”,那么:
import java.util.function.Predicate; public class Demo16PredicateAnd {private static void method(Predicate<String> one, Predicate<String> two) {boolean isValid = one.and(two).test("Helloworld");System.out.println("字符串符合要求嗎:" + isValid); }public static void main(String[] args) {method(s ‐> s.contains("H"), s ‐> s.contains("W"));} }默認(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); }如果希望實(shí)現(xiàn)邏輯“字符串包含大寫H或者包含大寫W”,那么代碼只需要將“and”修改為“or”名稱即可,其他都不變:
import java.util.function.Predicate; public class Demo16PredicateAnd {private static void method(Predicate<String> one, Predicate<String> two) {boolean isValid = one.or(two).test("Helloworld");System.out.println("字符串符合要求嗎:" + isValid); }public static void main(String[] args) {method(s ‐> s.contains("H"), s ‐> s.contains("W"));} }默認(rèn)方法:negate
“與”、“或”已經(jīng)了解了,剩下的“非”(取反)也會(huì)簡(jiǎn)單。默認(rèn)方法 negate 的JDK源代碼為
default Predicate<T> negate() {return (t) ‐> !test(t); }從實(shí)現(xiàn)中很容易看出,它是執(zhí)行了test方法之后,對(duì)結(jié)果boolean值進(jìn)行“!”取反而已。一定要在 test 方法調(diào)用之前
調(diào)用 negate 方法,正如 and 和 or 方法一樣:
練習(xí):集合信息篩選 題目
數(shù)組當(dāng)中有多條“姓名+性別”的信息如下,請(qǐng)通過 Predicate 接口的拼裝將符合要求的字符串篩選到集合 ArrayList 中,需要同時(shí)滿足兩個(gè)條件:
- 必須為女生;
- 姓名為4個(gè)字。
解答
import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; public class DemoPredicate {public static void main(String[] args) {String[] array = { "迪麗熱巴,女", "古力娜扎,女", "馬爾扎哈,男", "趙麗穎,女" }; List<String> list = filter(array,s ‐> "女".equals(s.split(",")[1]),s ‐> s.split(",")[0].length() == 4);System.out.println(list);}private static List<String> filter(String[] array, Predicate<String> one,Predicate<String> two) {List<String> list = new ArrayList<>();for (String info : array) {if (one.and(two).test(info)) {list.add(info);} }return list;} }總結(jié)
以上是生活随笔為你收集整理的Java笔记12-函数式接口的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 消除if-else/switch语句块来
- 下一篇: Java笔记02-OOP