JDK8 - lambda表達式、函數接口、級聯表達式和柯里化、方法引用、Stream流編程、JDK9 - Reactive Stream

Lambda表達式

  • 常規寫法
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("測試");
    }
};
new Thread(runnable).start();
  • lambda寫法
/**
 * 箭頭左側的括號爲方法的參數,無參只寫一個括號
 * 有參寫法(int a,int b) -> {//方法體},簡寫,(a,b) -> {//方法體}
 * 箭頭右側爲方法的執行體
 */
Runnable runnable1 = () -> System.out.println("測試");
new Thread(runnable).start();

/**
 * lambda表達式就是返回了一個實現了指定接口的對象實例,需要告訴他返回的類型
 * () -> System.out.println("測試")其實是函數調用的方式,
 * 需要告訴他返回的對象的要實現的接口類型,
 * 所以下面寫了強轉的方式也是可以的
 */
Object runnable2 = (Runnable)() -> System.out.println("測試");
new Thread((Runnable)runnable2).start();

接口新特性

  • @FunctionalInterface註解

上面的lambda表達式,對接口是有限制的,就是這個接口裏面只有一個要實現的方法。
比如Runnable裏只有一個run方法需要實現,Callable裏只有一個call方法需要實現,不然如果有多個方法,那表達式的寫法不知道作用於哪個方法,也就是函數式接口。
新增的註解:@FunctionalInterface,該註解用於標識接口爲函數式接口,它不是必須的,可加可不加,養成良好編碼習慣建議是要加上的,如果裏面存在多個要實現的方法,會有報錯提示,如下:
在這裏插入圖片描述

  • 接口默認方法

jdk8裏面新增了一個默認的方法是用default修飾的方法叫默認方法,接口裏面這個方法有默認的實現。

@FunctionalInterface
interface XXX{
    int ha(int i,int b);
    
    default int haha(int i,int b,int c){
        return i+b+c;
    }
}

public class Testt {
    public static void main(String[] args) {
        XXX xxx = (i,b) -> i*b;
        System.out.println(xxx.ha(7,5));//調用需要實現的方法
        System.out.println(xxx.haha(1,2,3));//調用默認方法
    }
}

接口默認方法理解:假設有一個接口,裏面有很多方法,也有很多個類已經實現了裏面的方法,程序正常運行,如果此時需要在該接口裏新加一個方法,那麼所有實現了的地方都要重改。
比如List接口,在1.2版本到1.7版本都沒任何改變,在1.8版本,新增了一個默認方法,他之前如果修改List接口,那麼會導致什麼就不用說了吧。
在這裏插入圖片描述
不過lambda表達式寫的話不衝突,default只是默認方法,而且有默認實現,lambda表達式還是會去尋找那麼未實現的方法的。可以把默認方法當做是類裏面的方法理解就行了。
如果接口有多繼承的情況,比如接口1裏有默認方法,接口2裏有默認方法,接口3實現了接口1和接口2,軟件會有報錯提示,會讓你指明要用哪個默認方法。

函數接口

  • Function
@FunctionalInterface
interface XXX{
    int ha(int i);
}

class TTT{
    private final int age = 30;
    public void ride(XXX xxx){
        System.out.println("多大歲數了:"+xxx.ha(age) + "了都");
    }
}

public class Testt {
    public static void main(String[] args) {
        TTT ttt = new TTT();
        ttt.ride(i -> i);
    }
}
輸出:多大歲數了:30了都

理解:
代碼中的ttt.ride(i -> i);,其中i -> i,箭頭左側爲輸入的類型,箭頭右側爲輸出的類型,那麼只需要關注輸入和輸出類型即可,將代碼修改如下:

class TTT{
    private final int age = 30;
    public void ride(Function<Integer,Integer> xxx){
        System.out.println("多大歲數了:"+xxx.apply(age) + "了都");
    }
}

public class Testt {
    public static void main(String[] args) {
        TTT ttt = new TTT();
        Function<Integer,Integer> function = i -> i;
        ttt.ride(function);
    }
}
多大歲數了:30了都

直接省略了一個接口,ride方法的參數類型變成了Function,泛型第一個是輸入的類型,第二個是輸出的類型,然後調用Function接口的apply方法。使用函數接口的好處就是不用定義那麼多的接口了,另一個好處是支持鏈式操作,就是Function接口的andThen方法。
class TTT{
    private final int age = 30;
    public void ride(Function<Integer,Integer> xxx){
        System.out.println("多大歲數了:"+xxx.apply(age) + "了都");
    }
}
public class Testt {
    public static void main(String[] args) {
        TTT ttt = new TTT();
        Function<Integer,Integer> function = i -> i;
        //函數接口鏈式操作
        ttt.ride(function.andThen(s -> 5 + s));
    }
}
輸出:多大歲數了:35了都
  • 自帶的函數接口

函數接口都是通用的,會了一個,其他的也就會了。

接口 輸入參數類型 返回類型 說明
Predicate T boolean 斷言
Consumer T / 消費一個數據
Function<T,R> T R 輸入T輸出R的函數
Supplier / T 提供一個數據
UnaryOperator T T 一元函數(輸入輸出類型相同)
BiFunction<T,U,R> (T,U) R 2個輸入的函數
BinaryOperator (T,T) T 二元函數(輸入輸出類型相同)

測試代碼:

public class Testt {
    public static void main(String[] args) {
        //斷言 測試傳入的5是否小於0
        Predicate<Integer> predicate = (i) -> i < 0;
        boolean test = predicate.test(5);//返回false

        //Consumer消費 有入參 調用accept方法返回類型是void
        Consumer<String> consumer = (s) -> System.out.println(s);
        consumer.accept("what?");

        //Supplier供應 無入參 返回類型就是泛型裏規定的
        Supplier<String> supplier = () -> "what?";
        String s = supplier.get();//返回what?

        //UnaryOperator 輸入參數類型和返回類型一致
        UnaryOperator<String> unaryOperator = (a) -> a + " are you doing?";
        String what = unaryOperator.apply("what");//返回what are you doing?

        //BiFunction 兩個入參,一個返回參數
        BiFunction<String,String,Integer> biFunction = (b,c) -> Integer.valueOf(b) + Integer.valueOf(c);
        Integer apply = biFunction.apply("58", "2");//返回60

        //BinaryOperator 兩個入參和一個返回類型一致
        BinaryOperator<Integer> binaryOperator = (d,f) -> d+f;
        Integer apply1 = binaryOperator.apply(58, 2);//返回60
    }
}

方法引用

  • 靜態方法的方法引用
class TTT{
    public static void ride(String str){
        System.out.println("你是誰:"+str);
    }
}
public class Testt {
    public static void main(String[] args) {
        Consumer<String> consumer = TTT::ride;//使用類名+兩個冒號
        consumer.accept("我叫趙山河");//傳入參數
    }
}
  • 非靜態方法的方法引用
class TTT{
    public String ride(String str){
        System.out.println("你是誰:");
        return str;
    }
}
public class Testt {
    public static void main(String[] args) {
        TTT ttt = new TTT();
        Function<String,String> function = ttt::ride;//使用實例+兩個冒號
        System.out.println(function.apply("我叫趙山河"));//傳入參數
    }
}

這裏面輸入是String類型,輸出是String類型,可將Function改爲:
UnaryOperator<String> function = ttt::ride;

另一種寫法

class TTT{
    public String ride(String str){
        System.out.println("你是誰:");
        return str;
    }
}
public class Testt {
    public static void main(String[] args) {
        TTT ttt = new TTT();
        BiFunction<TTT,String,String> function = TTT::ride;//使用類名+兩個冒號+方法名
        System.out.println(function.apply(ttt,"我叫趙山河"));
    }
}
這裏也是使用了類名+兩個冒號+方法名訪問,和之前的訪問靜態方法類似,區別是ride方法,雖然定義的參數是一個String類型,但實際默認還會傳入一個this參數,如下:
public String ride(TTT this,String str){
	System.out.println("你是誰:");
	return str;
}
所以使用BiFunction函數接口,第一個參數是TTT類,第二個參數是String,第三個參數是String。
  • 構造函數方法引用
無參構造函數
class TTT{
    public TTT(){}//這裏爲了演示清楚,顯示的寫了一個無參構造,默認是隱式的自動會有
}
public class Testt {
    public static void main(String[] args) {
        Supplier<TTT> supplier = TTT::new;//使用類名+兩個冒號+new關鍵字
        System.out.println("創建了對象:" + supplier.get());
    }
}

有參構造函數
class TTT{
    private String s;
    public TTT(String str){
        this.s = str;
    }
}
public class Testt {
    public static void main(String[] args) {
        Function<String,TTT> function = TTT::new;
        System.out.println("創建了對象:"+ function.apply("我叫趙山河"));
    }
}

變量引用

內部類引用外部變量,這個變量必須聲明爲final類型。lambda表達式如下:

public class Testt {
    public static void main(String[] args) {
        String str = "哈哈";
        Consumer<String> consumer = s -> System.out.println(s+str);//引用外部變量
        consumer.accept("what?");
    }
}

這裏用lambda表達式引用了外部的str變量是沒有加final的,不過它是會默認加了的,這個str已經是不可修改的了,修改會報錯:
在這裏插入圖片描述
那麼爲什麼內部類引用外部的變量要是final?因爲java裏傳參的形式是傳值,而不是傳引用
,比如:

public class Testt {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Consumer<String> consumer = s -> System.out.println(s+list);
        consumer.accept("what?");
    }
}

內部類裏訪問了list,傳入的list此時已經不可修改了,因爲傳入list的時候實際上傳的是new ArrayList<>()對象,也就是說在外部有一個list變量,在內部類裏面也有一個list變量,這是兩個變量,然後兩個變量都指向了new ArrayList<>()對象,這就是傳值。假設list創建的時候是List<String> list = new ArrayList<>();指向了new ArrayList<>()對象,在後面如果把list改變了,指向了另外一個list,那內部類裏的System.out.println(s+list);就可能導致list指向的還是原來初始化指向的new ArrayList<>()對象,而改變了list就會導致結果是不正確的,這就是爲什麼要用final修飾。

級聯表達式和柯里化

級聯表達式就是有多個箭頭的lambda表達式。

比如:a -> b -> a * b;

public class Testt {
    public static void main(String[] args) {
        Function<Integer,Function<Integer,Integer>> function = a -> b -> a * b;
        System.out.println("乘法運算:"+function.apply(3).apply(4));
    }
}

理解:分析輸入輸出參數類型,如下我畫了個圖:
在這裏插入圖片描述
柯里化:如上Function裏嵌套了Function,傳參的時候,每次就只傳1個參數,所以柯里化就是把多個參數的函數轉換爲只有一個參數的函數。也就是這段代碼:

function.apply(3).apply(4)

柯里化的目的:函數標準化。

public class Testt {
    public static void main(String[] args) {
        Function<Integer,Function<Integer,Function<Integer,Integer>>> function = a -> b -> c -> a * b * c;
        System.out.println("乘法運算:"+function.apply(3).apply(4).apply(5));
    }
}

理解了以後,還可將上面的代碼改下爲:

public class Testt {
    public static void main(String[] args) {
        Function<Integer,Function<Integer,Function<Integer,Integer>>> function = a -> b -> c -> a * b * c;
        int [] nums = {3,4,5};
        for (int i = 0;i < nums.length; i++){
            if(function instanceof Function){
                Object obj = function.apply(nums[i]);
                if(obj instanceof Function){
                    function = (Function) obj;
                }else{
                    System.out.println("結束,乘法運算:"+obj);
                }
            }
        }
    }
}

柯里化以後的好處是所有的函數都是一個樣子,可以批量來處理。

Stream流編程

  • 概念:

它是一個高級的迭代器,它不是一個數據結構,它不是一個集合,它不會存放數據。stream關注的是如何把數據高效的處理。

  • 外部迭代和內部迭代
public class Testt {
    public static void main(String[] args) {
        //外部迭代
        int [] nums = {3,4,5};
        int sum = 0;
        for (int i : nums){
            sum += i;
        }
        System.out.println("求和:"+sum);

        //stream內部迭代
        int sum2 = IntStream.of(nums).sum();
        System.out.println("求和:"+sum2);
    }
}
  • 中間操作/終止操作和惰性求值
public class Testt {
    public static void main(String[] args) {
        int [] nums = {3,4,5};
        //map就是中間操作(返回stream的操作)
        //sum就是終止操作
        int sum2 = IntStream.of(nums).map(i -> i * 2).sum();
        System.out.println("求和:"+sum2);
    }
}

map操作是將流中的元素映射成另外一種元素,接受一個Function類型的參數,是一箇中間操作,中間操作返回類型是流。
代碼裏的map爲中間操作,讓每個數都乘以2,sum是終止操作爲求和。
惰性求值就是終止操作沒有調用的情況下,中間操作不會執行。感覺類似懶漢模式。也就是在沒有調用sum的時候,中間操作不會執行。
區分中間操作和終止操作只需要關注它的返回類型,返回的是流那它就是中間操作,否則就是終止操作。

  • Stream流的創建
常見 相關方法
集合 Collection.stream/parallelStream
數組 Arrays.stream
數字Stream IntStream/ LongStream.range/rengeClosed 和 Random.ints/longs/doubles
自己創建 Stream.generate/iterate
public class Testt {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //從集合創建
        list.stream();
        list.parallelStream();//並行流

        //從數組創建
        Arrays.stream(new int [] {3,4,5});

        //數字流
        IntStream.of(1,2,3);
        IntStream.range(1,3);

        //使用random創建一個無限流
        new Random().ints().limit(10);

        //自己創建
        Random random = new Random();
        Stream.generate(() -> random.nextInt()).limit(10);
    }
}
  • Stream流的中間操作

無狀態操作:map/maptoXxx,flatMap、flatMapToXxx,filter,peek,unordered
有狀態操作:distinct,sorted,limit/skip
理解:無狀態標識當前的操作跟其他元素的前後沒有依賴關係,有狀態標識我現在的結果要依賴於其他的一些元素。比如排序操作,它就依賴所有的元素都計算完畢,它纔有一個最終的排序結果,這就是有狀態操作。
這些中間操作他們都返回的是一個stream流,可以繼續鏈式調用下去。

map:
String str = "123-456-789-1234-5678-9012";
//切分字符串爲數組,並輸出每個的長度
Stream.of(str.split("-")).map(i -> i.length()).forEach(System.out::println);

String str = "123-456-789-1234-5678-9012";
//切分字符串爲數組,加filter過濾條件,長度大於3的輸出
Stream.of(str.split("-")).filter(i -> i.length() > 3).map(i -> i.length()).forEach(System.out::println);

flatMap:
String str = "123-456-789-1234-5678-9012";
//flatMap A->B屬性(是個集合),最終得到A元素裏面的所有B屬性的集合
//IntStream/longStream 並不是Stream的子類,所以要進行裝箱 boxed
Stream.of(str.split("-")).flatMap(i -> i.chars().boxed()).forEach(i -> System.out.println((char)i.intValue()));
針對str變量來說,A屬性就是123、456、789......9012,它下面的B屬性就是單個字符1、2、3,4、5、6......9、0、1、2。
在比如下面代碼可能更容易理解:
//流創建了3個list,返回了 Stream<List<Integer>>
Stream<List<Integer>> inputStream = Stream.of(Arrays.asList(1),Arrays.asList(2, 3), Arrays.asList(4, 5, 6));
//使用flatMap,返回了 Stream<Integer>
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
outputStream.forEach(s -> System.out.print(s));//輸出123456,將之前的3個集合打平,返回其下面的子元素

peek:
String str = "123-456-789-1234-5678-9012";
//peek 用於debug,是個中間操作,forEach是終止操作
Stream.of(str.split("-")).peek(System.out::println).forEach(System.out::println);

limit:
//limit用於無限流
//隨意生成10個隨機數
new Random().ints().limit(10).forEach(System.out::println);
//對隨機數加限制條件
new Random().ints().filter(i -> i > 100 && i <1000).limit(10).forEach(System.out::println);
  • Stream流的終止操作

非短路操作:forEach/forEachOrdered、collect/toArray、reduce、min/max/count
短路操作:findFirst/findAny、allMatch/anyMatch/noneMatch
理解:短路操作就是不需要等待計算結果就可以結束流,比如findFirst/findAny,得到一個數據或者得到任何一個數據,這個流就可以結束了, 非短路操作相反。

forEachOrdered:
String str = "123-456-789-1234-5678-9012";
//使用並行流,無序  輸出->--7-6523-145890163412798-2
str.chars().parallel().forEach(i -> System.out.print((char) i));
//使用forEachOrdered保證有序   輸出->123-456-789-1234-5678-9012
str.chars().parallel().forEachOrdered(i -> System.out.print((char) i));

collect:
String str = "123-456-789-1234-5678-9012";
//collect收集器,收集到list,有點像string轉list
List<String> list = Stream.of(str.split("-")).collect(Collectors.toList());
//輸出:[123, 456, 789, 1234, 5678, 9012]
//collect收集器,收集到set
Set<String> set = Stream.of(str.split("-")).collect(Collectors.toSet());
//輸出:[123, 9012, 456, 1234, 789, 5678]
// 字符串拼接
String collect = Stream.of("a", "b", "c", "d").collect(Collectors.joining());

String str = "123-456-789-1234-5678-9012";
//使用reduce拼接字符串,Optional選項的意思,避免你做一些非空判斷
Optional<String> reduce = Stream.of(str.split("-")).reduce((a, b) -> a + "|" + b);
System.out.println(reduce.orElse(""));//reduce.orElse("")相當於if == null else 返回空串
//爲reduce加一個默認值爲空串,那麼返回的就直接是個String了
String reduce1 = Stream.of(str.split("-")).reduce("", (a, b) -> a + "|" + b);
System.out.println(reduce1);

//使用map和reduce求和
Integer reduce2 = Stream.of(str.split("-")).map(s -> s.length()).reduce(0, (a, b) -> a + b);
System.out.println(reduce2);
reduce這個方法的主要作用是把 Stream 元素組合起來。它提供一個起始值(種子),然後依照運算規則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字符串拼接、數值的 sum、min、max、average 都是特殊的 reduce。
  • 並行流
//非並行流
IntStream.range(1,10).peek(TTT::stream).count();
//調用parallel()產生一個並行流
IntStream.range(1,10).parallel().peek(TTT::stream).count();

//TTT::stream方法
class TTT{
    public static void stream(int a){
        System.out.println(Thread.currentThread().getName() + ":" +a);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

並行流使用的線程池:ForkJoinPool.commonPool,該線程池數量爲當前機器的cpu個數。修改線程數:

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","20");
IntStream.range(1,100).parallel().peek(TTT::stream).count();

如果程序內所有的並行流都使用同一個線程池,那麼就會出現阻塞。所以在某些場景,可以使用我們自己的線程池,這樣就不會阻塞了。

//使用自己的線程池,不使用默認的線程池,放置任務阻塞
ForkJoinPool pool = new ForkJoinPool(20);
pool.submit(() -> IntStream.range(1, 100).parallel().peek(TTT::stream).count());
pool.shutdown();

//測試代碼,防止主線程先退出
synchronized (pool){
    try{
        pool.wait();
    }
    catch (InterruptedException e){
        e.printStackTrace();
    }
}
  • 收集器

收集器就是把流處理後的數據收集起來。

以List<Map<String,Object>>格式的數據爲例:

public class Testt {
    public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        List<Map<String,Object>> list = new ArrayList<>();
        //構造了List<Map>格式的數據
        for (int i = 0;i<10;i++){
            Map<String,Object> map = new LinkedHashMap<>();
            map.put("80",new Random().nextDouble());//隨機數0.xxxxx
            map.put("81",new Random().nextDouble());
            map.put("82",new Random().nextDouble());
            map.put("83",new Random().nextDouble());
            map.put("84",new Random().nextDouble());
            map.put("85",new Random().nextDouble());
            map.put("86",new Random().nextDouble());
            map.put("time",LocalDateTime.now().format(DATETIME_FORMATTER));//日期
            list.add(map);
        }
        //將List中Map裏key爲80的value提取到List<Object>中,map中間操作可以對數據進行操作,collect結束操作將結果收集起來
        List<Object> collect2 = list.stream().map(m -> m.get("80")).collect(Collectors.toList());

        //提取80的value時,加入過濾條件,小於0.5的過濾掉
        List<Object> collect1 = list.stream().map(m -> m.get("80")).filter(a -> Double.valueOf(a + "") > 0.5).collect(Collectors.toList());
    }
}

Collectors爲java8提供的工具類,使用方式查看api就可以了。
在這裏插入圖片描述
以下是在網上粘過來的常用方法和使用示例

工廠方法 返回類型 用於
toList List<T> 把流中所有元素收集到List中
示例:List<Menu> menus=Menu.getMenus.stream().collect(Collectors.toList())
toSet Set<T> 把流中所有元素收集到Set中,刪除重複項
示例:Set<Menu> menus=Menu.getMenus.stream().collect(Collectors.toSet())
toCollection Collection<T> 把流中所有元素收集到給定的供應源創建的集合中
示例:ArrayList<Menu> menus=Menu.getMenus.stream().collect(Collectors.toCollection(ArrayList::new))
Counting Long 計算流中元素個數
示例:Long count=Menu.getMenus.stream().collect(counting);
SummingInt Integer 對流中元素的一個整數屬性求和
示例:Integer count=Menu.getMenus.stream().collect(summingInt(Menu::getCalories))
averagingInt Double 計算流中元素integer屬性的平均值
示例:Double averaging=Menu.getMenus.stream().collect(averagingInt(Menu::getCalories))
Joining String 連接流中每個元素的toString方法生成的字符串
示例:String name=Menu.getMenus.stream().map(Menu::getName).collect(joining(“, ”))
maxBy Optional<T> 一個包裹了流中按照給定比較器選出的最大元素的optional
如果爲空返回的是Optional.empty()
示例:Optional<Menu> fattest=Menu.getMenus.stream().collect(maxBy(Menu::getCalories))
minBy Optional<T> 一個包裹了流中按照給定比較器選出的最大元素的optional
如果爲空返回的是Optional.empty()
示例: Optional<Menu> lessest=Menu.getMenus.stream().collect(minBy(Menu::getCalories))
Reducing 歸約操作產生的類型 從一個作爲累加器的初始值開始,利用binaryOperator與流中的元素逐個結合,從而將流歸約爲單個值
示例:int count=Menu.getMenus.stream().collect(reducing(0,Menu::getCalories,Integer::sum));
collectingAndThen 轉換函數返回的類型 包裹另一個轉換器,對其結果應用轉換函數
示例:Int count=Menu.getMenus.stream().collect(collectingAndThen(toList(),List::size))
groupingBy Map<K,List<T>> 根據流中元素的某個值對流中的元素進行分組,並將屬性值做爲結果map的鍵
示例:Map<Type,List<Menu>> menuType=Menu.getMenus.stream().collect(groupingby(Menu::getType))
partitioningBy Map<Boolean,List<T>> 根據流中每個元素應用謂語的結果來對項目進行分區
示例:Map<Boolean,List<Menu>> menuType=Menu.getMenus.stream().collect(partitioningBy(Menu::isType));
  • Stream運行機制
1、所有操作都是鏈式,一個元素只迭代一次。比如.map(......).filter(......),會先執行map,在執行filter,依次的,不會等所有的map都執行完在執行filter,所以一個元素只迭代一次。
2、每一箇中間操作都返回一個新的流,流裏面有一個屬性sourcesStage,指向同一個地方,就是鏈表的頭,Head。
3、Head指向nextStage在指向nextStage知道nextStage爲null
4、有狀態操作會把無狀態操作截斷單獨處理。
5、並行環境下有狀態的中間操作不一定能並行操作。
6、parallel/sequetial 這2個操作也是中間操作(也是返回stream),但是他們跟其他的有區別,他們不創建流,他們只修改Head的並行標誌。

JDK9 - Reactive Stream

概念: 它是JDK9引入的一套標準,是一套基於發佈/訂閱者模式的數據處理的機制。它和stream流編程並沒有特別大的關係。
背壓: 發佈者跟訂閱者之間的一個交互。就是發佈者和訂閱者之間可以交流。訂閱者可以告訴發佈者我這裏需要多少數據,比如我現在數據處理完了,就可以多拿一些數據過來。我現在沒處理完,那就先不要給我數據。可以起到調節數據流量的作用。也不會導致發佈者那邊過多的產生數據,導致數據浪費。或者說發佈者創建了太多數據,全部發給訂閱者,把訂閱者這邊壓垮。
背壓舉例: 比如我們每天都是用的自來水。自來水公司就是一個發佈者,我們每家每戶就是訂閱者。水就是數據,在以前老的模式下,我們的訂閱者(每家每戶)它是一個被動接收的概念,那麼自來水公司只要有水就會源源不斷的發過來,那麼我們只能被動接受,不管你需不需要水你都沒法控制,只能接收。有了背壓以後,我們相當於有了一個水龍頭,對水(數據)的需求是可控的,需要就打開水龍頭,不需要就關閉水龍頭。

  • Reactive Stream主要接口

Java 9 通過java.util.concurrent.Flow 和java.util.concurrent.SubmissionPublisher 類來實現響應式流。
Flow 類中定義了四個嵌套的靜態接口,用於建立流量控制的組件,發佈者在其中生成一個或多個供訂閱者使用的數據項:

  • Publisher:數據項發佈者、生產者
  • Subscriber:數據項訂閱者、消費者
  • Subscription:發佈者與訂閱者之間的關係紐帶,訂閱令牌
  • Processor:數據處理器

發佈/訂閱Demo

package xxx;

import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.SubmissionPublisher;

public class Reactive {
    public static void main(String[] args) throws Exception{

        //1.定義發佈者,發佈的數據類型是Integer
        //使用JDK自帶的SubmissionPublisher,它實現了Publisher接口
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<Integer>();

        //2.定義訂閱者
        Subscriber<Integer> subscriber = new Subscriber(){
            private Subscription subscription;
            @Override
            public void onSubscribe(Subscription subscription) {
                //保存訂閱關係,需要用它來給發佈者響應
                this.subscription = subscription;
                //請求一個數據
                this.subscription.request(1);
            }
            @Override
            public void onNext(Object item) {
                //當有數據到了的時候出發該方法
                //接收到一個數據,處理
                System.out.println("接收到數據:"+item);
                //處理完在調用request再請求一個數據
                this.subscription.request(1);
                //或者 已經達到目標,調用cancel告訴發佈者不再接收數據了
                //this.subscription.cancel();
            }
            @Override
            public void onError(Throwable throwable) {
                //出錯時,觸發該方法
                //出現了異常(例如處理數據(onNext中)的時候產生了異常)
                throwable.printStackTrace();
                //異常後還可以告訴發佈者,不再接收數據了
                this.subscription.cancel();
            }
            @Override
            public void onComplete() {
                //數據全部處理完了(發佈者關閉了)
                //publisher.close()方法後觸發該方法
                System.out.println("處理完畢!");
            }
        };

        //3.發佈者和訂閱者 建立訂閱關係
        publisher.subscribe(subscriber);

        //4.生產數據,併發布
        //這裏忽略數據生產過程
        publisher.submit(111);//發送1條
        publisher.submit(222);//發送1條
        publisher.submit(333);//發送1條

        //5.結束後,關閉發佈者
        //正式環境 應該放finally或者使用try-resouce確保關閉
        publisher.close();

        //主線程延時停止,否則數據沒有消費就退出
        Thread.currentThread().join(1000);

    }
}

發佈/訂閱 - 帶發佈者過濾器Demo

package xxx;

import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.SubmissionPublisher;

/**
 * 發佈者過濾器
 * Processor,需要繼承SubmissionPublisher並實現Processor接口
 * 輸入源數據integer,過濾掉小於0的,然後轉成字符串發佈出去
 */
class ImplProcessor extends SubmissionPublisher<String> implements Processor<Integer,String> {
    private Subscription subscription;
    @Override
    public void onSubscribe(Subscription subscription) {
        //保存訂閱關係,需要用它來給發佈者響應
        this.subscription = subscription;
        //請求一個數據
        this.subscription.request(1);
    }
    @Override
    public void onNext(Integer item) {
        //接收到一個數據,處理
        System.out.println("準備發送的數據:"+item);

        //過濾掉小於0的數據,然後發佈出去
        if(item > 0){
            this.submit("處理後待發送的數據:"+item);
        }

        //處理完在調用request再請求一個數據
        this.subscription.request(1);
        //或者 已經達到目標,調用cancel告訴發佈者不再接收數據了
        //this.subscription.cancel();
    }
    @Override
    public void onError(Throwable throwable) {
        //出錯時,觸發該方法
        throwable.printStackTrace();
        //異常後還可以告訴發佈者,不再接收數據了
        this.subscription.cancel();
    }
    @Override
    public void onComplete() {
        //處理器處理完畢
        System.out.println("處理器處理完畢!");
        //關閉發佈者
        this.close();
    }
}
public class MyProcessor{
    public static void main(String[] args)throws Exception {
        //1.定義發佈者,發佈的數據類型是Integer
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<Integer>();

        //2.定義處理器,對數據進行過濾,並轉換String類型
        ImplProcessor processor = new ImplProcessor();

        //3發佈者 和 處理器建立訂閱關係
        publisher.subscribe(processor);

        //4.定義最終訂閱者,消費String數據
        Subscriber<String> subscriber = new Subscriber(){
            private Subscription subscription;
            @Override
            public void onSubscribe(Subscription subscription) {
                //保存訂閱關係,需要用它來給發佈者響應
                this.subscription = subscription;
                //請求一個數據
                this.subscription.request(1);
            }
            @Override
            public void onNext(Object item) {
                //當有數據到了的時候出發該方法
                //接收到一個數據,處理
                System.out.println("接收到數據:"+item);
                //處理完在調用request再請求一個數據
                this.subscription.request(1);
                //或者 已經達到目標,調用cancel告訴發佈者不再接收數據了
                //this.subscription.cancel();
            }
            @Override
            public void onError(Throwable throwable) {
                //出錯時,觸發該方法
                //出現了異常(例如處理數據(onNext中)的時候產生了異常)
                throwable.printStackTrace();
                //異常後還可以告訴發佈者,不再接收數據了
                this.subscription.cancel();
            }
            @Override
            public void onComplete() {
                //數據全部處理完了(發佈者關閉了)
                //publisher.close()方法後觸發該方法
                System.out.println("訂閱者數據處理完畢!");
            }
        };

        //5.處理器 和 最終訂閱者建立關係
        processor.subscribe(subscriber);

        //6.生產數據,併發布
        //這裏忽略數據生產過程
        publisher.submit(-111);//發送1條 - 此條數據會被過濾掉
        publisher.submit(222);//發送1條
        publisher.submit(333);//發送1條

        //7.結束後,關閉發佈者
        //正式環境 應該放finally或者使用try-resouce確保關閉
        publisher.close();

        //主線程延時停止,否則數據沒有消費就退出
        Thread.currentThread().join(1000);
    }
}

完~如有錯誤請指出,歡迎一起加羣交流43827511。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章