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视图。