1. Stream流
在java8中,得益於Lambda所帶來的函數式變成,引入一個***全新的Stream概念***,用於解決已有集合類庫既有的弊端。
1.1 傳統集合
1.1.1 傳統集合操作多步遍歷代碼
/**
* 用傳統的方式,遍歷集合,對集合中的元素進行過濾
* 從集合中查詢出以張開頭且姓名長度爲3的人,存儲到一個新的集合中
* @author kevin
*
*/
public class TestList1 {
public static void main(String[] args) {
//創建一個List集合,存儲姓名
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
//對List集合中的元素進行過濾,只要以張開頭的元素,存儲到一個新的集合中
List<String> listA = new ArrayList<>();
for (String str : list) {
if(str.startsWith("張")) {
listA.add(str);
}
}
//對listA集合進行過濾,只要姓名長度爲3的人,存儲到一個新的集合中
List<String> listB = new ArrayList<>();
for (String str : listA) {
if(str.length() == 3) {
listB.add(str);
}
}
//打印輸出listB集合
for (String str : listB) {
System.out.println(str);
}
}
}
輸出結果爲:
張無忌
張三丰
1.1.2 循環遍歷的弊端
Java8的Lambda讓我們可以更加專注於***做什麼***(What),而不是***怎麼做***(How)。
- for循環的語法就是“怎麼做”
- for循環的循環體纔是"做什麼"
爲什麼使用循環?因爲要進行遍歷,但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而***並不是從第一個到最後一個順序處理的循環***。前者是目的,後者是方式
。
1.1.3 Stream的更優寫法
/**
* 使用Stream流的方式,遍歷集合,對集合中的數據進行過濾
* Stream流是JDK1.8之後出現的
* 關注的是做什麼,而不是怎麼做
* @author kevin
*
*/
public class TestList2 {
public static void main(String[] args) {
//創建一個List集合,存儲姓名
List<String> list = new ArrayList<>();
list.add("張無忌");
list.add("周芷若");
list.add("趙敏");
list.add("張強");
list.add("張三丰");
list.stream()
.filter(name -> name.startsWith("張"))
.filter(name -> name.length() == 3)
.forEach(name -> System.out.println(name));
}
}
1.2 流式思想描述
類似於工廠車間的“生產流水線”
拼好一個“模型”步驟方案,然後再按照方案去執行它;方案就是一種“函數模型”。
而***模型***每一步都是一個***流***,調用指定的方法,可以從一個流模型轉換爲另一個流模型,直到最終步驟得到結果。
中間步驟對函數模型的操作,集合元素並沒有被真正處理,只有當終結方法執行時候,整個模型纔會按照指定策略執行操作,而這得益於Lambda的延遲執行特性。
“Stream流”其實是一個集合元素的函數模型,它並不是集合,也不是數據結構,其本身並不存儲任何元素(或其地址值)。
Stream(流)是一個來自數據源的元素隊列
- 元素是特定類型的對象,形成一個隊列。Java中的Stream並不會存儲元素,而是按需計算。
- 數據源 流的來源,可以是集合,數組等。
和以前的Collection操作不同,Stream操作還有兩個基礎的特徵:
- Pipelining:中操作都會返回流對象本身,這樣多個操作可以串聯成一個管道,如圖流式風格(fluent style),這樣做可以操作進行優化,比如延遲執行(laziness)和短路(short-circuting)
- 內部迭代:以前對集合遍歷都是通過Iterator或者增強for的方式。顯示的在集合外部進行迭代,這叫做外部迭代。Stream提供了內部迭代的方式,流可以直接調用遍歷方法。
當使用一個流的時候,通常包括三個基本步驟:獲取一個數據源(source)->數據轉換->執行操作獲取想要的結果,每次轉換原有Stream對象不改變,返回一個新的Stream對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。
1.3 獲取流
java.util.stream.Stream<T>
是Java8新加入的最常用的流接口。(這並不是一個函數式接口)
有以下幾種常用的方式獲取流:
- 所有的
Collection
集合都可以通過stream
默認方法獲取流
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
Stream
接口的靜態方法of
可以獲取數組對應的流
@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
參數是一個可變參數,那麼我們就可以傳遞一個數組
根據Collection獲取流
根據Map獲取流
根據數組獲取流
例子:獲取流Demo
public class TestGetStream {
public static void main(String[] args) {
//把集合轉換爲Stream流
List<String> list = new ArrayList<>();
Stream<String> streamList = list.stream();
Set<String> set = new HashSet<>();
Stream<String> streamSet = set.stream();
Map<String, String> map = new HashMap<>();
//獲取鍵,存儲到一個Set集合中
Set<String> keySet = map.keySet();
Stream<String> streamKeySet = keySet.stream();
//獲取值,存儲到一個Collection集合中
Collection<String> values = map.values();
Stream<String> streamValues = values.stream();
//獲取鍵值對(鍵與值的映射關係 entrySet)
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> streamEntries = entries.stream();
//將數組轉換爲Stream流
Stream<Integer> stream = Stream.of(1,2,3,4,5);
//可變參數可以傳遞數組
Integer[] arr = {1,2,3,4,5};
Stream<Integer> streamArr1 = Stream.of(arr);
String[] arr1 = {"aaa","bbb","ccc"};
Stream<String> streamArr2 = Stream.of(arr1);
}
}
1.4 常用方法
流模型的操作很豐富,這裏介紹一些常用的API。這些方法可以被分爲兩種:
[外鏈圖片轉存失敗(img-nLd7wneM-1562089104671)(E:\學習整理\java\jdk1.8新特性\img\stream方法1.png)]
- 延遲方法:返回值類型仍然是
Stream
接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其餘方法均爲延遲方法) - 終結方法:返回值類型不再是
Stream
接口自身類型的方法,因此不能支持類似StringBuilder
那樣的鏈式調用。包括count
和forEach
方法等
1.4.1 逐一處理:forEach
void forEach(Consumer<? super T> action);
該方法接收一個Consumer
接口函數,會將每一個流元素交給該函數進行處理。
java.util.function.Consumer接口是一個消費型接口。
Consumer接口包含抽象方法void accept(T t),意爲消費一個指定泛型的數據。
例子:
/**
* forEach方法用來遍歷流中的數據
* 是一個終結方法,遍歷之後就不能繼續使用Stream流中的其他方法
* @author kevin
*
*/
public class TestForEach {
public static void main(String[] args) {
//獲取一個Stream流
Stream<String> stream = Stream.of("Lily","Helena","Lucy","Simon");
//使用Stream流中的forEach對Stream流中的數據進行遍歷
/*stream.forEach((String str) -> {
System.out.println(str);
});*/
stream.forEach(str -> System.out.println(str));
}
}
輸出結果爲:
Lily
Helena
Lucy
Simon
1.4.2 過濾:filter
可以通過filter
方法將一個流轉換成另一個子集流,方法簽名:
Stream<T> filter(Predicate<? super T> predicate);
java.util.function.Predicate接口,其中唯一的抽象方法 boolean test(T t),該方法會產生一個boolean值結果,代表指定的條件是否滿足。如果結果爲true,那麼Stream流的filter方法將會留用元素;如果結果爲false,那麼filter方法將會捨棄元素。
例子:
public class TestFilter {
public static void main(String[] args) {
//創建一個Stream流
Stream<String> stream = Stream.of("Lily","Helena","Lucy","Simon");
//對Stream流中的元素進行過濾,只要以“L”開頭的
Stream<String> stream1 = stream.filter(str -> str.startsWith("L"));
//遍歷stream1流
stream1.forEach(name -> System.out.println(name));
/**
* Stream流屬於管道流,只能被消息(使用)一次
* 第一個Stream流調用完畢方法,數據就會流轉到下一個Stream上
* 而這時第一個Stream流已經使用完畢,就會關閉了
* 所以第一個Stream流就不能再調用方法了
* java.lang.IllegalStateException: stream has already been operated upon or closed
*/
//stream.forEach(name -> System.out.println(name));
}
}
輸出結果爲:
Lily
Lucy
1.4.3 映射:map
將流中的元素映射到另外一個流中,方法簽名爲:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
java.util.function.Function<T, R>接口
抽象方法 R apply(T t); 意爲將一種T類型轉換成R類型,而這種轉換的操作,就成爲“映射”
例子:
public class TestMap {
public static void main(String[] args) {
//獲取一個String類型的Stream流
Stream<String> stream = Stream.of("1","2","3","4");
//使用map方法,將字符串類型的整數,轉換(映射)爲Integer類型的整數
Stream<Integer> stream1 = stream.map((String str) -> {
return Integer.parseInt(str);
});
//遍歷Stream流
stream1.forEach(i -> System.out.println(i));
}
}
1.4.4 統計個數:count
如集合Collection中的size
方法一樣,流提供count
方法來返回其中的元素個數:
long count();
該方法返回一個long值代表元素個數(不像集合Collection那樣是int值)。
例子:
public class TestCount {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
Stream<Integer> stream = list.stream();
long count = stream.count();
System.out.println(count);//7
}
}
1.4.5 取用前幾個:limit
limit
方法可以對流進行截取,只取用前幾個,方法簽名:
Stream<T> limit(long maxSize);
參數是一個long類型,如果集合當前長度大於參數則進行截取;否則不進行操作。
例子:
public class TestLimit {
public static void main(String[] args) {
String[] arr = {"aaa","bbb","ccc","ddd","eee"};
Stream<String> stream = Stream.of(arr);
//使用limit對Stream流中的元素進行截取,只要前3個元素
Stream<String> stream2 = stream.limit(3);
stream2.forEach(str -> System.out.println(str));
}
}
輸出結果爲:
aaa
bbb
ccc
1.4.6 跳過前幾個:skip
如果需要跳過前n個元素,可以使用skip
方法獲取一個截取之後的新流:
Stream<T> skip(long n);
如果流的長度大於n,則跳過前n個;否則將會得到一個長度爲0的空流。
例子:
public class TestSkip {
public static void main(String[] args) {
String[] arr = {"aaa","bbb","ccc","ddd","eee"};
Stream<String> stream = Stream.of(arr);
//使用skip方法跳過前3個元素
Stream<String> stream2 = stream.skip(3);
stream2.forEach(str -> System.out.println(str));
}
}
輸出結果爲:
ddd
eee
1.4.7 組合:concat
如果有兩個流,希望合併成爲一個流,那麼可以使用Stream
接口的靜態方法concat
:
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
@SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}
備註:這是一個靜態方法,與
java.lang.String
當中的concat
方法是不同的。
例子:
public class TestConcat {
public static void main(String[] args) {
String[] arr = {"aaa","bbb","ccc","ddd","eee"};
Stream<String> stream1 = Stream.of(arr);
Stream<String> stream2 = Stream.of("Lily","Lucy","Helena","Simon");
//把以上兩個流組合爲一個流
Stream<String> stream = Stream.concat(stream1, stream2);
//遍歷concat流
stream.forEach(str -> System.out.println(str));
}
}
輸出結果爲:
aaa
bbb
ccc
ddd
eee
Lily
Lucy
Helena
Simon
2. 方法引用
在使用Lambda表達式的時候,我們實際上傳進去的代碼就是一種解決方案:拿什麼參數進行什麼操作。
如果已有地方存在相同的操作方案,這樣就會導致重複寫。可以使用方法引用來優化。
例子:
- 函數式接口 Printable.java
@FunctionalInterface
public interface Printable {
void print(String s);
}
- 測試類 TestPrintable.java
public class TestPrintable {
public static void printString(Printable p) {
p.print("Hello World");
}
public static void main(String[] args) {
printString((s) -> {
System.out.println(s);
});
/*
* 分析:
* Lambda表達式的目的,打印參數傳遞的字符串
* 把參數s,傳遞給了System.out對象,調用out對象中的方法println對字符串進行了輸出
* 注意:
* 1.System.out對象是已經存在的
* 2.println方法也是已經存在的
* 所以我們可以使用方法引用來優化Lambda表達式
* 可以使用System.out方法直接引用(調用)println方法
*/
printString(System.out :: println);
}
}
輸出結果爲:
Hello World
Hello World
2.1 方法引用符
雙冒號::
爲引用運算符,而它所在的表達式被稱爲方法引用。如果Lambda要表達的函數方案已經存在於某個方法實現中,那麼則可以通過雙冒號來引用該方法作爲Lambda的替代者。
2.2 通過對象名引用成員方法
- 函數式接口 Printable.java
@FunctionalInterface
public interface Printable {
void print(String s);
}
- MethodRerObject.java
public class MethodRerObject {
//定義一個成員方法,傳遞字符串,把字符串按照大寫輸出
public void printUpperCaseString(String str) {
System.out.println(str.toUpperCase());
}
}
- 測試類 TestPrintable2.java
/**
* 通過對象名引用成員方法
* 使用前提是對象名是已經存在的,成員方法也是已經存在
* 就可以使用對象名來引用成員方法
* @author kevin
*
*/
public class TestPrintable2 {
//定義一個方法,方法的參數傳遞Printable接口
public static void printString(Printable p) {
p.print("Hello World");
}
public static void main(String[] args) {
//調用printString方法,方法的參數Printable是一個函數式接口,所以可以傳遞Lambda表達式
printString((s) -> {
//創建MethodRerObject對象
MethodRerObject m = new MethodRerObject();
//調用MethodRerObject對象中的成員方法printUpperCaseString,把字符串按照大寫輸出
m.printUpperCaseString(s);
});
/*
* 使用方法引用優化Lambda
* 對象是已經存在的MethodRerObject
* 成員方法也是已經存在的printUpperCaseString
* 所以我們可以使用對象名引用成員方法
*/
//創建MethodRerObject對象
MethodRerObject m = new MethodRerObject();
printString(m :: printUpperCaseString);
}
}
輸出結果爲:
HELLO WORLD
HELLO WORLD
2.3 通過類名稱引用靜態方法
- 函數式接口 Calcable.java
@FunctionalInterface
public interface Calcable {
//定義一個抽象方法,傳遞一個整數,對整數進行絕對值計算並返回
int calsAbs(int number);
}
- 測試類 TestStaticMethodReference.java
/**
* 通過類名稱引用靜態方法
* 類已經存在,靜態成員方法也已經存在
* 就可以通過類名直接引用靜態成員方法
* @author kevin
*
*/
public class TestStaticMethodReference {
//定義一個方法,方法的參數傳遞要計算絕對值的整數和函數式接口Calcable
public static int method(int number, Calcable c) {
return c.calsAbs(number);
}
public static void main(String[] args) {
//調用method方法,傳遞計算絕對值的整數和Lambda表達式
int number = method(-10, (n) -> {
//對參數進行絕對值計算並返回結果
return Math.abs(n);
});
System.out.println(number);
/*
* 使用方法引用優化Lambda表達式
* Math類是存在的
* abs計算絕對值的靜態方法也是已經存在的
* 所以我們可以直接通過類名引用靜態方法
*/
int number2 = method(-10, Math :: abs);
System.out.println(number2);
}
}
輸出結果爲:
10
10
2.4 通過super引用成員方法
如果存在繼承關係,當Lambda中需要出現super調用時,也可以使用方法引用進行替代。
- 函數式接口 Greetable.java
/*
* 定義見面的函數式接口
*/
@FunctionalInterface
public interface Greetable {
//定義一個見面的方法
void greet();
}
- 父類 Human.java
/*
* 定義父類
*/
public class Human {
//定義一個sayHello的方法
public void sayHello() {
System.out.println("Hello 我是Human");
}
}
- 測試類 Man.java
/**
* 通過Super引用成員方法
* @author kevin
*
*/
public class Man extends Human {
//子類重寫父類sayHello的方法
@Override
public void sayHello() {
System.out.println("Hello,我是Man!");
}
//定義一個方法參數傳遞Greetable接口
public void method(Greetable g) {
g.greet();
}
public void show() {
//調用method方法
/*method(() -> {
Human h = new Human();
h.sayHello();
});*/
/*method(() -> {
super.sayHello();
});*/
/*
* 使用super引用類的成員方法
* super是已經存在的
* 父類的成員方法sayHello也是已經存在的
* 所以我們可以直接使用super引用弗雷德成員方法
*/
method(super :: sayHello);
}
public static void main(String[] args) {
new Man().show();
}
}
輸出結果爲:
Hello 我是Human
2.5 通過this引用成員方法
this代表當前對象,如果需要引用的方法就是當前類中的成員方法,那麼可以使用“this::成員方法”的各式來使用方法引用。
- 函數式接口 Richable.java
/*
* 定義一個富有的函數式接口
*/
@FunctionalInterface
public interface Richable {
//定義一個想買什麼就買什麼的方法
void buy();
}
- 測試類 Husband.java
/**
* 通過this引用本類的成員方法
* @author kevin
*
*/
public class Husband {
//定義一個買房子的方法
public void buyHouse() {
System.out.println("北京二環內買一套四合院!");
}
//定義一個結婚的方法,參數傳遞Richable接口
public void marry(Richable r) {
r.buy();
}
//定義一個非常高興的方法
public void soHappy() {
//調用結婚的方法,方法的參數Richable是一個函數式接口,傳遞Lambda表達式
/*marry(() -> {
this.buyHouse();
});*/
/*
* 使用方法引用優化Lambda表達式
* this是已經存在的
* 本類的成員方法buyHouse也是已經存在的
* 所以我們可以直接使用this引用本類的成員方法buyHouse
*/
marry(this :: buyHouse);
}
public static void main(String[] args) {
new Husband().soHappy();
}
}
輸出結果爲:
北京二環內買一套四合院!
2.6 類的構造器引用
由於構造器的名稱與類名完全一樣,並不固定。所以構造器引用使用類名稱::new
的格式表示。
- Person.java
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(String name) {
super();
this.name = name;
}
public Person() {
super();
}
}
- 函數式接口 PersonBuilder.java
/*
* 定義一個創建Person對象的函數式接口
*/
@FunctionalInterface
public interface PersonBuilder {
//定義一個方法,根據傳遞的姓名,創建Person對象返回
Person builderPerson(String name);
}
- 測試類 Demo.java
/**
* 類的構造器(構造方法)引用
* @author kevin
*
*/
public class Demo {
//定義一個方法,參數傳遞姓名和PersonBuilder接口,方法中通過姓名創建Person對象
public static void printName(String name, PersonBuilder pb) {
Person person = pb.builderPerson(name);
System.out.println(person.getName());
}
public static void main(String[] args) {
//調用printName方法,方法的參數PersonBuilder接口是一個函數式接口,可以傳遞Lambda
printName("迪麗熱巴", (String name) -> {
return new Person(name);
});
/*
* 使用方法引用優化Lambda表達式
* 構造方法new Person(String name)已知
* 創建對象已知 new
* 就可以使用Person引用new創建對象
*/
printName("古力娜扎", Person :: new);
}
}
輸出結果爲:
迪麗熱巴
古力娜扎
2.7 數組的構造器引用
數組也是object
的子類對象,所以同樣具有構造器,只是語法稍微不同。
- 函數式接口 ArrayBuilder.java
/*
* 定義一個創建數組的函數式接口
*/
@FunctionalInterface
public interface ArrayBuilder {
//定義一個創建int類型數組的方法,參數傳遞數組的長度,返回創建好的int類型數組
int[] builderArray(int length);
}
- 測試類 Demo.java
/*
* 數組的構造器引用
*/
public class Demo {
/*
* 定義一個方法
* 方法的參數傳遞創建數組的長度和ArrayBuilder接口
* 方法的內部根據傳遞的長度使用ArrayBuilder中的方法創建數組並返回
*/
public static int[] createArray(int length, ArrayBuilder ab) {
return ab.builderArray(length);
}
public static void main(String[] args) {
//調用createArray方法,傳遞數組的長度和Lambda表達式
int[] arr1 = createArray(10, (len) -> {
//根據數組的長度,創建數組並返回
return new int[len];
});
System.out.println(arr1.length);
/*
* 使用方法引用優化Lambda表達式
* 已知創建的就是int[]數組
* 數組的長度是已知的
* 就可以使用方法引用
* int[]引用new,根據參數傳遞的長度來創建數組
*/
int[] arr2 = createArray(10, int[] :: new);
System.out.println(Arrays.toString(arr2));
System.out.println(arr2.length);
}
}
輸出結果爲:
10
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
10