1.序
我们在把这张图拿出来,Jdk8最大的一个特点之一就是Lambda表达式,它支持JAVA也能进行简单的“函数式编程”。我原本以为用的很少。结果实习才发现,用的很多,因为它真的特别省劲,而且很简洁。公司其实代码的宗旨就是功能,可读性,简洁性。所以不会的同学赶紧来喵喵吧,不然也会像我一样面临不会改Lambda 代码的尴尬。
2.详解
2.1 Stream
Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。
Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
下面是Stream类
下面看一个例子证实为什么需要Stream,在Java7中如果实现排序和取值的话
public static void main(String[] args) {
List<Student> allStudents = new ArrayList<>();
List<Student> students = new ArrayList<>();
for(Student s : allStudents){
if(Student.pass == s.getGrade()){
students.add(s);
}
}
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.getGrade() - o1.getGrade();
}
});
List<Integer> studentIds = new ArrayList<>();
for(Student s : allStudents){
studentIds.add(s.getId());
}
}
在Java 8 使用Stream ,代码简洁易读,而且使用并发模式,程序执行速度更快。
public void Test(){
List<Student> allStudents = new ArrayList<>();
List<Integer> studentIds = allStudents.parallelStream().
filter(t -> t.getGrade() == Student.pass).
sorted(Comparator.comparing(Student::getGrade).reversed()).
map(Student::getId).
collect(Collectors.toList());
}
2.2.什么是lambda表达式?
它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。
λ表达式本质上是一个匿名方法。让我们来看下面这个例子:
public int add(int x,int y){
return x + y;
}
转为λ表达式之后就是这个样子:
(int x,int y) -> x + y;
参数类型也可以省略,Java编译器会根据上下文推断出来:
(x, y) -> x + y;//返回两数之和
或者
(x, y) -> {
return x + y}; //显式指明返回值
可见λ表达式有三部分组成:参数列表,箭头(-> ),以及一个表达式或者语句块。
下面这个例子里的λ表达式没有参数,也没有返回值(相当于一个方法接受0个参数,返回void,其实就是Runnable里run方法的一个实现):
() -> {
System.out.println(“Hello Lambda!”);
}
如果只有一个参数且可以被Java推断出来类型,那么参数列表的括号也可以省略:
c -> {return c.size();}
2.3方法引用
方法引用是 lambda 表达式的一种特殊形式,即方法引用就是 Lambda 表达式(方法引用是 lambda 表达式的一种语法糖)。
方法引用用于获取一个已有方法的 Lambda 表达式形式。
方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
方法引用通过方法的名字来指向一个方法。
方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
方法引用使用一对冒号 :: 。
上一节中我们也讲解了方法引用。
下面我们结合Lambda表达来讲解
// 无参数情况
Student student = ()-> new ArrayList<>();
// 可替换为
Student student = ArrayList::new;
// 一个参数情况
Student student = (String string) -> System.out.print(string);
// 可替换为
Student student = (System.out::println);
// 两个参数情况
Comparator c = (Student c1, Student c2) -> c1.getAge().compareTo(c2.getAge());
// 可替换为
Comparator c = (c1, c2) -> c1.getAge().compareTo(c2.getAge());
// 进一步可替换为
Comparator c = Comparator.comparing(Student::getAge);
2.4什么是流
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。
2.4.1流的构成
2.4.2中间操作
2.4.2.1filter
其定义为:Stream filter(Predicate<? super T> predicate),
- filter接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,
- 在介绍lambda表达式时我们介绍过Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。
现在我们希望从集合students中筛选出所有中国**大学的学生,那么我们可以通过filter来实现,并将筛选操作作为参数传递给filter:
List<Student> whuStudents = students.stream()
.filter(student -> "中国**大学".equals(student.getSchool()))
.collect(Collectors.toList());
2.4.2.2distinct
distinct操作类似于我们在写SQL语句时,添加的DISTINCT关键字,用于去重处理,distinct基于Object.equals(Object)实现,回到最开始的例子,假设我们希望筛选出所有不重复的偶数,那么可以添加distinct操作:
List<Integer> evens = nums.stream()
.filter(num -> num % 2 == 0).distinct()
.collect(Collectors.toList());
2.4.2.3limit
limit操作也类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度,比如下面的例子返回前两个专业为计算机专业的学生:
List<Student> civilStudents = students.stream()
.filter(student -> "计算机".equals(student.getMajor())).limit(2)
.collect(Collectors.toList());
2.4.2.4sorted
说到limit,不得不提及一下另外一个流操作:sorted。该操作用于对流中元素进行排序,
简单的List排序
List<Integer> list = new ArrayList<>();
list.add(23);
list.add(74);
list.add(11);
----------------stream排序操作(默认ASC排序)
List<Integer> collect = list.stream().sorted().collect(Collectors.toList());
System.out.println("list<Integer>元素正序:" + list);
打印结果:list元素正序:[11, 23, 74]
----------------stream倒序排序操作(DESC排序)
reverseOrder();//反转排序
List<Integer> collect = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println("list<Integer>元素倒序:" + collect );
sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator<? super T> comparator),比如我们希望筛选出专业为计算机的学生,并按年龄从小到大排序,筛选出年龄最小的两个学生,那么可以实现为:
List<Student> sortedCivilStudents = students.stream()
.filter(student -> "计算机".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge())
.limit(2)
.collect(Collectors.toList());
2.4.2.5skip
skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素,比如我们希望找出排序在2之后的计算机专业的学生,那么可以实现为:
List<Student> civilStudents = students.stream()
.filter(student -> "计算机".equals(student.getMajor()))
.skip(2)
.collect(Collectors.toList());
通过skip,就会跳过前面两个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。
除了上面这类基础的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();
2.4.2.6map
举例说明,假设我们希望筛选出所有专业为计算机科学的学生姓名,那么我们可以在filter筛选的基础之上,通过map将学生实体映射成为学生姓名字符串,具体实现如下:
List<String> names = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getName).collect(Collectors.toList());
通过将Student按照年龄直接映射为IntStream,我们可以直接调用提供的sum()方法来达到目的,此外使用这些数值流的好处还在于可以避免jvm装箱操作所带来的性能消耗。
2.4.2.7flatMap
通过以下示例讲解它与map区别
private static class Student {
private int studentId;
public Student(int studentId) {
this.studentId = studentId;
}
@Override
public String toString() {
return "field=" + studentId;
}
}
private static class StudentGroup {
private List<Student> studentGroup = new ArrayList<>();
public StudentGroup(Student... objList) {
for (Student student : objList) {
this.studentGroup.add(student);
}
}
public List<Student> getStudentList() {
return studentGroup;
}
}
StudentGroup类中定义了一个Student类的列表
现在我们有一组StudentGroup对象
List<StudentGroup> groupList = Arrays.asList(
new StudentGroup(new Student(1), new Student(2), new Student(3)),
new StudentGroup(new Student(4), new Student(5), new Student(6)),
new StudentGroup(new Student(7), new Student(8), new Student(9)),
new StudentGroup(new Student(10))
);
需要将每个StudentGroup对象中的那些Klass类取出来,放到一个ArrayList里面,得到一个List。我们尝试着用map方法来实现。
List<List<Student>> result = groupList.stream()
.map(it -> it.getStudentList())
.collect(Collectors.toList());
哈,不成功,我们想要的结果是List,现在得到了 List<List>。当然,我们可以轻而易举的解决这个问题
List<Student> result2 = new ArrayList<>();
for (StudentGroup group : groupList) {
for (Student student: group .getStudentList()) {
result2.add(student);
}
}
但是这种套了两层for循环的代码太丑陋了。面对这种需求,flatMap可以大展身手了
List<Student> result3 = groupList.stream()
.flatMap(it -> it.getStudentList().stream())
.collect(Collectors.toList());
一行代码就实现了 stream api 的 flatMap方法接受一个lambda表达式函数, 函数的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并,map方法做不到这一点,如果用map去实现,会变成这样一个东西
List<Stream<Student>> result3 = groupList.stream()
.map(it -> it.getStudentList().stream())
.collect(Collectors.toList());
2.4.3 终端操作
终端操作是流式处理的最后一步,我们可以在终端操作中实现对流查找、归约等操作。
2.4.3.1 allMatch
allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true,例如我们希望检测是否所有的学生都已满18周岁,那么可以实现为:
boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);
2.4.3.2anyMatch
anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true,例如我们希望检测是否有来自中国**大学的学生,那么可以实现为:
boolean hasWhu = students.stream().anyMatch(student -> “中国**大学”.equals(student.getSchool()));
2.4.3.3noneMathch
noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true,例如我们希望检测是否不存在专业为计算机科学的学生,可以实现如下:
boolean noneCs = students.stream().noneMatch(student -> “计算机科学”.equals(student.getMajor()));
2.4.3.4findFirst
findFirst用于返回满足条件的第一个元素,比如我们希望选出专业为计算机的排在第一个学生,那么可以实现如下:
Optional optStu = students.stream().filter(student -> “计算机”.equals(student.getMajor())).findFirst();
2.4.3.5 findAny
findAny相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个,比如我们希望返回任意一个专业为土木工程的学生,可以实现如下:
Optional optStu = students.stream().filter(student -> “土木工程”.equals(student.getMajor())).findAny();
实际上对于顺序流式处理而言,findFirst和findAny返回的结果是一样的,至于为什么会这样设计,是因为在下一篇我们介绍的并行流式处理,当我们启用并行流式处理的时候,查找第一个元素往往会有很多限制,如果不是特别需求,在并行流式处理中使用findAny的性能要比findFirst好。
2.4.3.6计算集合元素的最大值、最小值、总和以及平均值
IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。
//获取数字的个数、最小值、最大值、总和以及平均值
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : " + stats.getMax());
System.out.println("Lowest prime number in List : " + stats.getMin());
System.out.println("Sum of all prime numbers : " + stats.getSum());
System.out.println("Average of all prime numbers : " + stats.getAverage());
输出:
Highest prime number in List : 29
Lowest prime number in List : 2
Sum of all prime numbers : 129
Average of all prime numbers : 12.9
2.4.3.7reduce
reduce可实现根据指定的规则从Stream中生成一个值,比如之前提到的count,max和min方法是因为常用而被纳入标准库中。实际上,这些方法都是reduce的操作。
Stream.of(1, 2, 3).reduce(Integer::sum);
Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b);
以上两个方法都是对结果进行求和,不同的是第一个方法调用的是reduce的reduce((T, T) -> T)方法,而第二个调用的是reduce(T, (T, T) -> T)。其中第二个方法的第一个参数0,表示从第0个值开始操作。
2.4.3.8collect
收集方法,实现了很多归约操作,比如将流转换成集合和聚合元素等。
Stream.of(1, 2, 3).collect(Collectors.toList());
Stream.of(1, 2, 3).collect(Collectors.toSet());
除了以上的集合转换,还有类似joining字符串拼接的方法,具体可查看Collectors中的实现。
3.总结
其实Lambda表达式看的再多也不会让你使用的顺心应手,纸上得来终觉浅,绝知此事要躬行。一定要自己多实现,去尝试,这样才能熟练的掌握它,当你对它充满自信的时候,那么你的工作也会让你顺心。
4.个人推广
博客地址
https://blog.csdn.net/weixin_41563161
掘金https://juejin.cn/user/2814360172369271
知乎https://www.zhihu.com/people/hai-kuo-tian-kong-63-38-21