浅谈Java 8 API增强

这里针对Java 8在API方面的增强及相关使用方式做一些简单的介绍

abstract.png

Collection

removeIf方法

众所周知,对于List、Set等集合而言,如果期望删除某些元素,其实是一件非常麻烦的事情。例如下面的示例,删除列表中长度大于2的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1() {
List<String> list = new LinkedList<>();
list.add("乒乓球");
list.add("足球");
list.add("羽毛球");
list.add("篮球");

list.forEach( e- > {
// 移除长度大于2的元素
if( e.length()>2 ) {
list.remove(e);
}
});

System.out.println("list: " + list);
}

结果显然易见,如下所示,删除失败。原因自然是不言自明。在迭代器遍历过程中,通过list.remove方法进行删除是不允许的。除非我们显式使用迭代器进行遍历、删除,显然这样非常繁琐

figure 1.jpeg

为此Java8在Collection中提供removeIf默认方法,其接收一个谓词参数。大大方便了我们对集合进行删除的操作,示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test2() {
List<String> list = new LinkedList<>();
list.add("乒乓球");
list.add("足球");
list.add("羽毛球");
list.add("篮球");

// 移除长度大于2的元素
list.removeIf( e -> e.length()>2 );
System.out.println("list: " + list);
}

测试结果如下,符合预期

figure 2.jpeg

Map

计算模式

在Map中可以根据Key存在与否的情况,按条件执行相关操作并将计算结果作为Value存储到Map中。具体有以下方法:

  • compute:使用指定的Key计算新值,并将计算结果作为Value存储到Map中
  • computeIfAbsent:如果指定Key在Map中没有对应的值(Key不存在 或 其值为null), 则计算该Key的新值并存储到Map中
  • computeIfPresent:如果指定Key在Map中有对应的值(Key存在 且 其值不为null),则计算该Key的新值并存储到Map中

compute方法

比如下面的代码,是一个统计单词次数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void test1() {
// 每个单词出现的次数, key: 单词; value: 次数
Map<String, Integer> map = new HashMap<>();

String doc = "I am Aaron I like Aaron";
String[] words = doc.split(" ");

for(String word : words) {
Integer count = map.get(word);
if( count==null ) {
count = 0;
}
count++;
map.put(word, count);
}

System.out.println("map : " + map);
}

如果采用compute方法则可以大大简化代码,如下所示。利用Key及其Value进行计算,并将计算结果作为该Key的新值存储到Map中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test2() {
// 每个单词出现的次数, key: 单词; value: 次数
Map<String, Integer> map = new HashMap<>();

String doc = "I am Aaron I like Aaron";
String[] words = doc.split(" ");

for(String word : words) {
map.compute( word, (key, oldValue) ->{
if( oldValue==null ) {
oldValue = 0;
}
oldValue++;
return oldValue;
});
}

System.out.println("<Test 2> map : " + map);
}

两个方法测试结果如下,符合预期

figure 3.jpeg

computeIfAbsent方法

下面是一个在Map中保存各人所喜欢的书单的方法。每次在向value中添加书时,都需要判断该value是否为空,非常繁琐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test1() {
// 每个人喜欢的书单, key: 人名; value: 书名列表
Map<String, List<String>> map = new HashMap<>();

// 场景: 给Amy喜欢的书单添加《老人与海》
String name = "Amy";
String bookName = "老人与海";
List list = map.get(name);
if( list==null ) {
list = new LinkedList<>();
map.put(name, list);
}

list.add(bookName);

System.out.println("<Test 1> map : " + map);
}

幸运的是,在有了computeIfAbsent方法后,我们实现起来就简洁很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test2() {
// 每个人喜欢的书单, key: 人名; value: 书名列表
Map<String, List<String>> map = new HashMap<>();

// 场景: 给Amy喜欢的书单添加《老人与海》
String name = "Amy";
String bookName = "老人与海";
// 如果map中 key不存在 或 key所对应的值为null
// 则会通过第二个参数 function 计算新value,并put到map中
// 该方法最终会返回该key所对应的value值
List<String> list = map.computeIfAbsent( name, key -> new LinkedList<>() );

list.add(bookName);

System.out.println("<Test 2> map : " + map);
}

可以看到,computeIfAbsent方法非常适用于value为集合类型、且需要向该集合中添加元素的场景。测试结果如下,符合预期

figure 4.jpeg

与此同时,computeIfAbsent方法也适用于缓存信息的场景。在下面的示例中,对于已有签名数据作为Value的Key,显然没有必要重复计算、浪费资源

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
@Test
public void test3() {

Map<String, Integer> map = new HashMap<>();

System.out.println("\n-------------------- Test 1: Start --------------------");
map.computeIfAbsent("China", this::calcSign);
System.out.println("-------------------- Test 1: End ----------------------");

System.out.println("\n-------------------- Test 2: Start --------------------");
map.computeIfAbsent("USA", this::calcSign);
System.out.println("-------------------- Test 2: End ----------------------");

System.out.println("\n-------------------- Test 3: Start --------------------");
map.computeIfAbsent("China", this::calcSign);
System.out.println("-------------------- Test 3: End ----------------------");

System.out.println("\nmap: " + map);
}

/**
* 计算签名
* @param str
* @return
*/
private Integer calcSign(String str) {
System.out.println( "Start Calc Sign, str: " + str);
Integer result = str.hashCode();
return result;
}

测试结果如下,符合预期

figure 5.jpeg

computeIfPresent方法

下面是一个在Map中保存各人所喜欢的书单的方法。每次从value中移除书时,都需要判断该value是否为空,非常繁琐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1() {
// 每个人喜欢的书单, key: 人名; value: 书名列表
Map<String, List<String>> map = new HashMap();
// map数据初始化
map.put("Amy", new ArrayList(Arrays.asList("资治通鉴", "金瓶梅", "山海经")) );

// 场景: Amy喜欢的书单中不能有《金瓶梅》
String name = "Amy";
String bookName = "金瓶梅";
List<String> list = map.get(name);
if( list!=null ) {
list.remove( bookName );
}

System.out.println("<Test 1> map : " + map);
}

结果符合预期,如下所示

figure 6.jpeg

幸运的是,在有了computeIfPresent方法后,我们实现起来就简洁很多

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
@Test
public void test2() {
// 每个人喜欢的书单, key: 人名; value: 书名列表
Map<String, List<String>> map = new HashMap<>();
// map数据初始化
map.put("Amy", new ArrayList(Arrays.asList("资治通鉴", "金瓶梅", "山海经")) );
map.put("Bob", new ArrayList(Arrays.asList("三年高考五年模拟")) );
map.put("Tony", new ArrayList(Arrays.asList("21天入门理发")) );

// 场景: Amy喜欢的书单中不能有《金瓶梅》
String name = "Amy";
String bookName1 = "金瓶梅";
// 如果map中key所对应的value不为null
// 则会通过第二个参数, 利用key、oldValue 计算newValue,并put到map中
// 该方法最终会返回该key所对应的value值
map.computeIfPresent(name, (key, value) -> {
value.remove( bookName1 );
return value;
} );

// 场景: Bob喜欢的书单中不能有《三年高考五年模拟》
name = "Bob";
String bookName2 = "三年高考五年模拟";
map.computeIfPresent(name, (key, value) -> {
value.remove( bookName2 );
return value;
} );

// 场景: 用户Tony注销了,不需要再保存其喜欢的书单
name = "Tony";
// 在computeIfPresent方法中, 如果该key计算的newValue为null, 则该映射会被移除
map.computeIfPresent(name, (key, value) -> {
List newValue = null;
return newValue;
} );

System.out.println("<Test 2> map : " + map);
}

可以看到,computeIfPresent方法非常适用于value为集合类型、且需要从集合中移除元素的场景。测试结果如下,符合预期。值得一提的是在computeIfPresent方法中,如果该key计算的新值为null, 则该映射会被移除

figure 7.jpeg

merge方法

Map接口提供的默认方法merge,签名如下。其第一、二个参数就是我们期望存储到Map的key、newValue。但如果该key在map已经存在且其值(这里记为oldValue)不为空,则就需要通过
第三个参数(BiFunction类型),其定义了合并oldValue、newValue这两个值的计算规则

1
2
merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {

通过下面的例子,可以更好的帮助大家理解

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test1() {

Map<String, String> map = new HashMap<>();
map.put("Aaron", "篮球");

map.merge("Bob", "足球", (oldValue, newValue) -> oldValue+"&"+newValue );
System.out.println("map 1: " + map);

map.merge("Aaron", "乒乓球", (oldValue, newValue) -> oldValue+"&"+newValue );
System.out.println("map 2: " + map);
}

测试结果如下,符合预期

figure 8.jpeg

可以看到,由于merge方法对于欲插入的key是否在map中存在不为null的value,实际上是提供了两种不同的计算路径。故对于上文通过compute方法实现次数统计的例子而言,我们如果使用merge方法实现会更加简单,示例代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test2() {
// 每个单词出现的次数, key: 单词; value: 次数
Map<String, Integer> map = new HashMap<>();
String doc = "can Aaron can Aaron like I can like Aaron can";
String[] words = doc.split(" ");

for(String word : words) {
// 该单词在map中value为null,说明该单词首次被统计, 故直接记为1
// 该单词在map中value不为null,说明该单词非首次被统计, 故使用原有次数自增1
map.merge(word, 1, (oldValue, newValue)->oldValue+1 );
}

System.out.println("<Test 2> map : " + map);
}

测试结果如下,符合预期

figure 9.jpeg

Optional

orElse与orElseGet

二者都是用于在option实例中value值不存在时,提供一个默认值。但orElse方法是每次均会计算默认值,无论option实例中value值是否存在;而orElseGet方法则是延迟计算,即只有在option实例中value值不存在时,才会去计算默认值。故如果对于默认值的计算、获取过程比较昂贵,推荐使用orElseGet方法

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
@Test
public void test1() {
Optional<String> optional1 = Optional.ofNullable( "Aaron" );
Optional<String> optional2 = Optional.ofNullable( null );

System.out.println("\n-------------------- Test 1: Start --------------------");
String s1 = optional1.orElse( getDefault() );
System.out.println("s1: " + s1);
System.out.println("-------------------- Test 1: End ----------------------");

System.out.println("\n-------------------- Test 2: Start --------------------");
String s2 = optional2.orElse( getDefault() );
System.out.println("s2: " + s2);
System.out.println("-------------------- Test 2: End ----------------------");

System.out.println("\n-------------------- Test 3: Start --------------------");
s1 = optional1.orElseGet( () -> getDefault() );
System.out.println("s1: " + s1);
System.out.println("-------------------- Test 3: End ----------------------");

System.out.println("\n-------------------- Test 4: Start --------------------");
s2 = optional2.orElseGet( () -> getDefault() );
System.out.println("s2: " + s2);
System.out.println("-------------------- Test 4: End ----------------------");

}

public String getDefault() {
System.out.println("call getDefault method");
return "Tony";
}

测试结果如下,符合预期

figure 10.jpeg

map与flatMap

Optional的初衷就是为了解决NPE而设计的。比如在传统的代码中,为了防止出现NPE,开发者需要层层判空。示例代码如下所示,这样显然非常繁琐

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
public class OptionalTest {

@Test
public void test2() {
Person person = getPerson();

String addr = null;
if(person!=null) {
Person.Company company = person.getCompany();
if( company!=null ) {
addr = company.getAddr();
}
}

String tel = null;
if(person!=null) {
Person.Company company = person.getCompany();
if( company!=null ) {
tel = company.getTel();
}
}

System.out.println("addr: " + addr);
System.out.println("tel: " + tel);
}

/**
* 获取Person实例
* @return
*/
public Person getPerson() {
Person.Family family = Person.Family.builder()
.mother("Amy")
.build();

Person person = Person.builder()
.name("Tony")
.company( Person.Company.builder()
.type("外贸")
.addr("广东省")
.build())
.family( Optional.ofNullable(family) )
.build();

return person;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Person {
private String name;

private Integer age;

private Company company;

private Optional<Family> family;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Company {
private String type;

private String addr;

private String tel;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Family {
private String father;

private String mother;
}
}

}

测试结果如下,符合预期

figure 11.jpeg

而自从有了Optional后,情况就大不一样了。我们可以使用map、flatMap实现层层转化。在整个链式调用过程中一旦某个Optional的value为null,则会返回一个空Optional,继续执行。以免出现NPE

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
@Test
public void test3() {
Person person = getPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);

String addr = optionalPerson.map( Person::getCompany )
.map( Person.Company::getAddr )
.orElse(null);

String tel = optionalPerson.map( Person::getCompany )
.map( Person.Company::getTel )
.orElse(null);

String mother = optionalPerson.flatMap( Person::getFamily )
.map( Person.Family::getMother )
.orElse(null);

String father = optionalPerson.flatMap( Person::getFamily )
.map( Person.Family::getFather )
.orElse(null);

System.out.println("addr: " + addr);
System.out.println("tel: " + tel);
System.out.println("mother: " + mother);
System.out.println("father: " + father);
}

测试结果如下,符合预期

figure 12.jpeg

Note

  • 对于Map接口提供的putIfAbsent默认方法而言,其与computeIfAbsent方法在功能上虽然类似。但有一些不同的地方。首先,computeIfAbsent方法对于value的计算是延迟计算,即只有在key不存在 或 该key在Map中的value为null 时,其才会计算value。而putIfAbsent无论最终是否需要设置该键值对,都会去计算value。所以value的计算如果是一个昂贵的过程,推荐使用延迟计算特性的computeIfAbsent方法;其次,二者返回值不同。computeIfAbsent方法总是会返回该key在map中相应的value值,而putIfAbsent方法,如果 key不存在 或 该key在Map中的value为null 时,会返回null。否则返回该key在map中相应的value值

参考文献

  1. Java实战·第2版 拉乌尔-加布里埃尔·乌尔玛、马里奥·富斯科、艾伦·米克罗夫特著
0%