Java 函数式编程(Lambda表达式)与Stream API
1 函数式编程
函数式编程(Functional Programming)是编程范式的一种。最常见的编程范式是命令式编程(Impera Programming),比如面向过程、面向对象编程都属于命令式编程,大家用得最多、最熟悉。函数式编程并非近几年的新技术或新思维,其诞生已有50多年时间。
在函数式编程里面,一切都是数学函数。当然,函数式编程语言里也可以有对象,但这些对象是不可变的——要么是函数参数要么是返回值。函数式编程语言里没有for等循环,而是通过递归、把函数当成参数传递的方式实现循环效果。
简而言之,函数式编程的特点:一切皆函数(称算子?)、一切数据皆不可变,一切计算都是函数作用在不可变数据上产生新不可变数据的过程。
注:Java 8的主要变化(详情可参阅 Java8 新特性)
- lambda表达式
- 函数式接口
- 方法引用
- 接口默认方法
- Stream
- 用Optional取代null
- 新的日志和时间:推荐用Instant代替Date、用LocalDateTime代替Calendar、用DateTimeFormatter代替SimpleDateFormat
- CompletableFuture
- 去除了永久代(PermGen), 被元空间(Metaspace)代替
2 Java 8函数式编程(Lambda表达式)
Java 8开始支持函数式编程,其是通过Lambda表达式语法来支持的。
Java SE 8 adds a relatively small number of new language features -- lambda expressions, method references, default and static methods in interfaces, and more widespread use of type inference
2.1 Lambda表达式
Lambda表达式可以看成是对 特定接口的匿名实现类语法 的简写(只是语法上看如是,它们在JVM层面是有明显区别的,见后文),这里的“特定接口”是指函数式接口,若不是函数式接口则传参时若使用Lambda表达式编译器会报错。示例:
new Thread(new Runnable() { @Override public void run() { } });//通过匿名内部类创建Runnable接口实现类作为Thread的参数 new Thread(() -> { });//通过Labmda表达式创建Thread的参数 Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2); Comparator<String> c = (String s1, String s2) -> s1.compareToIgnoreCase(s2); FileFilter java = f -> f.getName().endsWith(".java"); button.addActionListener(e -> ui.dazzle(e.getModifiers()));
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);
Lambda表达式代替匿名内部类作为函数的参数时,相当于向函数传递了个算子。与匿名内部类相比,Lambda表达式省略了方法名、方法的参数类型有无均可,当然最好不要写类型以由编译器自动推断从而提高灵活性。
2.2 函数式接口
函数式接口:接口中【与Objec的方法签名不同的抽象方法有且只有一个】的接口。
注:
由于java中Object是任意类或接口的父类,故该抽象方法签名不能与Object中的方法一样。可以一样,但此时该方法不会被认为是函数式接口的抽象方法。
接口中可有非抽象方法,如default、static method等。
可在接口上加@FunctionalInterface,此时编译器会检查接口是否符合函数式接口规范。
可见:【抽象方法有且只有一个是是函数式接口的充要条件】,注意【有且只有一个抽象方法】不是【是函数式接口】的充要条件,体会两者的区别。也就是函数式接口中允许有非抽象方法存在。
示例:
@FunctionalInterface public interface CallBack { public String run();// 须有且只有一个抽象方法 public String toString();//允许,但不会被认为是函数式接口的抽象方法,因与Object中的方法的signature一样 default String getName() {// 允许有default方法 return "zhangsan"; } public static String getVersion() {// 允许有static方法 return "1"; } }
Java 8 定义了一系列常用的函数式接口(java.util.function包下),可覆盖绝大多数需求场景,可直接使用这些而不是自己另外定义。主要有:
函数接口 | 抽象方法 | 功能 | 示例 |
Predicate<T> | boolean test(T t) | 根据输入的T值得到布尔值 | 身高大于177cm? |
Supplier<T> | T get() | 产生T类型的消息 | |
Consumer<T> | void accept(T t) | 消费T类型的消息 | |
Function<T, R> | R apply(T t) | 将T类型值转换成R类型值 | 根据student对象获取名字 |
BiPredicate<T, U> | boolean test(T t, U u) | 根据输入的T、U两值得到布尔值 | |
BiConsumer<T, U> | void accept(T t, U u) | 消费T、U两种类型的两个消息 | |
BiFunction<T, R, R> | R apply(T t, U u) | 将T、U类型值转成R类型值 | 两个数乘积 |
UnaryOperator | T apply(T t) | Function的特例,一元操作 | 整数取反 |
BinaryOperator | T apply(T t, T u) | BiFunction的特例,二元操作 | 两数求和 |
IntPredicate | boolean test(int t) | Predicate的特例,只不过输入值为int类型 | |
IntSupplier | int getAsInt() | 类似于Supplier,只不过返回值为int类型 | |
IntConsumer | void accept(int t) | Consumer的特例,只不过输入轴为int类型 | |
IntFunction<R> | R apply(int t) | Function的特例,只不过输入值为int类型 | |
IntUnaryOperator | int applyAsInt(int operand) | 类似于UnaryOperator,只不过输入值为int类型 | |
IntBinaryOperator | int applyAsInt(int left, int right) | 类似于BinaryOperator,只不过输入值为int类型 | |
ToIntFunction<T> | int applyAsInt(T t) | 类似于Function,只不过返回值为int类型 | |
ToIntBiFunction<T, U> | int applyAsInt(T t, U u) | 类似于BiFunction,只不过返回值为int类型 | |
IntToLongFunction | long applyAsLong(int value) | 类似于Function,只不过输入和返回值类型分别为int、long |
关于上述各函数使用的详细介绍,首推去看源码(也可参阅:Java8新特性指南之函数式接口),这些函数式接口结合Java 8中引入的Stream一起使用能够可以很大程度提高开发者的编码效率。
除了上述几种函数式接口外,还提供了int、long、double 的primitive specializations,如IntSupplier、LongBinaryOperator、LongToDoubleFunction等,其他primitive types的可以转换为这三个primitive types的。详情可参阅 java.util.function 包下的源码。
Lambda表达式和匿名内部类的区别:
匿名内部类编译后编译器会为匿名类也单独生成class文件,lambda表达式则不会。=> lambda表达式中的this的意义跟在表达式外的一样。示例:
1 public class Hello { 2 Runnable r1 = () -> { System.out.println(this); } 3 Runnable r2 = () -> { System.out.println(toString()); } 4 5 public String toString() { return "Hello, world!"; } 6 7 public static void main(String... args) { 8 new Hello().r1.run(); 9 new Hello().r2.run(); 10 } 11 } 12 //结果:输出两个“Hello, world!” 13 //若r1、r2用匿名内部类,则输出的两个结果不同,类似于:Hello$1@5b89a773 and Hello$2@537a7706
关于Lambda表达式,推荐参阅:Lambda-State-final
关于Java Lambda表达式的易懂介绍,见:Java Lambda表达式简介
2.3 方法引用
若lambda表达式仅仅是调用某个方法,则可以写成方法引用,这种情况下使得方法引用代码更简洁易读。示例:
list.forEach(new Consumer() { @Override public void accept(Object s) { System.out.println(s); } });//1 匿名内部类 list.forEach(s->System.out.println(s));//2 lambda表达式 list.forEach(System.out::println);//3 方法引用
方法引用有很多种,语法如下:
- 静态方法引用:
ClassName::methodName
- 实例上的实例方法引用:
instanceReference::methodName
- 父类的实例方法引用:
super::methodName
- 类型上的实例方法引用:
ClassName::methodName
- 构造方法引用:
Class::new
- 数组构造方法引用:
TypeName[]::new
3 Java 8 Stream
Java 8的interface引入了default method,以对集合框架Collection进行了扩展(从而其继承者如List、Set、Map也扩展了):增加了很多default method以支持Stream及Lambda表达式操作。
3.1 what
Java 8 API添加了新的抽象Stream(这里的Stream不是IO中的流),可以让我们以声明的方式处理数据。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,如筛选、排序、聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由终止操作(terminal operation)得到前面处理的结果。
示例:
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); // 获取空字符串的数量 long count = strings.stream().filter(string -> string.isEmpty()).count();
Stream并不是某种数据结构,它更像是数据源(数据源可以是一个数组,Java容器或I/O channel等)的一种迭代器(Iterator),单向、不可重复、数据源只能遍历一次。要得到一个Stream通常不会手动创建,而是调用对应的工具方法,如:list1.stream()、list1.parallelStream()、Arrays.stream(T[] array)
3.2 Stream的特点
无存储:stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等,且可以是无限的。
无状态(为函数式编程而生):对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
惰式执行:stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
可消费性:stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成stream。
3.3 stream使用
对stream的操作分为两类,中间操作(intermediate operations)和终止操作(terminal operations):
中间操作总是会惰式执行——调用中间操作只是生成一个标记了该操作的新stream,仅此而已,不会执行数据计算,而是进行函数计算,如Function接口里的compose方法内部就是进行了函数计算,源码:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); }
结束操作会触发数据计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。
Stream“以声明的方式处理数据”,其提供了一系列操作,主要有:
forEach
distinct、filter、sorted
map、flatMap
reduce(虽也提供了max、min、count、sum,但这些均可由reduce实现等价效果)
collect,收集器,方法声明为 <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) ,三个参数分别指定目标容器是什么、元素如何产生、若是并行执行则多个结果如何合并。
实际用得更多的是只有一个Collector参数的collect方法,java.util.stream.Collectors工具类封装了生成大多数常用Collector的静态方法。如:
joining
summingDouble
生成列表:
toList、toSet:返回的集合类型为接口,具体类型由库决定
toCollection(ArrayList::new):返回的集合具体类型自己指定
生成map:
partitioningBy //只能分为两组,key为Boolean类型
groupingBy //可分为多组,类似于SQL中的group by操作
toMap
在使用时,就是通过为这些操作指定Lambda表达式(亦可称算子或函数?)来指定操作逻辑的。示例:
// 1. Collection集合的stream()或者parallelStream() List<String> list = Lists.newArrayList(); Stream<String> stream1 = list.stream(); // 2. 调用Arrays.stream(T[] array)静态方法 Integer[] array = {1, 2, 3}; Stream<Integer> stream2 = Arrays.stream(array); // 3. 调用Stream.of(T... values)静态方法 Stream<String> stream3 = Stream.of("aa", "bb", "cc"); // 4. 调用Stream.iterate(final T seed, final UnaryOperator<T> f),创建无限流 // (x) -> x + 2 为函数式接口,传入x返回x+2,0为最开始的值 Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2); // 一直输出 0 2 4 6 8 10 12 ... stream4.forEach(System.out::println); // 5. 调用调用Stream.generate(),创建无限流 Stream<Integer> stream5 = Stream.generate(() -> 10); // 一直输出10,你可以用Random等类随机生成哈 stream5.forEach(System.out::println);
1 //reduce 2 Integer reduce = Stream.of(1, 2, 3, 4).reduce(0, (acc, x) -> acc+ 1);//相当于count 3 4 //map 5 List<Student> students = new ArrayList<>(3); 6 students.add(new Student("路飞", 22, 175)); 7 students.add(new Student("红发", 40, 180)); 8 students.add(new Student("白胡子", 50, 185)); 9 List<String> names = students.stream().map(student -> student.getName()).collect(Collectors.toList()); 10 11 //flateMap 12 List<Student> students = new ArrayList<>(3); 13 students.add(new Student("路飞", 22, 175)); 14 students.add(new Student("红发", 40, 180)); 15 students.add(new Student("白胡子", 50, 185)); 16 List<Student> studentList = Stream.of(students, 17 asList(new Student("艾斯", 25, 183), 18 new Student("雷利", 48, 176))) 19 .flatMap(students1 -> students1.stream()).collect(Collectors.toList()); 20 21 22 //toSet 23 Set<String> set = stream.collect(Collectors.toSet()); // (2) 24 25 //toCollection 26 HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4) 27 28 //toMap 29 // 使用toMap()统计学生GPA 30 Map<Student, Double> studentToGPA = 31 students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key 32 student -> computeGPA(student)));// 如何生成value 33 34 //partitioningBy 35 Map<Boolean, List<Student>> passingFailing = students.stream() 36 .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD)); 37 38 //groupingBy 39 Map<Department, List<Employee>> byDept = employees.stream() 40 .collect(Collectors.groupingBy(Employee::getDepartment)); 41 42 Map<Department, Integer> totalByDept = employees.stream()// 使用下游收集器统计每个部门的人数 43 .collect(Collectors.groupingBy(Employee::getDepartment, 44 Collectors.counting()));// 下游收集器 45 46 Map<Department, List<String>> byDept = employees.stream()// 按照部门对员工分布组,并只保留员工的名字 47 .collect(Collectors.groupingBy(Employee::getDepartment, 48 Collectors.mapping(Employee::getName,// 下游收集器 49 Collectors.toList())));// 更下游的收集器 50 51 //joining 52 List<Student> students = new ArrayList<>(3); 53 students.add(new Student("路飞", 22, 175)); 54 students.add(new Student("红发", 40, 180)); 55 students.add(new Student("白胡子", 50, 185)); 56 57 String names = students.stream() 58 .map(Student::getName).collect(Collectors.joining(",","[","]"));
3.4 Stream工作流执行原理(stream-pipeline)
参阅:https://objcoding.com/2019/03/04/lambda/#stream-pipelines
3.5 parallel Stream
java并行API演变:
- 1.0-1.4 中的 java.lang.Thread
- 5.0 中的 java.util.concurrent
- 6.0 中的 Phasers 等
- 7.0 中的 Fork/Join 框架
- 8.0 中的 parallelStream
parallelStream是java 8引入的,用于并行操作。其内部用了Fork/Join框架。
适用场景:CPU密集型
为何可比普通的多线程或线程池快:使用Fork/Join框架(ForkJoinPool.commonPool()),ForkJoinPool会自动将大任务拆解成无交集的子任务给不同线程并行执行,最后汇集结果。与线程池不同的是,还采取了工作窃取(work-stealing)算法,当有线程完成计算任务时会从其他线程的任务队列取任务来执行,从而整体上提高执行效率。
优点:代码简介,执行效率高;缺点:黑箱、不好跟踪调试
使用:尽可能用Stream API,多核情况下尽可能用parallelStream
参阅:
parallelStream原理:https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/7-ParallelStream.md
Stream API性能测试:https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md
4 参考资料
使用:https://www.cnblogs.com/snowInPluto/p/5981400.html、http://www.jiangxinlingdu.com/thought/2019/09/06/jdk8s.html
原理:https://objcoding.com/2019/03/04/lambda/