一、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
类里面。
下面是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】响应式的、异步非阻塞的
2、WebFlux 应用场景
上面说到了, Spring WebFlux 是一个异步非阻塞式的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。
PS: IO 密集型包括:磁盘IO密集型, 网络IO密集型,微服务网关就属于网络 IO 密集型,使用异步非阻塞式编程模型,能够显著地提升网关对下游服务转发的吞吐量。
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、异同点
从上图中,可以一眼看出 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、实战案例
暂略