示例 1(computeIfPresent()
)
假设我们有以下Map
:
Map<String, String> map = new HashMap<>(); map.put("postgresql", "127.0.0.1"); map.put("mysql", "192.168.0.50");
我们使用这个映射为不同的数据库类型构建 JDBC URL。
假设我们要为 MySQL 构建 JDBC URL。如果映射中存在mysql键,则应根据相应的值jdbc:mysql://192.168.0.50/customers_db计算 JDBC URL。但是如果不存在mysql键,那么 JDBC URL 应该是null。除此之外,如果我们的计算结果是null(无法计算 JDBC URL),那么我们希望从映射中删除这个条目。
这是V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)的工作。
在我们的例子中,用于计算新值的BiFunction如下所示(k是映射中的键,v是与键关联的值):
BiFunction<String, String, String> jdbcUrl = (k, v) -> "jdbc:" + k + "://" + v + "/customers_db";
一旦我们有了这个函数,我们就可以计算出mysql
键的新值,如下所示:
// jdbc:mysql://192.168.0.50/customers_db String mySqlJdbcUrl = map.computeIfPresent("mysql", jdbcUrl);
由于映射中存在mysql
键,结果将是jdbc:mysql://192.168.0.50/customers_db
,新映射包含以下条目:
postgresql=127.0.0.1, mysql=jdbc:mysql://192.168.0.50/customers_db
再次调用computeIfPresent()将重新计算值,这意味着它将导致类似mysql= jdbc:mysql://jdbc:mysql://....的结果。显然,这是不可以的,所以请注意这方面。
另一方面,如果我们对一个不存在的条目进行相同的计算(例如,voltdb),那么返回的值将是null,映射保持不变:
// null String voldDbJdbcUrl = map.computeIfPresent("voltdb", jdbcUrl);
示例 2(computeIfAbsent()
)
假设我们有以下Map
:
Map<String, String> map = new HashMap<>(); map.put("postgresql", "jdbc:postgresql://127.0.0.1/customers_db"); map.put("mysql", "jdbc:mysql://192.168.0.50/customers_db");
我们使用这个映射为不同的数据库构建 JDBC URL。
假设我们要为 MongoDB 构建 JDBC URL。这一次,如果映射中存在mongodb键,则应返回相应的值,而无需进一步计算。但是如果这个键不存在(或者与一个null值相关联),那么它应该基于这个键和当前 IP 进行计算并添加到映射中。如果计算值为null,则返回结果为null,映射保持不变。
嗯,这是V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)的工作。
在我们的例子中,用于计算值的Function将如下所示(第一个String是映射中的键(k),而第二个String是为该键计算的值):
String address = InetAddress.getLocalHost().getHostAddress(); Function<String, String> jdbcUrl = k -> k + "://" + address + "/customers_db";
基于此函数,我们可以尝试通过mongodb键获取 MongoDB 的 JDBC URL,如下所示:
// mongodb://192.168.100.10/customers_db String mongodbJdbcUrl = map.computeIfAbsent("mongodb", jdbcUrl);
因为我们的映射不包含mongodb键,它将被计算并添加到映射中。
如果我们的Function被求值为null,那么映射保持不变,返回值为null。
再次调用computeIfAbsent()不会重新计算值。这次,由于mongodb在映射中(在上一次调用中添加),所以返回的值将是mongodb://192.168.100.10/customers_db。这与尝试获取mysql的 JDBC URL 是一样的,它将返回jdbc:mysql://192.168.0.50/customers_db,而无需进一步计算。
示例 3(compute()
)
假设我们有以下Map
:
Map<String, String> map = new HashMap<>(); map.put("postgresql", "127.0.0.1"); map.put("mysql", "192.168.0.50");
我们使用这个映射为不同的数据库类型构建 JDBC URL。
假设我们要为 MySQL 和 Derby DB 构建 JDBC URL。在这种情况下,不管键(mysql还是derby存在于映射中,JDBC URL 都应该基于相应的键和值(可以是null)来计算。另外,如果键存在于映射中,并且我们的计算结果是null(无法计算 JDBC URL),那么我们希望从映射中删除这个条目。基本上,这是computeIfPresent()和computeIfAbsent()的组合。
这是V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)的工作。
此时,应写入BiFunction以覆盖搜索键的值为null时的情况:
String address = InetAddress.getLocalHost().getHostAddress(); BiFunction<String, String, String> jdbcUrl = (k, v) -> "jdbc:" + k + "://" + ((v == null) ? address : v) + "/customers_db";
现在,让我们计算 MySQL 的 JDBC URL。因为mysql键存在于映射中,所以计算将依赖于相应的值192.168.0.50。结果将更新映射中mysql键的值:
// jdbc:mysql://192.168.0.50/customers_db String mysqlJdbcUrl = map.compute("mysql", jdbcUrl);
另外,让我们计算 Derby DB 的 JDBC URL。由于映射中不存在derby键,因此计算将依赖于当前 IP。结果将被添加到映射的derby键下:
// jdbc:derby://192.168.100.10/customers_db String derbyJdbcUrl = map.compute("derby", jdbcUrl);
在这两次计算之后,映射将包含以下三个条目:
postgresql=127.0.0.1
derby=jdbc:derby://192.168.100.10/customers_db
mysql=jdbc:mysql://192.168.0.50/customers_db
请注意,再次调用compute()将重新计算值。这可能导致不需要的结果,如jdbc:derby://jdbc:derby://...。
如果计算的结果是null(例如 JDBC URL 无法计算),并且映射中存在键(例如mysql),那么这个条目将从映射中删除,返回的结果是null。
示例 4(merge()
)
假设我们有以下Map
:
Map<String, String> map = new HashMap<>(); map.put("postgresql", "9.6.1 "); map.put("mysql", "5.1 5.2 5.6 ");
我们使用这个映射来存储每个数据库类型的版本,这些版本之间用空格隔开。
现在,假设每次发布数据库类型的新版本时,我们都希望将其添加到对应键下的映射中。如果键(例如,mysql)存在于映射中,那么我们只需将新版本连接到当前值的末尾。如果键(例如,derby)不在映射中,那么我们现在只想添加它。
这是V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)的完美工作。
如果给定的键(K与某个值没有关联或与null关联,那么新的值将是V。如果给定键(K与非null值相关联,则基于给定的BiFunction计算新值。如果此BiFunction的结果是null,并且该键存在于映射中,则此条目将从映射中删除。
在我们的例子中,我们希望将当前值与新版本连接起来,因此我们的BiFunction可以写为:
BiFunction<String, String, String> jdbcUrl = String::concat;
我们在以下方面也有类似的情况:
BiFunction<String, String, String> jdbcUrl = (vold, vnew) -> vold.concat(vnew);
例如,假设我们希望在 MySQL 的映射版本 8.0 中连接。这可以通过以下方式实现:
// 5.1 5.2 5.6 8.0 String mySqlVersion = map.merge("mysql", "8.0 ", jdbcUrl);
稍后,我们还将连接 9.0 版:
// 5.1 5.2 5.6 8.0 9.0 String mySqlVersion = map.merge("mysql", "9.0 ", jdbcUrl);
或者,我们添加 Derby DB 的版本10.11.1.1
。这将导致映射中出现一个新条目,因为不存在derby
键:
// 10.11.1.1 String derbyVersion = map.merge("derby", "10.11.1.1 ", jdbcUrl);
在这三个操作结束时,映射条目如下所示:
postgresql=9.6.1, derby=10.11.1.1, mysql=5.1 5.2 5.6 8.0 9.0
示例 5(putIfAbsent()
)
假设我们有以下Map
:
Map<Integer, String> map = new HashMap<>(); map.put(1, "postgresql"); map.put(2, "mysql"); map.put(3, null);
我们使用这个映射来存储一些数据库类型的名称。
现在,假设我们希望基于以下约束在该映射中包含更多数据库类型:
如果给定的键存在于映射中,那么只需返回相应的值并保持映射不变。
如果给定的键不在映射中(或者与一个null值相关联),则将给定的值放入映射并返回null。
嗯,这是putIfAbsent(K key, V value)的工作。
以下三种尝试不言自明:
String v1 = map.putIfAbsent(1, "derby"); // postgresql String v2 = map.putIfAbsent(3, "derby"); // null String v3 = map.putIfAbsent(4, "cassandra"); // null
映射内容如下:
1=postgresql, 2=mysql, 3=derby, 4=cassandra
112 从映射中删除
从Map
中删除可以通过一个键或者一个键和值来完成。
例如,假设我们有以下Map
:
Map<Integer, String> map = new HashMap<>(); map.put(1, "postgresql"); map.put(2, "mysql"); map.put(3, "derby");
通过键删除就像调用V Map.remove(Object key)
方法一样简单。如果给定键对应的条目删除成功,则返回关联值,否则返回null
。
检查以下示例:
String r1 = map.remove(1); // postgresql String r2 = map.remove(4); // null
现在,映射包含以下条目(已删除键 1 中的条目):
2=mysql, 3=derby
从 JDK8 开始,Map接口被一个新的remove()标志方法所丰富,该方法具有以下签名:boolean remove(Object key, Object value)。使用这种方法,只有在给定的键和值之间存在完美匹配时,才能从映射中删除条目。基本上,这种方法是以下复合条件的捷径:map.containsKey(key) && Objects.equals(map.get(key), value)。
让我们举两个简单的例子:
// true boolean r1 = map.remove(2, "mysql"); // false (the key is present, but the values don't match) boolean r2 = map.remove(3, "mysql");
结果映射包含一个剩余条目3=derby。
迭代和从Map中移除至少可以通过两种方式来完成:第一,通过Iterator(捆绑代码中存在的解决方案),第二,从 JDK8 开始,我们可以通过removeIf(Predicate<? super E> filter)来完成:
map.entrySet().removeIf(e -> e.getValue().equals("mysql"));
有关从集合中删除的更多详细信息,请参见“删除集合中与谓词匹配的所有元素”。
113 替换映射中的条目
从Map
替换条目是一个在很多情况下都会遇到的问题。要实现这一点并避免在辅助方法中编写一段意大利面条代码,方便的解决方案依赖于 JDK8replace()
方法。
假设我们有下面的Melon
类和Melon
的映射:
public class Melon { private final String type; private final int weight; // constructor, getters, equals(), hashCode(), // toString() omitted for brevity } Map<Integer, Melon> mapOfMelon = new HashMap<>(); mapOfMelon.put(1, new Melon("Apollo", 3000)); mapOfMelon.put(2, new Melon("Jade Dew", 3500)); mapOfMelon.put(3, new Melon("Cantaloupe", 1500));
通过V replace(K key, V value)可以完成按键 2 对应的甜瓜的更换。如果替换成功,则此方法将返回初始的Melon:
// Jade Dew(3500g) was replaced Melon melon = mapOfMelon.replace(2, new Melon("Gac", 1000));
现在,映射包含以下条目:
1=Apollo(3000g), 2=Gac(1000g), 3=Cantaloupe(1500g)
此外,假设我们想用键 1 和阿波罗甜瓜(3000g)替换条目。所以,甜瓜应该是同一个,才能获得成功的替代品。这可以通过布尔值replace(K key, V oldValue, V newValue)实现。此方法依赖于equals()合同来比较给定的值,因此Melon需要执行equals()方法,否则结果不可预知:
// true boolean melon = mapOfMelon.replace( 1, new Melon("Apollo", 3000), new Melon("Bitter", 4300));
现在,映射包含以下条目:
1=Bitter(4300g), 2=Gac(1000g), 3=Cantaloupe(1500g)
最后,假设我们要根据给定的函数替换Map中的所有条目。这可以通过void replaceAll(BiFunction<? super K,? super V,? extends V> function)完成。
例如,将所有重量超过 1000g 的瓜替换为重量等于 1000g 的瓜,下面的BiFunction形成了这个函数(k是键,v是Map中每个条目的值):
BiFunction<Integer, Melon, Melon> function = (k, v) -> v.getWeight() > 1000 ? new Melon(v.getType(), 1000) : v;
接下来,replaceAll()
出现在现场:
mapOfMelon.replaceAll(function);
现在,映射包含以下条目:
1=Bitter(1000g), 2=Gac(1000g), 3=Cantaloupe(1000g)
114 比较两个映射
只要我们依赖于Map.equals()
方法,比较两个映射是很简单的。在比较两个映射时,该方法使用Object.equals()
方法比较它们的键和值。
例如,让我们考虑两个具有相同条目的瓜映射(在Melon
类中必须存在equals()
和hashCode()
:
public class Melon { private final String type; private final int weight; // constructor, getters, equals(), hashCode(), // toString() omitted for brevity } Map<Integer, Melon> melons1Map = new HashMap<>(); Map<Integer, Melon> melons2Map = new HashMap<>(); melons1Map.put(1, new Melon("Apollo", 3000)); melons1Map.put(2, new Melon("Jade Dew", 3500)); melons1Map.put(3, new Melon("Cantaloupe", 1500)); melons2Map.put(1, new Melon("Apollo", 3000)); melons2Map.put(2, new Melon("Jade Dew", 3500)); melons2Map.put(3, new Melon("Cantaloupe", 1500));
现在,如果我们测试melons1Map
和melons2Map
是否相等,那么我们得到true
:
boolean equals12Map = melons1Map.equals(melons2Map); // true
但如果我们使用数组,这将不起作用。例如,考虑下面两个映射:
Melon[] melons1Array = { new Melon("Apollo", 3000), new Melon("Jade Dew", 3500), new Melon("Cantaloupe", 1500) }; Melon[] melons2Array = { new Melon("Apollo", 3000), new Melon("Jade Dew", 3500), new Melon("Cantaloupe", 1500) }; Map<Integer, Melon[]> melons1ArrayMap = new HashMap<>(); melons1ArrayMap.put(1, melons1Array); Map<Integer, Melon[]> melons2ArrayMap = new HashMap<>(); melons2ArrayMap.put(1, melons2Array);
即使melons1ArrayMap
和melons2ArrayMap
相等,Map.equals()
也会返回false
:
boolean equals12ArrayMap = melons1ArrayMap.equals(melons2ArrayMap);
这个问题源于这样一个事实:数组的equals()
方法比较的是标识,而不是数组的内容。为了解决这个问题,我们可以编写一个辅助方法如下(这次依赖于Arrays.equals()
,它比较数组的内容):
public static <A, B> boolean equalsWithArrays( Map<A, B[]> first, Map<A, B[]> second) { if (first.size() != second.size()) { return false; } return first.entrySet().stream() .allMatch(e -> Arrays.equals(e.getValue(), second.get(e.getKey()))); }
115 对映射排序
排序一个Map
有几种解决方案。首先,假设Melon
中的Map
:
public class Melon implements Comparable { private final String type; private final int weight; @Override public int compareTo(Object o) { return Integer.compare(this.getWeight(), ((Melon) o).getWeight()); } // constructor, getters, equals(), hashCode(), // toString() omitted for brevity } Map<String, Melon> melons = new HashMap<>(); melons.put("delicious", new Melon("Apollo", 3000)); melons.put("refreshing", new Melon("Jade Dew", 3500)); melons.put("famous", new Melon("Cantaloupe", 1500));
现在,让我们来研究几种排序这个Map的解决方案。基本上,我们的目标是通过一个名为Maps的工具类公开以下屏幕截图中的方法:
让我们在下一节中看看不同的解决方案。
通过TreeMap
和自然排序按键排序
对Map
进行排序的快速解决方案依赖于TreeMap
。根据定义,TreeMap
中的键按其自然顺序排序。此外,TreeMap
还有一个TreeMap(Map<? extends K,? extends V> m)
类型的构造器:
public static <K, V> TreeMap<K, V> sortByKeyTreeMap(Map<K, V> map) { return new TreeMap<>(map); }
调用它将按键对映射进行排序:
// {delicious=Apollo(3000g), // famous=Cantaloupe(1500g), refreshing=Jade Dew(3500g)} TreeMap<String, Melon> sortedMap = Maps.sortByKeyTreeMap(melons);
通过流和比较器按键和值排序
一旦我们为映射创建了一个Stream
,我们就可以很容易地用Stream.sorted()
方法对它进行排序,不管有没有Comparator
。这一次,让我们使用一个Comparator
:
public static <K, V> Map<K, V> sortByKeyStream( Map<K, V> map, Comparator<? super K> c) { return map.entrySet() .stream() .sorted(Map.Entry.comparingByKey(c)) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1, LinkedHashMap::new)); } public static <K, V> Map<K, V> sortByValueStream( Map<K, V> map, Comparator<? super V> c) { return map.entrySet() .stream() .sorted(Map.Entry.comparingByValue(c)) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v1, LinkedHashMap::new)); }
我们需要依赖LinkedHashMap
而不是HashMap
。否则,我们就不能保持迭代顺序。
让我们把映射分类如下:
// {delicious=Apollo(3000g), // famous=Cantaloupe(1500g), // refreshing=Jade Dew(3500g)} Comparator<String> byInt = Comparator.naturalOrder(); Map<String, Melon> sortedMap = Maps.sortByKeyStream(melons, byInt); // {famous=Cantaloupe(1500g), // delicious=Apollo(3000g), // refreshing=Jade Dew(3500g)} Comparator<Melon> byWeight = Comparator.comparing(Melon::getWeight); Map<String, Melon> sortedMap = Maps.sortByValueStream(melons, byWeight);
通过列表按键和值排序
前面的示例对给定的映射进行排序,结果也是一个映射。如果我们只需要排序的键(我们不关心值),反之亦然,那么我们可以依赖于通过Map.keySet()
创建的List
作为键,通过Map.values()
创建的List
作为值:
public static <K extends Comparable, V> List<K> sortByKeyList(Map<K, V> map) { List<K> list = new ArrayList<>(map.keySet()); Collections.sort(list); return list; } public static <K, V extends Comparable> List<V> sortByValueList(Map<K, V> map) { List<V> list = new ArrayList<>(map.values()); Collections.sort(list); return list; }
现在,让我们对映射进行排序:
// [delicious, famous, refreshing] List<String> sortedKeys = Maps.sortByKeyList(melons); // [Cantaloupe(1500g), Apollo(3000g), Jade Dew(3500g)] List<Melon> sortedValues = Maps.sortByValueList(melons);
如果不允许重复值,则必须依赖于使用SortedSet的实现:
SortedSet<String> sortedKeys = new TreeSet<>(melons.keySet()); SortedSet<Melon> sortedValues = new TreeSet<>(melons.values());
116 复制哈希映射
执行HashMap
的浅拷贝的简便解决方案依赖于HashMap
构造器HashMap(Map<? extends K,? extends V> m)
。以下代码是不言自明的:
Map<K, V> mapToCopy = new HashMap<>(); Map<K, V> shallowCopy = new HashMap<>(mapToCopy);
另一种解决方案可能依赖于putAll(Map<? extends K,? extends V> m)
方法。此方法将指定映射中的所有映射复制到此映射,如以下助手方法所示:
@SuppressWarnings("unchecked") public static <K, V> HashMap<K, V> shallowCopy(Map<K, V> map) { HashMap<K, V> copy = new HashMap<>(); copy.putAll(map); return copy; }
我们还可以用 Java8 函数式风格编写一个辅助方法,如下所示:
@SuppressWarnings("unchecked") public static <K, V> HashMap<K, V> shallowCopy(Map<K, V> map) { Set<Entry<K, V>> entries = map.entrySet(); HashMap<K, V> copy = (HashMap<K, V>) entries.stream() .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue)); return copy; }
然而,这三种解决方案只提供了映射的浅显副本。获取深度拷贝的解决方案可以依赖于克隆库在第 2 章中介绍,“对象、不变性和switch
表达式”。将使用克隆的助手方法可以编写如下:
@SuppressWarnings("unchecked") public static <K, V> HashMap<K, V> deepCopy(Map<K, V> map) { Cloner cloner = new Cloner(); HashMap<K, V> copy = (HashMap<K, V>) cloner.deepClone(map); return copy; }
117 合并两个映射
合并两个映射是将两个映射合并为一个包含两个映射的元素的映射的过程。此外,对于键碰撞,我们将属于第二个映射的值合并到最终映射中。但这是一个设计决定。
让我们考虑以下两个映射(我们特意为键 3 添加了一个冲突):
public class Melon { private final String type; private final int weight; // constructor, getters, equals(), hashCode(), // toString() omitted for brevity } Map<Integer, Melon> melons1 = new HashMap<>(); Map<Integer, Melon> melons2 = new HashMap<>(); melons1.put(1, new Melon("Apollo", 3000)); melons1.put(2, new Melon("Jade Dew", 3500)); melons1.put(3, new Melon("Cantaloupe", 1500)); melons2.put(3, new Melon("Apollo", 3000)); melons2.put(4, new Melon("Jade Dew", 3500)); melons2.put(5, new Melon("Cantaloupe", 1500));
从 JDK8 开始,我们在Map: V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)中有以下方法。
如果给定的键(K与值没有关联,或者与null关联,那么新的值将是V。如果给定键(K与非null值相关联,则基于给定的BiFunction计算新值。如果此BiFunction的结果是null,并且该键存在于映射中,则此条目将从映射中删除。
基于这个定义,我们可以编写一个辅助方法来合并两个映射,如下所示:
public static <K, V> Map<K, V> mergeMaps( Map<K, V> map1, Map<K, V> map2) { Map<K, V> map = new HashMap<>(map1); map2.forEach( (key, value) -> map.merge(key, value, (v1, v2) -> v2)); return map; }
请注意,我们不会修改原始映射。我们更希望返回一个包含第一个映射的元素与第二个映射的元素合并的新映射。在键冲突的情况下,我们用第二个映射(v2中的值替换现有值。
基于Stream.concat()可以编写另一个解决方案。基本上,这种方法将两个流连接成一个Stream。为了从一个Map创建一个Stream,我们称之为Map.entrySet().stream()。在连接从给定映射创建的两个流之后,我们只需通过toMap()收集器收集结果:
public static <K, V> Map<K, V> mergeMaps( Map<K, V> map1, Map<K, V> map2) { Stream<Map.Entry<K, V>> combined = Stream.concat(map1.entrySet().stream(), map2.entrySet().stream()); Map<K, V> map = combined.collect( Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (v1, v2) -> v2)); return map; }
作为奖励,Set
(例如,整数的Set
可以按如下方式排序:
List<Integer> sortedList = someSetOfIntegers.stream() .sorted().collect(Collectors.toList());
对于对象,依赖于sorted(Comparator<? super T>
。
118 删除集合中与谓词匹配的所有元素
我们的集合将收集一堆Melon
:
public class Melon { private final String type; private final int weight; // constructor, getters, equals(), // hashCode(), toString() omitted for brevity }
让我们在整个示例中假设以下集合(ArrayList
,以演示如何从集合中移除与给定谓词匹配的元素:
List<Melon> melons = new ArrayList<>(); melons.add(new Melon("Apollo", 3000)); melons.add(new Melon("Jade Dew", 3500)); melons.add(new Melon("Cantaloupe", 1500)); melons.add(new Melon("Gac", 1600)); melons.add(new Melon("Hami", 1400));
让我们看看下面几节给出的不同解决方案。