响应式编程

一、Java8 函数式编程/Lambda

1、什么是lambda表达式

lambda表达式本质上是一个匿名方法。让我们来看下面这个例子:

public int add(int x, int y) {
    return x + y;
}

转成lambda表达式后是这个样子:

(int x, int y) -> {
    return x + y;
}

参数类型也可以省略,Java编译器会根据上下文推断出来:

(x, y) -> { return x + y; } //显式指明返回值
或者
(x, y) -> x + y; // 语句块只有一条语句时可以省略{}和return

可见lambda表达式由三部分组成:参数列表,箭头(->),以及一个表达式或语句块。

下面这个例子里的λ表达式没有参数,也没有返回值(相当于一个方法接受0个参数,返回void,其实就是Runnable里run方法的一个实现):

() -> { System.out.println("Hello Lambda!"); }

如果只有一个参数且可以被Java推断出类型,那么参数列表的括号也可以省略:

list -> { return list.size(); }

2、lambda表达式的类型

lambda表达式的目标类型是“函数式接口(functional interface)”,这是Java8新引入的概念。它的定义是:一个接口,如果只有一个显式声明的抽象方法,那么它就是一个函数式接口。一般用@FunctionalInterface标注出来(也可以不标)。举例如下:

@FunctionalInterface
public interface Runnable { 
   void run(); 
}

lambda表达式返回的是实现函数接口的对象实例, 你可以用一个lambda表达式为一个函数式接口赋值:

Runnable r1 = () -> {System.out.println("Hello Lambda!");};

然后再赋值给一个Object:

Object obj = r1;

但却不能这样干:

Object obj = () -> {System.out.println("Hello Lambda!");}; // ERROR! Object is not a functional interface!

必须显式的转型成一个函数式接口才可以:

Object o = (Runnable) () -> { System.out.println("hi"); }; // correct

一个lambda表达式只有在转型成一个函数式接口后才能被当做Object使用。所以下面这句也不能编译:

System.out.println( () -> {} ); //错误! 目标类型不明

必须先转型:

System.out.println( (Runnable)() -> {} ); // 正确

假设你自己写了一个函数式接口,长的跟Runnable一模一样:

@FunctionalInterface
public interface MyRunnable {
    public void run();
}

那么

Runnable r1 =    () -> {System.out.println("Hello Lambda!");};
MyRunnable2 r2 = () -> {System.out.println("Hello Lambda!");};

都是正确的写法。这说明一个lambda表达式可以有多个目标类型(函数式接口),只要函数匹配成功即可。
但需注意一个lambda表达式必须至少有一个目标类型。

JDK预定义了很多函数式接口以避免用户重复定义。最典型的是Function:

@FunctionalInterface
public interface Function<T, R> { 
    R apply(T t);
}

这个接口代表一个函数,接受一个T类型的参数,并返回一个R类型的返回值。

另一个预定义函数式接口叫做Consumer,跟Function的唯一不同是它没有返回值。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

还有一个Predicate,用来判断某项条件是否满足。经常用来进行筛滤操作:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

示例代码:

package test;

import java.text.DecimalFormat;
import java.util.function.*;

@FunctionalInterface
interface IMoneyFormat {
	String format(int money);
}

class MyMoney {
	private int money;
	
	public MyMoney(int money) {
		this.money = money;
	}
	
	public void printMoney(IMoneyFormat moneyFormat) {
		System.out.println("我的存款:" + moneyFormat.format(this.money));
	}
	
//	public void printMoney(Function<Integer, String> moneyFormat) {
//		System.out.println("我的存款:" + moneyFormat.apply(this.money));
//	}
}

public class JDKFunctionDemo {

	public static void main(String[] args) {
		
		// 1. 使用自定义函数接口
		MyMoney me = new MyMoney(1000000);
		me.printMoney((i)-> {
			return new DecimalFormat("#,###").format(i);
		});
		
//		Function<Integer, String> moneyFormat = i-> new DecimalFormat("#,###").format(i);
//		// 函数接口链式操作
//		me.printMoney(moneyFormat.andThen(s-> "人民币" + s));
		
		// 2.JDK8 自带的常用函数接口
		// Function<T,R>, 输入T返回R的函数
        Function<Integer, Integer> function = i -> i * 2;
        System.out.println(function.apply(5));
        
    	// UnaryOperator<T>, 输入和输出类型相同的函数(一元函数)
		UnaryOperator<Integer> unaryOperator = i -> i * 2;
        System.out.println(unaryOperator.apply(5));
        
        // 输入和输出类型都是Integer的函数
        IntUnaryOperator intUnaryOperator = i -> i * 2;
        System.out.println(intUnaryOperator.applyAsInt(5));
        
		// Consumer<T>, 输入T无返回的函数(消费者)
		Consumer<String> consumer = i -> System.out.println(i);
		consumer.accept("hello word");
		
		// Supplier<T>, 无输入返回T的函数(提供者)
		Supplier<String> supplier = () -> new String("hello word");
		System.out.println(supplier.get());
		
		// Predicate<T>, 返回布尔类型的函数(断言)
		Predicate<Integer> predicate = i -> i > 10;
		System.out.println(predicate.test(5));
		
		// BiFunction<T,U,R>, 输入(T, U)返回R的函数(2个输入函数)
		BiFunction<Integer, Integer, Integer> biFunction = (i, j) -> i * j;
		System.out.println(biFunction.apply(5, 5));
		
		// BinaryOperator<T>, 输入和输出类型相同的函数(二元函数)
		BinaryOperator<Integer> binaryOperator = (i, j) -> i * j;
		System.out.println(binaryOperator.apply(5, 5));
	}
}

3、方法引用

任何一个lambda表达式都可以代表某个函数式接口的唯一方法的匿名描述符。我们也可以使用某个类的某个具体方法来代表这个描述符,叫做方法引用。例如:

Integer::parseInt //静态方法引用
System.out::print //实例方法引用
Person::new       //构造器引用

示例代码:

package test;

import java.util.function.Consumer;
import java.util.function.*;

class Dog {
	private String name = "哮天犬";
	private int food = 10; // 默认10斤狗粮
	
	public Dog() {
		
	}
	
	public Dog(String name) {
		this.name = name;
	}
	
	/**
	 * 静态方法
	 * 狗叫
	 * @param dog
	 */
	public static void bark(Dog dog) {
		System.out.println(dog + "叫了");
	}
	
	/**
	 * 非静态方法,编译器会默认将当前对象实例作为第一个参数,传入每一个非静态方法,参数名为this
	 * 吃狗娘
	 * @param num 几斤
	 * @return 还剩多少斤
	 */
	public int eat(int num) {
		System.out.println("吃了"+ num + "斤狗粮");
		this.food -= num;
		return this.food;
	}
	
	/**
	 * 吃狗娘
	 * @param num 几斤
	 * @return 还剩多少斤
	 */
	public int eat2(Dog this, int num) {
		System.out.println("吃了"+ num + "斤狗粮");
		this.food -= num;
		return this.food;
	}
	
	@Override
	public String toString() {
		return this.name;
	}
}

public class MethodReferenceDemo {

	public static void main(String[] args) {
		
		Dog dog = new Dog();

		// 方法引用
		Consumer<String> consumer = System.out::println; 
		consumer.accept("hello word");
		
		// 静态方法,使用类名引用方法
		Consumer<Dog> consumerDog = Dog::bark;
		consumerDog.accept(dog);
		
		// 非静态方法,使用对象实例的方法引用
		Function<Integer, Integer> function = dog::eat;
		System.out.println("还剩下" + function.apply(3) + "斤");
		
		// 非静态方法,使用类名引用方法
		BiFunction<Dog, Integer, Integer> biFunction = Dog::eat2;
		System.out.println("还剩下" + biFunction.apply(dog, 3) + "斤");
		
		// 无参的构造函数的方法引用
		Supplier<Dog> supplier = Dog::new;
		System.out.println("创建了新对象:" + supplier.get());
		
		// 带参数的构造函数的方法引用
		Function<String, Dog> functionDog = Dog::new;
		System.out.println("创建了新对象:" + functionDog.apply("旺财"));
	}
}

4、级联表达式和柯里化

柯里化:把多个参数的函数转换为只有一个参数的函数

柯里化的目的:函数标准化(柯里化之后所有的函数都是只有一个参数)

示例代码:

package test;

import java.util.function.*;

/**
 * 级联表达式和柯里化
 * 柯里化:把多个参数的函数转换为只有一个参数的函数
 * 柯里化的目的:函数标准化(柯里化之后所有的函数都是只有一个参数)
 * @author think
 *
 */
public class CurryDemo {

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static void main(String[] args) {
	    
	   // 输入两个参数, 返回一个参数
	   BiFunction<Integer, Integer, Integer> biFunction = (x, y) -> x + y;
	   System.out.println(biFunction.apply(2, 3));
	   
		// 实现了x + y的级联表达式
       Function<Integer, Function<Integer, Integer>> function = x -> y -> x + y;
       System.out.println(function.apply(2).apply(3));
       
       // 实现了x + y + z的级联表达式
       Function<Integer, Function<Integer, Function<Integer, Integer>>> function2 = x -> y -> z -> x + y + z;
       System.out.println(function2.apply(2).apply(3).apply(4));
       
       int[] nums = {2, 3, 4};
       Function f = function2;
       for (int i = 0; i < nums.length; i++) {
    	   if (f instanceof Function) {
    		   Object obj = f.apply(nums[i]);
    		   if (obj instanceof Function) {
    			   f = (Function) obj;
    		   } else {
    			   System.out.println("调用结束:结果为" + obj);
    		   }
    	   } 
       }
	}
}

综上所述,一个lambda表达式其实就是定义了一个匿名方法,只不过这个方法必须符合至少一个函数式接口。

二 、Java8 流编程/Stream

1、什么是Stream(流)

​ Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

2、Stream操作的三个步骤

创建Stream
通过一个数据源获取一个流,例如,List中的stream()方法可以直接返回一个Stream对象。

中间操作
我们需要对流中的数据进行的操作,比如循环处理(map),过滤(filter)等

终止操作
流都是惰性求值的,这个我们在后面会讲到,需要进行一个终止操作,这样才会返回中间操作后的数据。

如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQcsiYBp-1626679560257)(C:\Users\think\AppData\Roaming\Typora\typora-user-images\image-20210609145725205.png)]

注意:

Stream只是一个计算通道,自己不会存储元素;

Stream不会改变源对象,相反,他们会返回一个新的Stream对象。

Stream操作是延时的,只有在执行终止操作时才会执行。

3、创建Steam的方式

3.1 、由Collection子类来创建流

java8扩展了Collection接口,提供了stream(返回顺序流)和parallelStream(返回并行流)两个方法。

示例代码:

@Test
public void test(){
    List<String> list = Arrays.asList("a","b","c");
    Stream stram = list.stream();
    Stream parallelSteam = list.parallelStream();
}

3.2、由数据来创建流

数组可以通过Arrays工具类的stream方法来获得一个Steam对象

示例代码:

@Test
public void test() {
    String[] strArr = new String[]{"a","b","c"};
    Stream stram = Arrays.stream(strArr);
    // 还有许多重载形式的方法,可以返回带类型的Stream,例如:
    IntStream stram2 = Arrays.stream(new int []{1,2,3,4,5});
}

3.3、通过具体值来创建流

通过Stream的静态方法Stream.of(T…values)可以创建一个流,它可以接受任意个值

代码示例:

@Test
public void test() {
    String[] strArr = new String[]{"a","b","c"};
    Stream stram = Stream.of(strArr);
}

3.4、通过函数来创建流(无限流)

通过Stream.iterate()和Stream.generate()方法可以创建无限流

代码示例:

@Test
public void test4() {
    // 1. Stream.iterate方法第一个方法表示开始值得,第二个参数需要提供一个一元操作函数,我们用lambda表达式传递给它
    Stream stream1 = Stream.iterate(0, (x) -> x + 2);
    stream1.forEach(System.out::println); //输出的是0,2,4,6,8,10....将会一直循环下去,永远不停息
   
    // 2. Stream.generate需要一个供给型函数接口
    Stream stream2 = Stream.generate(() -> 1);
    stream2.forEach(System.out::println); //输出无数个1,将会一直循环下去,永远不停息
}

备注:实际运用中,我们肯定不会生成一个无限流,除非你想要死循环,我们会结合Stream的终止操作,如limit来获取有指定个数元素的流:

@Test
public void test5(){
    // 我们从0开始获取前50个偶数
    Stream stream1 = Stream.iterate(0, (x) -> x + 2).limit(50);
    stream1.forEach(System.out::println);  //输出0,2,4,6,8.....98
}

4、Stream的中间操作

​ Stream可以进行一系列的流水线式的中间操作,除非流水线上触发终止操作,否则,这些中间操作不会进行任何处理,而在终止操作时一次性处理,这个我们叫做Stream的惰性求值。

记住,中间操作不管做多少次,都不会改变原来的流,只会返回一个新的流;

Stream的中间操作可以分为以下几类:

4.1 中间操作:筛选与切片

方法 描述 filter(Predicate d) 接受一个断言型函数,对Stream流中的元素进行处理,过滤掉不满足条件的元素 distinct 筛选元素,通过Stream元素中的hasCode和equals方法来去除重复元素 limit(long maxSize) 截断流,使元素不超过manSize指定的数量 skip(Long n) 跳过元素,返回一个扔掉了前n个元素的流,若流中的元素不足n个,则会返回一个空流

代码示例 :

package test;

import java.util.Arrays;
import java.util.List;

/**
 * 员工信息
 */
class Employee {
	
	private String name;   // 姓名
	private int age;       // 年龄
	private double salary; // 工资
	
	public Employee(String name, int age, double salary) {
		this.name = name;
		this.age = age;
		this.salary = salary;
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public double getSalary() {
		return salary;
	}
	public void setSalary(double salary) {
		this.salary = salary;
	}
	
	@Override
	public String toString() {
		return "Employee [name=" + name + ", age=" + age + ", salary=" + salary + "]";
	}
}

/**
 * 流编程中间操作练习(筛选与切片)
 * 流操作三个步骤:1.创建  2.中间操作  3.终止操作
 * 流编程中必须有一个终止操作,并且只能有1个终止操作,中间操作可以有(0-n)个
 * 流的创建
 *    1.通过Collection创建
 *    2.通过Arrays创建
 *    3.通过Stream.of()静态方法获取流
 *    4.创建无限流(迭代,生成)
 * 流的中间操作 
 *    无状态  filter(), map(), flatMap()  依赖于元素
 *    有状态 distinct() sorted(), limit(), 
 * 流的终止操作(流结束)
 *    终止操作:查找与匹配 allMatch, anyMatch, noneMatch, findFirst, findAny, count, max, min ,forEach
 *    终止操作:规则  reduce
 *    终止操作:收集  
 */
public class StreamMiddleOperateDemo {

	public static void main(String[] args) {
		
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 18, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );
		
		// 1.过滤掉年龄小于25的员工
		System.out.println("过滤掉年龄小于25的员工");
        emps.stream().filter((e) -> e.getAge() > 25).forEach(System.out::println);
        
        // 2.先获取前3名员工,再获取其中年龄大于25的员工。(中间操作可以任意次)
    	System.out.println("\n先获取前3名员工,再获取其中年龄大于25的员工");
        emps.stream().filter(e -> e.getAge() > 25).limit(3).forEach(System.out::println);
        
        // 3.过滤掉姓名重复的员工
		System.out.println("\n过滤掉姓名重复的员工");
        emps.stream().distinct().forEach(System.out::println);
        
        // 4.获取第三名以后的员工
		System.out.println("\n获取第三名以后的员工");
        emps.stream().skip(3).forEach(System.out::println);
	}
}

4.2 中间操作:映射

方法 描述 map(Function f) 接受一个函数型接口作为参数,该函数会对流中的每个元素进行处理,返回处理后的流 mapToDouble(ToDoubleFunction f) 接口一个函数型接口作为参数,该函数会对流中的每个元素进行处理,并返回一个Double值,最终得到一个Stream mapToInt(ToIntFunction f) 接口一个函数型接口作为参数,该函数会对流中的每个元素进行处理,并返回一个Int值,最终得到一个Stream mapToLong(ToLongFunction f) 接口一个函数型接口作为参数,该函数会对流中的每个元素进行处理,并返回一个Long值,最终得到一个Stream flatMap(Function f) 接受一个函数作为参数,将流中的每个值都转换成一个新流,最后再将这些流连接到一起

代码示例:

package test;

import java.util.Arrays;
import java.util.List;

/**
 * 流编程中间操作练习(映射)
 */
public class StreamMiddleOperateDemo {

	public static void main(String[] args) {
		
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 18, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );
		
        // 1. 获取所有员工的姓名
        System.out.println("\n获取所有员工的姓名");
        emps.stream().map(e -> e.getName()).forEach(System.out::println);
        
        // 2. 获取所有员工的工资,这里工资是Double类型,我们可以用mapToDouble方法
        System.out.println("\n获取所有员工的工资");
        emps.stream().mapToDouble(e -> e.getSalary()).forEach(System.out::println);
        
        // 3. 获取所有员工的年龄,用mapToInt方法
        System.out.println("\n获取所有员工的年龄");
        emps.stream().mapToInt(e -> e.getAge()).forEach(System.out::println);
	}
}

4.3 中间操作:排序

方法 描述 sorted 返回一个新流,流中的元素按照自然排序进行排序 sorted(Comparator comp) 返回一个新流,并且Comparator指定的排序方式进行排序

代码示例:

package test;

import java.util.Arrays;
import java.util.List;

/**
 * 流编程中间操作练习(排序)
 */
public class StreamMiddleOperateDemo {

	public static void main(String[] args) {
		
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 18, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );
        
        // 1.按照工资高低进行排序
        System.out.println("\n按照工资高低进行排序");
        emps.stream().sorted((x, y) -> Double.compare(x.getSalary(), y.getSalary()))
            .forEach(System.out::println);
	}
}    

5、Stream的终止操作

Stream的终止操作用来获取一系列流水线操作的最终结果,这个结果可以是任何值,例如boolean,List,Integer甚至可以是void,终止操作也分为以下几大类:

5.1 终止操作:查找与匹配

方法 描述 allMatch(Predicate p) 传入一个断言型函数,对流中所有的元素进行判断,如果都满足返回true,否则返回false。 anyMatch(Predicate p) 传入一个断言型函数,对流中所有的元素进行判断,只要有一个满足条件就返回true,都不满足返回false。 noneMatch(Predicate p) 所有条件都不满足,返回true,否则返回false。 findFirst() 返回流中的第一个元素。 findAny() 返回流中的任意一个元素。 count() 返回流中元素的个数。 max(Comparator c) 按照给定的排序规则进行排序后,返回流中最大值的元素 min(Comparator c) 按照给定的排序规则进行排序后,返回流中最小值的元素 forEach(Consumer c) 内部迭代。

代码示例:

package test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * 流编程终止操作练习(查找与匹配)
 * 流操作三个步骤:1.创建  2.中间操作  3.终止操作
 * 流编程中必须有一个终止操作,并且只能有1个终止操作,中间操作可以有(0-n)个
 * 流的创建
 *    1.通过Collection创建
 *    2.通过Arrays创建
 *    3.通过Stream.of()静态方法获取流
 *    4.创建无限流(迭代,生成)
 * 流的中间操作 
 *    无状态  filter(), map(), flatMap()  依赖于元素
 *    有状态 distinct() sorted(), limit(), 
 * 流的终止操作(流结束)
 *    终止操作:查找与匹配 allMatch, anyMatch, noneMatch, findFirst, findAny, count, max, min ,forEach
 *    终止操作:规则  reduce
 *    终止操作:收集  
 */
public class StreamEndOperateDemo {

	public static void main(String[] args) {
         
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 17, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );

		 // 1.查看是否有员工年龄是否都大于18
        boolean flag1 = emps.stream().allMatch(e -> e.getAge() > 18);
        System.out.println("\n查看是否有员工年龄是否都大于18: " + flag1);   // false
        
        // 2.是否有员工年龄大于50(除张三外)
        boolean flag3 = emps.stream().filter(e -> !"张三".equals(e.getName())).anyMatch(e -> e.getAge() > 50);
        System.out.println("\n是否有员工年龄大于50(除张三外): " + flag3);  //true
        
        // 3.没有员工的年龄大于50?
        boolean flag4 = emps.stream().noneMatch(e -> e.getAge() > 50);
        System.out.println("\n没有员工的年龄大于50: " + flag4);  //false
        
        // 4.先按照年龄进行排序,然后返回第一个员工。optional是java8用来包装可能出现空指针的对象的对象
        Optional<Employee> op1 = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge())).findFirst();
        System.out.println("\n先按照年龄进行排序,然后返回第一个员工: " + op1.get()); 
        
        // 5. 查找任意一名员工的姓名,当使用顺序流时,返回的是第一个对象,当使用并行流时,会随机返回一名员工的姓名
        Optional<String> op2 = emps.parallelStream().map(e -> e.getName()).findAny();
        System.out.println("\n查找任意一名员工的姓名: " + op2.get()); //会随机获取一名员工
        
        // 6. 查询员工人数
        Long count = emps.stream().count();
        System.out.println("\n查询员工人数: " + count);
        
        // 7.查询员工工资最大的员工信息。PS: 这个也可以通过先按照工资排序,然后取第一个元素来实现
        Optional<Employee> maxSalary = emps.stream().max((x, y) -> Double.compare(x.getSalary(), y.getSalary()));
        System.out.println("\n查询员工工资最大的员工信息: " + maxSalary.get());
        
        // 8.查询员工最小年龄
        Optional<Employee> minAge = emps.stream().max((x, y) -> -Integer.compare(x.getAge(), y.getAge()));
        System.out.println("\n查询员工最小年龄: " + minAge.get());
        
        // 9.循环输出所有员工的信息
        System.out.println("\n循环输出所有员工的信息");
        emps.stream().forEach(System.out::println);
	}
}

5.2 终止操作:规约

方法 描述 reduce(T iden, BinaryOperator bo) 可以将流中的元素反复结合起来,得到一个值,返回T reduce(BinaryOperator bo) 可以将流中的元素反复结合起来,得到一个值,返回Optional。(Optional我们后面再聊)

代码示例:

package test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * 流编程终止操作练习(规约)
 */
public class StreamEndOperateDemo {

	public static void main(String[] args) {
         
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 17, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );
        
        // 1.将所有员工的名字加上| 下一个员工的名字: 如 张三|李四
        Optional<Employee> op3 = emps.stream().reduce((x,y) -> {
            x.setName(x.getName() + "|" + y.getName()); 
        	return x;
        });
        System.out.println(op3.get().getName());  //张三|李四|王五|田七|赵六|赵六
        
        // 2.将所有员工的名字加上| 下一个员工的名字,并且开始以王八开始;
        Employee emp = emps.stream()
                .reduce(new Employee("王八", 65, 8888.88)
                        , (x,y) -> {
                            x.setName(x.getName() + "|" + y.getName());
                            return x;
                        });
        System.out.println(emp.getName());  //王八|张三|李四|王五|田七|赵六|赵六
	}
}

5.3 终止操作:收集

方法 描述 collect(Collector c) 将流中的元素转换成其他形式,接受一个Collector接口的实现,用于处理Stream流中的元素,将流转换成其他形式的对象。

collector接口中方法的实现决定了如何对流执行收集操作(如收集到List,Set,Map)。java8中的Collectors类提供了很多静态方法,可以非常方便的创建常用的收集器实例,具体方法与实例:

代码示例:

package test;

import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 流编程终止操作练习(收集)
 */
public class StreamEndOperateDemo {

	public static void main(String[] args) {
         
		List<Employee> emps = Arrays.asList(
	            new Employee("张三", 17, 6666.66),
	            new Employee("李四", 20, 7777.77),
	            new Employee("王五", 36, 8888.88),
	            new Employee("田七", 55, 11111.11),
	            new Employee("赵六", 55, 9999.99),
	            new Employee("赵六", 45, 12222.22)
	    );
        
		// 1.按年龄排序后收集成一个list并返回
		System.out.println("\n按年龄排序后收集成一个list并返回");
        List<Employee> list = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
                  .collect(Collectors.toList());
        list.forEach(System.out::println);
        
        // 2.计算流中元素的个数:
        long count = emps.stream().collect(Collectors.counting());
        System.out.println("\n计算流中元素的个数: " + count);
        
        // 3.对所有员工的年龄求和:
        int inttotal = emps.stream().collect(Collectors.summingInt(Employee::getAge));
        System.out.println("\n对所有员工的年龄求和: " + inttotal);
        
        // 4.计算所有员工工资的平均值:
        Double doubleavg = emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
        System.out.println("\n计算所有员工工资的平均值: " + doubleavg);
        
        // 5.返回一个IntSummaryStatistics,可以通过这个对象获取统计值,如平均值:
        IntSummaryStatistics iss = emps.stream().collect(Collectors.summarizingInt(Employee::getAge));
        System.out.println("\n平均值: " + iss.getAverage());
        System.out.println("最大值: " + iss.getMax());
        System.out.println("最小值: " + iss.getMin());
        
        // 6.连接所有员工的名字:
        String str= emps.stream().map(Employee::getName).collect(Collectors.joining());
        System.out.println("\n连接所有员工的名字: " + str);
        
        // 7.相当于先按照工资进行排序,再取出排在第一位的员工
        Optional<Employee> min = emps.stream().collect(
            Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary()))
        );
        System.out.println("\n工资最低的员工: " + min);
        
        // 8.根据某属性对结果进行分组,属性为K,结果为V:
        Map<String, List<Employee>> kv = emps.stream().collect(
            Collectors.groupingBy(Employee::getName)
        );
        System.out.println("\n根据某属性对结果进行分组,属性为K,结果为V:\n" + kv);
        
        // 9.根据true或false进行分区,年龄大于30的分在true区,小于30的分在false区
        Map<Boolean,List<Employee>> vd = emps.stream().collect(
            Collectors.partitioningBy(e -> e.getAge() > 30)
        );
        System.out.println("\n根据true或false进行分区,年龄大于30的分在true区,小于30的分在false区: \n" + vd);
	}
}

Stream的总结其实就是一句话,记住Stream操作的三个步骤,创建流 -> 一系列中间操作 -> 终止操作拿到返回结果。

三 、Java9 响应式流编程/Reactive Stream

​ Java 9的 Reactive Streams是对异步流式编程的一种实现。它基于异步发布和订阅模型,具有非阻塞“背压”数据处理的特点。

Non-blocking Back Pressure(非阻塞背压):它是一种机制,让发布订阅模型中的订阅者避免接收大量数据(超出其处理能力),订阅者可以异步通知发布者降低或提升数据生产发布的速率。它是响应式编程实现效果的核心特点!

1、Java9 Reactive Stream API

Java 9提供了一组定义响应式流编程的接口。所有这些接口都作为静态内部接口定义在java.util.concurrent.Flow类里面。

file

下面是Java 响应式编程中的一些重要角色和概念,先简单理解一下

  • 发布者(Publisher)是潜在的无限数量的有序数据元素的生产者。 它根据收到的需求(subscription)向当前订阅者发布一定数量的数据元素。

  • 订阅者(Subscriber)从发布者那里订阅并接收数据元素。与发布者建立订阅关系后,发布者向订阅者发送订阅令牌(subscription),订阅者可以根据自己的处理能力请求发布者发布数据元素的数量。

  • 订阅令牌(subscription)表示订阅者与发布者之间建立的订阅关系。 当建立订阅关系后,发布者将其传递给订阅者。 订阅者使用订阅令牌与发布者进行交互,例如请求数据元素的数量或取消订阅。

2、Java响应式编程四大接口

2.1.Subscriber Interface(订阅者订阅接口)

public static interface Subscriber<T> {
    public void onSubscribe(Subscription subscription);
    public void onNext(T item);
    public void onError(Throwable throwable);
    public void onComplete();
}
  • onSubscribe:在发布者接受订阅者的订阅动作之后,发布任何的订阅消息之前被调用。新创建的Subscription订阅令牌对象通过此方法传递给订阅者。
  • onNext:下一个待处理的数据项的处理函数
  • onError:在发布者或订阅遇到不可恢复的错误时调用
  • onComplete:当没有订阅者调用(包括onNext()方法)发生时调用。

2.2.Subscription Interface (订阅令牌接口)

订阅令牌对象通过Subscriber.onSubscribe()方法传递

public static interface Subscription {
    public void request(long n);
    public void cancel();
}
  • request(long n)是无阻塞背压概念背后的关键方法。订阅者使用它来请求n个以上的消费项目。这样,订阅者控制了它当前能够接收多少个数据。
  • cancel()由订阅者主动来取消其订阅,取消后将不会在接收到任何数据消息。

2.3.Publisher Interface(发布者接口)

@FunctionalInterface
public static interface Publisher<T> {
    public void subscribe(Subscriber<? super T> subscriber);
}

调用该方法,建立订阅者Subscriber与发布者Publisher之间的消息订阅关系。

2.4.Processor Interface(处理器接口)

处理者Processor 可以同时充当订阅者和发布者,起到转换发布者——订阅者管道中的元素的作用。用于将发布者T类型的数据元素,接收并转换为类型R的数据并发布。

public static interface Processor<T,R> extends Subscriber<T>, Publisher<R> {
}

3、实战案例

现在我们要去实现上面的四个接口来完成响应式编程

  • Subscription Interface订阅令牌接口通常不需要我们自己编程去实现,我们只需要在知道request()方法和cancle()方法含义即可。
  • Publisher Interface发布者接口,Java 9 已经默认为我们提供了实现SubmissionPublisher,该实现类除了实现Publisher接口的方法外,提供了一个方法叫做submit()来完成消息数据的发送。
  • Subscriber Interface订阅者接口,通常需要我们自己去实现。因为在数据订阅接收之后,不同的业务有不同的处理逻辑。
  • Processor实际上是 Publisher Interface和Subscriber Interface的集合体,有需要数据类型转换及数据处理的需求才去实现这个接口

代码示例1:

package test;

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

public class FlowDemo {

	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<Integer>() {
			
			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);
				// 处理完调用请求request再请求一个数据
				this.subscription.request(1);
				// 或者 已经达到目标, 调用cancel告诉发布者不再接收数据了
				// this.subscription.cancel();
			}
			
			@Override
			public void onError(Throwable throwable) {
				// 出现了异常(例如处理数据的时候产生了异常)
				throwable.printStackTrace();
				// 调用cancel告诉发布者不再接收数据了
				this.subscription.cancel();
			}

			@Override
			public void onComplete() {
				// 全部数据处理完了(发布者关闭了)
				System.out.println("处理完了");
			}
		};
		
		// 3.发布者与订阅者, 建立订阅关系
		publisher.subscribe(subscriber);
		
		// 4. 生产数据, 并发布
		int data = 111;
		publisher.submit(data);
//		publisher.submit(222);
//		publisher.submit(333);
		
		// 5. 结束后, 关闭发布者
		publisher.close();
		
		// 主线程模拟延迟停止, 否则数据没有消费就退出
		Thread.currentThread().join(1000);
	}
}

代码示例2:

package test;

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

/**
 * 自定义处理器, 需要继承SubmissionPublisher并实现Processor接口
 * 输入源数据integer, 过滤掉小于0的, 然后转换成字符串发布出去
 */
class Myprocessor 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();
		// 调用cancel告诉发布者不再接收数据了
		this.subscription.cancel();
	}

	@Override
	public void onComplete() {
		// 全部数据处理完了(发布者关闭了)
		System.out.println("处理器处理完了");
	}
}

public class FlowDemo2 {

	public static void main(String[] args) throws Exception {
		
		// 1. 定义发布者,发布的数据类型是Integer
		// 直接使用JDK自带的SubmissionPublisher, 它实现了Publisher接口
		SubmissionPublisher<Integer> publisher = new SubmissionPublisher<Integer>();
		
		// 2. 定义处理器, 对数据进行过滤, 并转换为String类型
		MyProcessor myProcessor = new MyProcessor();
		
		// 3. 发布者与处理器, 建立订阅关系
		publisher.subscribe(myProcessor);

		// 4. 定义最终订阅者, 消费String类型数据
		Subscriber<String> subscriber = new Subscriber<String>() {
			
			private Subscription subscription; // 订阅对象引用
			
			@Override
			public void onSubscribe(Subscription subscription) {
				// 保存订阅关系, 需要用它来给发布者响应
				this.subscription = subscription;
				// 请求一个数据
				this.subscription.request(1);
			}
			
			@Override
			public void onNext(String item) {
				// 接收到一个数据, 处理
				System.out.println("接收到数据: " + item);
				// 处理完调用请求request再请求一个数据
				this.subscription.request(1);
				// 或者 已经达到目标, 调用cancel告诉发布者不再接收数据了
				// this.subscription.cancel();
			}
			
			@Override
			public void onError(Throwable throwable) {
				// 出现了异常(例如处理数据的时候产生了异常)
				throwable.printStackTrace();
				// 调用cancel告诉发布者不再接收数据了
				this.subscription.cancel();
			}

			@Override
			public void onComplete() {
				// 全部数据处理完了(发布者关闭了)
				System.out.println("处理完了");
			}
		};
		
		// 5.处理器与订阅者, 建立订阅关系
		myProcessor.subscribe(subscriber);
		
		// 6. 生产数据, 并发布
		publisher.submit(111);
		publisher.submit(-111);
		publisher.submit(222);
		
		// 7. 结束后, 关闭发布者
		publisher.close();
		
		// 主线程模拟延迟停止, 否则数据没有消费就退出
		Thread.currentThread().join(1000);
	}
}

四 、Spring5响应式编程/SpringWebFlux

1、webflux介绍

Spring WebFlux 是 Spring Framework 5.0中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。

Spring WebFlux 用于创建基于事件循环执行模型的完全异步且非阻塞的应用程序。

(PS:所谓异步非阻塞是针对服务端而言的,是说服务端可以充分利用CPU资源去做更多事情,这与客户端无关,客户端该怎么请求还是怎么请求。)

Reactive Streams是一套用于构建高吞吐量、低延迟应用的规范。而Reactor项目是基于这套规范的实现,它是一个完全非阻塞的基础,且支持背压。Spring WebFlux基于Reactor实现了完全异步非阻塞的一套web框架,是一套响应式堆栈。

【spring-webmvc + Servlet + Tomcat】命令式的、同步阻塞的

【spring-webflux + Reactor + Netty】响应式的、异步非阻塞的

img

2、WebFlux 应用场景

上面说到了, Spring WebFlux 是一个异步非阻塞式的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。

PS: IO 密集型包括:磁盘IO密集型, 网络IO密集型,微服务网关就属于网络 IO 密集型,使用异步非阻塞式编程模型,能够显著地提升网关对下游服务转发的吞吐量。

WebFlux网关

3、选 WebFlux 还是 Spring MVC?

首先你需要明确一点就是:WebFlux 不是 Spring MVC 的替代方案!,虽然 WebFlux 也可以被运行在 Servlet 容器上(需是 Servlet 3.1+ 以上的容器),但是 WebFlux 主要还是应用在异步非阻塞编程模型,而 Spring MVC 是同步阻塞的,如果你目前在 Spring MVC 框架中大量使用非同步方案,那么,WebFlux 才是你想要的,否则,使用 Spring MVC 才是你的首选。

在微服务架构中,Spring MVC 和 WebFlux 可以混合使用,比如已经提到的,对于那些 IO 密集型服务(如网关),我们就可以使用 WebFlux 来实现。

选 WebFlux 还是 Spring MVC? This is not a problem!

咱不能为了装逼而装逼,为了技术而技术,还要考量到转向非阻塞响应式编程学习曲线是陡峭的,小组成员的学习成本等诸多因素。

总之一句话,在合适的场景中,选型最合适的技术

4、异同点

WebFlux 适用性

从上图中,可以一眼看出 Spring MVC 和 Spring WebFlux 的相同点和不同点:

相同点:

  • 都可以使用 Spring MVC 注解,如 @Controller, 方便我们在两个 Web 框架中自由转换;
  • 均可以使用 Tomcat, Jetty, Undertow Servlet 容器(Servlet 3.1+);

注意点:

  • Spring MVC 因为是使用的同步阻塞式,更方便开发人员编写功能代码,Debug 测试等,一般来说,如果 Spring MVC 能够满足的场景,就尽量不要用 WebFlux;
  • WebFlux 默认情况下使用 Netty 作为服务器;
  • WebFlux 不支持 MySql;

5、实战案例

暂略

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值