一:Lambda 表達式
😀Lambda表達式的标準格式:由三部分組成
- 1 : 一些參數
- 2 : 一個箭頭
- 3 : 一段代碼
格式:
( 參數清單 ) -> { 一些重寫方法的代碼 }
說明:
- () : 接口中抽象方法的參數清單,沒有參數,就空着;有參數就寫出參數,多個參數使用逗号分隔
- -> : 傳遞的意思,把參數傳遞給方法體 { }
- { } : 重寫接口的抽象方法的方法體
簡寫Lambda表達式格式:
- 1:(參數清單):括号中參數清單的資料類型 可以省略不寫
- 2:(參數清單):括号中的參數如果隻有一個,那麼類型 和 ( ) 都可以省略
- 3:{代碼}:如果 { }中的代碼隻有一行,無論是否有傳回值,都可以省略 { }, return ,分号
- 注意事項:如果要省略 { }, return ,分号 ,必須一起省略,否則都不能省略
🤭光說不練 假把式 上代碼
使用Lambda表達式建立線程
之前我們使用匿名内部類建立線程是這樣子滴~
使用Lambda表達式替換匿名内部類實作 建立多線程
還可以再簡寫
Lambda表達式的優化:
😀1、方法引用
定義一個列印的函數式接口
//定義一個列印的函數式接口
@FunctionalInterface
interface Printable{
//列印字元串的抽象方法
public abstract void print(String s);
}
Main方法
public class Demo1Print {
/*方法參數傳遞Printable接口,對字元串列印輸出
* */
public static void printString(Printable p) {
p.print("I Love Java");
}
public static void main(String[] args) {
printString(s->System.out.println(s));
/*分析;Lambda表達式的目的,列印參數傳遞的字元串
* 把s傳給了System.out對象,調用out中的方法pritnln(),對字元串進行輸出
* 注意:
* 此時的 System.out對象是存在的,println()方法也是存在的
* 我們可以使用方法引用來優化Lambda表達式,使用System.out對象直接調用println()方法
* 格式: 對象::方法
* 雙冒号為 引用運算符,它所在的表達式被稱為方法引用
* */
printString(System.out::println);
}
}
😀2、通過對象名 引用成員方法
使用前提:對象名 和 成員方法 已經存在 才可以使用 對象名 來引用 成員方法
注意:如果類中的方法是靜态的 就無法使用對象調用 直接用類名 引用 靜态成員方法
練習
public class Demo2_ObjectMethodReference {
public static void printString(Printable p) {
p.print("java");
}
public static void main(String[] args) {
//調用printString方法,方法的參數Printable是一個函數式接口,是以可以傳遞Lambda表達式
printString((s)->{
//建立MethodFer對象
MethodFer mf = new MethodFer();
//使用MethodFer建立的對象 調用 該類中的方法printUpperCase
mf.printUpperCase(s); //輸出JAVA
});
//方法引用(優化)
MethodFer mf2 = new MethodFer();
printString(mf2::printUpperCase); //輸出JAVA
//或者
printString(new MethodFer()::printUpperCase); //輸出JAVA
/*
* printString(MethodFer::printUpperCase);
* Cannot make a static reference to the non-static method
* printUpperCase(String) from the type MethodFer
*/
//通過類 引用 靜态成員方法
printString(MethodFer::printLowerCase); //輸出java
}
}
class MethodFer{
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
public static void printLowerCase(String str) {
System.out.println(str.toLowerCase());
}
}
😀3、通過類名引用靜态成員方法
比如:類Math 中的abs方法是靜态方法
前提:
類已經存在,靜态成員方法也存在, 就可以直接通過類型引用靜态成員方法
測試
public class Demo3_StaticMethodReference {
public static int calcator(int num,Calc c) {
return c.calcABS(num);
}
public static void main(String[] args) {
int num = calcator(-10,n->Math.abs(n));
System.out.println(num);
/*使用方法引用來優化Lambda表達式
* Math類是存在的 abs方法也是存在的
* */
int num2 = calcator(-20,Math::abs);
System.out.println(num2);
}
}
@FunctionalInterface
interface Calc{
public abstract int calcABS(int num);
}
😀4、通過super 引用父類的成員方法,通過this 引用本類的成員方法
public class Demo4_Super_this_MethodReference {
public static void main(String[] args) {
Man man = new Man();
man.show1();
man.show2();
}
}
//定義一個父類
class Human{
//定義sayHello方法
public void Say() {
System.out.println("Say Hello! I am Father");
}
}
//定義一個子類
class Man extends Human{
//重寫父類sayHello方法
public void Say() {
System.out.println("Say Hi! I am son");
}
//定義一個方法 參數傳遞Greetable接口
public void method(Greetable g) {
g.greet();
}
//
public void show1() {
//調用method方法,方法的參數Greetable是一個函數式接口,是以可以傳遞Lambda表達式
// method(()->{
// //建立父類對象
// Human h = new Human();
// //調用父類的sayHello方法
// h.Say();
// });
//因為有子父類關系 是以存在關鍵字super 代表父類 可以直接使用super調用父類的方法
// method(()->{
// super.Say();
// });
/*繼續優化 既然 可以直接使用super調用父類的方法
* 那麼就可以使用super引用類的成員方法
* 因為super是已經存在的 Say方法是已經存在的
* 是以可以直接使用super引用父類的Say方法
*/ method(super::Say);
}
public void show2() {
//調用method方法,方法的參數Greetable是一個函數式接口,是以可以傳遞Lambda表達式
// method(()->{
// //建立本類對象
// Man man = new Man();
// //通過本類對象 調用本類的成員方法
// man.Say();
// });
//關鍵字this 代表本類 可以直接使用this調用本類的方法
// method(()->{
// this.Say();
// });
//繼續優化 使用this 引用本類方法
method(this::Say);
}
}
//定義一個見面的函數式接口
@FunctionalInterface
interface Greetable{
public abstract void greet();
}
😀5、通過構造器 引用成員方法
public class Demo5_ConstructorMethodReference {
//定義一個方法 參數傳遞 姓名 和create接口
public static Person getName(String n,Create c) {
return c.create(n);
}
public static void main(String[] args) {
//調用getName方法
// Person name = getName("神廚小福貴",(s)->{
// return new Person(s);
// });
// System.out.println(name);
/*使用方法引用優化Lambda表達式
* 構造方法 new Person(String name); 已知
* 建立對象 new 已知
* 就可以使用Person 引用new 建立對象
* */
Person name = getName("比卡丘",Person::new);
System.out.println(name);
}
}
//定義一個函數式接口
@FunctionalInterface
interface Create{
//方法傳回值為Person 因為要傳回建立的對象
public abstract Person create(String name);
}
//定義一個類
class Person{
private String name;
public Person(String name) {
super();
this.name = name;
}
public Person() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person [name=" + name + "]";
}
}
😀6、數組的構造器引用
public class Demo6_ArrayMethodReference {
//定義一個傳回數組的方法 參數傳遞 數組的長度 和接口
public static int[] getArr(int len,CreateArr c) {
return c.create(len);
}
public static void main(String[] args) {
//調用getArr方法
int[] arr1 = getArr(10,L->new int[L]);
System.out.println(Arrays.toString(arr1));
System.out.println(arr1.length);
/*使用方法引用優化 Lambda表達式
* 已知 數組類型 和數組建立方式
* int[]引用new,根據參數傳遞的長度來建立數組
* */
int[] arr2 = getArr(20,int[]::new);
System.out.println(Arrays.toString(arr2));
System.out.println(arr2.length);
}
}
//定義一個函數式接口
@FunctionalInterface
interface CreateArr{
//定義一個建立int類型數組的抽象方法 傳回值為int[]
public abstract int[] create(int len);
}
二:鍊式程式設計
思想:
是将多個操作(多行代碼)通過點号
.
連結在一起成為一句代碼,使代碼可讀性好。a(1).b(2).c(3)
下面的函數式程式設計就是用的這種風格
list.stream().filter(str->str.startsWith("李")).forEach(str->System.out.println(str));
三:常用的函數式接口
定義:有且隻有一個抽象方法的接口,稱之為函數式接口
當然接口中可以包含其他的方法( 預設,靜态,私有)
函數式接口的使用:
一般可以作為方法的參數和傳回值類型
函數式接口作為 方法的參數 的執行個體
java.lang.Runnable接口就是一個函數式接口
- 假設有一個startThread方法使用該接口作為參數,那麼就可以使用Lambda表達式進行傳參
- 這種情況其實跟Thread類的構造方法參數為Runnable沒有本質差別
public class RunnableDemo {
//定義一個方法startThread 方法的參數使用 函數式接口Runnable
public static void startThread(Runnable run) {
//開啟多線程
new Thread(run) {}.start();
}
public static void main(String[] args) {
//調用startThread方法,方法的參數是一個接口,那麼我們可以傳遞這個接口的匿名内部類(或實作類)
startThread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" --> "+"線程開啟");
}
});
//調用startThread方法,方法的參數是一個函數式接口,使用Lambda表達式進行傳參
startThread( ()->{
System.out.println(Thread.currentThread().getName()+" --> "+"線程開啟");
});
//Lambda表達式優化版本 省略 {} 和 ;
startThread( ()->System.out.println(Thread.currentThread().getName()+" --> "+"線程開啟"));
}
}
函數式接口作為 傳回值類型 的執行個體
- 當一個方法的傳回值類型是一個函數式接口,那麼就可以直接傳回一個Lambda表達式
- 當需要通過一個方法來擷取一個java.util.Comparator接口類型的對象作為排序器時,就可以調用該方法擷取
public class ComparatorDemo {
//定義一個方法,方法的傳回值類型使用函數式接口Comparator
public static Comparator<String> getComparator1(){
//方法的傳回值類型是一個接口,那麼我們可以傳回這個接口的匿名内部類
return new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//按照字元串的降序排序
return o2.length()-o1.length();
}
};
}
//改進版本 方法的傳回值類型是一個函數式接口,是以我們可以傳回一個Lambda表達式
public static Comparator<String> getComparator2(){
return (String o1,String o2)->{
//使用字元串的降序排序
return o2.length()-o1.length();
};
}
//Lambda表達式優化版本
public static Comparator<String> getComparator3(){
return (o1, o2)-> o2.length()-o1.length();
}
public static void main(String[] args) {
//建立一個字元串數組
String[] arr = {"aaa","ccccc","bbbb"};
//排序前
System.out.println("排序前的數組:");
System.out.println(Arrays.toString(arr));
//進行升序排序
Arrays.sort(arr);
System.out.println("預設升序排序後的數組:");
System.out.println(Arrays.toString(arr));
//進行降序排序
Arrays.sort(arr,getComparator3());
System.out.println("降序排序後的數組:");
System.out.println(Arrays.toString(arr));
}
}
運作結果
這個包就包含了所有的函數式接口
該包下有許多接口,其中 主要的是這四個接口
1、java.util.function.Function<T , R>接口
根據一個類型的資料得到另一個類型的資料,前者 為前置條件 後者 為後置條件
Function接口中最主要的抽象方法為:
R apply(T t),根據類型T的參數擷取類型R的結果,andThen():用來進行組合操作
題目:将String 轉換為Integer
方法:
/*方法參數傳遞一個字元串類型的整數,和一個Function接口,泛型使用<String,Integer>
* 使用Function接口中的方法apply,把字元串類型的整數,轉換為Integer類型的整數
* */
public static void method(String str,Function<String,Integer> fun) {
int num = fun.apply(str);
num+=100;
System.out.println(num+0);
}
main:
// 輸出 234
method("123",new Function<String,Integer>(){
@Override
public Integer apply(String t) {
return Integer.parseInt(t);
}
});
//使用Lambda表達式簡化
method("234",str-> Integer.parseInt(str));
題目:把String類型的“123” 轉換為Integer類型的整數,再加10 ,再轉為String 使用 andThen
方法:
public static void method(String str,Function<String,Integer> fun1,Function<Integer,String> fun2) {
/* 分兩步操作
int num = fun1.apply(str);
str = fun2.apply(num);
System.out.println(str);
*/
//使用andThen合成一步
String app = fun1.andThen(fun2).apply(str);
System.out.println(app+0);
}
main:
2、java.util.function.Supplier 接口
僅包含一個無參方法
- T get() 用來擷取一個泛型參數指定類型的對象資料
- Supplier 接口被稱之為生産型接口(供應商接口)
- 指定接口的泛型是什麼類型,那麼接口中的get方法就會生産什麼類型的資料
練習
public class SupplierDemo1 {
//定義一個方法,方法的參數傳遞Supplier<T>接口,泛型執行String,get方法就會傳回一個String
public static String getString(Supplier<String> sup) {
return sup.get();
}
public static void main(String[] args) {
System.out.println(getString( ()->{ return "光頭強";}));
System.out.println(getString( ()-> "天才威"));
}
}
題目:求數組元素的最大值:
使用Supplier接口作為方法參數類型,通過Lambda表達式求出int數組的最大值
public class SupplierDemo_GetArrMax {
public static int getMax(Supplier<Integer> sup) {
return sup.get();
}
public static void main(String[] args) {
//定義一個整型數組
int[] arr = {1,8,5,9,2,55,11,77,66};
//調用getMax方法,使用Supplier接口作為方法參數類型
//通過Lambda表達式求出int數組的最大值
int maxNum = getMax( ()->{
//定義一個變量存放數組中第一個數
int num = arr[0];
//循環查找
for (int i : arr) {
if(i>num)
num = i;
}
return num;
});
System.out.println("數組中最大值為:"+maxNum);
}
}
3、java.util.function.Predicate 接口
作用:對某種資料類型的資料進行判斷,結果傳回一個boolean值
Predicate接口中包含一個抽象方法:
boolean test(T t):用來對指定資料類型資料進行判斷的方法
- 結果:
- 符合條件:傳回true
- 不符合條件:傳回false
練習
public class PredicateDemo {
/*
* 定義一個方法
* 參數傳遞一個String類型的字元串
* 傳遞一個Predicate接口,泛型使用String
* 使用Predicate中的方法test對字元串進行判斷,并把判斷結果傳回
* */
public static boolean method(String str,Predicate<String> pre) {
return pre.test(str);
}
public static void main(String[] args) {
//如果字元串長度大于五則 傳回true
System.out.println(method("absafjsdf",(str)->{ return str.length()>5;}));
System.out.println(method("abc",str-> str.length()>5 ));
}
}
其他方法測試
public class PredicateDemo {
//and執行個體 條件必須都滿足 相當于&&
public static boolean checkingAnd(String str,Predicate<String> p1,Predicate<String> p2) {
// return p1.test(str) && p2.test(str);
return p1.and(p2).test(str);
}
//or執行個體 條件滿足一個即可 相當于 ||
public static boolean checkingOr(String str,Predicate<String> p1,Predicate<String> p2) {
// return p1.test(str) || p2.test(str);
return p1.or(p2).test(str);
}
//negate表示取反 即為!
public static boolean checkingNegate(String str,Predicate<String> p1) {
// return !p1.test(str);
return p1.negate().test(str);
}
public static void main(String[] args) {
//And執行個體 true
System.out.println(checkingAnd("ABFDFD",str->str.length()>5 , str->str.contains("A")));
//Or執行個體 true
System.out.println(checkingOr("ABFDFD",str->str.length()>10 , str->str.contains("A")));
//Negate執行個體 false
System.out.println(checkingNegate("adadad",str->str.length()>5));
}
}
4、java.util.function.Consumer 接口
正好與Supplier接口相反,它不是生産一個資料,而是消費一個資料,其資料類型由泛型決定
- Consumer接口中包含抽象方法 void accept( T t),意為消費一個指定泛型的資料
- Consumer接口是一個消費型接口,泛型指定什麼類型,就可以使用accpet方法消費什麼類型的資料
- 至于具體如何消費( 使用 ),需要我們自定義( 輸出,計算。。。)
練習
public class ConsumerDemo {
/*定義一個方法 方法的參數傳遞一個字元串的姓名
* 方法的參數傳遞Consumer接口,泛型使用String
* 可以使用Consumer接口消費字元串的姓名
* */
public static void consumerName(String name,Consumer<String> con) {
con.accept(name);
}
public static void main(String[] args) {
//使用匿名内部類
consumerName("光頭強",new Consumer<String>() {
public void accept(String t) {
System.out.println(t);
}
});
//因為此接口為 函數式接口 是以可以使用Lambda表達式
// (String t )->{ System.out.println(t); }
consumerName("天才威", t->System.out.println(t));
//實作字元串反轉輸出
consumerName("翠花",t-> System.out.println(new StringBuffer(t).reverse()) );
}
}
Consumer接口的預設方法addThen
作用:需要兩個Consumer接口,可以把兩個Consumer接口組合到一起,再對資料進行消費
例如:
Consumer con1
Consumer con2
String s = “Hello”;
con1.accpet(s);
con2.accpet(s);
- 改善:連接配接兩個Consumer接口,再進行消費
- con1.addThen(con2).accpet(s); 誰寫前邊誰先消費
public class ConsumerDemo {
//定義一個方法,方法的參數傳遞一個字元串和兩個Consumer接口,接口泛型使用字元串類型
public static void method(String str,Consumer<String> con1,Consumer<String> con2) {
con1.accept(str);
con2.accept(str);
}
public static void method2(String str,Consumer<String> con1,Consumer<String> con2) {
con1.andThen(con2).accept(str);
}
public static void main(String[] args) {
//調用method方法,傳遞一個字元串,兩個Lambda表達式
method("I am CSNZ",t->System.out.println(t.toUpperCase()),t->System.out.println(t.toLowerCase()));
method2("I am CSNZ",t->System.out.println(t.toUpperCase()),t->System.out.println(t.toLowerCase()));
}
}
題目:字元串數組中存有多條資訊,按照格式:“姓名:xx。性别:xx。”列印
使用兩個Consumer接口 最後andThen
public static void print(String[] a,Consumer<String> c1,Consumer<String> c2) {
for (String str : a) {
c1.andThen(c2).accept(str);
}
}
public static void main(String[] args) {
//定義一個字元串數組
String[] arr = {"迪麗熱巴,女","古力娜紮,女","馬兒紮哈,男"};
print(arr,(str)->{
System.out.print("姓名:"+str.split(",")[0]+"。");
},(str)->{
System.out.print("性别:"+str.split(",")[1]+"。\n");
});
}
}
執行結果:
四:Stream流式計算
java.util.stream.Stream 是java 1.8 新加入的最常用接口。(這并不是一個函數式接口,裡面不止一個抽象方法)
擷取一個流的方式:
所有的Collection集合都可以通過stream的預設方法擷取流
Stream接口的靜态方法of可以擷取可變長參數(底層即數組)對應的流
參數是一個可變參數,那麼我們就可以傳遞一個數組
練習
public class StreamDemo {
public static void main(String[] args) {
//把集合轉換為stream
List<String> arrayList = new ArrayList<String>();
Stream<String> stream1 = arrayList.stream();
Set<String> hashset = new HashSet<>();
Stream<String> stream2 = hashset.stream();
//如果是Map集合的話(雙列集合)
Map<String,String> hashMap = new HashMap<String,String>();
//擷取鍵,存儲到一個set集合中,再轉換成流
Set<String> keySet = hashMap.keySet();
Stream<String> stream3 = keySet.stream();
//擷取值,存儲到一個Collection集合中,再轉換成流
Collection<String> values = hashMap.values();
Stream<String> stream4 = values.stream();
//擷取鍵值對(鍵與值的映射關系 entrySet)
Set<Entry<String, String>> entrySet = hashMap.entrySet();
Stream<Entry<String, String>> stream5 = entrySet.stream();
//把數組轉換為Stream流
Stream<Integer> stream6 = Stream.of(1,2,3,4,5);
//可變長參數可傳遞數組
int[] arr1 = {666,7,888};
Stream<int[]> stream7 = Stream.of(arr1);
Integer[] arr2 = {666,7,888};
Stream<Integer> stream8 = Stream.of(arr2);
String[] arr3 = {"a","bb","ccc"};
Stream<String> stream9 = Stream.of(arr3);
}
}
Stream流中常用方法 forEach
- void forEach( Consumer <? super T> action);
- 該方法接收一個Consumer接口函數,會将每一個流元素交給該函數處理
- Consumer接口是一個消費型的函數式接口,可以傳遞Lambda表達式進行消費
- Consumer中的抽象方法
- void accept(T t);
- forEach:即周遊流中的資料 是一個終結方法 周遊之後就不能調用stream流中的其他方法
public static void method1() {
//建立一個Stream流
Stream<Integer> stream1 = Stream.of(1,6,4,2,8);
//使用Stream流中的方法forEach對流中的資料進行周遊
stream1.forEach( str->System.out.println(str));
}
Stream流中的常用方法filter:用于對Stream流中的資料進行過濾
- Stream filter( Predicate<? super T> pre);
- filter方法的參數是一個函數式接口,是以可以傳遞Lambda表達式,對資料進行過濾
- Predicate中的抽象方法
- boolean test(T t);
public static void method2() {
//建立一個流
Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);
//對流中的資料進行過濾 隻要偶數
Stream<Integer> stream3 = stream2.filter(num->num%2==0);
//對新的流進行周遊
stream3.forEach(num->System.out.println(num));
}
如果需要将流中資料映射到另一個流中,可以使用map方法
- Stream map(Function<? super T, ? extends R> mapper);
- 該接口需要一個Function函數式接口參數,可以将目前流中的T類型資料轉換為另一種R類型的資料
- Function中的抽象方法
- R apply(T t);
public static void method3() {
//建立一個流
Stream<String> stream1 = Stream.of("1","2","3","4","5");
//将字元串轉換為整數類型
Stream<Integer> stream2 = stream1.map(str->Integer.parseInt(str));
stream2.forEach(num->System.out.println(num));
}
Stream流中的常用方法:count:用于統計 流中元素個數
- 是一個終結方法,傳回值是一個long類型的整數,是以在這之後無法調用其他的方法
public static void method4() {
//建立一個流
Stream<String> stream1 = Stream.of("1","2","3","4","5");
long count = stream1.count();
System.out.println(count);
}
Stream流中的常用方法:limit:用于截取流中的元素,隻取前n個
- Stream limit(long maxSize):
- 參數是一個long型,如果集合目前長度大于參數則進行截取,否則不進行操作
- limit方法是一個延遲方法,對流進行截取,傳回一個新的流,可以繼續調用流中的其他方法
public static void method5() {
//建立一個流
Stream<String> stream1 = Stream.of("火花","水藍籃","喵喵","少林呱呱","邪惡玄武");
//對流中的元素進行截取
Stream<String> stream2 = stream1.limit(3);
//對截取的流進行周遊
stream2.forEach(str->System.out.println(str));
}
Stream流中的常用方法:skip:用于跳過元素
- Stream skip(long n);
- 如果流的目前長度大于n,則跳過前n個,否則會得到一個長度為0的流
public static void method6() {
//建立一個流
Stream<String> stream1 = Stream.of("火花","水藍籃","喵喵","少林呱呱","邪惡玄武");
//對流進行跳過截取
Stream<String> stream2 = stream1.skip(3);
//對截取的流進行周遊
stream2.forEach(str->System.out.println(str));
}
Stream流中的常用方法:concat:用于把流組合到一起
- static Stream concat(Stream <? extends T> a , Stream<? extends T> b)
public static void method7() {
//建立一個流
Stream<String> stream1 = Stream.of("火花","水藍籃","喵喵","少林呱呱","邪惡玄武");
//建立另一個流
Stream<String> stream2 = Stream.of("1","2","3","4","5");
//将兩個流進行合并
Stream<String> stream3 = Stream.concat(stream1, stream2);
//周遊合并的流
stream3.forEach(str->System.out.println(str));
}
執行結果: 題目:使用stream流對數組中的元素進行篩選周遊
public class Stream_List_demo {
public static void main(String[] args) {
//建立一個存儲元素的集合
ArrayList<String> list = new ArrayList<>();
list.add("李白");
list.add("李黑");
list.add("白黑");
list.add("黑白");
list.add("李黑白");
/*普通方法篩選和周遊
ArrayList<String> list2 = new ArrayList<>();
for(String s:list) {
if(s.startsWith("李") && s.length()==2) {
list2.add(s);
}
}
for (String str : list2) {
System.out.println(str);
}
*/
//使用stream流篩選周遊 結合函數式程式設計
list.stream().filter(str->str.startsWith("李"))
.filter(str->str.length()==2)
.forEach(str->System.out.println(str));
}
}
Stream流的特點:
- 它屬于管道流,隻能被消費(使用)一次
- 第一個Stream流調用完畢後,資料就會傳到下一個Stream上
- 此時第一個流 就會自動關閉 無法再進行使用了