lambda表达式是java8给我们带来的几个重量级新特性之一,借用lambda表达式,可以让我们的java程序设计更加简洁。最近新的项目摒弃了1.6的版本,全面基于java8进行开发,本文是java8新特性的第一篇,将探讨行为参数化、lambda表达式,以及方法引用。
行为参数化简单的说就是函数的主体仅包含模板类通用代码,而一些会随着业务场景而变化的逻辑则以参数的形式传递到函数之中,采用行为参数化可以让程序更加的通用,以应对频繁变更的需求。
考虑一个业务场景,假设我们需要通过程序对苹果进行筛选,我们先定义一个苹果的实体:
/** * 苹果实体 * * @author zhenchao.wang 2016-09-17 12:49 * @version 1.0.0 */ public class Apple { /** 编号 */ private long id; /** 颜色 */ private Color color; /** 重量 */ private float weight; /** 产地 */ private String origin; public Apple() { } public Apple(long id, Color color, float weight, String origin) { this.id = id; this.color = color; this.weight = weight; this.origin = origin; } // 省略getter和setter }
用户最开始的需求可能只是简单的希望能够通过程序筛选出绿色的苹果,于是我们可以很快的通过程序实现:
/** * 筛选绿苹果 * * @param apples * @return */ public static ListfilterGreenApples(List apples) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (Color.GREEN.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
如果过了一段时间用户提出了新的需求,希望能够通过程序筛选出红色的苹果,于是我们又针对性的添加了筛选红色苹果的功能:
/** * 筛选红苹果 * * @param apples * @return */ public static ListfilterRedApples(List apples) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (Color.RED.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
更好的实现是把颜色作为一个参数传递到函数中,这样就可以应对以后用户提出的各种颜色筛选请求了:
/** * 自定义筛选颜色 * * @param apples * @param color * @return */ public static ListfilterApplesByColor(List apples, Color color) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (color.equals(apple.getColor())) { filterApples.add(apple); } } return filterApples; }
这样设计了之后,再也不用担心用户的颜色筛选需求变化了,但是不幸的是,某一天用户提了一个需求要求能够选择重量达到某一标准的苹果,有了前面的教训,我们也把重量的标准作为参数传递给筛选函数,于是得到:
/** * 筛选指定颜色,且重要符合要求 * * @param apples * @param color * @param weight * @return */ public static ListfilterApplesByColorAndWeight(List apples, Color color, float weight) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (color.equals(apple.getColor()) && apple.getWeight() >= weight) { filterApples.add(apple); } } return filterApples; }
这样通过传递参数的方式真的好吗?如果筛选条件越来越多,组合模式越来越复杂,我们是不是需要考虑到所有的情况,并针对每一种情况都有相应的应对策略呢,并且这些函数仅仅是筛选条件的部分不一样,其余部分都是相同的模板代码(遍历集合),这个时候我们就可以将行为 参数化 ,让函数仅保留模板代码,而把筛选条件抽离出来当做参数传递进来,在java8之前,我们通过定义一个过滤器接口来实现:
/** * 苹果过滤接口 * * @author zhenchao.wang 2016-09-17 14:21 * @version 1.0.0 */ @FunctionalInterface public interface AppleFilter { /** * 筛选条件抽象 * * @param apple * @return */ boolean accept(Apple apple); } /** * 将筛选条件封装成接口 * * @param apples * @param filter * @return */ public static ListfilterApplesByAppleFilter(List apples, AppleFilter filter) { List filterApples = new ArrayList<>(); for (final Apple apple : apples) { if (filter.accept(apple)) { filterApples.add(apple); } } return filterApples; }
通过上面行为抽象化之后,我们可以在具体调用的地方设置筛选条件,并将条件作为参数传递到方法中:
public static void main(String[] args) { Listapples = new ArrayList<>(); // 筛选苹果 List filterApples = filterApplesByAppleFilter(apples, new AppleFilter() { @Override public boolean accept(Apple apple) { // 筛选重量大于100g的红苹果 return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100; } }); }
上面的行为参数化方式采用匿名类来实现,这样的设计在jdk内部也经常采用,比如java.util.Comparator
,java.util.concurrent.Callable
等,使用这一类接口的时候,我们都可以在具体调用的地方用过匿名类来指定函数的具体执行逻辑,不过从上面的代码块来看,虽然很极客,但是不够简洁,在java8中我们可以通过lambda来简化:
// 筛选苹果 ListfilterApples = filterApplesByAppleFilter(apples, (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
通过lambda表达式极大的精简了代码,下面来学习java的lambda表达式吧~
我们可以将lambda表达式定义为一种 简洁、可传递的匿名函数,首先我们需要明确lambda表达式本质上是一个函数,虽然它不属于某个特定的类,但具备参数列表、函数主体、返回类型,以及能够抛出异常;其次它是匿名的,lambda表达式没有具体的函数名称;lambda表达式可以像参数一样进行传递,从而极大的简化代码的编写。格式定义如下:
格式一: 参数列表 -> 表达式 格式二: 参数列表 -> {表达式集合}
需要注意的是,lambda表达式隐含了return关键字,所以在单个的表达式中,我们无需显式的写return关键字,但是当表达式是一个语句集合的时候,则需要显式添加return,并用花括号{ }
将多个表达式包围起来,下面看几个例子:
//返回给定字符串的长度,隐含return语句 (String s) -> s.length() // 始终返回42的无参方法 () -> 42 // 包含多行表达式,则用花括号括起来 (int x, int y) -> { int z = x * y; return x + z; }
lambda表达式的使用需要借助于函数式接口,也就是说只有函数式接口出现地方,我们才可以将其用lambda表达式进行简化。
函数式接口定义为只具备 一个抽象方法 的接口。java8在接口定义上的改进就是引入了默认方法,使得我们可以在接口中对方法提供默认的实现,但是不管存在多少个默认方法,只要具备一个且只有一个抽象方法,那么它就是函数式接口,如下(引用上面的AppleFilter):
/** * 苹果过滤接口 * * @author zhenchao.wang 2016-09-17 14:21 * @version 1.0.0 */ @FunctionalInterface public interface AppleFilter { /** * 筛选条件抽象 * * @param apple * @return */ boolean accept(Apple apple); }
AppleFilter
仅包含一个抽象方法accept(Apple apple)
,依照定义可以将其视为一个函数式接口,在定义时我们为该接口添加了@FunctionalInterface
注解,用于标记该接口是函数式接口,不过这个接口是可选的,当添加了该接口之后,编译器就限制了该接口只允许有一个抽象方法,否则报错,所以推荐为函数式接口添加该注解。
jdk为lambda表达式已经内置了丰富的函数式接口,如下表所示(仅列出部分):
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate |
T -> boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer |
T -> void | IntConsumer, LongConsumer, DoubleConsumer |
Funcation |
T -> R | IntFuncation |
Supplier |
() -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator |
T -> T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator |
(T, T) -> T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate |
(L, R) -> boolean | |
BiConsumer |
(T, U) -> void | |
BiFunction |
(T, U) -> R |
下面分别就Predicate
、Consumer
、Function
的使用示例说明。
Predicate
@FunctionalInterface public interface Predicate{ /** * Evaluates this predicate on the given argument. * * @param t the input argument * @return {@code true} if the input argument matches the predicate, * otherwise {@code false} */ boolean test(T t); }
Predicate的功能类似于上面的AppleFilter
,利用我们在外部设定的条件对于传入的参数进行校验,并返回验证结果boolean
,下面利用Predicate
对List集合的元素进行过滤:
/** * 按照指定的条件对集合元素进行过滤 * * @param list * @param predicate * @param* @return */ public List filter(List list, Predicate predicate) { List newList = new ArrayList (); for (final T t : list) { if (predicate.test(t)) { newList.add(t); } } return newList; }
利用上面的函数式接口过滤字符串集合中的空字符串:
demo.filter(list, (String str) -> null != str && !str.isEmpty());
Consumer
@FunctionalInterface public interface Consumer{ /** * Performs this operation on the given argument. * * @param t the input argument */ void accept(T t); }
Consumer提供了一个accept抽象函数,该函数接收参数,但不返回值,下面利用Consumer
遍历集合:
/** * 遍历集合,执行自定义行为 * * @param list * @param consumer * @param*/ public void filter(List list, Consumer consumer) { for (final T t : list) { consumer.accept(t); } }
利用上面的函数式接口,遍历字符串集合,并打印非空字符串:
demo.filter(list, (String str) -> { if (StringUtils.isNotBlank(str)) { System.out.println(str); } });
Function
@FunctionalInterface public interface Function{ /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); }
Funcation执行转换操作,输入是类型T的数据,返回R类型的数据,下面利用Function
对集合进行转换:
/** * 遍历集合,执行自定义转换操作 * * @param list * @param function * @param* @param * @return */ public List filter(List list, Function function) { List newList = new ArrayList (); for (final T t : list) { newList.add(function.apply(t)); } return newList; }
下面利用上面的函数式接口,将一个封装字符串(整型数字的字符串表示)的接口,转换成整型集合:
demo.filter(list, (String str) -> Integer.parseInt(str));
上面这些函数式接口还提供了一些逻辑操作的默认实现,留到后面介绍java8接口的默认方法时再讲吧~
类型推断
在编码过程中,有时候可能会疑惑我们的调用代码会去具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等做正确的判定。
在具体调用时,在一些时候可以省略参数的类型,从而进一步简化代码:
// 筛选苹果 ListfilterApples = filterApplesByAppleFilter(apples, (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100); // 某些情况下我们甚至可以省略参数类型,编译器会根据上下文正确判断 List filterApples = filterApplesByAppleFilter(apples, apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);
局部变量
上面所有例子我们的lambda表达式都是使用其主体参数,我们也可以在lambda中使用局部变量,如下:
int weight = 100; ListfilterApples = filterApplesByAppleFilter(apples, apple -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= weight);
该例子中我们在lambda中使用了局部变量weight,不过在lambda中使用局部变量必须要求该变量 显式声明为final或事实上的final ,这主要是因为局部变量存储在栈上,lambda表达式则在另一个线程中运行,当该线程视图访问该局部变量的时候,该变量存在被更改或回收的可能性,所以用final修饰之后就不会存在线程安全的问题。
采用方法引用可以更近一步的简化代码,有时候这种简化让代码看上去更加的直观,先看一个例子:
/* ... 省略apples的初始化操作 */ // 采用lambda表达式 apples.sort((Apple a, Apple b) -> Float.compare(a.getWeight(), b.getWeight())); // 采用方法引用 apples.sort(Comparator.comparing(Apple::getWeight));
方法引用通过::
将方法隶属和方法自身连接起来,主要分为三类:
静态方法
(args) -> ClassName.staticMethod(args) 转换成 ClassName::staticMethod
参数的实例方法
(args) -> args.instanceMethod() 转换成 ClassName::instanceMethod // ClassName是args的类型
外部的实例方法
(args) -> ext.instanceMethod(args) 转换成 ext::instanceMethod(args)
NullPointException可以说是所有java程序员都遇到过的一个异常,虽然java从设计之初就力图让程序员脱离指针的苦海,但是指针确实是实际存在的,而java设计者也只能是让指针在java语言中变得更加简单、易用,而不能完全的将其剔除,所以才有了我们日常所见到的关键字null
。
空指针异常是一个运行时异常,对于这一类异常,如果没有明确的处理策略,那么最佳实践在于让程序早点挂掉,但是很多场景下,不是开发人员没有具体的处理策略,而是根本没有意识到空指针异常的存在。当异常真的发生的时候,处理策略也很简单,在存在异常的地方添加一个if语句判定即可,但是这样的应对策略会让我们的程序出现越来越多的null判定,我们知道一个良好的程序设计,应该让代码中尽量少出现null关键字,而java8所提供的Optional
类则在减少NullPointException的同时,也提升了代码的美观度。但首先我们需要明确的是,它并 不是对null
关键字的一种替代,而是对于null判定提供了一种更加优雅的实现,从而避免NullPointException。
假设我们需要返回一个字符串的长度,如果不借助第三方工具类,我们需要调用str.length()
方法:
if(null == str) { // 空指针判定 return 0; } return str.length();
如果采用Optional类,实现如下:
return Optional.ofNullable(str).map(String::length).orElse(0);
Optional的代码相对更加简洁,当代码量较大时,我们很容易忘记进行null判定,但是使用Optional类则会避免这类问题。
创建空对象
OptionaloptStr = Optional.empty();
上面的示例代码调用empty()
方法创建了一个空的Optional
对象型。
创建对象:不允许为空
Optional提供了方法of()
用于创建非空对象,该方法要求传入的参数不能为空,否则抛NullPointException
,示例如下:
OptionaloptStr = Optional.of(str); // 当str为null的时候,将抛出NullPointException
创建对象:允许为空
如果不能确定传入的参数是否存在null值的可能性,则可以用Optional的ofNullable()
方法创建对象,如果入参为null,则创建一个空对象。示例如下:
OptionaloptStr = Optional.ofNullable(str); // 如果str是null,则创建一个空对象
流式处理也是java8给我们带来的一个重量级新特性,让我们对集合的操作变得更加简洁和高效,下一篇关于java8新特性的文章,将对流失处理进行全面的讲解。这里Optional也提供了两个基本的流失处理:映射和过滤。
为了演示,我们设计了一个User
类,如下:
/** * @author: zhenchao.Wang 2016-9-24 15:36:56 */ public class User { /** 用户编号 */ private long id; private String name; private int age; private Optionalphone; private Optional email; public User(String name, int age) { this.name = name; this.age = age; } // 省略setter和getter }
手机和邮箱不是一个人的必须有的,所以我们利用Optional定义。
映射:map与flatMap
映射是将输入转换成另外一种形式的输出的操作,比如前面例子中,我们输入字符串,而输出的是字符串的长度,这就是一种隐射,我们利用方法map()
得以实现。假设我们希望获得一个人的姓名,那么我们可以如下实现:
String name = Optional.ofNullable(user).map(User::getName).orElse("no name");
这样当入参user不为空的时候则返回其name,否则返回no name
如我我们希望通过上面方式得到phone或email,利用上面的方式则行不通了,因为map之后返回的是Optional,我们把这种称为Optional嵌套,我们必须在map一次才能拿到我们想要的结果:
long phone = optUser.map(User::getPhone).map(Optional::get).orElse(-1L);
其实这个时候,更好的方式是利用flatMap,一步拿到我们想要的结果:
long phone = optUser.flatMap(User::getPhone).orElse(-1L);
flapMap可以将方法返回的各个流扁平化成为一个流,具体在下一篇专门讲流式处理的文章中细说。
过滤:fliter
filiter,顾名思义是过滤的操作,我们可以将过滤操作做为参数传递给该方法,从而实现过滤目的,加入我们希望筛选18周岁以上的成年人,则可以实现如下:
optUser.filter(u -> u.getAge() >= 18).ifPresent(u -> System.out.println("Adult:" + u));
默认行为是当Optional为不满足条件时所执行的操作,比如在上面的例子中我们使用的orElse()
就是一个默认操作,用于在Optional对象为空时执行特定操作,当然也有一些默认操作是当满足条件的对象存在时执行的操作。
get()
get用于获取变量的值,但是当变量不存在时则会抛出NoSuchElementException
,所以如果不确定变量是否存在,则不建议使用
orElse(T other)
当Optional的变量不满足给定条件时,则执行orElse,比如前面当str为null时,返回0。
orElseGet(Supplier extends X> expectionSupplier)
如果条件不成立时,需要执行相对复杂的逻辑,而不是简单的返回操作,则可以使用orElseGet实现:
long phone = optUser.map(User::getPhone).map(Optional::get).orElseGet(() -> { // do something here return -1L; });
orElseThrow(Supplier extends X> expectionSupplier)
与get()方法类似,都是在不满足条件时返回异常,不过这里我们可以指定返回的异常类型。
ifPresent(Consumer super T>)
当满足条件时执行传入的参数化操作。
Optional是一个final类,未实现任何接口,所以当我们在利用该类包装定义类的属性的时候,如果我们定义的类有序列化的需求,那么因为Optional没有实现Serializable接口,这个时候执行序列化操作就会有问题:
public class User implements Serializable{ /** 用户编号 */ private long id; private String name; private int age; private Optionalphone; // 不能序列化 private Optional email; // 不能序列化
不过我们可以采用如下替换策略:
private long phone; public OptionalgetPhone() { return Optional.ofNullable(this.phone); }
看来Optional在设计的时候就没有考虑将它作为类的字段使用~
在我接触到java8流式处理的时候,我的第一感觉是流式处理让集合操作变得简洁了许多,通常我们需要多行代码才能完成的操作,借助于流式处理可以在一行中实现。比如我们希望对一个包含整数的集合中筛选出所有的偶数,并将其封装成为一个新的List返回,那么在java8之前,我们需要通过如下代码实现:
Listevens = new ArrayList<>(); for (final Integer num : nums) { if (num % 2 == 0) { evens.add(num); } }
通过java8的流式处理,我们可以将代码简化为:
Listevens = nums.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());
先简单解释一下上面这行语句的含义,stream()
操作将集合转换成一个流,filter()
执行我们自定义的筛选处理,这里是通过lambda表达式筛选出所有偶数,最后我们通过collect()
对结果进行封装处理,并通过Collectors.toList()
指定其封装成为一个List集合返回。
由上面的例子可以看出,java8的流式处理极大的简化了对于集合的操作,实际上不光是集合,包括数组、文件等,只要是可以转换成流,我们都可以借助流式处理,类似于我们写SQL语句一样对其进行操作。java8通过内部迭代来实现对流的处理,一个流式处理可以分为三个部分:转换成流、中间操作、终端操作。如下图:
以集合为例,一个流式处理的操作我们首先需要调用stream()
函数将其转换成流,然后再调用相应的中间操作
达到我们需要对集合进行的操作,比如筛选、转换等,最后通过终端操作
对前面的结果进行封装,返回我们需要的形式。
我们定义一个简单的学生实体类,用于后面的例子演示:
public class Student { /** 学号 */ private long id; private String name; private int age; /** 年级 */ private int grade; /** 专业 */ private String major; /** 学校 */ private String school; // 省略getter和setter }
// 初始化 Liststudents = new ArrayList () { { add(new Student(20160001, "孔明", 20, 1, "土木工程", "武汉大学")); add(new Student(20160002, "伯约", 21, 2, "信息安全", "武汉大学")); add(new Student(20160003, "玄德", 22, 3, "经济管理", "武汉大学")); add(new Student(20160004, "云长", 21, 2, "信息安全", "武汉大学")); add(new Student(20161001, "翼德", 21, 2, "机械与自动化", "华中科技大学")); add(new Student(20161002, "元直", 23, 4, "土木工程", "华中科技大学")); add(new Student(20161003, "奉孝", 23, 4, "计算机科学", "华中科技大学")); add(new Student(20162001, "仲谋", 22, 3, "土木工程", "浙江大学")); add(new Student(20162002, "鲁肃", 23, 4, "计算机科学", "浙江大学")); add(new Student(20163001, "丁奉", 24, 5, "土木工程", "南京大学")); } };
过滤,顾名思义就是按照给定的要求对集合进行筛选满足条件的元素,java8提供的筛选操作包括:filter、distinct、limit、skip。
filter
在前面的例子中我们已经演示了如何使用filter,其定义为:Stream
,filter接受一个谓词Predicate
,我们可以通过这个谓词定义筛选条件,在介绍lambda表达式时我们介绍过Predicate
是一个函数式接口,其包含一个test(T t)
方法,该方法返回boolean
。现在我们希望从集合students
中筛选出所有武汉大学的学生,那么我们可以通过filter来实现,并将筛选操作作为参数传递给filter:
ListwhuStudents = students.stream() .filter(student -> "武汉大学".equals(student.getSchool())) .collect(Collectors.toList());
distinct
distinct操作类似于我们在写SQL语句时,添加的DISTINCT
关键字,用于去重处理,distinct基于Object.equals(Object)
实现,回到最开始的例子,假设我们希望筛选出所有不重复的偶数,那么可以添加distinct操作:
Listevens = nums.stream() .filter(num -> num % 2 == 0).distinct() .collect(Collectors.toList());
limit
limit操作也类似于SQL语句中的LIMIT
关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度,比如下面的例子返回前两个专业为土木工程
专业的学生:
ListcivilStudents = students.stream() .filter(student -> "土木工程".equals(student.getMajor())).limit(2) .collect(Collectors.toList());
说到limit,不得不提及一下另外一个流操作:sorted
。该操作用于对流中元素进行排序,sorted要求待比较的元素必须实现Comparable
接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator super T> comparator)
,比如我们希望筛选出专业为土木工程的学生,并按年龄从小到大排序,筛选出年龄最小的两个学生,那么可以实现为:
ListsortedCivilStudents = students.stream() .filter(student -> "土木工程".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge()) .limit(2) .collect(Collectors.toList());
skip
skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素,比如我们希望找出排序在2之后的土木工程专业的学生,那么可以实现为:
ListcivilStudents = students.stream() .filter(student -> "土木工程".equals(student.getMajor())) .skip(2) .collect(Collectors.toList());
通过skip,就会跳过前面两个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。
在SQL中,借助SELECT
关键字后面添加需要的字段名称,可以仅输出我们需要的字段数据,而流式处理的映射操作也是实现这一目的,在java8的流式处理中,主要包含两类映射操作:map和flatMap。
map
举例说明,假设我们希望筛选出所有专业为计算机科学的学生姓名,那么我们可以在filter筛选的基础之上,通过map将学生实体映射成为学生姓名字符串,具体实现如下:
Listnames = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .map(Student::getName).collect(Collectors.toList());
除了上面这类基础的map,java8还提供了mapToDouble(ToDoubleFunction super T> mapper)
,mapToInt(ToIntFunction super T> mapper)
,mapToLong(ToLongFunction super T> mapper)
,这些映射分别返回对应类型的流,java8为这些流设定了一些特殊的操作,比如我们希望计算所有专业为计算机科学学生的年龄之和,那么我们可以实现如下:
int totalAge = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .mapToInt(Student::getAge).sum();
通过将Student按照年龄直接映射为IntStream
,我们可以直接调用提供的sum()
方法来达到目的,此外使用这些数值流的好处还在于可以避免jvm装箱操作所带来的性能消耗。
flatMap
flatMap与map的区别在于 flatMap是将一个流中的每个值都转成一个个流,然后再将这些流扁平化成为一个流 。举例说明,假设我们有一个字符串数组String[] strs = {"java8", "is", "easy", "to", "use"};
,我们希望输出构成这一数组的所有非重复字符,那么我们可能首先会想到如下实现:
ListdistinctStrs = Arrays.stream(strs) .map(str -> str.split("")) // 映射成为Stream .distinct() .collect(Collectors.toList());
在执行map操作以后,我们得到是一个包含多个字符串(构成一个字符串的字符数组)的流,此时执行distinct操作是基于在这些字符串数组之间的对比,所以达不到我们希望的目的,此时的输出为:
[j, a, v, a, 8] [i, s] [e, a, s, y] [t, o] [u, s, e]
distinct只有对于一个包含多个字符的流进行操作才能达到我们的目的,即对Stream
进行操作。此时flatMap就可以达到我们的目的:
ListdistinctStrs = Arrays.stream(strs) .map(str -> str.split("")) // 映射成为Stream .flatMap(Arrays::stream) // 扁平化为Stream .distinct() .collect(Collectors.toList());
flatMap将由map映射得到的Stream
,转换成由各个字符串数组映射成的流Stream
,再将这些小的流扁平化成为一个由所有字符串构成的大流Steam
,从而能够达到我们的目的。
与map类似,flatMap也提供了针对特定类型的映射操作:flatMapToDouble(Function super T,? extends DoubleStream> mapper)
,flatMapToInt(Function super T,? extends IntStream> mapper)
,flatMapToLong(Function super T,? extends LongStream> mapper)
。
终端操作是流式处理的最后一步,我们可以在终端操作中实现对流查找、归约等操作。
allMatch
allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true,例如我们希望检测是否所有的学生都已满18周岁,那么可以实现为:
boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);
anyMatch
anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true,例如我们希望检测是否有来自武汉大学的学生,那么可以实现为:
boolean hasWhu = students.stream().anyMatch(student -> "武汉大学".equals(student.getSchool()));
noneMathch
noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true,例如我们希望检测是否不存在专业为计算机科学的学生,可以实现如下:
boolean noneCs = students.stream().noneMatch(student -> "计算机科学".equals(student.getMajor()));
findFirst
findFirst用于返回满足条件的第一个元素,比如我们希望选出专业为土木工程的排在第一个学生,那么可以实现如下:
OptionaloptStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findFirst();
findFirst不携带参数,具体的查找条件可以通过filter设置,此外我们可以发现findFirst返回的是一个Optional类型,关于该类型的具体讲解可以参考上一篇:Java8新特性 – Optional类。
findAny
findAny相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个,比如我们希望返回任意一个专业为土木工程的学生,可以实现如下:
OptionaloptStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findAny();
实际上对于顺序流式处理而言,findFirst和findAny返回的结果是一样的,至于为什么会这样设计,是因为在下一篇我们介绍的并行流式处理,当我们启用并行流式处理的时候,查找第一个元素往往会有很多限制,如果不是特别需求,在并行流式处理中使用findAny的性能要比findFirst好。
前面的例子中我们大部分都是通过collect(Collectors.toList())
对数据封装返回,如我的目标不是返回一个新的集合,而是希望对经过参数化操作后的集合进行进一步的运算,那么我们可用对集合实施归约操作。java8的流式处理提供了reduce
方法来达到这一目的。
前面我们通过mapToInt将Stream
映射成为IntStream
,并通过IntStream
的sum方法求得所有学生的年龄之和,实际上我们通过归约操作,也可以达到这一目的,实现如下:
// 前面例子中的方法 int totalAge = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .mapToInt(Student::getAge).sum(); // 归约操作 int totalAge = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .map(Student::getAge) .reduce(0, (a, b) -> a + b); // 进一步简化 int totalAge2 = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .map(Student::getAge) .reduce(0, Integer::sum); // 采用无初始值的重载版本,需要注意返回Optional OptionaltotalAge = students.stream() .filter(student -> "计算机科学".equals(student.getMajor())) .map(Student::getAge) .reduce(Integer::sum); // 去掉初始值
前面利用collect(Collectors.toList())
是一个简单的收集操作,是对处理结果的封装,对应的还有toSet
、toMap
,以满足我们对于结果组织的需求。这些方法均来自于java.util.stream.Collectors
,我们可以称之为收集器。
3.3.1 归约
收集器也提供了相应的归约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()
实现。
例1:求学生的总人数
long count = students.stream().collect(Collectors.counting()); // 进一步简化 long count = students.stream().count();
例2:求年龄的最大值和最小值
// 求最大年龄 OptionalolderStudent = students.stream().collect(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge())); // 进一步简化 Optional olderStudent2 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge))); // 求最小年龄 Optional olderStudent3 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
例3:求年龄总和
int totalAge4 = students.stream().collect(Collectors.summingInt(Student::getAge));
对应的还有summingLong
、summingDouble
。
例4:求年龄的平均值
double avgAge = students.stream().collect(Collectors.averagingInt(Student::getAge));
对应的还有averagingLong
、averagingDouble
。
例5:一次性得到元素个数、总和、均值、最大值、最小值
IntSummaryStatistics statistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));
输出:
IntSummaryStatistics{count=10, sum=220, min=20, average=22.000000, max=24}
对应的还有summarizingLong
、summarizingDouble
。
例6:字符串拼接
String names = students.stream().map(Student::getName).collect(Collectors.joining()); // 输出:孔明伯约玄德云长翼德元直奉孝仲谋鲁肃丁奉 String names = students.stream().map(Student::getName).collect(Collectors.joining(", ")); // 输出:孔明, 伯约, 玄德, 云长, 翼德, 元直, 奉孝, 仲谋, 鲁肃, 丁奉
3.3.2 分组
在数据库操作中,我们可以通过GROUP BY
关键字对查询到的数据进行分组,java8的流式处理也为我们提供了这样的功能Collectors.groupingBy
来操作集合。比如我们可以按学校对上面的学生进行分组:
Map> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));
groupingBy
接收一个分类器Function super T, ? extends K> classifier
,我们可以自定义分类器来实现需要的分类效果。
上面演示的是一级分组,我们还可以定义多个分类器实现 多级分组,比如我们希望在按学校分组的基础之上再按照专业进行分组,实现如下:
Map>> groups2 = students.stream().collect( Collectors.groupingBy(Student::getSchool, // 一级分组,按学校 Collectors.groupingBy(Student::getMajor))); // 二级分组,按专业
实际上在groupingBy
的第二个参数不是只能传递groupingBy,还可以传递任意Collector
类型,比如我们可以传递一个Collector.counting
,用以统计每个组的个数:
Mapgroups = students.stream().collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));
如果我们不添加第二个参数,则编译器会默认帮我们添加一个Collectors.toList()
。
3.3.3 分区
分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,java8的流式处理利用ollectors.partitioningBy()
方法实现分区,该方法接收一个谓词,例如我们希望将学生分为武大学生和非武大学生,那么可以实现如下:
Map> partition = students.stream().collect(Collectors.partitioningBy(student -> "武汉大学".equals(student.getSchool())));
分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。
以上介绍的所有收集器均实现自接口java.util.stream.Collector
,该接口的定义如下:
public interface Collector{ /** * A function that creates and returns a new mutable result container. * * @return a function which returns a new, mutable result container */ Supplier supplier(); /** * A function that folds a value into a mutable result container. * * @return a function which folds a value into a mutable result container */ BiConsumer accumulator(); /** * A function that accepts two partial results and merges them. The * combiner function may fold state from one argument into the other and * return that, or may return a new result container. * * @return a function which combines two partial results into a combined * result */ BinaryOperator combiner(); /** * Perform the final transformation from the intermediate accumulation type * {@code A} to the final result type {@code R}. * * If the characteristic {@code IDENTITY_TRANSFORM} is * set, this function may be presumed to be an identity transform with an * unchecked cast from {@code A} to {@code R}. * * @return a function which transforms the intermediate result to the final * result */ Function finisher(); /** * Returns a {@code Set} of {@code Collector.Characteristics} indicating * the characteristics of this Collector. This set should be immutable. * * @return an immutable set of collector characteristics */ Set
characteristics(); }
我们也可以实现该接口来定义自己的收集器,此处不再展开。
流式处理中的很多都适合采用 分而治之 的思想,从而在处理集合较大时,极大的提高代码的性能,java8的设计者也看到了这一点,所以提供了 并行流式处理。上面的例子中我们都是调用stream()
方法来启动流式处理,java8还提供了parallelStream()
来启动并行流式处理,parallelStream()
本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。
启动并行流式处理虽然简单,只需要将stream()
替换成parallelStream()
即可,但既然是并行,就会涉及到多线程安全问题,所以在启用之前要先确认并行是否值得(并行的效率不一定高于顺序执行),另外就是要保证线程安全。此两项无法保证,那么并行毫无意义,毕竟结果比速度更加重要,以后有时间再来详细分析一下并行流式数据处理的具体实现和最佳实践。
java8可以看做是java版本更新迭代过程中变化最大的一个版本(与时俱进,方能不灭,我们应该感到欣慰),但是经过这么多年的发展和迭代,java的源码俨然已是一个庞然大物,要在这样庞大的体积上大动干戈,肯定不易。所以当第一次看到java8的默认接口方法的时候,我第一感觉就是这是java的设计人员在填自己之前挖的坑。
从前几篇的讲解中我们知道java8在现有的接口上添加了许多方法,比如List的sort(Comparator super E> c)
方法。如果按照java8之前接口的设计思路,当给一个接口添加方法声明的时候,实现该接口的类都必须为该新添加的方法添加相应的实现。考虑兼容性,这样是不可取的,所以说这是一个坑,而新的特性又要求不得不为接口添加一些新的方法,为了兼得鱼和熊掌,java8的设计人员提出了默认接口方法的概念。
这样说来,默认接口方法似乎是为api的设计人员而开发的,离我们普通开发人员还有些距离,这样想有点图森破啦,虽然我们不用去设计jdk,但是我们在日常的开发过程中还是会有提供api给别的业务方调用的需求,当我们在更新我们api的时候,就可以采用默认方法来提供更加高级的功能,同时保持兼容性。
默认接口方法的定义很简单,只要在接口的方法定义前添加一个default
关键字即可,如下:
public interface A { /** * 默认方法定义 */ default void method() { System.out.println("This is a default method!"); } }
当我们这样定义一个默认方法之后,所有实现该接口的子类都间接持有了该方法。或者你会和我一样觉得接口和抽象类越来越像了,确实,不过它们之间还是有如下差别:
1. 一个类只能继承一个类,但是可以实现多个接口 2. 抽象类可以定义变量,而接口却不能
抽象除了解决了我们上面提及到的问题,还具有如下好处:
1. 对于一些不是每个子类都需要的方法,我们给它一个默认实现,从而避免我们在子类中对其无意义的实现(一般我们都会throw new UnsupportedException()) 2. 默认方法为java的多重继承提供了新的途径(虽然我们只能继承一个类,但是我们可以实现多个接口啊,现在接口也可以定义默认方法了)
因为一个类可以实现多个接口,所以当一个类实现了多个接口,而这些接口中存在两个或两个以上方法签名相同的默认方法时就会产生冲突,java8定义如下三条原则来解决冲突:
1. 类或父类中显式声明的方法,其优先级高于所有的默认方法 2. 如果1规则失效,则选择与当前类距离最近的具有具体实现的默认方法 3. 如果2规则也失效,则需要显式指定接口
下面通过几个例子加以说明:
例1
public interface A { /** * 默认方法定义 */ default void method() { System.out.println("A's default method!"); } } public interface B extends A { /** * 默认方法定义 */ default void method() { System.out.println("B's default method!"); } } public class C implements A, B { public static void main(String[] args) { new C().method(); } } // 输出:B's default method!
此处因为接口B相对于A距离C更近,同时B的method是一个具体的默认实现,依据规则2,所以此处实际上调用的是接口B的默认方法
例2
public class D implements A { } public class C extends D implements A, B { public static void main(String[] args) { new C().method(); } } // 输出:B's default method!
例2在原有接口A、B的基础上,添加了一个实现接口A的类D,然后类C继承于D,并实现A和B,此处虽然C离D更近,但因为D的具体实现在A中,所以B中的默认方法还是距离最近的默认实现,依据规则2,此处实际上调用的是B的默认方法。
例3
// A接口不变 public interface B { /** * 默认方法定义 */ default void method() { System.out.println("B's default method!"); } } public class C implements A, B { @Override public void method() { // 必须显式指定 B.super.method(); } public static void main(String[] args) { new C().method(); } }
例3中接口B不再继承自接口A,所以此时C中调用默认方法method()
距离接口A和B的具体实现距离相同,编译器无法确定,所以报错,此时需要显式指定:B.super.method()
。