Skip to content

Commit b65c27c

Browse files
committed
加入对JCF框架新方法的讲解
1 parent 3fff60e commit b65c27c

File tree

5 files changed

+394
-6
lines changed

5 files changed

+394
-6
lines changed

3-Labmda and Collections.md

+388
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
# Lambda and Collections
2+
3+
我们先从最熟悉的*Java集合框架(Java Collections Framework, JCF)*开始说起。
4+
5+
为引入Lambda表达式,Java8新增了`java.util.funcion`包,里面包含常用的**函数接口**,这是Lambda表达式的基础,Java集合框架也新增部分接口,以便与Lambda表达式对接。
6+
7+
首先回顾一下Java集合框架的接口继承结构:
8+
9+
![JCF_Collection_Interfaces](./Figures/JCF_Collection_Interfaces.png)
10+
11+
上图中绿色标注的接口类,表示在Java8中加入了新的接口方法,当然由于继承关系,他们相应的子类也都会继承这些新方法。下表详细列举了这些方法。
12+
13+
| 接口名 | Java8新加入的方法 |
14+
|--------|--------|
15+
| Collection |removeIf() spliterator() stream() parallelStream() forEach()|
16+
| List |replaceAll() sort()|
17+
| Map |getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()|
18+
19+
这些新加入的方法大部分要用到`java.util.function`包下的接口,这意味着这些方法大部分都跟Lambda表达式相关。我们将逐一学习这些方法。
20+
21+
## Collection中的新方法
22+
23+
如上所示,接口`Collection``List`新加入了一些方法,我们以是`List`的子类`ArrayList`为例来说明。了解[Java7`ArrayList`实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md),将有助于理解下文。
24+
25+
### forEach()
26+
27+
该方法的签名为`void forEach(Consumer<? super E> action)`,作用是对容器中的每个元素执行`action`指定的动作,其中`Consumer`是个函数接口,里面只有一个待实现方法`void accept(T t)`(后面我们会看到,这个方法叫什么根本不重要,你甚至不需要记忆它的名字)。
28+
29+
需求:*假设有一个字符串列表,需要打印出其中所有长度大于3的字符串.*
30+
31+
Java7及以前我们可以用增强的for循环实现:
32+
33+
```Java
34+
// 使用曾强for循环迭代
35+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
36+
for(String str : list){
37+
if(str.length()>3)
38+
System.out.println(str);
39+
}
40+
```
41+
42+
现在使用`forEach()`方法结合匿名内部类,可以这样实现:
43+
44+
```Java
45+
// 使用forEach()结合匿名内部类迭代
46+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
47+
list.forEach(new Consumer<String>(){
48+
@Override
49+
public void accept(String str){
50+
if(str.length()>3)
51+
System.out.println(str);
52+
}
53+
});
54+
```
55+
上述代码调用`forEach()`方法,并使用匿名内部类实现`Comsumer`接口。到目前为止我们没看到这种设计有什么好处,但是不要忘记Lambda表达式,使用Lambda表达式实现如下:
56+
```Java
57+
// 使用forEach()结合Lambda表达式迭代
58+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
59+
list.forEach( str -> {
60+
if(str.length()>3)
61+
System.out.println(str);
62+
});
63+
```
64+
上述代码给`forEach()`方法传入一个Lambda表达式,我们不需要知道`accept()`方法,也不需要知道`Consumer`接口,类型推导帮我们做了一切。
65+
66+
### removeIf()
67+
68+
该方法签名为`boolean removeIf(Predicate<? super E> filter)`,作用是**删除容器中所有满足`filter`指定条件的元素**,其中`Predicate`是一个函数接口,里面只有一个待实现方法`boolean test(T t)`,同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。
69+
70+
需求:*假设有一个字符串列表,需要删除其中所有长度大于3的字符串。*
71+
我们知道如果需要在迭代过程冲对容器进行删除操作,必须使用迭代器,否则会抛出`ConcurrentModificationException`,所以上述任务传统的写法是:
72+
73+
```Java
74+
// 使用迭代器删除列表元素
75+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
76+
Iterator<String> it = list.iterator();
77+
while(it.hasNext()){
78+
if(it.next().length()>3) // 删除长度大于3的元素
79+
it.remove();
80+
}
81+
```
82+
83+
现在使用`removeIf()`方法结合匿名内部类,我们可是这样实现:
84+
```Java
85+
// 使用removeIf()结合匿名名内部类实现
86+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
87+
list.removeIf(new Predicate<String>(){ // 删除长度大于3的元素
88+
@Override
89+
public boolean test(String str){
90+
return str.length()>3;
91+
}
92+
});
93+
```
94+
上述代码使用`removeIf()`方法,并使用匿名内部类实现`Precicate`接口。相信你已经想到用Lambda表达式该怎么写了:
95+
96+
```Java
97+
// 使用removeIf()结合Lambda表达式实现
98+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
99+
list.removeIf(str -> str.length()>3); // 删除长度大于3的元素
100+
```
101+
使用Lambda表达式不需要记忆`Predicate`接口名,也不需要记忆`test()`方法名,只需要知道此处需要一个返回布尔类型的Lambda表达式就行了。
102+
103+
### replaceAll()
104+
105+
该方法签名为`void replaceAll(UnaryOperator<E> operator)`,作用是**对每个元素执行`operator`指定的操作,并用操作结果来替换原来的元素**。其中`UnaryOperator`是一个函数接口,里面只有一个待实现函数`T apply(T t)`
106+
107+
需求:*假设有一个字符串列表,将其中所有长度大于3的元素转换成大写,其余元素不变。*
108+
109+
使用传统方式似乎没有优雅的办法:
110+
111+
```Java
112+
// 使用下标实现元素替换
113+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
114+
for(int i=0; i<list.size(); i++){
115+
String str = list.get(i);
116+
if(str.length()>3)
117+
list.set(i, str.toUpperCase());
118+
}
119+
```
120+
121+
使用`replaceAll()`方法结合匿名内部类可以实现如下:
122+
```Java
123+
// 使用匿名内部类实现
124+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
125+
list.replaceAll(new UnaryOperator<String>(){
126+
@Override
127+
public String apply(String str){
128+
if(str.length()>3)
129+
return str.toUpperCase();
130+
return str;
131+
}
132+
});
133+
```
134+
上述代码调用`replaceAll()`方法,并使用匿名内部类实现`UnaryOperator`接口。当然我们都知道可以用更为简洁的Lambda表达式实现:
135+
```Java
136+
// 使用Lambda表达式实现
137+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
138+
list.replaceAll(str -> {
139+
if(str.length()>3)
140+
return str.toUpperCase();
141+
return str;
142+
});
143+
```
144+
### sort()
145+
146+
该方法定义在`List`接口中,方法签名为`void sort(Comparator<? super E> c)`,该方法**根据`c`指定的比较规则对容器元素进行排序**`Comparator`接口我们并不陌生,其中有一个方法`int compare(T o1, T o2)`需要实现,显然该接口是个函数接口。
147+
148+
需求:*假设有一个字符串列表,按照字符串长度增序对元素排序。*
149+
150+
由于Java7以及之前`sort()`方法在`Collections`工具类中,所以代码要这样写:
151+
152+
```Java
153+
// Collections.sort()方法
154+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
155+
Collections.sort(list, new Comparator<String>(){
156+
@Override
157+
public int compare(String str1, String str2){
158+
return str1.length()-str2.length();
159+
}
160+
});
161+
```
162+
163+
现在可以直接使用`List.sort()方法`,结合Lambda表达式,可以这样写:
164+
165+
```Java
166+
// List.sort()方法结合Lambda表达式
167+
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
168+
list.sort((str1, str2) -> str1.length()-str2.length());
169+
```
170+
171+
### spliterator()
172+
173+
方法签名为`Spliterator<E> spliterator()`,该方法返回容器的**可拆分迭代器**。从名字来看该方法跟`iterator()`方法有点像,我们知道`Iterator`是用来迭代容器的,`Spliterator`也有类似作用,但二者有如下不同:
174+
175+
1. `Spliterator`既可以像`Iterator`那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。
176+
2. `Spliterator`是可拆分的,一个`Spliterator`可以通过调用`Spliterator<T> trySplit()`方法来尝试分成两个。一个是`this`,另一个是新返回的那个,并且这两个迭代器代表的元素没有重叠。
177+
178+
可通过(多次)调用`Spliterator.trySplit()`方法来分解负载,以便多线程处理。
179+
180+
### stream()和parallelStream()
181+
182+
`stream()``parallelStream()`分别**返回该容器的`Stream`视图表示**,不同之处在于`parallelStream()`返回并行的`Stream`**`Stream`是Java函数式编程的核心类**,我们会在后面章节中学习。
183+
184+
## Map中的新方法
185+
相比`Collection``Map`中加入了更多的方法,我们以`HashMap`为例来逐一探秘。了解[Java7`HashMap`实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet%20and%20HashMap.md),将有助于理解下文。
186+
187+
### forEach
188+
189+
该方法签名为`void forEach(BiConsumer<? super K,? super V> action)`,作用是**`Map`中的每个映射执行`action`指定的操作**,其中`BiConsumer`是一个函数接口,里面有一个待实现方法`void accept(T t, U u)``BinConsumer`接口名字和`accept()`方法名字都不重要,请不要记忆他们。
190+
191+
需求:*假设有一个数字到对应英文单词的Map,请输出Map中的所有映射关系.*
192+
193+
Java7以及之前经典的代码如下:
194+
195+
```Java
196+
// Java7以及之前迭代Map
197+
HashMap<Integer, String> map = new HashMap<>();
198+
map.put(1, "one");
199+
map.put(2, "two");
200+
map.put(3, "three");
201+
for(Map.Entry<Integer, String> entry : map.entrySet()){
202+
System.out.println(entry.getKey() + "=" + entry.getValue());
203+
}
204+
```
205+
206+
使用`Map.forEach()`方法,结合匿名内部类,代码如下:
207+
```Java
208+
// 使用forEach()结合匿名内部类迭代Map
209+
HashMap<Integer, String> map = new HashMap<>();
210+
map.put(1, "one");
211+
map.put(2, "two");
212+
map.put(3, "three");
213+
map.forEach(new BiConsumer<Integer, String>(){
214+
@Override
215+
public void accept(Integer k, String v){
216+
System.out.println(k + "=" + v);
217+
}
218+
});
219+
```
220+
上述代码调用`forEach()`方法,并使用匿名内部类实现`BiConsumer`接口。当然,我们知道实际场景中没有人会使用匿名内部类的写法,因为有Lambda表达式:
221+
222+
```Java
223+
// 使用forEach()结合Lambda表达式迭代Map
224+
HashMap<Integer, String> map = new HashMap<>();
225+
map.put(1, "one");
226+
map.put(2, "two");
227+
map.put(3, "three");
228+
map.forEach((k, v) -> System.out.println(k + "=" + v));
229+
}
230+
```
231+
232+
### getOrDefault()
233+
234+
该方法跟Lambda表达式没关系,但是很有用。方法签名为`V getOrDefault(Object key, V defaultValue)`,作用是**按照给定的`key`查询`Map`中对应的`value`,如果没有找到则返回`defaultValue`**。使用该方法程序员可以省去查询指定键值是否存在的麻烦.
235+
236+
需求;*假设有一个数字到对应英文单词的Map,输出4对应的英文单词,如果不存在则输出NoValue*
237+
238+
```Java
239+
// 查询Map中指定的值,不存在时使用默认值
240+
HashMap<Integer, String> map = new HashMap<>();
241+
map.put(1, "one");
242+
map.put(2, "two");
243+
map.put(3, "three");
244+
// Java7以及之前做法
245+
if(map.containsKey(4)){ // 1
246+
System.out.println(map.get(4));
247+
}else{
248+
System.out.println("NoValue");
249+
}
250+
// Java8使用Map.getOrDefault()
251+
System.out.println(map.getOrDefault(4, "NoValue")); // 2
252+
```
253+
### putIfAbsent()
254+
255+
该方法跟Lambda表达式没关系,但是很有用。方法签名为`V putIfAbsent(K key, V value)`,作用是只有在**不存在`key`值的映射或映射值为`null`**,才将`value`指定的值放入到`Map`中,否则不对`Map`做更改.该方法将条件判断和赋值合二为一,使用起来更加方便.
256+
257+
### remove()
258+
259+
我们都知道`Map`中有一个`remove(Object key)`方法,来根据指定`key`值删除`Map`中的映射关系;Java8新增了`remove(Object key, Object value)`方法,只有在当前`Map`**`key`正好映射到`value`**才删除该映射,否则什么也不做.
260+
261+
### replace()
262+
263+
在Java7及以前,要想替换`Map`中的映射关系可通过`put(K key, V value)`方法实现,该方法总是会用新值替换原来的值.为了更精确的控制替换行为,Java8在`Map`中加入了两个`replace()`方法,分别如下:
264+
265+
* `replace(K key, V value)`,只有在当前`Map`**`key`的映射存在时**才用`value`去替换原来的值,否则什么也不做.
266+
* `replace(K key, V oldValue, V newValue)`,只有在当前`Map`**`key`的映射存在且等于`oldValue`**才用`newValue`去替换原来的值,否则什么也不做.
267+
268+
### replaceAll()
269+
270+
该方法签名为`replaceAll(BiFunction<? super K,? super V,? extends V> function)`,作用是对`Map`中的每个映射执行`function`指定的操作,并用`function`的执行结果替换原来的`value`,其中`BiFunction`是一个函数接口,里面有一个待实现方法`R apply(T t, U u)`.不要被如此多的函数接口吓到,因为使用的时候根本不需要知道他们的名字.
271+
272+
需求:*假设有一个数字到对应英文单词的Map,请将原来映射关系中的单词都转换成大写.*
273+
274+
Java7以及之前经典的代码如下:
275+
276+
```Java
277+
// Java7以及之前替换所有Map中所有映射关系
278+
HashMap<Integer, String> map = new HashMap<>();
279+
map.put(1, "one");
280+
map.put(2, "two");
281+
map.put(3, "three");
282+
for(Map.Entry<Integer, String> entry : map.entrySet()){
283+
entry.setValue(entry.getValue().toUpperCase());
284+
}
285+
```
286+
287+
使用`replaceAll()`方法结合匿名内部类,实现如下:
288+
289+
```Java
290+
// 使用`replaceAll()`结合匿名内部类实现
291+
HashMap<Integer, String> map = new HashMap<>();
292+
map.put(1, "one");
293+
map.put(2, "two");
294+
map.put(3, "three");
295+
map.replaceAll(new BiFunction<Integer, String, String>(){
296+
@Override
297+
public String apply(Integer k, String v){
298+
return v.toUpperCase();
299+
}
300+
});
301+
```
302+
上述代码调用`replaceAll()`方法,并使用匿名内部类实现`BiFunction`接口。更进一步的,使用Lambda表达式实现如下:
303+
304+
```Java
305+
// 使用`replaceAll()`结合Lambda表达式实现
306+
HashMap<Integer, String> map = new HashMap<>();
307+
map.put(1, "one");
308+
map.put(2, "two");
309+
map.put(3, "three");
310+
map.replaceAll((k, v) -> v.toUpperCase());
311+
```
312+
313+
简洁到让人难以置信.
314+
315+
### merge()
316+
317+
该方法签名为`merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)`,作用是:
318+
319+
1. 如果`Map``key`对应的映射不存在或者为`null`,则将`value`(不能是`null`)关联到`key`上;
320+
2. 否则执行`remappingFunction`,如果执行结果非`null`则用该结果跟`key`关联,否则在`Map`中删除`key`的映射.
321+
322+
参数中`BiFunction`函数接口前面已经介绍过,里面有一个待实现方法`R apply(T t, U u)`
323+
324+
`merge()`方法虽然语义有些复杂,但该方法的用方式很明确,一个比较常见的场景是将新的错误信息拼接到原来的信息上,比如:
325+
326+
```Java
327+
map.merge(key, newMsg, (v1, v2) -> v1+v2);
328+
```
329+
330+
### compute()
331+
332+
该方法签名为`compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)`,作用是把`remappingFunction`的计算结果关联到`key`上,如果计算结果为`null`,则在`Map`中删除`key`的映射.
333+
334+
要实现上述`merge()`方法中错误信息拼接的例子,使用`compute()`代码如下:
335+
336+
```Java
337+
map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));
338+
```
339+
340+
### computeIfAbsent()
341+
342+
该方法签名为`V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)`,作用是:只有在当前`Map`**不存在`key`值的映射或映射值为`null`**,才调用`mappingFunction`,并在`mappingFunction`执行结果非`null`时,将结果跟`key`关联.
343+
344+
`Function`是一个函数接口,里面有一个待实现方法`R apply(T t)`
345+
346+
`computeIfAbsent()`常用来对`Map`的某个`key`值建立初始化映射.比如我们要实现一个多值映射,`Map`的定义可能是`Map<K,Set<V>>`,要向`Map`中放入新值,可通过如下代码实现:
347+
348+
```Java
349+
Map<Integer, Set<String>> map = new HashMap<>();
350+
// Java7及以前的实现方式
351+
if(map.containsKey(1)){
352+
map.get(1).add("one");
353+
}else{
354+
Set<String> valueSet = new HashSet<String>();
355+
valueSet.add("one");
356+
map.put(1, valueSet);
357+
}
358+
// Java8的实现方式
359+
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
360+
```
361+
362+
使用`computeIfAbsent()`将条件判断和添加操作合二为一,使代码更加简洁.
363+
364+
### computeIfPresent()
365+
366+
该方法签名为`V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)`,作用跟`computeIfAbsent()`相反,即,只有在当前`Map`**存在`key`值的映射且非`null`**,才调用`remappingFunction`,如果`remappingFunction`执行结果为`null`,则删除`key`的映射,否则使用该结果替换`key`原来的映射.
367+
368+
这个函数的功能跟如下代码是等效的:
369+
370+
```Java
371+
// Java7及以前跟computeIfPresent()等效的代码
372+
if (map.get(key) != null) {
373+
V oldValue = map.get(key);
374+
V newValue = remappingFunction.apply(key, oldValue);
375+
if (newValue != null)
376+
map.put(key, newValue);
377+
else
378+
map.remove(key);
379+
return newValue;
380+
}
381+
return null;
382+
```
383+
384+
385+
## 总结
386+
387+
1. Java8为容器新增一些有用的方法,这些方法有些是为**完善原有功能**,有些是为**引入函数式编程**,学习和使用这些方法有助于我们写出更加简洁有效的代码.
388+
2. **函数接口**虽然很多,但绝大多数时候我们根本不需要知道它们的名字,书写Lambda表达式时类型推断帮我们做了一切.

Figures/JCF_Collection_Interfaces.png

68.6 KB
Loading

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ Name: 李豪
44
55
URL: https://github.com/CarpenterLee/JavaLambdaInternals
66

7-
欢迎转载,注明出处就行,谢谢~~
7+
欢迎转载,转载请注明出处,谢谢~~

0 commit comments

Comments
 (0)