Lambda表达式与Stream

Lambda表达式

背景

java 8中增加了许多特性,到现在才来总结写一下Lamdba表达式,让我们来遛一遛吧,毕竟简化了我们的代码量。简化了匿名委托的使用,让代码更加简洁,但是如果你不懂,那么久一路懵吧,为了高大上的代码我们必须学并用之实践。

简介

来看下百度百科如何进行解释的:“Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。

函数式接口的重要属性是:我们能够使用 Lambda 实例化它们,Lambda 表达式让你能够将函数作为方法参数,或者将代码作为数据对待。Lambda 表达式的引入给开发者带来了不少优点:在 Java 8 之前,匿名内部类,监听器和事件处理器的使用都显得很冗长,代码可读性很差,Lambda 表达式的应用则使代码变得更加紧凑,可读性增强;Lambda 表达式使并行操作大集合变得很方便,可以充分发挥多核 CPU 的优势,更易于为多核处理器编写代码。

语法

特性

  • 可选类型生命:不用声明参数类型,编译器可以从参数值来进行推断它是什么类型
  • 可选参数周围括号:单个参数无需括号,但是多个参数括号是必需的
  • 可选大括号:如果表达式只一个语句,那么不必用大括号括起来
  • 如果表达式体只有单个表达式用于值的返回,那么编译器会自动完成这一步。若要指示表达式来返回某个值,则需要使用大括号。

一般语法

(Type1 param1,Type2 param2,…,TypeN paramN ) -> {

statment1;

statment2;

return statmentM;

}

方法引用

lambda表达式还有两种简化代码的手段,它们是方法引用构造引用

方法引用是什么呢?如果我们要实现接口的方法与另一个方法A类似,(这里的类似是指参数类型与返回值部分相同),我们直接声明A方法即可。也就是,不再使用lambda表达式的标准形式,改用高级形式。无论是标准形式还是高级形式,都是lambda表达式的一种表现形式。

举例:

1
2
Function function1 = (x) -> x;
Function function2 = String::valueOf;

对比Function接口的抽象方法与String的value方法,可以看到它们是类似的。

1
2
3
4
5
R apply(T t);

public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

方法引用的语法:

1
2
3
对象::实例方法
类::静态方法
类::实例方法

前两个很容易理解,相当于对象调用实例方法,类调用静态方法一样。只是第三个需要特殊说明。

当出现如下这种情况时:

1
Compare<Boolean> c = (a, b) -> a.equals(b);

用lambda表达式实现Compare接口的抽象方法,并且方法体只有一行,且该行代码为参数1调用方法传入参数2。此时,就可以简化为下面这种形式:

1
Compare<Boolean> c = String::equals;

也就是“类::实例方法”的形式。

值得一提的是,当参数b不存在时,该方式依旧适用。例如:

1
2
3
Function function1 = (x) -> x.toString();

Function function1 = Object::toString;

构造引用

先来创建一个供给型接口对象:

1
Supplier<String> supplier = () -> new String();

在这个lammbda表达式中只做了一件事,就是返回一个新的Test对象,而这种形式可以更简化:

1
Supplier<String> supplier = String::new;

提炼一下构造引用的语法

1
类名::new

当通过含参构造方法创建对象,并且参数列表与抽象方法的参数列表一致,也就是下面的这种形式:

1
Function1 function = (x) -> new String(x);

也可以简化为:

1
Function1 function = String::new;

特殊点的数组类型:

1
Function<Integer,String[]> function = (x) -> new String[x];

可以简化为:

1
Function<Integer,String[]> function = String[]::new;

示例

迭代列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//迭代列表
private static void iteratorList() {
List<String> list = new ArrayList<>();

list.add("one");
list.add("two");
list.add("three");

//传统遍历
System.out.println("传统方式:");
for (String s : list) {
System.out.println(s);
}

//Lambda表达式
System.out.println("Lambda表达式:");
list.forEach(n-> System.out.println(n));

// 使用Java 8的方法引用更方便,方法引用由::双冒号操作符标示,
list.forEach(System.out::println);
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
传统方式:
one
two
three
Lambda表达式:
one
two
three
one
two
three

实现Runnable

lambda可以替换匿名内部类,在实现runnable的时候,我们能够明显看出来,代码的简洁性的差异。

1
2
3
4
5
6
7
8
9
10
11
12
//多线程
private static void threads() {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("传统的代码");
}
}).start();


new Thread( ()-> System.out.println("我是lambda表达式")).start();
}

结果:

1
2
传统的代码
我是lambda表达式

集合排序比较

传统的排序代码笼统繁琐,看起来虽然易懂,但是代码量较多,使用起来不方便。如果使用Lambda简单明了,至于Comparator.comparing相关内容会在其他章节进行介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//排序
private static void sort() {

List<User> users = new ArrayList<User>() {
{
add(new User("0004", "四"));
add(new User("0001", "一"));
add(new User("0002", "二"));
add(new User("0003", "三"));
}
};

System.out.println("传统方式");

Collections.sort(users, new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
return user1.getDah().compareTo(user2.getDah());
}
});

users.forEach(user -> System.out.println(user.getDah() + ":" + user.getName()));


//lambda表达式
System.out.println("使用lambda表达式");
Collections.sort(users, (s1, s2) -> (s1.getDah().compareTo(s2.getDah())));
users.forEach(user -> System.out.println(user.getDah() + ":" + user.getName()));

System.out.println("使用Comparator.comparing 与lambda表达式");
users.sort(Comparator.comparing(e -> e.getDah()));
users.forEach(user -> System.out.println(user.getDah() + ":" + user.getName()));

// 该方法引用 user::getDah 可以代替 lambda表达式
System.out.println("使用Comparator.comparing 与 ::");
users.sort(Comparator.comparing(User::getDah));
users.forEach(user -> System.out.println(user.getDah() + ":" + user.getName()));
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
传统方式
0001:一
0002:二
0003:三
0004:四
使用lambda表达式
0001:一
0002:二
0003:三
0004:四
使用Comparator.comparing 与lambda表达式
0001:一
0002:二
0003:三
0004:四
使用Comparator.comparing 与 ::
0001:一
0002:二
0003:三
0004:四

使用lambda表达式和函数式接口Predicate

除了在语言层面支持函数式编程风格,java8也增加了一个java.util.function包,它包含了很多类,用来支持java的函数式编程,其中一个为Predicate,使用 java.util.function.Predicate 函数式接口以及lambda表达式,可以向API方法添加逻辑,用更少的代码支持更多的动态行为。下面是Java 8 Predicate 的例子,展示了过滤集合数据的多种常用方法。Predicate接口非常适用于做过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void predicateTry() {
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

System.out.println("Languages 用J开头 :");
filter(languages, (str)->str.startsWith("J"));

System.out.println("Languages 用a结尾");
filter(languages, (str)->str.endsWith("a"));

System.out.println("languages所有数据 :");
filter(languages, (str)->true);

System.out.println("不打印 : ");
filter(languages, (str)->false);

System.out.println(" language 长度大于 4:");
filter(languages, (str)->str.length() > 4);
}

public static void filter(List<String> names, Predicate<String> condition) {
for(String name: names) {
if(condition.test(name)) {
System.out.println(name + " ");
}
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Languages 用J开头 :
Java
Languages 用a结尾
Java
Scala
languages所有数据 :
Java
Scala
C++
Haskell
Lisp
不打印 :
language 长度大于 4:
Scala
Haskell

还有一种更加简洁的办法,与上面的结果是一致的。我们可以看到,Stream API过滤方法也是支持Predicate,这个意味着我们可以定制filter()方法替换里面的内联代码,还有Predicate接口也允许进行多重条件的测试。

1
names.stream().filter(name->(condition.test(name))).forEach(name-> System.out.println(name));

如何在lambda表达式中增加Predicate

java.util.function.Predicate 允许将两个或更多的 Predicate 合成一个。它提供类似于逻辑操作符AND和OR的方法,名字叫做and()、or()和xor(),用于将传入 filter() 方法的条件合并起来。例如,要得到所有以J开始,长度为四个字母的语言,可以定义两个独立的 Predicate 示例分别表示每一个条件,然后用 Predicate.and() 方法将它们合并起来,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void predicateTry2() {

// 甚至可以用and()、or()和xor()逻辑函数来合并Predicate,
// 例如要找到所有以J开始,长度为四个字母的名字,你可以合并两个Predicate并传入
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

Predicate<String> startWithJ = n->n.startsWith("J");
Predicate<String> length4 = n->n.length() == 4;


languages.stream().filter(language->startWithJ.and(length4).test(language)).forEach(language-> System.out.println(language));


}

结果如下:

1
Java

类似地,也可以使用 or() 和 xor() 方法。本例着重介绍了如下要点:可按需要将 Predicate 作为单独条件然后将其合并起来使用。简而言之,你可以以传统Java命令方式使用 Predicate 接口,也可以充分利用lambda表达式达到事半功倍的效果。

Map和Reduce

可以看到map将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是 reduce 操作,因为它们接收多个值并返回一个值。流API定义的 reduceh() 函数可以接受lambda表达式,并对所有值进行合并。IntStream这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个Java 8的Map Reduce示例里,我们首先对所有价格应用 12% 的VAT,然后用 reduce() 方法计算总和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void mapAndReduce() {

//每个订单增加12%的关税
List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = 0;

for(Integer cost : costBeforeTax){
total = total+(cost+cost*0.12);
}

System.out.println("总价为:" + total);

//使用新方法
double bill = costBeforeTax.stream().map(cost->(cost+cost*0.12)).reduce((sum,cost)->sum+cost).get();
System.out.println(bill);
}

结果为:

1
2
总价为:1680.0
1680.0

通过过滤创建String列表

过滤是Java开发者在大规模集合上的一个常用操作,而现在使用lambda表达式和流API过滤大规模数据集合是惊人的简单。流提供了一个 filter() 方法,接受一个 Predicate 对象,即可以传入一个lambda表达式作为过滤逻辑。下面的例子是用lambda表达式过滤Java集合,将帮助理解。

1
2
3
4
5
6
7
8
private static void stringFilter() {
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");


List<String> stringList = languages.stream().filter(x -> x.length() > 4).collect(Collectors.toList());
stringList.forEach(language-> System.out.println(language));

}

结果如下:

1
2
Scala
Haskell

另外,关于 filter() 方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用filter()方法则是获得一个新的列表,且其每个元素符合过滤原则。

列表的每个元素应用函数

我们通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用 map() 方法,可以将转换逻辑以lambda表达式的形式放在 map() 方法里,就可以对集合的各个元素进行转换了,如下所示。

1
2
3
4
5
6
7
private static void stringToUpperCase() {
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");
String collect = languages.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(","));

System.out.println(collect);

}

结果如下:

1
JAVA,SCALA,C++,HASKELL,LISP

复制不同的值,创建一个子列表

本例展示了如何利用流的 distinct() 方法来对集合进行去重。

1
2
3
4
5
6
7
8
9
private static void listDistinct() {
List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Java","Lisp");

List<String> stringList = languages.stream().distinct().collect(Collectors.toList());

stringList.forEach(n -> System.out.println(n));


}

结果如下:

1
2
3
4
5
Java
Scala
C++
Haskell
Lisp

计算集合元素的最大值、最小值、总和以及平均值

IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

1
2
3
4
5
6
7
8
9
10
11
private static void listDeal() {
//获取数字的个数、最小值、最大值、总和以及平均值
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics intSummaryStatistics = primes.stream().mapToInt(x -> x).summaryStatistics();

System.out.println("最大数:"+intSummaryStatistics.getMax());
System.out.println("平均数:"+intSummaryStatistics.getAverage());
System.out.println("最小数:"+intSummaryStatistics.getMin());
System.out.println("和:"+intSummaryStatistics.getSum());

}

结果:

1
2
3
4
最大数:29
平均数:12.9
最小数:2
和:129

Lambda表达式 vs 匿名类

既然lambda表达式即将正式取代Java代码中的匿名内部类,那么有必要对二者做一个比较分析。一个关键的不同点就是关键字 this。匿名类的 this 关键字指向匿名类,而lambda表达式的 this 关键字指向包围lambda表达式的类。另一个不同点是二者的编译方式。Java编译器将lambda表达式编译成类的私有方法。使用了Java 7的 invokedynamic 字节码指令来动态绑定这个方法。

Lambda总结

上面举了很多lambda实例,也侧重讲了如何去理解lambda表达式。到这里,不要懵。要记住lambda的本质:为函数型接口的匿名实现进行简化与更简化

所谓的简化就是lambda的标准形式,所谓的更简化是在标准形式的基础上进行方法引用和构造引用。

方法引用是拿已有的方法去实现此刻的接口。

构造引用是对方法体只有一句new Object()的进一步简化

Stream

Stream是什么

Stream不是集合元素,也不是数据结构并不保存数据,它是有关算法与计算的,它更像一个高版本的Iterator。简单来说,它的作用就是通过一系列操作将数据源(集合、数组)转换为想要的结果。

Stream特点

  1. Stream是不会存储元素的。
  2. Stream不会改变原对象,相反,他们会返回一个持有结果的新Stream。
  3. Strream是延迟执行的,意味着他们会等到需要结果时候才执行。

生成Stream的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void createStream() {

//Collection系的 stream() 和 parallelStream();
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
Stream<String> stringStream = list.parallelStream();

//通过Arrays
Stream<String> stream1 = Arrays.stream(new String[10]);

//通过Stream
Stream<Integer> stream2 = Stream.of(1, 2, 3);

//无限流
//迭代
Stream<Integer> iterate = Stream.iterate(0, (x) -> x + 2);
iterate.limit(10).forEach(System.out::println);

//生成
Stream<Double> generate = Stream.generate(() -> Math.random());
generate.forEach(System.out::println);


}

Stream的中间操作

多个中间操作连接而成为流水线,流水线不遇到终止操作是不触发任何处理的,所以有称为“惰性求值”。

1
2
3
4
5
6
7
8
9
list.stream()
.map(s -> s+1)//映射
.flatMap(s->Stream.of(s))//和map差不多,但返回类型为Stream,类似list.add()和list.addAll()的区别
.filter(s->s.length()<10)////过滤
.limit(5) //限制
.skip(1) //跳过
.distinct() //去重
.sorted() //自然排序
.sorted(String::compareTo); //自定义排序

关于map方法,参数为一个Function函数型接口的对象,也就是传入一个参数返回一个对象。这个参数就是集合中的每一项。类似Iterator遍历。其它的几个操作思想都差不多。

Stream的终止操作

1
2
3
4
5
6
7
8
9
10
11

list.stream().allMatch((x) -> x.length()<5); // 检查是否匹配所有元素
list.stream().anyMatch(((x) -> x.length()>6)); // 检查是否至少匹配一个元素
list.stream().noneMatch((x) ->x.length()>5); //检查是否没有匹配所有元素
list.stream().findFirst(); // 返回第一个元素
list.stream().findAny(); // 返回当前流中的任意一个元素
list.stream().count(); // 返回流中元素的总个数
list.stream().forEach(System.out::println); //内部迭代
list.stream().max(String::compareTo); // 返回流中最大值
Optional<String> min = list.stream().min(String::compareTo);//返回流中最小值
System.out.println("min "+min.get());

reduce (归约)

将流中元素反复结合起来得到一个值

1
2
3
4
5
6
7
8
9
Integer reduce = list.stream()
.map(s -> (s + 1))
.reduce(0, (x, y) -> x + y); //归约:0为第一个参数x的默认值,x是计算后的返回值,y为每一项的值。
System.out.println(reduce);

Optional<Integer> reduce1 = list.stream()
.map(s -> (s + 1))
.reduce((x, y) -> x + y); // x是计算后的返回值,默认为第一项的值,y为其后每一项的值。
System.out.println(reduce);

collect(收集)

将流转换为其他形式。需要Collectors类的一些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//转集合
Set<Integer> collect = list.stream()
.collect(Collectors.toSet());

List<Integer> collect2 = list.stream()
.collect(Collectors.toList());

HashSet<Integer> collect1 = list.stream()
.collect(Collectors.toCollection(HashSet::new));

//分组 {group=[444, 555, 666, 777, 555]}
Map<String, List<Integer>> collect3 = list.stream()
.collect(Collectors.groupingBy((x) -> "group"));//将返回值相同的进行分组
System.out.println(collect3);

//多级分组 {group={777=[777], 666=[666], 555=[555, 555], 444=[444]}}
Map<String, Map<Integer, List<Integer>>> collect4 = list.stream()
.collect(Collectors.groupingBy((x) -> "group", Collectors.groupingBy((x) -> x)));
System.out.println(collect4);

//分区 {false=[444], true=[555, 666, 777, 555]}
Map<Boolean, List<Integer>> collect5 = list.stream()
.collect(Collectors.partitioningBy((x) -> x > 500));
System.out.println(collect5);

//汇总
DoubleSummaryStatistics collect6 = list.stream()
.collect(Collectors.summarizingDouble((x) -> x));
System.out.println(collect6.getMax());
System.out.println(collect6.getCount());

//拼接 444,555,666,777,555
String collect7 = list.stream()
.map(s -> s.toString())
.collect(Collectors.joining(","));
System.out.println(collect7);

//最大值
Optional<Integer> integer = list.stream()
.collect(Collectors.maxBy(Integer::compare));
System.out.println(integer.get());

参考内容:

Java8 lambda表达式10个示例

lambda表达式和Stream API

本文标题:Lambda表达式与Stream

文章作者:wsylp

发布时间:2019年01月02日 - 07:01

最后更新:2020年01月02日 - 10:01

原始链接:http://wsylp.top/2019/01/02/Lambda表达式与Stream/

许可协议: 本文为 wsylp 版权所有 转载请保留原文链接及作者。

-------------本文结束感谢阅读-------------