java8

java8 基础知识

image

java8 Lambda表达式

从苹果List中找出红颜色的苹果

  • 定义一个Bean类Apple.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Apple {
private String color;
private int weight;
// 省略 getter 和 setter
public Apple(String color,int weight) { this.color = color; this.weight = weight; }
public Apple() {}
@Override
public String toString() {
return "苹果的颜色是:"+color + "苹果的重量是:" + weight;
}
}
  • 定义一个接口AppleService.java,里面包含一个过滤红色苹果的方法
1
2
3
4
5
public interface AppleService {
List<Apple> filterRedApple(List<Apple> list);
}
  • 实现方法AppleServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class AppleServiceImpl implements AppleService {
@Override
public List<Apple> filterRedApple(List<Apple> list) {
List<Apple> apples = new ArrayList<>();
for (Apple apple : list) {
if (apple.getColor().equals("red")) {
apples.add(apple);
}
}
return apples;
}
}
  • 测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestApple {
public static List<Apple> list;
static {
list = new ArrayList<>();
String colors[] = {"green", "red"};
Apple apple = null;
int i = 0;
for (String color : colors) {
apple = new Apple();
apple.setColor(colors[i]);
apple.setWeight(new Random().nextInt(100));
list.add(apple);
i++;
}
}
public static void main(String[] args) {
System.out.println(list.size());
new AppleServiceImpl().filterRedApple(list);
}
}
  • [x] 上面这段代码在优化一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestApple {
public static List<Apple> list;
static {
list = new ArrayList<>();
List<Apple> apples = Arrays.asList(new Apple("green",65), new Apple("red",50));
Apple apple = null;
int i = 0;
for (Apple a : apples) {
apple = new Apple();
apple.setColor(a.getColor());
apple.setWeight(a.getWeight());
list.add(apple);
i++;
}
}
public static void main(String[] args) {
System.out.println(list.size());
new AppleServiceImpl().filterRedApple(list);
}
  • [x] 输出结果
1
2
苹果的颜色是:red

此时如果增加新的需求要过滤绿色怎么办

  • 如果此时增加需求要过滤绿色的苹果,怎么办?
    • 可能会在AppleService中在增加一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface AppleService {
/**
* 过滤红色的苹果
* @param list
* @return
*/
List<Apple> filterRedApple(List<Apple> list);
/**
* 过滤绿色的苹果
* @param list
* @return
*/
List<Apple> filterGreenApple(List<Apple> list);
}

这样就造成了代码的冗余

使用接口Predicate

  • 这个类在 java.util.function.Predicate 单独写出来为了清晰
1
2
3
4
public interface Predicate<T> {
boolean test(T t);
  • 你也可以单独定义一个接口,重新定义其中的方法名,使其清晰一点。

  • 定义一个接口 Predicate.java

1
2
3
4
5
6
public interface Predicatet<T>{
boolean filterApple(T t);
}
  • 修改AppleService中的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AppleServiceImpl implements AppleService {
@Override
public List<Apple> filterApple(List<Apple> list,Predicatet<Apple> predicatet) {
List<Apple> apples = new ArrayList<Apple>();
for (Apple apple : list) {
if (predicatet.filterApple(apple)) {
apples.add(apple);
}
}
return apples;
}
}
  • 测试类:
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
42
43
44
45
46
public class TestApple {
public static List<Apple> list;
static {
list = new ArrayList<>();
List<Apple> apples = Arrays.asList(new Apple("green"), new Apple("red"));
Apple apple = null;
int i = 0;
for (Apple a : apples) {
apple = new Apple();
apple.setColor(a.getColor());
list.add(apple);
i++;
}
}
/**
* 过了红色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
if ("red".equals(apple.getColor())) { ① 此处代码提取出来
return true;
}
return false;
}
});
}
/**
* 过了绿色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
if ("green".equals(apple.getColor())) { ① 此处代码提取出来
return true;
}
return false;
}
});
}
}

优化一下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
public class TestA {
public static List<Apple> list;
static {
list = new ArrayList<Apple>();
List<Apple> apples = Arrays.asList(new Apple("green"), new Apple("red"));
Apple apple = null;
int i = 0;
for (Apple a : apples) {
apple = new Apple();
apple.setColor(a.getColor());
list.add(apple);
i++;
}
}
/**
* 过了红色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
if (isRedApple(apple)) {
return true;
}
return false;
}
});
}
/**
* 过了绿色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
if (isGreenApple(apple)){}
return true;
}
return false;
}
});
}
}
private static boolean isRedApple(Apple apple) {
return "red".equals(apple.getColor());
}
private static boolean isGreenApple(Apple apple) {
return "red".equals(apple.getColor());
}

使用大招 Lamda表达式 重构之前的方法,以过滤红苹果示例:

  • 重构之前:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 过了红色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
if (isRedApple(apple)) {
return true;
}
return false;
}
});
}
  • [x] 重构之后:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 过了红色的苹果
*/
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, new Predicatet<Apple>() {
@Override
public boolean filterApple(Apple apple) {
return isRedApple(apple));
}
});
}
  • [x] 再重构之后
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
new AppleServiceImpl().filterApple(list, apple -> {
if (isRedApple(apple)) {
return true;
}
return false;
});
}
  • [x] 再重构之后:
1
2
3
4
5
6
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, apple -> {
return isRedApple(apple);
});
}
  • [x] 再重构之后:
1
2
3
4
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, apple -> isRedApple(apple));
}
  • [x] 再重构之后:
1
2
3
4
public static void main(String[] args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, TestApple :: isRedApple);
}

注意: ★★★★★★

上面代码中:TestApple 代表的是当前类的名字。

概念介绍:谓词
上面的代码传递了方法TestApple :: isReApple(它接受参数apple并返回一个boolean值)给filterApple,后者则希望接受一个Predicate<Apple>参数。
谓词(predicate)
在数学上尝尝用来表示一个类似函数的东西,它接受一个参数值,并返回true或false,在后面你会看到,java8也会允许你写Function<Apple,Boolean>,但是用Predicate是更标准的方式,效率也会高一点,避免了boolean封装在Boolean装箱的过程。
简单点说谓词(就是一个返回boolean值的函数)


  • 完整版示例代码:
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public class FilteringApples{
public static void main(String ... args){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"),
new Apple(155, "green"),
new Apple(120, "red"));
// [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
System.out.println(greenApples);
// [Apple{color='green', weight=155}]
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);
System.out.println(heavyApples);
// [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
List<Apple> greenApples2 = filterApples(inventory, (Apple a) -> "green".equals(a.getColor()));
System.out.println(greenApples2);
// [Apple{color='green', weight=155}]
List<Apple> heavyApples2 = filterApples(inventory, (Apple a) -> a.getWeight() > 150);
System.out.println(heavyApples2);
// []
List<Apple> weirdApples = filterApples(inventory, (Apple a) -> a.getWeight() < 80 ||
"brown".equals(a.getColor()));
System.out.println(weirdApples);
}
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
public static List<Apple> filterHeavyApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
public static class Apple {
private int weight = 0;
private String color = "";
public Apple(int weight, String color){
this.weight = weight;
this.color = color;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public String toString() {
return "Apple{" +
"color='" + color + '\'' +
", weight=" + weight +
'}';
}
}
}

从方法传递到 lambda

  • 把方法作为值来传递显然很有用,但要是为了类似于isRedApple()isGreenApple()这种只用一两次的短方法写一堆的定义有点烦人。
  • 所以在java8引入了一套新记法(匿名函数或者lambda),你可以这样写
1
2
3
4
5
6
public static void main(String... args) {
new AppleServiceImpl().filterApple(list, (Apple apple) -> "red".equals(apple.getColor()));
}
`
  • [x] 或者
1
2
3
4
public static void main(String... args) {
new AppleServiceImpl().filterApple(list, (Apple apple) ->a.getWeight() > 150);
}
  • [x] 再或者
1
2
3
4
5
public static void main(String... args) {
List<Apple> apples = new AppleServiceImpl().filterApple(list, (Apple apple) -> "red".equals(apple.getColor()) || apple.getWeight() > 10);
System.out.println(apples.size());
}
  • 所以你甚至不需要为只用一次的方法写定义;
  • 代码更干净,更清晰
  • 因为你用不着去找自己到底传递了什么代码。

  • 但是如果Lambda的长度多以几行(它的行为也不是一目了然)的话,你还是应该用方法引用来指向一个有描述性名称的方法,而不是使用匿名的Lambda。
  • 应该以代码的清晰度为准绳

本来java加上filter和几个相关的东西作为通用库方法就足以让人满意了。

  • 比如:
1
2
static <T> Collection<T> filter( CollectionM<T> c , Predicate<T> );
  • [x] 这样你甚至都不需要写filterApple(List list,Predicatet predicatet)
  • 比如
1
2
List<Apple> apples = new AppleServiceImpl().filterApple(list, (Apple apple) -> "red".equals(apple.getColor()));
  • [x] 直接使用调用库方法filter
1
2
3
4
5
6
List<Apple> greenApples2 = filter(inventory, new AppleColorPredicate());
interface ApplePredicate{
public boolean test(Apple a);
}
  • 但是为了更好地利用并行,java的设计师没有这么做。java8中有一整套新的集合API——Stream,他有一套函数式类似于filter的操作。
    • 比如 map、reduce 还有接下来要学习的Collections和Streams之间做转换的方法。

  • 先简单的让你体验一下Stream和Lambda表达式顺序或并行地从一个列表里筛选红颜色的苹果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestApple {
public static List<Apple> list;
static {
list = new ArrayList<Apple>();
String colors[] = {"green", "red"};
Apple apple = null;
int i = 0;
for (String color : colors) {
apple = new Apple();
apple.setColor(colors[i]);
apple.setWeight(new Random().nextInt(100));
list.add(apple);
i++;
}
}
public static void main(String[] args) {
List<Apple> red = list.stream().filter((Apple a) -> a.getColor().equals("red")).collect(Collectors.toList());
}
}
  • 做了几个简单的测试
  • stream()比在数据量低于百万的时候 效率第一倍

集合的操作更加的人性化

  • jdk1.8之前,我们对集合的一些操作比如,排序,在之前,我们使用排序
1
2
3
4
5
6
7
Collections.sort(appleList, new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
});
  • [x] 在1.8之后,我们可以 集合 . sort() 示例:
1
2
3
4
5
6
7
appleList.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
});
  • [x] 请出神器 Lambda 示例:
1
2
appleList.sort((o1, o2) -> o1.getWeight() - o2.getWeight());

默认方法

  • 在java8里,你可以直接对List调用sort方法
  • 它使用java8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态的方法。
1
2
3
4
default void sort (Comparator<? super E> c){
Coolections.sort(this,c);
}

第2章 通过行为参数化传递代码

重构你的垃圾代码

  • 定义一个谓词方法
1
2
3
4
public interface Predicatet<T>{
boolean test(T t);
}
  • 在列中定义一个公共的静态的泛型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public class PredicateService {
public static <T> List<T> filter(List<T> list, Predicatet<T> predicatet) {
List<T> result = new ArrayList<T>();
for (T t : list) {
if (predicatet.test(t)) {
result.add(t);
}
}
return result;
}
}
  • [x] 测试类
1
2
3
4
5
6
7
8
9
10
11
12
public class TestApple {
public static void main(String... args) {
PredicateService.filter(list, new Predicatet<Apple>() {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 19;
}
});
}
}

此时如果

  • [x] 使用Lambda表达式
1
2
3
4
5
6
7
8
// 过滤苹果重量>19的
List<Apple> filter = PredicateService.filter(list, (Apple apple) -> apple.getWeight() > 19);
// 此时如果橘子也要过滤重量>19的
List<Orange> filter = PredicateService.filter(list, (Orange orange) -> orange.getWeight() > 19);
// 此时就非常的方便拓展了

用Runnable 执行代码块

1
2
3
4
5
6
7
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
}).start();
  • [x] Lambda 重构后
1
2
new Thread(() -> System.out.println("hello")).start();

这里要说明的,像run()方法一样,方法没有参数,那么书写Lamda表达式的时候,写一对 空括号()

第3章 Lambda 表达式

Lambda 表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  • 匿名:
    • 说其匿名,因为它不像普通的方法那样有一个明确的名称:写得少而想得多
  • 函数
    • 说其实函数,是因为Lambda函数不像方法那样属于某个特定的类,但和方法一样,lambda有参数列表、函数主体、返回类型、还可能有可以抛出的异常列表
  • 传递
    • Lambda表达式可以作为参数传递给方法或存储在变量中
  • 简洁
    • 无需像匿名类那样写很多模板代码

Lambda表达式构成

详解 Lambda 表达式

  • Lambda 没有return语句 ,因为已经隐含了return
1
2
(String s ) -> s.length()
  • Lambda表达式可以包含多行语句,用一对大括号包裹
1
2
3
4
5
(int x , int y ) -> {
System.out.println("hello");
System.out.println("world");
}
  • 如果return是一个控制流语句,要让Lambda有效,需要添加花括号{}包裹
1
2
( Integer i ) -> { return "benny" + i;}
  • Lambda 没有参数,并返回String作为表达式,这种情况下不能有{}
1
2
( ) -> "benny"

java中有效的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(String s) -> s.length()
(Apple apple) -> apple.getWeight() > 10
(int x , int y ) -> {
System.out.println("hello");
System.out.println("world");
}
() -> 42
(Apple apple1, Apple apple2) -> apple1.getWeight().compareTo(apple2.getWeight())
( Integer i ) -> { return "Alan" + i }

函数式接口

  • 我们之前声明的Predicate<T>接口就是一个函数式接口
    • 因为Predicate仅仅定义了一个抽象方法
1
2
3
4
5
pubilc interface Predicate<T>{
Boolean test( T t );
}
  • 简单点说,函数式接口就是只定义一个抽象方法的接口。
  • 在java API中有很多函数式接口,例如ComparatorRunnable接口

接口可以拥有默认方法,不管接口中有多少默认方法,只要接口中只定义了一个抽象方法(有且仅有一个),它就是一个函数式接口,重载的方法也不行哦。

  • 如果你声明了一个函数式接口,用什么办法检查你是否声明的正确呢。
  • 使用@FunctionalInterface 注解(新版的API才有)

如果你声明了一个函数式接口,而它却不是函数式接口,编译器将返回一个提示原因的错误。

  • 例如:错误提示可能是:
    • Multiple non-overriding abstract methods found in interface Foo, 表名存在多个抽象方法。
    • @FunctionalInterface 并不是必须的,但是对于设计函数式接口是比较好的做法。
    • 它就像是@Override标注表示方法被重写了一样。
  • 看到这里 你可能还是不清楚为什么要要定义函数式接口(仅含有一个抽象方法的接口),因为,你定义多个方法,lambda无法确定是哪一个方法。

附录2:Java8中常用的函数式接口:

函数式接口 函数描述符 原始烈性特化
Predicate<T> T -> boolean IntPredicate<T>, LongPredicate<T>, DoublePredicate<T>
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T> () -> T BooleanSupplier, BooleanSupplier, BooleanSupplier, BooleanSupplier
UnaryOperator<T> T -> T IntUnaryOperator, DoubleUnaryOperator, LongUnaryOperator
BinaryOperator<T> (T,T) -> T IntBinaryOperator, DoubleBinaryOperator, LongBinaryOperator
BiPredicate<L,R> (L,R) -> boolean
BiConsumer<T,U> (T,U) -> void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) -> R ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

函数描述符

  • 函数式接口的抽象方法的签名基本就是Lambda表达式的签名,我们将这种抽象方法叫作函数描述符。

把Lambda 付诸实践,环绕执行模式

  • 第一步:记得行为参数化
    • 方法传参传递行为(也就是方法)
  • 第二步: 使用函数式接口来传递行为
  • 第三步:执行一个行为
    • Lambda 表达式允许你直接内联系
    • 再次阐述一下流程
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
①:新建一个函数式接口 例:
public interface BufferReaderProcess {
String process(BufferedReader bufferedReader) throws IOException;
}
②:在Service中定义一个抽象方法
public interface FileService {
String processFile(BufferReaderProcess b);
}
③:实现FileReadService接口
public class FileServiceImpl implements FileService {
@Override
public String processFile(BufferReaderProcess b) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return b.process(br);
}
}
}
④ :测试
String s = new FileServiceImpl().processFile(new BufferReaderProcess() {
@Override
public String process(BufferedReader bufferedReader) throws IOException {
return bufferedReader.readLine() + bufferedReader.readLine();
}
});
⑤:使用Lambda
String s = new FileServiceImpl().processFile((BufferedReader buffer) -> buffer.readLine());

以上四个步骤就是应用环绕执行模式所采取的四个步骤

使用函数式接口

  • 函数式接口
    + 函数式接口只定义了一个抽象方法(有且仅有一个)
  • java8设计师在java.util.function包中引入了几个新的函数式接口。

Predicate(谓词)

  • java.util.function.Predicate<T>接口定义了一个叫 test()的抽象方法,它接受泛型<T>对象,并返回一个boolean
  • 在你需要表示一个涉及类型<T>的布尔表达式时,你就可以使用此接口。
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
FunctionalInterface // 此注解表示当前接口是一个函数式接口
public interface Predicate<T> {
boolean test(T t);
// 接口可以拥有默认方法,不管接口中有多少默认方法,只要接口中只定义了一个抽象方法(有且仅有一个),它就是一个函数式接口
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}

使用javaApi自带的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
27
28
29
30
31
public class TestPredicate {
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<T>();
for (T t : list) {
if (predicate.test(t)) {
result.add(t);
}
}
return result;
}
List<String> result = Arrays.asList("My", "name", "is","benny");
List<String> benny = filter(result, new Predicate<String>() {
@Override
public boolean test(String s) {
return s.equals("benny");
}
});
}
// 使用Lambda 表达式
// 一步一步来
Predicate<String> predicate = (String str) -> str.equals("benny");
List<String> benny = filter(result, predicate);
// 一步到位
List<String> benny1 = filter(result, (String str) -> str.equals("benny"));
}

Consumer(消费者)

  • java.util.function.Consumer<T>定义了一个accept的抽象方法它接受泛型<T>的对象,没有返回值 void
  • 在你需要访问类型<T>的对象,并对其执行某些操作,就可以使用此接口 。
1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

使用javaApi自带的Consumer函数式接口,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* foreach(result, new Consumer<String>() {
@Override
public void accept(String s) {
System.out.print(s+"\t");
}
});*/
// 使用Lambda 表达式
// 一步一步来
Consumer<String> consumer = (String s) -> System.out.print(s + "\t");
foreach(result, consumer);
// 一步到位
foreach(result, (String s) -> System.out.print(s + "\t"));

Function

  • java.util.function.Function<T,R>接口定义了一个apply的抽象方法它接受泛型<T>的对象,它接受一个泛型<T>的对象,并返回一个泛型<R>的对象。
  • 如果你需要定义一个Lambda,将输入的对象的信息映射到输出,就可以使用这个接口 。
    • 比如你传入一个 List<String> 返回一个 List<integer>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}

使用javaApi自带的Function函数式接口,代码示例:

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
public class TestFunction {
public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
List<R> result = new ArrayList<R>();
for (T t : list) {
result.add(function.apply(t));
}
return result;
}
public static void main(String[] args) {
List<String> result = Arrays.asList("My", "name", "is","benny");
// 使用Lambda 表达式
// 一步一步来
List<Integer> map = map(result, new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
});
// 一步到位
List<Integer> map = map(result, (String str) -> str.length());
}
}

原始类型特化

  • java类型有两种
    • 引用类型
    • 基本数据类型
  • 基本数据类型转换成引用类型,是装箱的过程。
  • 装箱的本质
    • 将基本数据类型包裹起来,并保存在堆里。
    • 因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

java8为我们的函数式接口带来了一个专门的版本,以便在输入和输出值都是基本数据类型时避免自动装箱的操作。


使用javaApi自带的IntPredicate函数式接口,代码示例:

1
2
3
4
5
public static void main(String[] args) {
IntPredicate intPredicate = (int k) -> i % 2 == 0;
boolean flag = intPredicate.test(1000);
}

使用javaApi自带的IntPredicate函数式接口,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
IntBinaryOperator intBinaryOperator = new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
};
int i = intBinaryOperator.applyAsInt(5, 21);
// 使用Lambda表达式
IntBinaryOperator intBinaryOperator = (int i, int j) -> i + j;
int i = intBinaryOperator.applyAsInt(2, 3);
System.out.println(i);

使用javaApi自带的BiFunction函数式接口,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
BiFunction<String, String, String> stringStringStringBiFunction = new BiFunction<String, String, String>(){
@Override
public String apply(String s, String s2) {
return s + s2;
}
};
// 使用Lambda表达式
BiFunction<String, String, String> stringStringStringBiFunction = (String s, String s2) -> s + s2;
String apply = stringStringStringBiFunction.apply("hello", "world");

使用javaApi自带的BiFunction函数式接口,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
Supplier<String> stringSupplier = new Supplier<String>() {
@Override
public String get() {
return "hello world";
}
};
String s = stringSupplier.get();
// 使用Lambda表达式
Supplier<String> stringSupplier = () -> "hello world";
String s = stringSupplier.get();
}

一般来说,针对专门的输入输出参数类型的函数式接口的名称都要加上对应的原始类型前缀。
比如:

DoublePredicate、IntConsumer、ingBinaryOperator,IntFunction等


Function接口还有针对输出参数类型的变种:

ToIntFunction<T>、IntToDoubleFunction等

java8常用函数式接口

函数式接口 函数描述符 原始烈性特化
Predicate<T> T -> boolean IntPredicate<T>, LongPredicate<T>, DoublePredicate<T>
Consumer<T> T -> void IntConsumer, LongConsumer, DoubleConsumer
Function<T,R> T -> R IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction<R>, LongToDoubleFunction, LongToIntFunction, DoubleFunction<R>, ToIntFunction<T>, ToDoubleFunction<T>, ToLongFunction<T>
Supplier<T> () -> T BooleanSupplier, BooleanSupplier, BooleanSupplier, BooleanSupplier
UnaryOperator<T> T -> T IntUnaryOperator, DoubleUnaryOperator, LongUnaryOperator
BinaryOperator<T> (T,T) -> T IntBinaryOperator, DoubleBinaryOperator, LongBinaryOperator
BiPredicate<L,R> (L,R) -> boolean
BiConsumer<T,U> (T,U) -> void ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>
BiFunction<T,U,R> (T,U) -> R ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

附录1:Lambda及函数式接口的例子:

使用案例 lambda例子 对应函数式接口
布尔表达式 (List list) -> list.isEmpty() Predicate<List<String>>
创建对象 () -> new Fan() Supplier<Fan>
消费一个对象void (Fan f) -> sys…out(f.toString) Consumer<Fan>
从一个对象中选择提取 (String s) -> s.length() Function<String,Integer>ToIntFunction<String>
合并2个值 (int a , int b) -> a+b IntBinaryOperator
比较2个对象 (Fan a , Fan b) -> a.getXX().compareTo(b.getXX()) Comparator<Fan>BiFunction<Fan,Fan,Integer>ToIntBiFunction<Fan.Fan>

异常、Lambda,还有函数式接口是什么鬼

注意: 任何函数式接口都不允许抛出受检异常

  • 如果你需要Lambda表达式抛出异常,有两种方式:
    • 定义一个自己的函数式接口,并声明受检异常
    • 将Lambda表达式包裹在一个try/catch块中

自定义函数式接口,代码示例:

1
2
3
4
5
6
7
8
@FunctionalInterface
pubilc interface BufferedReaderPrcessor {
String process(BufferedReader br ) throw IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
  • 但是你可能使用一个接受函数式接口的API,比如Function,没有办法自己创建一个,在这种情况下,你可以显式的捕获受检异常
1
2
3
4
5
6
7
8
9
Function<BufferedReader,String> f = (BufferedReader br) -> {
try {
return br.readLine();
}
catch(IOException e){
throw new RuntimeException(e);
}
}

类型检查 类型推断 以及限制

类型检查

  • 类型检查过程可以分解为如下几个步骤:
    • 首先,你要找出filter方法的声明
      • filter(List<Apple> inventory,Predicate<Apple> p)
    • 第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数
      • filter(List<Apple> inventroy , Predicate<Apple> p)
    • 第三,Predicate<Apple>是一个函数式接口,定义了一个叫做test的抽象方法
      • boolean test(Apple apple)
    • 第四,test方法描述了一个函数描述符,他可以接受一个Apple,并返回一个boolean
      • Apple -> boolean
    • 最后filter的任何实际参数都必须匹配这个要求
      • filter(inventory,(Apple apple) -> a.getWeight()>150);

如果Lambda表达式抛出一个异常,那么抽象方法所声明的Throws语句要必须与之匹配

类型推断

1
2
3
4
5
6
Predicatet<Apple> p = (Apple a) -> a.getWeight() > 11; ☛ 有显式类型Apple
Predicate<Apple> p1 = (a) -> a.getWeight() > 11; ☛ 没有类型推断 因为反省中已经制定类型为Apple
Predicate<Apple> p2 = a -> a.getWeight() > 11; ☛

当Lambda表达式仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。

使用局部变量

  • 局部变量必须显式的被声明为final

对局部变量的限制

  • 实例变量和局部变量有一个关键不同
    • 实例变量都储存在堆中
    • 局部变量存储在栈上

方法引用

1
2
3
4
inventory.sort( ( Apple a1,Apple a2 ) -> a1.getWeight() .compareTo(a2.getWeight() ) );
inventory.sort( comparing( Apple :: getWeight()) );

管中窥豹

  • 方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。

它的基本思想:
如果一个Lambda代表的只是”直接调用这个方法“,那最好还是用名称来调用它,而不是去描述如何调用它。
方法引用就是,让你根据已有的方法实现来创建Lambda表达式。

Lambda及其等效方法引用的例子
Lambda 等效的方法引用
( Apple a ) -> a.getWeight() Apple :: getWeight
() -> Thread.currentThread().dumpStack() Thread.currenThread() :: dumpStack
( str , i )-> str.subtring(i) String :: substring
( String s )-> System.out.pringln(s) System.sout :: println
如何构建方法引用

方法引用有三类

  • 指向静态方法的方法引用
    • 例如Integer的parseInt方法,写作 Integer :: parseInt
  • 指向任意类型示例方法的方法引用
    • 例如String的length方法,写作String :: length
  • 指向现有对象的实例方法的方法引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<String> l = new ArrayList<>();
l.sort((String str, String str2) -> str.compareToIgnoreCase(str2));
// 方法引用
l.sort(String::compareToIgnoreCase);
Function<String, Integer> stringToInteger1 = (String strs) -> Integer.parseInt(strs);
// 方法引用
Function<String, Integer> stringToInteger2 = Integer :: parseInt;
BiPredicate<List<String>,String> contains = (lists, element) -> list.contains(element);
// 方法引用
BiPredicate<List<String>, String> contains2 = List::contains;

构造函数引用

  • 对于一个现有的构造函数,你可以使用它的名称和它的关键字new来创建它的一个引用, ClassName :: new
  • Supplier<Apple> s = Apple::new;
  • 它的功能与指向静态方法的引用类似, 它适合Supplier的签名 () -> Apple
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Supplier<Apple> supplier = new Supplier<Apple>() {
@Override
public Apple get() {
return new Apple();
}
};
Apple apple = supplier.get();
// 使用Labmda表达式
Supplier<Apple> s = () -> new Apple();
Apple apple1 = s.get();
// 使用构造方法引用
Supplier<Apple> s1 = Apple::new;
Apple apple2 = s1.get();
  • 如果你的构造函数的签名是Apple(Integer weight), 那么它就适合Function接口的签名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function<Integer, Apple> function = new Function<Integer, Apple>() {
@Override
public Apple apply(Integer integer) {
return new Apple(integer);
}
};
Apple apple = function.apply(888);
// 等价于
Function<Integer, Apple> function1 = (Integer weight) -> new Apple(weight);
Apple apple1 = function1.apply(888);
// 等价于
Function<Integer, Apple> function2 = (weight) -> new Apple(weight);
Apple apple2 = function2.apply(888);
// 等价于
Function<Integer, Apple> function3 = weight -> new Apple(weight);
Apple apple3 = function3.apply(888);
// 等价于
Function<Integer, Apple> function4 = Apple::new;
Apple apple4 = function4.apply(888);
  • 一个由Integer构成的List中的每个元素都通过我们定义的map方法传递给了Apple的构造函数,得到一个不同重量的苹果的List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 254, 6, 7, 7, 8, 7, 8, 76, 867);
List<Apple> appleList = getAppleList(integers, (Integer i) -> new Apple(i));
// 使用方法引用
List<Apple> appleList1 = getAppleList(integers, Apple::new);
}
public static List<Apple> map(List<Integer> list, Function<Integer, Apple> function) {
List result = new ArrayList();
for (Integer integer : list) {
Apple apply = function.apply(integer);
result.add(apply);
}
return result;
}
  • 如果你有一个具有两个参数的构造函数Apple(String color , Integer weigth),那么它就适合BiFunction接口的签名
1
2
3
4
5
6
7
8
9
10
11
BiFunction<String, Integer, Apple> biFunction = new BiFunction<String, Integer, Apple>() {
@Override
public Apple apply(String s, Integer integer) {
return new Apple(s, integer);
}
};
Apple apple = biFunction.apply("red", 888);
BiFunction<String, Integer, Apple> biFunction1 = (String color, Integer weight) -> new Apple(color, weight);
Apple apple1 = biFunction1.apply("green", 888);

Lambda和方法引用实战

第一步:传递代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.sort(new AppleCompator());
}
static class AppleCompator implements Comparator<Apple> {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getColor().compareTo(o2.getColor());
}
}

第二步:使用匿名类

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getColor().compareTo(o2.getColor());
}
});
}

使用Lambda表达式

1
2
3
4
5
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.sort(((Apple apple, Apple apple2) -> apple.getColor().compareTo(apple2.getColor())));
}

使用方法引用

1
2
3
4
5
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.sort(Comparator.comparing(Apple::getWeight));
}

复合Lambda表达式的有用方法

比较器复合

逆序

  • 比如你要对苹果的重量递减
    • .reversed()
1
2
apples.sort(Comparator.comparing(Apple::getWeight).reversed());

比较器链

  • 比如你要对苹果的重量递减,但是你发现欧两个苹果一样重怎么办,你可以在提供一个Comparator
    • .thenComparing(...)
1
2
3
4
apples.sort(Comparator.comparing(Apple::getWeight)
.reversed() // 按照递减排序
.thenComparing(Apple::getColor)); // 当苹果一样重时,按照颜色排序

谓词复合

  • 谓词接口包括三个方法:
    • negate
      • 使用negate()方法返回一个谓词接口的
    • and
      • 可以使用and(...)方法连接 Lambda 表达式
    • or
      • 可以使用or(...)方法表达是它或是它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Predicate<Apple> redApplePredicate = new Predicate<Apple>() {
@Override
public boolean test(Apple apple) {
return apple.getColor().equals("red");
}
};
// 筛选红颜色的苹果
Predicate<Apple> redApplePredicate1 = (Apple a) -> a.getColor().equals("red");
// 筛选不是红颜色的苹果
Predicate<Apple> notRedApplePredicate = redApplePredicate1.negate();
List<Apple> list = new ArrayList<>();
for (Apple apple : apples) {
if (notRedApplePredicate.test(apple)) {
list.add(apple);
}
}
  • 组合谓词,表达既是红苹果又是绿苹果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 筛选红颜色的苹果
Predicate<Apple> redApplePredicate1 = (Apple a) -> a.getColor().equals("red");
// 筛选重量大于100g的苹果
Predicate<Apple> weightApplePredicate = (Apple b) -> b.getWeight() > 100;
// 既是红苹果并且苹果大于100g
Predicate<Apple> colorAndWeightApple = redApplePredicate.and(weightApplePredicate);
// 等价于
redApplePredicate.negate().and((Apple apple) -> apple.getWeight() > 150);
List<Apple> list = new ArrayList<Apple>();
for (Apple apple : apples) {
if (colorAndWeightApple.test(apple)) {
list.add(apple);
}
}
  • 进一步组合谓词,要么是中(150g以上)的红苹果,要么是绿苹果
1
2
3
4
5
6
7
8
9
10
Predicate<Apple> redOrGreenApplePredicate = redApplePredicate
.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
List<Apple> list = new ArrayList<>();
for (Apple apple : apples) {
if (redOrGreenApplePredicate.test(apple)) {
list.add(apple);
}
}

注意:

and和or方法是按照在表达式链中的位置,从左到右确定优先级的。
因此,a.or(b).and(c)可以看作 ( a || b ) && c

函数复合

  • 最后你还可以吧Function函数式接口所代表的Lambda表达式复合起来。
  • Function函数式接口为此配备了andThen()compose()两个默认方法,他们都会返回Function的一个实例。
1
2
3
4
5
6
7
8
9
10
Function<Integer, Integer> times2 = e -> e * 2;
Function<Integer, Integer> squared = e -> e * e;
times2.compose(squared).apply(4);
// Returns 32
times2.andThen(squared).apply(4);
// Returns 64
  • 不知道你是否看懂了,说下这两个方法
    • a.andThen( b ) 先执行 a,然后再执行 b
    • a.compose(b) 先执行 b,然后再执行 a

f.andThen(g)

1
2
3
4
5
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);
输入 f.andthen(g) 结果
1 ——f—> 2 ——g——> 4
2 ——f—> 3 ——g——> 6
3 ——f—> 4 ——g——> 8

f.compose(g)

1
2
3
4
5
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);
输入 f.compose(g) 结果
1 ——f—> 2 ——g——> 3
2 ——f—> 4 ——g——> 5
3 ——f—> 6 ——g——> 7

实际应用:

  • 比如你有一个工具类,用来处理文本。

Letter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Letter {
public static String addHeader(String text) {
return "benny" + text;
}
public static String addFooter(String text) {
return text + "by benny";
}
public static String checkSpelling(String text) {
return text.replaceAll("beny", "benny");
}
}
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformation = addHeader.andThen(Letter::checkSpelling).andThen(Letter::addFooter);
String text = transformation.apply("i miss beny,because i have not seen him for a long time");
System.out.println(text);
}
  • 结果:
1
2
3
4
5
Dear:
i miss benny,because i have not seen him for a long time
by benny
  • 只加抬头和落款,而不做拼写检查:
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformation = addHeader.andThen(Letter::addFooter);
String text = transformation.apply("i miss beny,because i have not seen him for a long time");
System.out.println(text);
}
  • 结果:
1
2
3
4
5
Dear:
i miss beny,because i have not seen him for a long time
by benny

小结

  • Lambda表达式可以理解为一种匿名函数:它没有名称、但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表
  • Lambda表达式可以让你简洁地传递代码
  • 函数式接口就是仅仅声明了一个抽象方法的接口
  • 只有在接受函数式接口的地方才可以使用Lambda表达式
  • 只有在接受函数式接口的地方才可以使用Lambda表达式
  • Lambda表达式允许你直接内联,为函数是接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例
  • java8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate<T>Function<T,R>Supplier<T>Consumer<T>、和BinaryOperatory<T>,详情见函数式接口里的表。
  • 为了避免装箱操作,对Predicate<T>Function<T,R>等通用函数式接口的原始类型特化,IntPredicateIntToLongFunction
  • 环绕执行模式(即在方法所必须的代码中间,你需要执行点什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
  • Lambda表达式所需要代表的类型成为目标类型
  • 方法引用让你重复只是用现有的方法实现并直接传递它们
  • ComparatorPredicateFunction等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

第二部分 函数式数据处理

第4章 引入流

流是什么

  • 流是java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句表达而不是临时编写一个实现)
  • 用两个例子来看下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
//省略 getter 和 setter
}

java 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
List<Dish> menu = new ArrayList<>();
List<Dish> lowCaloricDish = new ArrayList<>();
//用累加器筛选元素
for (Dish dish : menu) {
if(dish.getCalories() < 400){
lowCaloricDish.add(dish);
}
}
// 用匿名类对才要排序
Collections.sort(lowCaloricDish, new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories() - o2.getCalories();
}
});
List<String> lowCaloricDishName = new ArrayList<>();
// 处理排序后的菜名列表
for (Dish dish : lowCaloricDish) {
lowCaloricDishName.add(dish.getName());
}

java8

1
2
3
4
5
6
menu.stream().
filter((Dish dish) -> dish.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
  • 为了利用多核架构并行执行这段代码,你只需要吧stream()换成parallelStream()
1
2
3
4
5
6
menu.parallelStream()
.filter((Dish dish) -> dish.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
  • 使用java8的新方法有几个显而易见的好处
    • 代码是以声明性方式写的
      • 说明想要完成什么(筛选热量低的菜肴),而不是说明如何实现一个操作(利用循环和if条件等控制语句)
      • 这种方法加上行为参数化让你可以轻松应对变化的需求:你很容易创建一个代码版本,利用Lambda表达式筛选高卡路里的菜肴,而不用赋值粘贴代码
    • 你可以将几个基础操作链接起来,来表达复杂的数据处理流水线(filter后面接上sorted、map和collect操作),同事保证代码清晰可读。

小结

  • 声明性——更简洁、更易读
  • 可复合——更灵活
  • 可并行——性能更好

流简介

流的定义

从支持数据处理操作的源生成的元素序列

  • 元素序列
    • 就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。
    • 集合是数据结构:
      • 它的主要目的是以特定的时间/控件复杂度存储和访问元素(如ArrayList和LinkedList)
    • 流的目的在于计算
      • 比如filter、sorted和map
    • 集合讲的是数据,流讲的计算
    • 流会使用一个提供数据的源,如集合、数组、或输入/输出资源
    • 从有序集合生成流时会保留原有的顺序。
    • 由列表生成的流,其元素顺序与列表一致
  • 数据处理操作
    • 流的数据处理功能支持类似与数据库的操作,以及函数式编程语言中的常用操作,如:filter、map、reduce、find、match、sort等。
    • 流操作可以顺序执行,也可以并行
  • 流水线
    • 很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线
    • 流的操作可以看做对数据源进行数据库式查询
  • 内部迭代
    • 与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

流与集合

只能遍历一次

  • 和迭代器类似,流只能遍历一次。
1
2
3
4
5
List<String> title = Arrays.asList("java8", "by", "heart");
Stream<String> stream = title.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 抛出 java.lang.IllegalStateException: 流已被操作或关闭

外部迭代与内部迭代

  • 集合:用for-each循环外部迭代
1
2
3
4
5
6
List<Dish> menu = new ArrayList<>();
List<String> names = new ArrayList<>();
for (Dish dish : menu) {
names.add(dish.getName());
}
  • 集合:用背后的迭代器做外部迭代
1
2
3
4
5
6
7
8
List<Dish> menu = new ArrayList<>();
List<String> names = new ArrayList<>();
Iterator<Dish> iterator = menu.iterator();
while (iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}
  • 流内部迭代
1
2
3
4
List<String> names = menu.stream()
.map(Dish::getName) // 用getName方法参数化map
.collect(Collectors.toList());

流操作

1
2
3
4
5
6
List<String> names = menu.stream() // 获得流
.filter(d -> d.getCalories() > 300) // 中间操作
.map(Dish::getName) // 中间操作
.limit(3) // 中间操作
.collect(Collectors.toList()); // 将Stream转换为List 终端操作
  • 可以链接起来的流操作称为:中间操作
  • 关闭流的操作称为:终端操作

中间操作

  • 诸如filter和sorted等中间操作会返回另外一个,这让多个操作可以连接起来形成一个查询。
  • 中间操作不会执行任何处理,除非触发一个终端操作。

终端操作

  • 终端操作会从流的流水线生成结果,其结果是任何不是流的值,可以使List,Integer,甚至void

使用流

  • 流的使用一般包括三件事:
    • 一个数据源(如集合)来执行一个查询
    • 一个中间操作链,形成一条流的流水线
    • 一个终端操作,执行流水线,并能生成结果
中间操作
操作 类型 返回类型 操作参数 函数描述符
filter 中间 Stream<T> Predicate<T> T -> boolean
map 中间 Stream<R> Function<T,R> T -> R
limit 中间 Stream<T>
sorted 中间 Stream<T> Comparator<T> (T,T) -> int
distinct 中间 Stream<T>
终端操作
操作 类型 目的
forEach 终端 消费流中的每个元素并对其应用Lambda,这一操作返回void
count 终端 返回流中元素的个数,这一操作返回long
collect 终端 把流归约承一个集合,比如List,Map甚至是Integer

小结

  • 流是从支持数据处理操作的源生成的一系列元素
  • 流利用内部迭代:迭代通过filter、map、sorted等操作被抽象掉了
  • 流操作有两类:中间操作和终端操作
  • filter和map等中间操作会返回一个流,并可以链接在一起。可以用它们来设置一条流水线,但并不会生成任何结果
  • forEach和Count等终端操作会返回一个非流的值,并处理流水线以返回结果
  • 流中的元素是按需计算的

第5章

筛选和切片

用谓词筛选

  • Stream接口支持filter方法,改操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包含所有复合谓词的元素的流。

筛选各异的元素

  • 流还支持一个叫做distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。
  • 简单点说就是去重
1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(1, 2, 5, 4, 7, 8);
list.stream()
.filter(num -> num % 2 == 0)
.distinct()
.forEach(System.out::print);
outPut: 2 4 8

截断流

  • 流支持limit方法,该方法会返回一个不超过给定长度的流。所需参数作为参数传递给limit
  • 如果流是有序的,则最多会返回前n个元素

limit也可以用在无序流上,比如源是一个Set,这种情况下,limit的结果不会以任何形式排序

1
2
3
4
5
6
7
8
List<Integer> list = Arrays.asList(1, 2, 5, 4, 7, 8);
list.stream().filter(num -> num % 2 == 0)
.distinct()
.limit(2)
.forEach(System.out::print);
outPut: 2 4

跳过元素

  • 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。
  • 如果流中元素个数不足n个,则放回一个空流。
1
2
3
4
5
6
7
List<Integer> list = Arrays.asList(1, 2, 5, 4, 7, 8);
list.stream().filter(num -> num % 2 == 0)
.distinct()
.skip(2)
.forEach(System.out::print);
outPut: 8

映射

对流中每一个元素应用函数

  • 流支持map方法,它会接受一个函数作为参数。
  • 这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射,是因为他和转换类似,但其中的细微差别在于它是“创建一个新版本”,而不是去“修改”)
1
2
3
4
strs.stream()
.map(String::length)
.sorted()
.forEach(System.out::println);
  • 当你不知道一个方法中的Lambda表达式应该如何写的时候,你可以这样做
  • 先传入匿名内部类,然后一步一步的提炼
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
strs.stream()
.map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
})
.sorted()
.forEach(System.out::println);
strs.stream()
.map((String s) -> s.length())
.sorted()
.forEach(System.out::println);
strs.stream()
.map(s -> s.length());
.sorted()
.forEach(System.out::println);
strs.stream()
.map(String::length)
.sorted()
.forEach(System.out::println);

流的扁平化

  • 如何返回一张单词表,列出里面各不相同的字符呢。
  • 第一个版本,返回的是一个list,里面包含了两个string[]数组
1
2
3
4
5
6
7
List<String> words = Arrays.asList("hello", "world");
List<String[]> collect = words.stream()
.map(str -> str.split(""))
.distinct()
.collect(Collectors.toList());
outPut: [[Ljava.lang.String;@7a79be86, [Ljava.lang.String;@34ce8af7]

这与我们想要的不同,我们要的是List<String>而现在是List<String[]>

  • 尝试使用Map和Arrays.stream()
1
2
3
4
5
6
String[] word = {"hello", "world"};
Stream<String> wordStream = Arrays.stream(word);
List<String[]> collect = wordStream
.map(str -> str.split(""))
.collect(Collectors.toList());

依然搞不定,返回的仍然是List<String[]>

  • 我们可以使用flatMap来解决这个问题 ☆☆☆☆☆☆
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
List<String> words = Arrays.asList("hello", "world");
List<String> collect = words.stream()
.map(str -> str.split(""))
.flatMap(new Function<String[], Stream<String>>() {
@Override
public Stream<String> apply(String[] strings) {
return Arrays.stream(strings);
}
})
.distinct()
.collect(Collectors.toList());
// 使用lambda表达式
List<String> words = Arrays.asList("hello", "world");
List<String> collect = words.stream()
.map(str -> str.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
// 另一种方式
String[] word = {"hello", "world"};
Stream<String> wordStream = Arrays.stream(word);
List<String> collect = wordStream
.map(str -> str.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
outPut: h, e, l, o, w, r, d
  • flatMap()方法
  • 让你将流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流
1
2
3
4
5
6
7
8
// 给定给一个数字列表,返回一个由每个数的平方构成的列表
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collect = integers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
outPut: 1,4,9,16,25
1
2
3
4
5
6
7
// 给定两个数字列表,如何返回所有的数对
List<int[]> collect1 = integers1.stream().
flatMap(i -> integers2.stream()
.map(j -> new int[]{i, j}))
.collect(Collectors.toList());
outPut: (1.3),(1,4(2,3)(2,4),(3,3),(3,4)
1
2
3
4
5
6
7
8
// 拓展上一个例子,只返回总和能被3整除的数对
List<int[]> collect1 = integers1.stream().
flatMap(i -> integers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
.sorted(Comparator.comparing(ints -> ints[0] - ints[1])))
.collect(Collectors.toList());
  • 和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

借用之并发编程网

查找和匹配

  • Stream API 通过allMatch、anyMatch、noneMatch、findFirst和findAny()方法提供了这样的工具

检查谓词是否至少匹配一个元素

  • anyMatch()方法
    • 查看流中是否有一个元素能匹配给定的谓词
    • anyMatch()方法返回一个人boolean
    • 因为返回boolean,所以是终端操作

比如查看菜单中是否有素食

1
2
3
4
5
boolean flag = menu.stream().anyMatch(Dish::isVegetarian);
if (flag) {
System.out.println("the meat is vegetarian");
}

检查谓词是否匹配所有元素

  • allMatch()方法
    • 工作原理和anyMatch()类似,但它会查看流中的元素是否都能匹配给定的谓词。

查看菜品是否健康

1
boolean flag = menu.stream().allMatch(d -> d.getCalories() < 1000);
  • noneMatch()
    • 与allMatch()相反,它可以确保流中没有任何元素与给定的谓词匹配。
1
boolean flag = menu.stream().allMatch(d -> d.getCalories() < 1000);
  • 短路求值
    • 有些操作不需要处理整个流就能得到结果。

查找元素

  • findAny()方法
  • 返回当前流中的任意元素,它可以与其他操作结合使用
1
2
3
Optional<Dish> any = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
  • 流水线将在后台只需走一遍,并利用短路找到结果立即返回。
  • Optional<T>类是一个容器类,代表一个值存在或不存在
  • Optional里面有几种可以迫使你显式的检查值是否存在或处理值不存在的情形的方法
方法 说明
isPresent() 将在optional包含值的时候返回true,反之返回false
isPresent(Consumer<T> block) 会在值存在时执行给定的代码块,在第3章介绍过Consumer函数式接口,他让你传递一个接受T类型参数,并返回void的Lambda表达式
T get() 会在值存在时返回值,否则抛出一个NoSuchElement异常
T orElse(T other) 会在值存在时防具机制,否则返回一个默认值
1
2
3
4
menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(dish -> System.out.println(dish.getName())); //如果包含值就打印它,反之什么都不做

查找第一个元素

  • findFirser()方法
  • 工作方式类似于findAny(),查找第一个元素
1
2
3
4
5
List<Integer> integers1 = Arrays.asList(1, 2, 3, 5, 6, 9);
integers1.stream()
.filter(x -> x % 3 == 0)
.findFirst()
.ifPresent(System.out::println);

那么问题来了:何时使用findFirstfindAny

  • 答案是并行,找到第一个元素在并行上限制更多,如果你不关心返回的元素是哪个,请使用findAny()
  • 因为*findAny()在并行流时限制较少

归并

  • 使用reduce来操作更为复杂的查询
    • 此类查询需要将流中的所有元素反复结合起来,得到一个值
  • 这样的查询可以被归类为归约操作(将流归约承一个值)

元素求和

  • 使用for-each循环来对数字列表中的元素求和
1
2
3
4
5
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer integer : integers) {
sum += integer;
}
  • 在这里代码有两个参数
    • 总和变量的初始值,在这里是0
    • 将列表中的所有元素结合在一起的操作,这里是+
  • 要是想要将所有的数字想成,而不必复制粘贴这些代码,就要用reduce

reduce()方法

  • reduce()方法
    • 对这种重复应用的模式做了抽象
    • 它接受两个参数
      • 一个初始值,这里是0
      • 一个BinaryOperator 来讲两个元素结合起来产生一个新值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream()
.reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
});
// 使用Lambda表达式
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream()
.reduce(0, (integer, integer2) -> integer + integer2);
// 使用方法引用让这段代码更加整洁
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream()
.reduce(0, Integer::sum);

无初始值

  • reduce()还有一个重载的辩题,它不接受初始值,但是会返回一个Optional对象
1
2
3
4
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream()
.reduce(Integer::sum)
.ifPresent(System.out::println);
  • 为什么返回一个Optional<Integer>考虑流中没有任何元素的情况。
  • reduce操作无法返回其和,因为它没有初始值

最大值和最小值

  • reduce只要接受两个参数
  • 一个初始值
  • 一个Lambda来把两个流元素应用到流中每个元素上
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
/*最大值*/
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
if(integer>integer2){
return integer;
}
return integer2;
}
});
// 使用三目运算符
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer > integer2 ? integer : integer2;
}
});
// 使用Lambda表达式
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(0, (integer, integer2) -> integer > integer2 ? integer : integer2);
// 修改变量名 太长不优雅
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(0, (x, y) -> x > y ? x : y);
// 使用方法引用 这样就优雅多了
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(0, Integer::max);
// 还要更优雅
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream().reduce(Integer::max);
/*最小值*/
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream().reduce(0, Integer::min);
// 与上面相同 只是没有初始值的时候返回值不同
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream().reduce(Integer::min).get();

怎样用map和reduce来查询流中有多少元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*查询流中共有多少元素*/
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
int reduce = integers.stream().map(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return 1;
}
}).reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer o, Integer o2) {
return o + o2;
}
});
// 使用Lambda表达式
int reduce = integers.stream().map(x -> 1).reduce(0, (x, y) -> x + y);
// 优雅点
int reduce = integers.stream().map(x -> 1).reduce(0, Integer::sum);
// 就是算下大小有点太复杂了,直接使用count
long count = integers.stream().count();
中间操作和终端操作
操作 类型 返回类型 操作参数 函数描述符
filter 中间操作 Stream<T> Predicate<T> T -> boolean
distinct 中间操作(有状态——无界) Stream<T>
skip 中间操作(有状态——有界) Stream<T> long
limit 中间操作(有状态——有界) Stream<T> long
map 中间操作 Stream<T> Function<T,R> T -> R
flatMap 中间操作 Stream<T> Function<T,Stream<R>> T -> Stream<R>
sorted 中间操作(有状态——无界) Stream<T> Comparator<T> ( T , T ) -> int
anyMatch 终端操作 boolean Predicate<T> T -> boolean
noneMatch 终端操作 boolean Predicate<T> T -> boolean
allMatch 终端操作 boolean Predicate<T> T -> boolean
findAny 终端操作 Optional<T>
findFirst 终端操作 Optional<T>
forEach 终端操作 void consumer<T> T -> void
collect 终端操作 R Collector<T,A,R>
reduce 终端操作(有状态——有界) Optional<T> BinaryOperator<T> ( T , T ) -> T
count 终端操作 long

付诸实践

  • 当你对list中的元素进行去重的时候,可以考虑使用toSet()
1
2
3
4
List<String> collect = transactions.stream()
.map(x -> x.getTrader().getCity())
.distinct()
.collect(Collectors.toList());
  • [x] 使用toSet()
1
2
3
4
//使用toSet()
Set<String> collect = transactions.stream()
.map(x -> x.getTrader().getCity())
.collect(Collectors.toSet());
  • 连接字符串效率不高,可以考虑使用Collectors.joining()
1
2
3
4
5
String collect = transactions.stream()
.map((Transaction t) -> t.getTrader().getName())
.sorted()
.distinct()
.reduce((str1, str2) -> str1 + str2).get();
  • [x] 使用Collectors.joining()
1
2
3
4
String collect = transactions.stream()
.map((Transaction t) -> t.getTrader().getName())
.sorted()
.distinct().collect(Collectors.joining());
  • 流支持 min()max() 方法,他们可以接受一个Comparator作为参数,制定计算最大或最小值要比较哪个键值
1
2
3
Transaction transaction = transactions.stream()
.reduce((x, y) -> x.getValue() > y.getValue() ? x : y)
.get();
  • [x] 使用 min()方法
1
2
3
Transaction transaction = transactions.stream()
.min(Comparator.comparing(Transaction::getValue))
.get();

数值流

  • 在前面我们看到了可以使用 reduce() 方法计算流中元素的总和
1
2
3
int reduce = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
  • 这段代码的问题是,它有一个暗含的装箱成本, 每个Integer都必须拆箱承一个原始类型再进行求和
  • 为此,Stream API 提供了原始类型流特化,专门处理数值流的办法

原始类型流特化

  • Java8 引入了三个原始类型特化流接口
    • IntStream
    • DoubleStream
    • LongStream
  • 分别将流中的元素特化为 int 、long 和 double,从而避免暗含的装箱成本

映射到数值流

  • 将流转换为特化版本的常用方法 mapToInt()mapToDouble()mapToLong()
  • 这些方法和 map 方法的工作方式一样,只是他们返回的是一个特化流,而不是 `Stream``
1
2
3
int sum = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
  • 如果流是空的,sum()方法 默认返回0
  • IntStream 还支持其他的方法方法,如 max()min()average()

转换为对象流

  • 要将原始流转换为一般流,可以使用 boxed() 方法
1
2
3
IntStream intStream = menu.stream()
.mapToInt(Dish::getCalories);
Stream<Integer> boxed = intStream.boxed();

默认值 OptionalInt

  • 求和的例子很简单,因为它有一个默认值: 0
  • 如果你要计算 IntStream 中的最大元素,就得换个方法了,因为0是错误的结果
  • 如何区分没有元素的流和元素最大的流呢?
  • 对于三种原始流特化,也分别有一个 Optional 原始类型特化版本:
    • OptionalInt
    • OptionDouble
    • OptionalLong
1
2
3
4
OptionalInt opt = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = opt.orElse(520); // 如果没有默认值的话,显式的提供一个默认最大值
  • 有的时候无法区分是没有元素的流,还是最大值真的是0的流,可以显式处理 OptionalInt 去定义一个默认最大值

如果没有最大值的话,可以显式的处理 OptionalInt 去定义一个默认值

1
int max = opt.orElse(520); // 如果没有默认值的话,显式的提供一个默认最大值

数值范围

  • java8 引入了两个一个用于 IntStream 和 LongStream 的静态方法,帮助生成这种范围
    • range()
    • rangeClosed()
  • 这两个方法都是第一个参数接受起始值,第二个参数接受结束值。
  • range() 是不包括结束值的 [x,y)
  • rangeClosed() 包含结束值 [x,y]
1
2
3
4
5
6
7
8
9
10
11
// 调用range(...)方法
IntStream range = IntStream.range(1, 5);
range.forEach(System.out::print);
outPut: 1 2 3 4
// 调用rangeClosed(...)方法
IntStream range2 = IntStream.rangeClosed(1, 5);
range2.forEach(System.out::print);
outPut: 1 2 3 4 5

数值流应用 勾股数

构建流

由值创建流

  • 可以使用静态方法 Stream.of(…) 通过显式值创建一个流
  • 可以使用 empty()创建一个空流
1
2
3
4
5
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
integerStream.forEach(System.out::println);
// 创建一个空流
Stream<Object> emptyStream = Stream.empty();

由数组创建流

  • 你可以使用静态方法 Arrays.stream(…) 从数组创建一个流
1
2
int[] nums = {1, 2, 3, 4, 5};
int sum = Arrays.stream(nums).sum();

由文件创建流

  • java中用与处理文件等I/O操作的NIO API(非阻塞I/O)已更新,以便利用 Stream API
  • java.nio.file.Files 中的很多静态方法都会返回一个流。
  • 一个很有用的方法 Files.readLines()
    • 它会返回一个由指定文件中的各行构成的字符串流。
1
2
3
Files.lines(Paths.get("C:\\Users\\Administrator\\Desktop\\dd.txt"), Charset.defaultCharset())
.flatMap(line -> Arrays.stream(line.split(" ")))
.forEach(System.out::println);
  • [x] 上面的方法是每一行生成一个单词流,我们修改一下产生一个扁平化的单词流
1
2
3
4
Files.lines(Paths.get("C:\\Users\\Administrator\\Desktop\\dd.txt"), Charset.defaultCharset())
.map(line -> line.split(" "))
.flatMap(lines -> Arrays.stream(lines))
.forEach(System.out::println);

由函数创建流 创建无限流

  • Stream API 提供了两个静态方法从函数创建流:
    • Stream.iterate(…)
    • Stream.generate(…)
  • 这两个操作可以创建所谓的无限流:不想固定集合创建的流那样有固定大小的流
  • iterategenerate 创建的流会用给定的函数按需创建主,因此可以无线创建下去

Stream.iterate(…)
第一个参数是初始值,第二个参数是一个Lambda表达式(UnaryOperator<T>类型【UnaryOperator ♥ 一元运算符】)

1
2
3
4
5
6
7
8
9
Stream.iterate(0, new UnaryOperator<Integer>() {
@Override
public Integer apply(Integer integer) {
return integer + 2;
}
}).forEach(System.out::println);
// 使用Lambda表达式
Stream.iterate(0, n -> n + 2).forEach(System.out::println);

这样得到的是一个无限流,我们使用 limit(…)方法显式的限制流的大小。

1
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

Stream.generate(…)

  • 与iterate方法类似,generate方法也可以让你按需生成一个无限流。但generate不是一次对每个新生成的值应用函数的。
  • 它接受一个供应源Supplier<T>类型的Lambda提供的值。
  • [x] 示例代码
1
2
3
Stream.generate(Math::random)
.limit(10)
.forEach(System.out::println);
  • [x] 使用 IntStream 说明避免装箱操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
IntStream.generate(new IntSupplier() {
@Override
public int getAsInt() {
return 2;
}
}).limit(20)
.forEach(System.out::print);
// 使用Lambda表达式
IntStream.generate(() -> 2)
.limit(20)
.forEach(System.out::print);
IntStream.generate(() -> 2)
.limit(20)
.forEach(System.out::print);
  • generate() 方法将是用给定的供应源,并反复的调用 getAsInt() 而这个方法总是返回2
  • [x] 比较 generate()iterate()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用iterate()方法
Stream.iterate(0, x -> x + 2).limit(5).forEach(System.out::println);
// 使用 generate(...)方法
IntSupplier intSupplier = new IntSupplier() {
int previous = 0;
@Override
public int getAsInt() {
int oldPrevious = this.previous;
this.previous = this.previous + 2;
;
return oldPrevious;
}
};
Stream.generate(intSupplier).limit(5).forEach(System.out::println);
outPut: 0 2 4 6 8
  • 前面的代码创建了一个 IntSupplier 的实例
  • 此对象有可变的状态:他在两个实例变量中记录了前一个数,getAsInt() 在调用时会改变对象的状态,由此在每次调用时产生新的值

注意:

  • 你应该始终采用不变的方法,以便并行处理流,并保持结果正确。
  • 因为你处理的是一个无限流,所以必须使用 limit 操作来显式的限制流的大小,否则,终端操作(这里是forEach)将永远计算下去
  • 你不能对无限流做排序或归约,因为所有的元素都要处理,而这永远也完不成

小结

  • Stream API 可以表达复杂的数据处理查询
  • 你可以使用 filterdistinctskiplimit 对流做筛选和切片
  • 你可以使用 mapflatMap 提取或转换流中的元素
  • 你可以使用 findFirstfindAny 方法查找流中的元素
  • 你可以用 allMatchnoneMatchanyMatch 方法让流匹配给定的谓词
  • 这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流
  • 你可以利用 reduce 方法将流中的所有元素迭代合并成一个结果,例如求和或查找最大元素
  • filtermap 等操作是无状态的,他们并不存储任何状态。resuce 等操作要存储状态才能计算出一个值。 sorteddistinct 等操作也要存储状态 ,因为他们要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作
  • 流有三种最基本的原始类型特化: IntStreamDoubleStreamLongStream ,他们的操作也有相应的特化
  • 流不仅可以从集合创建,也可从数值、数组、文件以及 iterategenerate 等特定方法创建
  • 无限流是没有固定大小的流

第6章 用流收集数据

收集器简介

预定义收集器

  • 主要探讨预定义处理器的功能,也就是Collectors类提供的工厂方法(例如:groupingBy()方法)创建的收集器
  • 主要提供了三大功能:
    • 将流元素归约和汇总为一个值
    • 元素分组
    • 元素分区

归约和汇总

在需要将流项目重组成集合时,一般会使用收集器(Stream方法 collect()的参数)。在宽泛一点说,但凡要把流中所有的项目合并成一个结果时就可以用,这个结果可以使任何类型

  • [x] 示例代码
1
2
3
4
Long collect = menu.stream().collect(Collectors.counting());
// 还可以写的更为直接
Long collect = menu.stream().count();
  • counting收集器在和其他收集器联合使用的时候特别有用,后续或细说。

查找流汇总的最大值和最小值

  • 要查找流中的最大值和最小值,可以使用这两个收集器
    • Collectors.maxBy(Comparator<? super T> comparator)
    • Collectors.minBy(Comparator<? super T> comparator)
  • 这两个收集器接收Comparator参数来比较流中的元素

  • [x] 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Comparator<Dish> dishComparator = Comparator.comparingInt((Dish d) -> d.getCalories());
// 使用方法引用创建比较器
Comparator<Dish> dishComparator1 = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> collect = menu.stream().collect(Collectors.maxBy(dishComparator));
Optional<Dish> collect1 = menu.stream().collect(Collectors.maxBy(new Comparator<Dish>() {
@Override
public int compare(Dish d1, Dish d2) {
return d1.getCalories() - d2.getCalories();
}
}));
Optional<Dish> collect2 = menu.stream().collect(Collectors.maxBy((d1, d2) -> d1.getCalories() - d2.getCalories()));
  • 另一个常见的返回单个值的归约操作是对六中的一个数值字段求和,或者你可能想要求平均数,这种操作被称为汇总操作
  • 使用收集器来表达汇总操作

汇总

  • Collectors 类专门提供了一个工厂方法:Collectors.summingInt(),它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;将该收集器传递给普通的colllect方法后及执行我们需要的汇总操作
  • [x] 代码示例
1
2
3
4
5
6
7
8
9
10
int totalCalories = menu.stream().collect(Collectors.summingInt(new ToIntFunction<Dish>() {
@Override
public int applyAsInt(Dish value) {
return value.getCalories();
}
}));
int totalCalories1 = menu.stream().collect(Collectors.summingInt(value -> value.getCalories()));
int totalCalories2 = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
  • Collectors.summingLong()Collectors.summingDouble()方法的作用完全一样,可以用于求和字段为long或double的情况
  • 但是汇总不仅仅是求和;还有Collectors.averagingInt(),连同对应的Collectors.averagingLong()Collectors.averagingDouble()可以计算数值的平均数

很度时候,你可能想要得到两个或更多这样的结果,而且你只需一次操作就可以完成,在这种情况下,你可以使用 summarizingInt 工厂方法返回的收集器。

  • 通过一次 summarizing 操作你就可以查出元素的个数,并得到总和、平均值、最大值和最小值
  • 代码示例:
1
2
3
4
5
6
7
8
9
10
IntSummaryStatistics collect1 = menu.stream().collect(Collectors.summarizingInt(new ToIntFunction<Dish>() {
@Override
public int applyAsInt(Dish value) {
return value.getCalories();
}
}));
// 使用Lambda表达式
IntSummaryStatistics collect2 = menu.stream().collect(Collectors.summarizingInt(value -> value.getCalories()));
// 使用方法引用
IntSummaryStatistics collect3 = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
  • 同样,相应的 summarizingDoublesummarizingLong 有相关的DoubleSummaryStatisticsLongSummaryStatistics

连接字符串

  • joining工厂方法返回的收集器会把对流中的每个对象用toString()方法得到的所有字符串连接成一个字符串。

注意:
joining方法在内部使用了StringBuilder 来把生成的字符串逐个追加起来。
如果Dish类中有一个toString方法来返回名称的字符串,那你就不需要 map(Dish::getName) 函数来对流做映射就能得到相同的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String collect = menu.stream().map(new Function<Dish, String>() {
@Override
public String apply(Dish dish) {
return dish.getName();
}
}).collect(Collectors.joining());
String collect = menu.stream().map(dish -> dish.getName()).collect(Collectors.joining());
String collect = menu.stream().map(Dish::getName).collect(Collectors.joining());
System.out.println(collect);
outPut: porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
  • 但该字符串的可读性不好,不过joinging工厂有一个重载版本可以接受元素之间的分界符
1
2
3
4
String collect = menu.stream().map(Dish::getName).collect(Collectors.joining(" ^-^ "));
System.out.println(collect);
outPut: pork ^-^ beef ^-^ chicken ^-^ french fries ^-^ rice ^-^ season

guaj广义的归约汇总

  • 之前介绍的所有收集器,都是一个可以用Collectors.reducing(...)工厂方法定义个归约过程的特殊情况而已。
  • Collections.resucing(...)工厂方法是所有这些特殊情况的一般化
1
2
3
4
5
Collector<String, ?, String> reducing(String, BinaryOperator<String>)
Collector<String, ?, Optional<String>> reducing(BinaryOperator<String>)
Collector<String, ?, Object> reducing(Object, Function<? super String, ?>, BinaryOperator<Object>)
  • 看最后一个方法,它需要三个参数
  • 第一个参数是归约操作的起始值,也是六中没有元素时的返回值,所有很显然对于数值和而言0是一个何时的值
  • 第二个参数是返回int的函数,将对象转换为表示某个值的int
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值
  • [x] 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int collect = menu.stream().collect(Collectors.reducing(0, new Function<Dish, Integer>() {
@Override
public Integer apply(Dish dish) {
return dish.getCalories();
}
}, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
}
));
int collect = menu.stream().collect(Collectors.reducing(0, dish -> dish.getCalories(), (integer, integer2) -> integer + integer2));
int collect = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (integer, integer2) -> integer + integer2));
int collect = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (x, y) -> x + y));

收集和归约

  • Stream接口的collecthereduce方法有和不同,这两种方法通常会获得相同的结果
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
Stream<Integer> stream = Arrays.asList(1, 2, 34, 5, 67).stream();
List<Integer> numbers = stream.reduce(new ArrayList<Integer>(), new BiFunction<ArrayList<Integer>, Integer, ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> integers, Integer integer) {
integers.add(integer);
return integers;
}
}, new BinaryOperator<ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> integers, ArrayList<Integer> integers2) {
integers.addAll(integers2);
return integers;
}
});
// 使用Lambda 表达式
Stream<Integer> stream = Arrays.asList(1, 2, 34, 5, 67).stream();
List<Integer> numbers = stream.reduce(new ArrayList<Integer>(), (integers, integer) -> {
integers.add(integer);
return integers;
}, (integers, integers2) -> {
integers.addAll(integers2);
return integers;
});
// 使用collector(Collectors.toList())
List<Integer> numbers1 = streams.collect(Collectors.toList());
outPut: [1, 2, 34, 5, 67]
  • 一个语义问题,一个实际问题。
  • 语义问题在于:
    • reduce方法旨在将两个值结合起来生成一个新值,他是一个不可变的归约。
    • collect方法的设计就是要改变容器,从而累积要输出的结果。
  • 这意味着上面得到代码片段是滥用reduce方法,因为它在原地改变了作为累加器的List。以错误的语义使用reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同意个数据结构可能会破会List本身。在这种情况下,如果你想要线程安全,就需要多次分配一个新的List,而对象分配又会影响性能。
  • 这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作。

收集框架的灵活性:以不同的方法执行同样的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int number = menu.stream().collect(Collectors.reducing(0, new Function<Dish, Integer>() {
@Override
public Integer apply(Dish dish) {
return dish.getCalories();
}
}, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
}
));
// 简化代码
int number = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));

根据情况选择最佳解决方法

1
2
3
4
5
int number = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));
int integer = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
int sum = menu.stream().mapToInt(Dish::getCalories).sum();
  • 函数式编程(特别是java8的Collections框架中加入的基于函数式风格原理设计的新API)通常提供了多种方法来执行统一操作。这个例子说明,收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于他们能提供更高水平的抽象和概括,也更容易重用和自定义
  • 始终选择最专门化的一个。
    • 因为无论从性能上看,还是可读性上看,这都是一个最好的决定。

分组

  • Collectiors.groupingBy(...)工厂方法返回的收集器可以轻松完成分组
    • public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
1
2
3
4
5
6
7
8
9
10
11
12
Map<Dish.Type, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(new Function<Dish, Dish.Type>() {
@Override
public Dish.Type apply(Dish dish) {
return dish.getType();
}
}));
Map<Dish.Type, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType));
System.out.println(collect.toString());
outPut: MEAT['pork', 'beef', 'chicken']FISH['prawns', 'salmon']OTHER['french fries', 'rice', 'pizza']
  • 分组函数不一定想方法引用那样可用,,因为你想用以分类的条件可能比简单的属性访问器要复杂。
  • 由于作者没有吧这个操作写成一个方法,你无法使用方法引用,但你可以吧这个逻辑写成Lambda表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Map<CaloricLevel, List<Dish>> collect = menu.stream().collect(Collectors.groupingBy(dish -> {
if (dish.getCalories() < 400) {
return CaloricLevel.DIEF;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}));
System.out.println(collect.toString());
}
enum CaloricLevel {
DIET, NORMAL, FAT
}

多级分组

  • 我们可以使用由多参版本的Collections.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。
  • 所以要进行二级分组的话,我们可以将一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy((Dish dish) -> {
if (dish.getCalories() < 400) {
return CaloricLevel.DIEF;
} else if (dish.getCalories() > 700 && dish.getCalories() < 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}
)));
``
#### 按子组收集数据
+ 我们可以将第二个groupingBy()收集器传递给外层收集器来实现多级分组。
+ 但是进一步说,传递给第一个groupingBy()的第二个收集器可以使任何类型,而不一定是一个groupingBy。
- [x] 代码示例:
```java
Map<Dish.Type, Long> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
outPut: {MEAT=3, FISH=2, OTHER=4}

要注意:
普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy( f , toList( ) )的简便写法

1
2
3
4
5
6
7
8
9
10
Map<Dish.Type, Optional<Dish>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories() - o2.getCalories();
}
})));
Map<Dish.Type, Optional<Dish>> collect1 = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy((o1, o2) -> o1.getCalories() - o2.getCalories())));
Map<Dish.Type, Optional<Dish>> collect2 = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparing(Dish::getCalories))));
  • 这个分组中的结果显然是一个map,以Dish的类型为键,以包装了该类型中热量最好的Dish的Option<Dish>作为值
将收集器中的结果转换为另一种类型
  • 因为分组操作中的Map结果中的每个值上包装的Optional没什么用,可以使用Collectors.collectingAndThen(...)工厂方法返回的收集器
  • 这个工厂方法接受两个参数
    • 要转换的收集器
    • 转换函数
  • 返回另一个收集器
  • 这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射
1
2
3
4
5
6
7
8
9
Map<Dish.Type, Dish> collect2 = menu.stream()
.collect(
Collectors.groupingBy(Dish::getType, Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparingInt(Dish::getCalories)), Optional::get)));
System.out.println(collect2);
outPut: {MEAT='pork', FISH='salmon', OTHER='pizza'}
与groupingBy联合使用的其它收集器的例子
  • 一般来说,通过groupingBy()工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。
  • [x] 代码示例:求出所有菜肴热量融合,对每一组Dish求和
1
2
3
4
5
6
7
8
9
10
11
12
Map<Dish.Type, Integer> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.summingInt(new ToIntFunction<Dish>() {
@Override
public int applyAsInt(Dish value) {
return value.getCalories();
}
})));
// 使用方法引用
Map<Dish.Type, Integer> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.summingInt(Dish::getCalories)));
System.out.println(collect);
outPut: {MEAT=1900, FISH=850, OTHER=1550}

Collectors.groupingBy(...)联合使用的另一个收集器是 Collectors.mapping(Function mapper,Collector downstream) 方法生成的。

  • 这个方法接受两个参数
    • 一个函数对流中元素做变换
    • 另一个将变换的结果对象收集起来。
  • 目的是在累加之前对每个元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象
  • [x] 代码示例: 想知道每种Dish,菜单中都有哪些CaloricLevel,可以将groupingBy(…)和mapping(…)收集器结合起来
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
Map<Dish.Type, Set<CaloricLevel>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping(new Function<Dish, CaloricLevel>() {
@Override
public CaloricLevel apply(Dish dish) {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}
}, Collectors.toSet())));
Map<Dish.Type, Set<CaloricLevel>> collect = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping(dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}, Collectors.toSet())));
System.out.println(collect);
outPut: MEAT=[FAT, NORMAL, DIET], FISH=[DIET, NORMAL], OTHER=[NORMAL, DIET]
  • 传递给映射方法的转换函数将Dish映射成了它的 CaloricLevel:生成的CaloricLevel流传递给一个toSet收集器,它和toList()类似,不过时将流中的元素累积到一个Set而不是List中,以便仅保留各不相同的值。

此处有个问题,对于返回的Set是什么类型并没有任何保证,但是通过Collectors.toCollection(...),你就可以更多的控制

  • [x] 代码示例: 你可以传递一个构造函数引用来要求HashSet
1
2
3
4
5
6
7
8
9
Map<Dish.Type, HashSet<CaloricLevel>> collect6 = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping(dish -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
} else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
} else {
return CaloricLevel.FAT;
}
}, Collectors.toCollection(HashSet::new))));

分区

  • 分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它被称为分区函数
  • 分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组
    • true 是一组
    • false 是一组
  • Collectors.partitioningBy(Predicate predicate) 根据…区分
  • [x] 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
Map<Boolean, List<Dish>> partitionMenu = menu.stream().collect(Collectors.partitioningBy(new Predicate<Dish>() {
@Override
public boolean test(Dish dish) {
return dish.isVegetarian();
}
}));
// 使用Lambda 表达式
Map<Boolean, List<Dish>> partitionMenu = menu.stream().collect(Collectors.partitioningBy(dish -> dish.isVegetarian()));
// 使用方法引用
Map<Boolean, List<Dish>> partitionMenu = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
outPut:{false=['pork', 'beef', 'chicken', 'prawns'], true=[ 'rice', 'season fruit', 'pizza']}

注意
用同样的区分谓词,对菜单List创建的流作筛选,然后把结果收集到另外一个List中也可以获得相同的结果

  • [x] 代码示例
1
2
3
List<Dish> partitionMenu = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
outPut: ['rice', 'season fruit', 'pizza']

分区的优势

  • 分区的好处在于保留了分去函数返回true或false的两套流元素列表。
  • 如果你要得到非素食Dish的list,你可以使用两个筛选操作来访问partitionedMenu这个map中的false键的值:一个利用谓词,一个利用谓词的非。
  • partitioningBy(Predicate predicate)工厂方法有一个重载版本,可以传递而第二个收集器
  • [x] 代码示例
1
2
3
4
5
6
7
8
Map<Boolean, Map<Dish.Type, List<Dish>>> collect = menu.stream().collect(Collectors.partitioningBy(
Dish::isVegetarian,
Collectors.groupingBy(Dish::getType)
));
System.out.println(collect);
outPut: {false={FISH=['prawns', 'salmon'], MEAT=['pork', 'beef', 'chicken']}, true={OTHER=['season fruit', 'pizza']}}
  • 再结合前面的代码举个例子
  • 找出素食和非素食中热量最高的菜
  • [x] 代码示例
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
menu.stream().collect(
Collectors.partitioningBy(
new Predicate<Dish>() {
@Override
public boolean test(Dish dish) {
return dish.isVegetarian();
}
}, Collectors.collectingAndThen(
Collectors.maxBy(
new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories() - o2.getCalories();
}
}),
new Function<Optional<Dish>, Dish>() {
@Override
public Dish apply(Optional<Dish> dish) {
return dish.get();
}
})
));
// 使用lambda表达式
menu.stream().collect(Collectors.partitioningBy(
(Dish dish) -> dish.isVegetarian(),
Collectors.collectingAndThen(
Collectors.maxBy((o1, o2) -> o1.getCalories() - o2.getCalories()),
(Optional<Dish> dish) -> dish.get())
));
// 使用方法引用
menu.stream().collect(Collectors.partitioningBy(
Dish::isVegetarian,
Collectors.collectingAndThen(
Collectors.maxBy(
Comparator.comparing(Dish::getCalories))
, Optional::get)
));
System.out.println(collect);
outPut: {false='pork', true='pizza'}
  • Collectors.partitioningBy(Predicate predicate,Collector downStream)
1
2
3
4
5
6
7
8
Map<Boolean, Long> collect1 = menu.stream()
.collect(
Collectors.partitioningBy(
Dish::isVegetarian, Collectors.counting()));
System.out.println(collect1);
outPut: {false=5, true=4}

将数字按质数和非质数分区

  • 什么是质数?

    • 只有1它本身两个约数的数,叫质数。
    • 如:2÷1=2,2÷2=1,所以2的约数只有1和它本身2这两个约数,2就是质数。)
  • 先介绍两个方法,在第5张介绍过,再次回顾一下

方法 说明
range() 生成一组数值范围,不包括结束值的 [x,y)
rangeClosed() 生成一组数值范围,包括结束值的 [x,y]
  • [x] 定义一个判断是否为质数的谓词方法
1
2
3
4
5
6
7
8
9
10
11
12
public static boolean isPrime(int number) {
return IntStream.range(2, number).noneMatch(new IntPredicate() {
@Override
public boolean test(int value) {
return number % value == 0;
}
});
// 使用Lambda表达式
return IntStream.range(2, number).noneMatch(value -> number % value == 0);
// 简化一下
return IntStream.range(2, number).noneMatch(i -> number % i == 0);
}
  • [x] 一个简单的优化是仅测试小于等于待测数平方根的因子
1
2
3
4
public static boolean isPrime(int number) {
int numberRoot = (int) Math.sqrt(number);
return IntStream.rangeClosed(2, numberRoot).noneMatch(i -> number % i == 0);
}
  • 为了把前n个数字分为质数和非质数,只要创建一个包含这n个数的流,用刚刚写的isPirme()方法作为谓词,再传给Collectors.partitioningBy()收集器归约就好了
1
2
3
4
5
6
7
8
public static boolean isPrime(int number) {
int numberRoot = (int) Math.sqrt(number);
return IntStream.rangeClosed(2, numberRoot).noneMatch(i -> number % i == 0);
}
// 测试方法
System.out.println(partitionPrimes(10));
outPut: {false=[4, 6, 8, 9, 10], true=[2, 3, 5, 7]}
Collectors 类的静态方法
工厂方法 返回类型 用于 示例
toList List<T> 把流中的所有数据元素收集到List集合中。 stream.collect(toList());
toSet Set<T> 把流中的所有数据元素收集到Set集合中,以维持Set自身的特性,即不会出现重复项。 stream.collect(toSet());
toCollection Collection<T> 把流中的数据元素收集你所指定的集合中 list.stream().collect(toCollection(ArrayList::new));
counting Long 计算流中元素的个数 list.stream().collect(counting());
summingInt Integer 对流中所有元素上指定的整数属性求和 list.stream().collect(summingInt(User::getAge));
averagingInt Double 对流中所有元素上指定的整数属性求平均数 list.stream().collect(averagingInt(User::getAge));
summarizingInt IntSummaryStatistics 收集流中所有元素上指定的整数属性的统计值,包括最大值、最小值、总数、平均值 list.stream().collect(summarizingInt(User::getAge));
joining String 连接流中元素上指定的属性 list.stream().map(s -> s).collect(joining(“-“))
maxBy Optional<T> 使用指定的比较器去比较得到流中所有元素上指定属性的最大值 list.stream().collect(maxBy(Comparator.comparing(String::length)))
minBy Optional<T> 使用指定的比较器去比较得到流中所有元素上指定属性的最小值 list.stream().collect(minBy(Comparator.comparing(String::length)))
reducing 规约操作产生的类型 从一个累加器的初始值开始,使用BinaryOperator与流中的元素逐个集合,最后将流规约为单个值 list.stream().collect(reducing(0, UserVO::getAge, Integer::sum));
collectingAndThen 转换函数返回的类型 对最终结果转换为另一种类型 list.stream().collect(collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<k, List<T>> 根据指定的属性来分组 views.stream().collect(groupingBy(String::length));
partitioningBy Map<boolean, List<T>> 根据指定的属性来分区 views.stream().collect(partitioningBy(str -> str.startsWith(“ws”))

收集器接口

  • 要开始使用Collector接口,
    • 要先了解Collector接口是如何定义的
    • 以及他的方法所返回的函数在内部是如何为collect方法所用的

Collector 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
// ...
}
  • 本列表适用以下定义:
    • T 是流中要收集的项目的泛型
    • A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象
    • R 是收集操作得到的对象(通常但并不一定是集合)的类型
  • [x] 我们可以实现一个ToListCollector<T>类,将Stream中的所有元素收集到一个List里面,它的签名如下:
1
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>

理解 Collector接口声明的方法

  • 前四个方法都会返回一个会被collect方法调用的函数,而第五个方法characteristics 则提供了一系列特征,也就是一个提示列表,告诉collect方法在执行归约操作的时候可以使用那些优化(比如并行化)

建立新的结果容器:supplier() 方法

  • supplier() 方法必须返回一个空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,共数据收集过程使用。
  • 在我们的ToListCollector中,supplier返回一个空的List
1
2
3
4
5
6
public <T> Supplier<List<T>> supplier(){
return () -> new ArrayList<T>();
// 或者使用构造函数引用
return ArrayList::new;
}

将元素添加到结果容器: accumulator() 方法

  • accumulator() 方法会返回执行归约操作的函数
  • 当遍历到流中第n个元素时,这个函数执行时会有两个参数:
    1. 保存归约结果的累加器(已收集了流中的 n-1 个项目)
    2. 第 n 个元素本身
  • 该函数返回void,,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的想过
1
2
3
4
5
6
public <T> BiConsumer<List<T>,T> accumulator(){
return (list, item) -> list.add(item);
// 使用方法引用
return List::add;
}

对结果容器应用最终转换: finisher() 方法

  • 在遍历完流后,finisher()方法必须返回在累积过程的最后要调用的一个参数,以便累加器对象转换为整个集合操作的最终结果
  • 像ToListCollector()的情况一样,累加器恰好符合预期的最终结果,因此无需转换
1
2
3
public <T> Function<List<T>, List<T>> finisher() {
return Function.identity();
}

合并两个结果容器: combiner() 方法

  • combiner() 方法会返回一个供归约操作使用的函数,它定义了对流的各个部分进行并行处理时,各个子部分古月所得的累加器要如何合并
1
2
3
4
5
6
public <T> BinaryOperator<List<T>> combiner(){
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
  • 有了 combiner() 方法就可以对流进行并行归约了

characteristics() 方法

  • characteristics() 方法会返回一个不可变的Characteristics集合,他定义了收集器的行为
    • 尤其是关于刘是否可以并行归约,以及可以使用那些优化的提示
  • characteristics 是一个包含三个项目的枚举
    • UNOROERED
      • 归约结果不受流中项目的遍历和累积顺序的影响
    • CONCURRENT
      • accmulator 函数可以从多个线程同事调用,且该收集器可以并行归约流
      • 如果收集器没有标为UNOROERED,那它仅仅在用于无需数据源时才可以并行归约
    • IDENTIFITY_FINISH
      • 这表明完成器方法返回的函数是一个恒等函数,可以跳过
      • 这种情况下,累加器对象将会直接用作归约过程的最终结果,这也意味着,将累加器A不加检查地转换为R是安全的

全部融合到一起

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class ToListCollection<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() { // ☆ 创建集合操作的起始点
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() { // ☆ 累积遍历过的项目,原位修改累加器
return new BiConsumer<List<T>, T>() {
@Override
public void accept(List<T> ts, T t) {
ts.add(t);
}
};
// 使用Labmda 表达式
return (ts, t) -> ts.add(t);
// 使用方法引用
return List::add;
}
@Override
public BinaryOperator<List<T>> combiner() { // ☆ 修改第一个累加器,将与其第二个累加器的内容合并
return new BinaryOperator<List<T>>() {
@Override
public List<T> apply(List<T> ts, List<T> ts2) {
ts.addAll(ts2);
return ts;
}
};
// 使用Lambda 表达式
return (ts, ts2) -> {
ts.addAll(ts2);
return ts;
};
}
@Override
public Function<List<T>, List<T>> finisher() { // ☆ 恒等函数
return new Function<List<T>, List<T>>() {
@Override
public List<T> apply(List<T> ts) {
return ts;
}
};
// 使用Lambda 表达式
return ts -> ts;
// 等价于
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() { // ☆ 为收集器添加 IDENTITY_FINISH 和 CONCURRENT标志
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
}
}
  • [x] 进行自定义收集而不去实现 Collector
  • 对于IDENTITY_FINISH的手机操作,还有一种方法可以得到同样的结果而无需从新实现Collector接口。
  • Stream 有一个重载的collect方法可以接受另外三个函数【supplier、accumulator和combiner】,其语义和Collector接口的相应方法返回的函数完全相同
1
2
3
4
5
ArrayList<Dish> collect = menu.stream()
.collect(ArrayList::new,
List::add,
List::addAll
);

开发你自己的收集器以获得更好的性能

仅用质数做除数

  • 之前求素数的方法还可以进行优化,看被测试数是否能够被质数整除

定义一个方法

1
2
3
4
5
6
7
8
9
10
public static <T> List<A> takeWhile(List<A> list, Predicate<A> predicate) {
int i = 0;
for (A item : list) {
if (!predicate.test(item)) {
return list.subList(0, i);
}
i++;
}
return list;
}
  • 利用这个方法,你就可以优化isPrime方法,只用不大于被测数平方根的质数去测试了
1
2
3
4
5
6
public static boolean isPrime(List<Integer> primes,int candicate){
int candidateRoot = (int) Math.sqrt((double) candicate);
return takeWhile(primes, i -> i <= candicate)
.stream()
.noneMatch(p -> candicate % p == 0);
}

第一步: 定义Collector类的签名

  • 从类签名开始,Collector接口的定义
1
public interface Collector<T, A, R>
  • 其中T,A 和 R 分别是流中元素的类型、用于累积部分结果的对象类型,以及collect操作最终结果的类型。
  • 这里应该收集 Integer 流,而累加器和结果类型则都是Map<Boolean,List<Integer>>
  • 键是true和false, 值则分别是质数和非质数的List
1
2
3
4
5
6
7
public class PrimeNumbersCollector implements Collector<
Integer, // 流中元素的类型
Map<Boolean, List<Integer>>, // 累加器类型
Map<Boolean, List<Integer>>> { // collect 操作的结果类型
{
}

第二步:实现归约过程

  • 接下来你需要实现Collector接口中的五个方法。supplier方法会返回一个在调用时候创建累加器的函数
1
2
3
4
5
6
7
8
9
10
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
// ☆ 从一个有两个孔List的Map开始收集过程
return () -> new HashMap<Boolean, List<Integer>>() {
{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}
};
}
  • 这里不但创建了用作累加器的Map,还为true和false两个键下面初始化了对应的空列表
  • 在收集过程中会把质数和非质数分别添加到这里。
  • 收集器最重要的方法是accumulator() ,因为它定义了如何收集流中元素的逻辑。
1
2
3
4
5
6
7
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
// ☆ 根据isPrime方法的返回值,从Map中去质数或非质数列表,将当前的被测数加进去
return (Map<Boolean, List<Integer>> acc,Integer candicate) ->{
acc.get(isPrime(acc.get(true), candicate)).add(candicate);
};
}

第三步: 让收集器并行工作(如果可能)

  • 下一个方法要在并行收集时把两个部分累加器合并起来,这里,它只需要合并两个Map,即将第二个Map中质数和非质数列表中的所有数字合并到第一个Map对应的列表中就行了
1
2
3
4
5
6
7
8
9
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
// ☆ 将第二个map合并到第一个map
return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
  • 实际上 这个收集器是不能并行使用的,因为该算法本身是是顺序的

第四步: finisher方法和收集器的characteristics方法

  • 这两个方法的实现都很简单,前面说过,accumulator正好就是收集器的结果,用不着进一步转换,那么finisher方法就返回identity函数
1
2
3
4
5
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
// ☆ 收集过程最后无需转换,因此用 identity函数收尾
return Function.identity();
}
1
2
3
4
5
@Override
public Set<Characteristics> characteristics() {
//☆ 这个收集器是 IDENTITY_FINISH ,但既不是 UNORDERED 也不是 CONCURRENT ,因为质数是按顺序发现的
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}

-[x] 完整代码示例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package test06;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
/**
* Description: 自定义累加器
* Created @version 1.0 2016/9/19 13:53 by Benny
*/
public class PrimeNumbersCollector implements Collector<
Integer, // 流中元素的类型
Map<Boolean, List<Integer>>, // 累加器类型
Map<Boolean, List<Integer>>> { // collect 操作的结果类型
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return () -> new HashMap<Boolean, List<Integer>>() {
{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}
};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candicate) -> {
acc.get(isPrime(acc.get(true), candicate)).add(candicate);
};
}
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
}
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
private static <T> List<Integer> takeWhile(List<Integer> list, Predicate<Integer> predicate) {
int i = 0;
for (Integer item : list) {
if (!predicate.test(item)) {
return list.subList(0, i);
}
i++;
}
return list;
}
private static boolean isPrime(List<Integer> primes, int candicate) {
int candidateRoot = (int) Math.sqrt((double) candicate);
return takeWhile(primes, i -> i <= candicate)
.stream()
.noneMatch(p -> candicate % p == 0);
}
}

小结

  • collect是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)
  • 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值,这些收集器总结在前面的Collectors 类的静态方法
  • 预定义收集器可以用groupingBy对流中元素进行分组,或用partitioningBy进行分区
  • 收集器可以高效的复合起来,进行多级分组、分区和归纳
  • 你可以实现Collector接口中定义的方法来开发你自己的收集器

第7章 并行数据处理与性能

Contents
  1. 1. java8 基础知识
  2. 2. java8 Lambda表达式
    1. 2.1. 从苹果List中找出红颜色的苹果
    2. 2.2. 此时如果增加新的需求要过滤绿色怎么办
    3. 2.3. 使用接口Predicate
    4. 2.4. 使用大招 Lamda表达式 重构之前的方法,以过滤红苹果示例:
    5. 2.5. 从方法传递到 lambda
    6. 2.6.
    7. 2.7. 集合的操作更加的人性化
    8. 2.8. 默认方法
  3. 3. 第2章 通过行为参数化传递代码
    1. 3.1. 重构你的垃圾代码
    2. 3.2. 用Runnable 执行代码块
  4. 4. 第3章 Lambda 表达式
    1. 4.1. 详解 Lambda 表达式
    2. 4.2. java中有效的表达式
    3. 4.3. 函数式接口
      1. 4.3.1. 附录2:Java8中常用的函数式接口:
    4. 4.4. 函数描述符
    5. 4.5. 把Lambda 付诸实践,环绕执行模式
    6. 4.6. 使用函数式接口
      1. 4.6.1. Predicate(谓词)
      2. 4.6.2. Consumer(消费者)
      3. 4.6.3. Function
    7. 4.7. 原始类型特化
      1. 4.7.1. java8常用函数式接口
      2. 4.7.2. 附录1:Lambda及函数式接口的例子:
    8. 4.8. 异常、Lambda,还有函数式接口是什么鬼
    9. 4.9. 类型检查 类型推断 以及限制
      1. 4.9.1. 类型检查
      2. 4.9.2. 类型推断
      3. 4.9.3. 使用局部变量
    10. 4.10. 方法引用
      1. 4.10.1. 管中窥豹
        1. 4.10.1.1. Lambda及其等效方法引用的例子
        2. 4.10.1.2. 如何构建方法引用
      2. 4.10.2. 构造函数引用
    11. 4.11. Lambda和方法引用实战
      1. 4.11.1. 第一步:传递代码
      2. 4.11.2. 第二步:使用匿名类
      3. 4.11.3. 使用Lambda表达式
      4. 4.11.4. 使用方法引用
    12. 4.12. 复合Lambda表达式的有用方法
      1. 4.12.1. 比较器复合
      2. 4.12.2. 谓词复合
      3. 4.12.3. 函数复合
    13. 4.13. 小结
  • 第二部分 函数式数据处理
    1. 1. 第4章 引入流
      1. 1.1. 流是什么
        1. 1.1.1. 小结
      2. 1.2. 流简介
        1. 1.2.1. 流的定义
      3. 1.3. 流与集合
        1. 1.3.1. 只能遍历一次
        2. 1.3.2. 外部迭代与内部迭代
      4. 1.4. 流操作
        1. 1.4.1. 中间操作
        2. 1.4.2. 终端操作
        3. 1.4.3. 使用流
          1. 1.4.3.1. 中间操作
          2. 1.4.3.2. 终端操作
      5. 1.5. 小结
    2. 2. 第5章
      1. 2.1. 筛选和切片
        1. 2.1.1. 用谓词筛选
        2. 2.1.2. 筛选各异的元素
        3. 2.1.3. 截断流
        4. 2.1.4. 跳过元素
      2. 2.2. 映射
        1. 2.2.1. 对流中每一个元素应用函数
        2. 2.2.2. 流的扁平化
      3. 2.3. 查找和匹配
        1. 2.3.1. 检查谓词是否至少匹配一个元素
        2. 2.3.2. 检查谓词是否匹配所有元素
        3. 2.3.3. 查找元素
        4. 2.3.4. 查找第一个元素
      4. 2.4. 归并
        1. 2.4.1. 元素求和
        2. 2.4.2. 最大值和最小值
          1. 2.4.2.1. 中间操作和终端操作
      5. 2.5. 付诸实践
      6. 2.6. 数值流
        1. 2.6.1. 原始类型流特化
        2. 2.6.2. 数值范围
        3. 2.6.3. 数值流应用 勾股数
      7. 2.7. 构建流
        1. 2.7.1. 由值创建流
        2. 2.7.2. 由数组创建流
        3. 2.7.3. 由文件创建流
        4. 2.7.4. 由函数创建流 创建无限流
      8. 2.8. 小结
    3. 3. 第6章 用流收集数据
      1. 3.1. 收集器简介
        1. 3.1.1. 预定义收集器
      2. 3.2. 归约和汇总
        1. 3.2.1. 查找流汇总的最大值和最小值
        2. 3.2.2. 汇总
        3. 3.2.3. 连接字符串
        4. 3.2.4. guaj广义的归约汇总
      3. 3.3. 分组
        1. 3.3.1. 多级分组
          1. 3.3.1.1. 将收集器中的结果转换为另一种类型
          2. 3.3.1.2. 与groupingBy联合使用的其它收集器的例子
      4. 3.4. 分区
        1. 3.4.1. 分区的优势
        2. 3.4.2. 将数字按质数和非质数分区
          1. 3.4.2.1. Collectors 类的静态方法
      5. 3.5. 收集器接口
        1. 3.5.1. 理解 Collector接口声明的方法
      6. 3.6. 开发你自己的收集器以获得更好的性能
        1. 3.6.1. 仅用质数做除数
      7. 3.7. 小结
    4. 4. 第7章 并行数据处理与性能