Collection工厂
Java 9引入了一些创建小型集合对象的方法,比如Arrays.asList()
:
1
|
List<String> friends = Arrays.asList("Raphael", "Olivia", "Thibaut");
|
Arrays.asList()
返回一个固定大小的列表,可以对其进行更新,但不能添加或删除元素,否则抛出UnsupportedModificationException异常。
1
2
3
|
List<String> friends = Arrays.asList("Raphael", "Olivia");
friends.set(0, "Richard");
friends.add("Thibaut"); // throws an UnsupportedOperationException
|
没有Arrays.asSet()
方法,但是可以使用一个小技巧,比如接受列表参数的HashSet构造函数:
1
|
Set<String> friends = new HashSet<>(Arrays.asList("Raphael", "Olivia", "Thibaut"));
|
或者可以使用Streams API:
1
|
Set<String> friends = Stream.of("Raphael", "Olivia", "Thibaut").collect(Collectors.toSet());
|
然而,这两种解决方案都远远不够优雅,而且涉及不必要的对象分配。注意结果是一个可变Set。Java 9添加了工厂方法,让你可以更简单地创建小的List、Set或Map。
List工厂
你可以调用List.of
工厂方法创建一个List:
1
|
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
|
List.of
生成的列表是不可更改的,不能添加或删除元素,也不能修改元素。为了防止意外错误并使用更紧凑的内部表示,元素不许为空。
1
2
|
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
friends.add("Chih-Chun"); // throws an UnsupportedOperationException
|
Set工厂
和List.of
一样,可以使用Set.of
从一组元素创建不可变Set:
1
|
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
|
如果用重复元素来创建Set,将抛出IllegalArgumentException异常:
1
|
Set<String> friends = Set.of("Raphael", "Olivia", "Olivia"); // java.lang.IllegalArgumentException: duplicate element: Olivia
|
Map工厂
在Java 9中初始化不可变Map有2中方法。第一种是使用Map.of
,其参数在键值之间交替:
1
|
Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
|
第二种方法是使用Map.ofEntries
,其参数为一组Map.Entry<K, V>
对象。该方法需要额外的对象分配来封装键和值:
1
2
|
import static java.util.Map.entry;
Map<String, Integer> ageOfFriends = Map.ofEntries(entry("Raphael", 30), entry("Olivia", 25), entry("Thibaut", 26));
|
其中Map.entry
是创建Map.Entry
对象的工程方法。
使用List和Set
Java 8引入了一些方法到List和Set接口中:
- removeIf:删除匹配谓词的元素,在所有实现了List或Set接口的类中可用。
- replaceAll:List上可用,使用一个UnaryOperator替换元素。
- sort:List上可用,就地排序List。
removeIf
考虑下面这个buggy例子:
1
2
3
4
5
|
for (Transaction transaction : transactions) {
if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
transactions.remove(transaction);
}
}
|
遍历删除符合条件的元素是个很常见的用法,然而使用for循环很容易出错。removeIf
不仅简单而且使你远离bug:
1
|
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));
|
replaceAll
replaceAll
方法允许你用一个新的元素替换列表中的每个元素。使用Streams接口,可以如下处理:
1
2
3
|
referenceCodes.stream()
.map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1))
.collect(Collectors.toList());
|
这个代码返回一个新的List,如果你想要修改原有List,可以使用ListIterator,如下:
1
2
3
4
|
for (ListIterator<String> iterator = referenceCodes.listIterator(); iterator.hasNext(); ) {
String code = iterator.next();
iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}
|
这段代码相当冗长,而且将迭代器对象与集合对象结合使用很容易出错,因为混合了对集合的迭代和修改。在Java 8中,可以简单的使用replaceAll
:
1
|
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
|
使用Map
forEach
遍历Map通常不太方便,你需要使用Map.Entry<K, V>
的迭代器:
1
2
3
4
5
|
for (Map.Entry<String, Integer> entry : ageOfFriends.entrySet()) {
String friend = entry.getKey();
Integer age = entry.getValue();
System.out.println(friend + " is " + age + " years old");
}
|
从Java 8开始,Map接口支持forEach
方法,接受一个BiConsumer<K, V>
参数:
1
2
|
ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " +
age + " years old"));
|
Sorting
有两个新方法允许按键或值对Map进行排序:
- Entry.comparingByValue
- Entry.comparingByKey
1
2
3
4
|
Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"), entry("Olivia", "James Bond"));
favouriteMovies.entrySet().stream().sorted(Entry.comparingByKey())
.forEachOrdered(System.out::println);
|
getOrDefault
当查找的键不存在时,你将得到一个null值。Map支持getOrDefault
方法,接受键和默认值作为参数:
1
2
3
4
|
Map<String, String> favouriteMovies = Map.ofEntries(entry("Raphael", "Star Wars"),
entry("Olivia", "James Bond"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix"));
System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix"));
|
注意,如果键关联的值是null,getOrDefault仍然返回null,而且作为默认值的表达式总是被求值。
计算模式
有时候你想要有条件地执行操作并存储其结果:
- computeIfAbsent:如果指定的key在Map中没有值,计算新值并添加到Map中
- computeIfPresent:如果指定的key存在,计算新值并添加到Map中
- compute:计算指定key的值并添加到Map中
computeIfAbsent
的一个用途是缓存信息。假设你需要计算一个每一行的SHA-256,如果已经处理就不需要再次计算。假设信息使用Map缓存,如下:
1
2
|
Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
然后你可以遍历数据并缓存结果:
1
2
3
4
|
lines.forEach(line -> dataToHash.computeIfAbsent(line, this::calculateDigest));
private byte[] calculateDigest(String key) {
return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
}
|
注意,如果生成值的函数返回null,则从Map删除当前键值对。
删除模式
从Java 8开始,Map的remove
方法的一个重载版本,当key关联到指定的value才删除记录:
1
|
favouriteMovies.remove(key, value);
|
替换模式
有2个新方法可以替换Map中的记录:
- replaceAll:用
BiFunction
返回的结果替换每个记录的值
- Replace:如果key存在则替换其值。另一个重载版本替换指定值。
1
2
3
4
5
|
Map<String, String> favouriteMovies = new HashMap<>();
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "james bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies);
|
合并
putAll
方法可以将2个Map合并:
1
2
3
4
5
|
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends);
System.out.println(everyone);
|
如果需要更灵活地合并值,可以使用merge
方法。该方法使用BiFunction
合并具有重复键的值。假设Cristina同时出现在family和friends的Map中,但与之相关的电影却各不相同:
1
2
|
Map<String, String> family = Map.ofEntries(entry("Teo", "Star Wars"), entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"), entry("Cristina", "Matrix"));
|
使用merge
方法可以如下处理键冲突:
1
2
3
|
Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) -> everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));
System.out.println(everyone);
|
merge
方法有一种相当复杂的方法来处理null:
- 如果指定的键不存在或关联的值是null,merge将其与指定的非null值关联
- 否则,使用指定的重映射函数的结果替换当前值。如果重映射函数返回null,则删除当前记录
也可以用merge
实现初始化检查。假设你有一个记录电影观看次数的Map,在增加次数前需要检查电影是否在Map中:
1
2
3
4
5
6
7
8
|
Map<String, Long> moviesToCount = new HashMap<>();
String movieName = "JamesBond";
long count = moviesToCount.get(movieName);
if (count == null) {
moviesToCount.put(movieName, 1);
} else {
moviesToCount.put(moviename, count + 1);
}
|
上面的代码可以被重写为:
1
|
moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);
|
改善的ConcurrentHashMap
ConcurrentHashMap允许并发添加和更新操作,只锁定内部某些数据结构。
Reduce和Search
ConcurrentHashMap支持三种新的操作:
- forEach:对每个(key, value)执行指定的操作
- reduce:使用指定的归纳函数将所有记录合并为一个结果
- search:对每个(key, value)应用一个函数,直到该函数产生一个非空结果
每个操作支持4种形式:
- 操作键值:forEach,reduce,search
- 操作键:forEachKey,reduceKeys,searchKeys
- 操作值:forEachValue,reduceValues,searchValues
- 操作Map.Entry对象:forEachEntry,reduceEntries,searchEntries
注意,这些操作不会锁定ConcurrentHashMap的状态。提供给这些操作的函数不应该依赖于任何顺序,也不应该依赖于计算过程中可能发生变化的任何其他对象或值。
此外,你需要为这些操作指定并行阈值。如果Map大小小于给定阈值,则按顺序执行操作。阈值1代表使用公共线程池实现最大的并行度,阈值为Long.MAX_VALUE表示在单个线程上运行该操作。
1
2
3
4
|
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
long parallelismThreshold = 1;
Optional<Integer> maxValue =
Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
|
每个reduce操作的原始类型版本更高效,比如reduceValuesToInt, reduceKeysToLong等等。
计数
mappingCount
方法返回ConcurrentHashMap类的记录数,类型为long。你应该在新代码中使用mappingCount
替换返回类型为int的size
方法。
Set视图
ConcurrentHashMap类提供一个新方法keySet
返回一个Set视图,Map的修改会反映在Set视图中,反之亦然。你可以使用静态方法newKeySet
创建一个ConcurrentHashMap的Set视图。