應廣大讀者的須要,霈哥給你們帶來新一期的乾貨啦!java
帶你解惑大廠必會使用的 Lambda表達式、函數式接口
帶你解惑大廠必會使用的 Stream流、方法引用
若對你和身邊的朋友有幫助, 抓緊關注 IT霈哥 點贊! 點贊! 點贊! 評論!收藏! 分享給更多的朋友共同窗習交流, 天天持續掘金離不開你的點贊支持!
在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端。算法
傳統集合的多步遍歷代碼編程
幾乎全部的集合(如Collection
接口或Map
接口等)都支持直接或間接的遍歷操做。而當咱們須要對集合中的元素進行操做的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:後端
public class Demo10ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
for (String name : list) {
System.out.println(name);
}
}
}
複製代碼
這是一段很是簡單的集合遍歷操做:對集合中的每個字符串都進行打印輸出操做。數組
循環遍歷的弊端markdown
Java 8的Lambda讓咱們能夠更加專一於作什麼(What),而不是怎麼作(How),這點此前已經結合內部類進行了對比說明。如今,咱們仔細體會一下上例代碼,能夠發現:數據結構
爲何使用循環?由於要進行遍歷。但循環是遍歷的惟一方式嗎?遍歷是指每個元素逐一進行處理,而並非從第一個到最後一個順次處理的循環。前者是目的,後者是方式。app
試想一下,若是但願對集合中的元素進行篩選過濾:dom
那怎麼辦?在Java 8以前的作法可能爲:函數式編程
這段代碼中含有三個循環,每個做用不一樣:
public class Demo11NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("張")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
System.out.println(name);
}
}
}
複製代碼
每當咱們須要對集合中的元素進行操做的時候,老是須要進行循環、循環、再循環。這是理所固然的麼?不是。循環是作事情的方式,而不是目的。另外一方面,使用線性循環就意味着只能遍歷一次。若是但願再次遍歷,只能再使用另外一個循環從頭開始。
那,Lambda的衍生物Stream能給咱們帶來怎樣更加優雅的寫法呢?
Stream的更優寫法
下面來看一下藉助Java 8的Stream API,什麼才叫優雅:
public class Demo12StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
list.stream()
.filter(s -> s.startsWith("張"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}
複製代碼
直接閱讀代碼的字面意思便可完美展現無關邏輯方式的語義:獲取流、過濾姓張、過濾長度爲三、逐一打印。代碼中並無體現使用線性循環或是其餘任何算法進行遍歷,咱們真正要作的事情內容被更好地體如今代碼中。
注意:請暫時忘記對傳統IO流的固有印象!
總體來看,流式思想相似於工廠車間的「生產流水線」。
當須要對多個元素進行操做(特別是多步操做)的時候,考慮到性能及便利性,咱們應該首先拼好一個「模型」步驟方案,而後再按照方案去執行它。
這張圖中展現了過濾、映射、跳過、計數等多步操做,這是一種集合元素的處理方案,而方案就是一種「函數模型」。圖中的每個方框都是一個「流」,調用指定的方法,能夠從一個流模型轉換爲另外一個流模型。而最右側的數字3是最終結果。
這裏的filter
、map
、skip
都是在對函數模型進行操做,集合元素並無真正被處理。只有當終結方法count
執行的時候,整個模型纔會按照指定策略執行操做。而這得益於Lambda的延遲執行特性。
備註:「Stream流」實際上是一個集合元素的函數模型,它並非集合,也不是數據結構,其自己並不存儲任何元素(或其地址值)。
java.util.stream.Stream<T>
是Java 8新加入的最經常使用的流接口。(這並非一個函數式接口。)
獲取一個流很是簡單,有如下幾種經常使用的方式:
Collection
集合均可以經過stream
默認方法獲取流;Stream
接口的靜態方法of
能夠獲取數組對應的流。方式1 : 根據Collection獲取流
首先,java.util.Collection
接口中加入了default方法stream
用來獲取流,因此其全部實現類都可獲取流。
import java.util.*;
import java.util.stream.Stream;
/* 獲取Stream流的方式 1.Collection中 方法 Stream stream() 2.Stream接口 中靜態方法 of(T...t) 向Stream中添加多個數據 */
public class Demo13GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
}
}
複製代碼
方式2: 根據數組獲取流
若是使用的不是集合或映射而是數組,因爲數組對象不可能添加默認方法,因此Stream
接口中提供了靜態方法of
,使用很簡單:
import java.util.stream.Stream;
public class Demo14GetStream {
public static void main(String[] args) {
String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
Stream<String> stream = Stream.of(array);
}
}
複製代碼
備註:
of
方法的參數實際上是一個可變參數,因此支持數組。
流模型的操做很豐富,這裏介紹一些經常使用的API。這些方法能夠被分紅兩種:
Stream
接口自身類型的方法,所以再也不支持相似StringBuilder
那樣的鏈式調用。本小節中,終結方法包括count
和forEach
方法。Stream
接口自身類型的方法,所以支持鏈式調用。(除了終結方法外,其他方法均爲非終結方法。)備註:本小節以外的更多方法,請自行參考API文檔。
雖然方法名字叫forEach
,可是與for循環中的「for-each」暱稱不一樣,該方法並不保證元素的逐一消費動做在流中是被有序執行的。
void forEach(Consumer<? super T> action);
複製代碼
該方法接收一個Consumer
接口函數,會將每個流元素交給該函數進行處理。例如:
import java.util.stream.Stream;
public class Demo15StreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爺爺","蛇精","蠍子精");
//Stream<String> stream = Stream.of("張無忌", "張三丰", "周芷若");
stream.forEach((String str)->{System.out.println(str);});
}
}
複製代碼
在這裏,lambda表達式(String str)->{System.out.println(str);}
就是一個Consumer函數式接口的示例。
能夠經過filter
方法將一個流轉換成另外一個子集流。方法聲明:
Stream<T> filter(Predicate<? super T> predicate);
複製代碼
該接口接收一個Predicate
函數式接口參數(能夠是一個Lambda)做爲篩選條件。
基本使用
Stream流中的filter
方法基本使用的代碼如:
public class Demo16StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.filter((String s) -> {return s.startsWith("張");});
}
}
複製代碼
在這裏經過Lambda表達式來指定了篩選的條件:必須姓張。
正如舊集合Collection
當中的size
方法同樣,流提供count
方法來數一數其中的元素個數:
long count();
複製代碼
該方法返回一個long值表明元素個數(再也不像舊集合那樣是int值)。基本使用:
public class Demo17StreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("張"));
System.out.println(result.count()); // 2
}
}
複製代碼
limit
方法能夠對流進行截取,只取用前n個。方法簽名:
Stream<T> limit(long maxSize):獲取Stream流對象中的前n個元素,返回一個新的Stream流對象 複製代碼
參數是一個long型,若是集合當前長度大於參數則進行截取;不然不進行操做。基本使用:
import java.util.stream.Stream;
public class Demo18StreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
}
複製代碼
若是但願跳過前幾個元素,可使用skip
方法獲取一個截取以後的新流:
Stream<T> skip(long n): 跳過Stream流對象中的前n個元素,返回一個新的Stream流對象 複製代碼
若是流的當前長度大於n,則跳過前n個;不然將會獲得一個長度爲0的空流。基本使用:
import java.util.stream.Stream;
public class Demo19StreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("張無忌", "張三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
}
複製代碼
若是有兩個流,但願合併成爲一個流,那麼可使用Stream
接口的靜態方法concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把參數列表中的兩個Stream流對象a和b,合併成一個新的Stream流對象 複製代碼
備註:這是一個靜態方法,與
java.lang.String
當中的concat
方法是不一樣的。
該方法的基本使用代碼如:
import java.util.stream.Stream;
public class Demo20StreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("張無忌");
Stream<String> streamB = Stream.of("張翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
}
複製代碼
如今有兩個ArrayList
集合存儲隊伍當中的多個成員姓名,要求使用傳統的for循環(或加強for循環)依次進行如下若干操做步驟:
兩個隊伍(集合)的代碼以下:
public class Demo21ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪麗熱巴");
one.add("宋遠橋");
one.add("蘇星河");
one.add("老子");
one.add("莊子");
one.add("孫子");
one.add("洪七公");
List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("張無忌");
two.add("張三丰");
two.add("趙麗穎");
two.add("張二狗");
two.add("張天愛");
two.add("張三");
// ....
}
}
複製代碼
傳統方式
使用for循環 , 示例代碼:
public class Demo22ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字爲3個字的成員姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一個隊伍篩選以後只要前3我的;
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}
// 第二個隊伍只要姓張的成員姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("張")) {
twoA.add(name);
}
}
// 第二個隊伍篩選以後不要前2我的;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}
// 將兩個隊伍合併爲一個隊伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 打印整個隊伍的姓名信息。
for (String name : totalNames) {
System.out.println(name);
}
}
}
複製代碼
運行結果爲:
宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
複製代碼
Stream方式
等效的Stream流式處理代碼爲:
public class Demo23StreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一個隊伍只要名字爲3個字的成員姓名;
// 第一個隊伍篩選以後只要前3我的;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二個隊伍只要姓張的成員姓名;
// 第二個隊伍篩選以後不要前2我的;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("張")).skip(2);
// 將兩個隊伍合併爲一個隊伍;
// 根據姓名建立Person對象;
// 打印整個隊伍的Person對象信息。
Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
}
}
複製代碼
運行效果徹底同樣:
宋遠橋
蘇星河
洪七公
張二狗
張天愛
張三
複製代碼
在上述介紹的各類方法中,凡是返回值仍然爲Stream
接口的爲函數拼接方法,它們支持鏈式調用;而返回值再也不爲Stream
接口的爲終結方法,再也不支持鏈式調用。以下表所示:
方法名 | 方法做用 | 方法種類 | 是否支持鏈式調用 |
---|---|---|---|
count | 統計個數 | 終結 | 否 |
forEach | 逐一處理 | 終結 | 否 |
filter | 過濾 | 函數拼接 | 是 |
limit | 取用前幾個 | 函數拼接 | 是 |
skip | 跳過前幾個 | 函數拼接 | 是 |
concat | 組合 | 函數拼接 | 是 |
來看一個簡單的函數式接口以應用Lambda表達式 , 在accept方法中接收字符串 , 目的就是爲了打印顯示字符串 , 那麼經過Lambda來使用它的代碼很簡單:
public class DemoPrintSimple {
private static void printString(Consumer<String> data, String str) {
data.accept(str);
}
public static void main(String[] args) {
printString(s -> System.out.println(s), "Hello World");
}
}
複製代碼
因爲lambda表達式中,調用了已經實現的println方法 ,可使用方法引用替代lambda表達式.
符號表示 : ::
符號說明 : 雙冒號爲方法引用運算符,而它所在的表達式被稱爲方法引用。
**應用場景 : **若是Lambda要表達的函數方案 , 已經存在於某個方法的實現中,那麼則可使用方法引用。
如上例中,System.out對象中有個println(String)方法 , 剛好就是咱們所須要的 , 那麼對於Consumer接口做爲參數,對比下面兩種寫法,徹底等效:
- Lambda表達式寫法:s -> System.out.println(s); 拿到參數以後經Lambda之手,繼而傳遞給System.out.println方法去處理。
- 方法引用寫法:System.out::println 直接讓System.out中的println方法來取代Lambda。
**推導與省略 : ** 若是使用Lambda,那麼根據「可推導就是可省略」的原則,無需指定參數類型,也無需指定的重載形式——它們都將被自動推導。而若是使用方法引用,也是一樣能夠根據上下文進行推導。函數式接口是Lambda的基礎,而方法引用是Lambda的簡化形式。
只要「引用」過去就行了:
public class DemoPrintRef {
private static void printString(Consumer<String> data, String str) {
data.accept(str);
}
public static void main(String[] args) {
printString(System.out::println, "HelloWorld");
}
}
複製代碼
請注意其中的雙冒號::
寫法,這被稱爲「方法引用」,而雙冒號是一種新的語法。
這是最多見的一種用法,與上例相同。若是一個類中已經存在了一個成員方法,則能夠經過對象名引用成員方法,代碼爲:
public class DemoMethodRef {
public static void main(String[] args) {
String str = "hello";
printUP(str::toUpperCase);
}
public static void printUP(Supplier< String> sup ){
String apply =sup.get();
System.out.println(apply);
}
}
複製代碼
因爲在java.lang.Math
類中已經存在了靜態方法random
,因此當咱們須要經過Lambda來調用該方法時,可使用方法引用 , 寫法是:
public class DemoMethodRef {
public static void main(String[] args) {
printRanNum(Math::random);
}
public static void printRanNum(Supplier<Double> sup ){
Double apply =sup.get();
System.out.println(apply);
}
}
複製代碼
在這個例子中,下面兩種寫法是等效的:
n -> Math.abs(n)
Math::abs
因爲構造器的名稱與類名徹底同樣,並不固定。因此構造器引用使用類名稱::new
的格式表示。首先是一個簡單的Person
類:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
複製代碼
要使用這個函數式接口,能夠經過方法引用傳遞:
public class Demo09Lambda {
public static void main(String[] args) {
String name = "tom";
Person person = createPerson(Person::new, name);
System.out.println(person);
}
public static Person createPerson(Function<String, Person> fun , String name){
Person p = fun.apply(name);
return p;
}
}
複製代碼
在這個例子中,下面兩種寫法是等效的:
name -> new Person(name)
Person::new
數組也是Object
的子類對象,因此一樣具備構造器,只是語法稍有不一樣。若是對應到Lambda的使用場景中時,須要一個函數式接口:
在應用該接口的時候,能夠經過方法引用傳遞:
public class Demo11ArrayInitRef {
public static void main(String[] args) {
int[] array = createArray(int[]::new, 3);
System.out.println(array.length);
}
public static int[] createArray(Function<Integer , int[]> fun , int n){
int[] p = fun.apply(n);
return p;
}
}
複製代碼
在這個例子中,下面兩種寫法是等效的:
length -> new int[length]
int[]::new
注意 : 方法引用是對Lambda表達式符合特定狀況下的一種縮寫,它使得咱們的Lambda表達式更加的精簡,也能夠理解爲Lambda表達式的縮寫形式 , 同窗們能夠嘗試着 , 將以前使用lambda的地方 , 改寫成方法引用的形式 ,不過要注意的是方法引用只能"引用"已經存在的方法!
後續精彩連載文章, 敬請期待:
- 歡迎留言,寫下你感興趣的技術話題,霈哥話優先編寫哦~!
觀看更多文章,請移步至 搞後端開發,邊摸魚邊跟他學就夠了,靠譜!🔥
若對你和身邊的朋友有幫助, 抓緊關注 IT霈哥 點贊! 點贊! 點贊! 評論!收藏! 分享給更多的朋友共同窗習交流, 天天持續更新離不開你的支持!
歡迎關注個人B站,未來會發布文章同步視頻~~~ 歡迎關注個人公衆號,獲取更多資料~~~ 歡迎關注個人公衆號,獲取更多資料~~~