Map接口总结
- Map的基本使用
- 默认方法的问题,有什么坑
- 常用的默认方法应用场景
- 并发支持
- 函数式应用
重写了我一年前写的博客,新增了一些例子和并发的讨论,并与函数式编程思想进行了比较。
基本操作
Java中Map接口常用方法:
方法 | 描述 |
---|---|
V get(Object key) | 根据指定键获取其对应的值。若Map中不存在该键,则返回null。 |
boolean isEmpty() | 判断Map是否为空,即不包含任何键值对。 |
V put(K key, V value) | 添加或更新Map中的键值对。若该键已存在,则用新的值替换原来的值,并返回原来的值;若该键不存在,则添加该键值对,并返回null。 |
void putAll(Map<? extends K,? extends V> m) | 将另一个Map中的所有键值对复制到当前Map中。 |
void clear() | 清空Map中所有的键值对。 |
boolean containsKey(Object key) | 查看Map中是否包含指定键所对应的值。 |
boolean containsValue(Object value) | 查看Map中是否包含指定值所对应的键。 |
V remove(Object key) | 根据指定键删除Map中对应的键值对,并返回该键对应的值。若Map中不存在该键,则返回null。 |
int size() | 获取Map中键值对的数量。 |
Set<K> keySet() | 获取Map中所有键组成的Set集合。 |
Collection<V> values() | 获取Map中所有值所组成的Collection集合。 |
Set<Map.Entry<K,V>> entrySet() | 获取Map中所有键值对所组成的Set集合。 |
需要注意的是,这里的 K
和 V
分别代表Map中键和值的数据类型。而 Map.Entry
接口则表示Map中的一个键值对。
import java.util.HashMap;
import java.util.Map;
public class MapExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
// Check if a key is in the map
if (map.containsKey("Alice")) {
System.out.println("Alice is in the map");
}
// Check if a value is in the map
if (map.containsValue(30)) {
System.out.println("There is a person aged 30 in the map");
}
// Get a value from the map using a key
int age = map.get("Charlie");
System.out.println("Charlie is " + age + " years old");
// Remove a key-value pair from the map
map.remove("Bob");
// Get the size of the map
int size = map.size();
System.out.println("The map has " + size + " entries");
// Get the key set from the map
Set<String> keys = map.keySet();
System.out.println("Keys: " + keys);
// Get the value collection from the map
Collection<Integer> values = map.values();
System.out.println("Values: " + values);
// Get the set of key-value pairs (entries) from the map
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " is " + value + " years old");
}
// Clear all the entries from the map
map.clear();
}
}
实现规约
以下两个方法提示实现类需要重写用来比较相等的方法。
equals hashcode
以下两个方法是标准库的约定,但是Java中接口不支持定义构造器约束,只能保证标准库中都实现了这两个方法。
构造器(空参数)
构造器(Map other)
default方法
排序在前的方法较为常用。
默认方法是为了在标准库中添加默认支持函数式方法,同时也不必修改现有的类,这样做虽然有一定的好处,但是实际上在子类中使用default方法还是极有可能出错的,因为一个default方法无法满足所有的子类,也不可能满足。如果随意使用default方法,可能破坏了原有子类的一致性,产生意想不到的问题。
在标准库中,除了并发相关类,比如ConcurrentHashMap等,一般没有问题。
但是在其他类库的实现类中使用一定要慎之又慎,比如使用一个老版本的Map子类。
引入default方法破坏了前向兼容性,容易产生运行时异常。
V getOrDefault(Object key, V defaultValue)
常用,获取值或者默认值,可类比Optional.orElse。
以下是先做判断,后计算的(if true)
V putIfAbsent(K key, V value)
V replace(K key, V value)
感觉叫putIfPresent更好
boolean replace(K key, V oldValue, V newValue)
boolean remove(Object key, Object value)
如果匹配的话,remove
以下四个方法为一组,都是对entry(k, v)的更新,只是条件不一样。
V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction)
// 如果没有entry则更新
// 返回值为value,可以用于后续操作
// 常用于MultiMap,如下面一句话表示把用户添加到用户组里
map.computeIfAbsent(userGroup, k -> new HashMap<User>()).add(user);
V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction)
// 如果有entry则更新
V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction)
// 不常用,因为如果不包含entry则可能抛出空指针异常
// 可以理解为同时支持上面两个方法,但是我们一般都需要对是否包含entry进行判断,所以不常用。
// compute方法也可以实现merge这样的规约操作,既然如此,在需要规约操作时,我们为什么不用merge呢。
// v 可能为空指针,程序员极有可能忘记检查,编译器也不能帮助检查。
// 总之,这个compute方法不常用。
// compute进行null判断
map.compute(key, (k, v) -> (v == null) ? msg : v.concat(msg))
// merge中更简单
map.merge(key, msg, String::concat)
V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction)
// 没有则使用默认值,有则进行类似reduce的操作
// 规约结果为null时,则删除
// 好用
// 常用来计数
map.merge(key, 1, Integer::sum);
void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
// 使用泛型达到了更广的匹配
// 通配符使用原则,使用的对象 (consumer) 使用 super,生成的对象 (provider) 使用 extends。
forEach(BiConsumer<? super K, ? super V> action)
!!!经常滥用的方法,不建议使用,action只建议使用简单的逻辑。每回看到别人使用forEach方法都感觉很恶心,这个方法看上去好用,可以传入(k,v)→ ... lambda表达式,但是函数式方法应该尽量不产生副作用,使用函数式方法的目的应该便于理解。然而在项目中经常看到大段的lambda表达式传入,在不便于调试的同时,还不能产生副作用。由于副作用的问题,每回想修改forEach的逻辑时,都必须改为for (Map.Entry<K, V> entry : map.entrySet()),然后再修改逻辑。
包括Collections下的List,Set等都有滥用forEach方法。
以下是我在网上找的一个例子,不知道你看到forEach后啥感觉,我反正是要吐了,况且这还是一段逻辑相对简单的代码。
public static void main(String[] args)
{
// create a HashMap and add some values
HashMap<Integer, String>
map1 = new HashMap<>();
map1.put(1, "Ram");
map1.put(2, "Rohan");
map1.put(3, "Shivam");
HashMap<Integer, String>
map2 = new HashMap<>();
map2.put(1, "Tushar");
map2.put(10, "Satya");
map2.put(12, "Sundar");
// print map details
System.out.println("HashMap1: "
+ map1.toString());
System.out.println("HashMap2: "
+ map2.toString());
// provide value for new key which is absent
// using computeIfAbsent method
map2.forEach(
(key, value)
-> map1.merge(
key,
value,
(v1, v2)
-> v1.equalsIgnoreCase(v2)
? v1
: v1 + ", " + v2));
// print new mapping
System.out.println("New HashMap: " + map1);
}
总之,函数式方法使用的函数应该足够简单,便于理解。
如果使用默认方法可以简化了理解,代码更简洁,而且没有副作用,确保代码兼容性,可以使用默认方法,其他情况下还是老老实实用命令式编程吧。
并发支持:原子操作
ConcurrentMap
接口继承自 Map
接口,并添加了额外的并发特性。ConcurrentMap
接口中提供了一些原子操作方法,这些方法的原子性保证了在多线程并发访问的情况下,对 ConcurrentMap
的操作不会产生竞争问题。
其中,ConcurrentMap
接口中提供的默认方法都是原子操作。这些方法包括:
- putIfAbsent(key, value):将指定键/值对添加到Map中,当且仅当在Map中不存在指定的键时才添加,否则返回已存在的值。
- remove(key, value):从Map中移除指定键/值对,当且仅当指定的键目前在Map中映射到指定的值时才移除,否则不执行操作。
- replace(key, oldValue, newValue):将Map中指定键映射到给定值的操作必须满足当且仅当键目前映射到给定的值时才进行更新操作。
- replace(key, value):将Map中指定键映射到给定值,更新成功返回true,更新失败返回false。
值得注意的是,这些默认方法提供的原子操作并不会保证其组合操作的原子性。例如,在使用 ConcurrentMap
接口提供的以下方法时也需要进行额外的同步操作:
- computeIfAbsent(key, mappingFunction):在Map中将指定变换应用于键 key,并在缺少条目(或值为null)时插入指定的值(key,newValue),返回新值。
- computeIfPresent(key, remappingFunction):在Map中将指定映射重新应用于键key,并返回新值。
- compute(key, remappingFunction):计算一个新映射,把它放到映射表中,除非它的值为null。
在使用 ConcurrentMap
接口方法时需要注意,单纯的使用这些方法并不能保证整个操作是原子性的,还需要额外的同步机制。
为什么Map没有实现Collection接口
虽然这个问题有点找茬的感觉,但是如果重新定义Map的话,Map 确实可以实现Collection接口,迭代的对象即为 entry/tuple,Java 标准库中没有实现 Tuple。
如果 Map 接口实现了 Collection 接口,那么它需要实现 Collection 接口中的所有方法,包括一些与键值对无关的方法,这样的实现会违背接口设计的初衷,也会增加维护的复杂性。因此,Java 库的设计者将 Map 接口独立出来,以满足键值对存储的需求。
函数式思想下Map的实现
Vavr 是一个实用的 Java 函数式编程库,提供了许多丰富的数据类型和操作函数,其中就包括 Map 类型。Vavr 中的 Map 与 Java 标准库中的 Map 类型相似,但它提供了更多的函数式编程的特性和操作函数,如不可变、链式操作和模式匹配等特性。
以下是一个简单的使用 Vavr 中的 Map 类型的示例:
import io.vavr.collection.Map;
public class MapExample {
public static void main(String[] args) {
// 创建一个空的不可变 Map
Map<String, Integer> map1 = Map.empty();
// 创建一个包含键值对的不可变 Map
Map<String, Integer> map2 = Map.of("John", 30, "Jane", 25);
// 使用 with 方法添加或更新元素,返回一个新的 Map 对象
Map<String, Integer> map3 = map2.with("John", 31)
.with("Bob", 40);
// 使用 remove 方法移除元素,返回一个新的 Map 对象
Map<String, Integer> map4 = map3.remove("Jane");
// 使用 contains 方法判断是否包含键或值
boolean hasKey = map4.containsKey("John");
boolean hasValue = map4.containsValue(31);
// 使用 get 方法根据键获取值,返回一个 Option 对象
Option<Integer> age = map4.get("John");
// 使用 map 方法对 Map 中的值加 1
Option<Integer> johnAge = map.get("John").map(age -> age + 1);
// 使用 flatMap 方法,如果存在 Bob 的键则返回一个 Option 对象,否则返回 Option.none()
Option<Integer> bobAge = map.get("Bob").flatMap(age -> Option.of(age + 1));
// 使用 filter 方法根据条件过滤 Map 中的元素
Map<String, Integer> result =
map.filter((name, age) -> age > 26 && name.startsWith("J"));
// 使用 forEach 方法遍历 Map 中的元素
map.forEach((name, age) -> System.out.println(name + " -> " + age));
// 使用 iterator 方法产生一个迭代器
Iterator<Tuple2<String, Integer>> iterator = map4.iterator();
while (iterator.hasNext()) {
Tuple2<String, Integer> entry = iterator.next();
System.out.println(entry._1 + " -> " + entry._2);
}
}
}
需要注意的是,Vavr 中的 Map 类型是不可变的,所有的操作函数都返回一个新的 Map 对象,而原有的 Map 对象不会被修改。这种不可变性保证了数据的线程安全,避免了多线程下的竞态条件和不一致性。
Vavr 中 Map 的 get 方法返回的是一个 Option 类型,它主要是为了避免 Java 常见的空指针异常。Option 类型是 Vavr 中函数式编程的一个核心类型,它表示一个可能为空的值,并提供了通用的 API 来处理这个值。这样的方式可以在 Map 中获取的值可能为空的情况,优雅地避免在代码中使用 null,使用易于理解的链式调用。
那么如何在项目中使用呢?
vavr是一个很优秀的类库,Spring Data 也已添加对其的支持,Resilience4j 是基于vavr开发的类库,支持熔断、限流、重试等功能。
我们可以将vavr看做便利的工具类,入参为Java集合类库,通过vavr的遍历操作,输出为Java集合类库。相比与其他的Utils工具类,vavr可以很方便地实现链式操作。
我在使用 joda time 类库时,也有类似的体验。
不过函数式编程毕竟是少数人的娱乐,不同人的代码风格也不尽相同。涉及到团队合作的项目,还是老老实实使用标准库或guava吧。
附,vavr Map 提供了大量方便的api:
Basic operations Signature containsKey(Object) boolean containsKey(Object key)
containsValue(Object) boolean containsValue(Object value)
get(Object) Option<V> get(K key)
keySet() Set<K> keySet()
merge(Map) Map<K, V> merge(Map<? extends K, ? extends V> that)
merge(Map, BiFunction) Map<K, V> merge(Map<? extends K, ? extends V> that, BiFunction<V, ? super V, ? extends V> collisionResolution)
put(Object, Object) Map<K, V> put(K key, V value)
put(Tuple2) Map<K, V> put(Tuple2<? extends K, ? extends V> entry)
put(Object, Object, BiFunction) Map<K, V> put(K key, V value, BiFunction<? super V, ? super V, ? extends V> mergeFunction)
put(Tuple2, BiFunction) Map<K, V> put(Tuple2<? extends K, ? extends V> entry, BiFunction<? super V, ? super V, ? extends V> mergeFunction)
values() Iterator<V> values()
Conversion Signature toJavaMap() java.util.Map<K, V> toJavaMap()
Filtering Signature filter(BiPredicate) Map<K, V> filter(BiPredicate<? super K, ? super V> predicate)
filterKeys(Predicate) Map<K, V> filterKeys(Predicate<? super K> predicate)
filterValues(Predicate) Map<K, V> filterValues(Predicate<? super V> predicate)
reject(BiPredicate) Map<K, V> reject(BiPredicate<? super K, ? super V> predicate)
rejectKeys(Predicate) Map<K, V> rejectKeys(Predicate<? super K> predicate)
rejectValues(Predicate) Map<K, V> rejectValues(Predicate<? super V> predicate)
remove(Object) Map<K, V> remove(Object key)
removeAll(Iterable) Map<K, V> removeAll(Iterable<? extends K> keys)
Iteration Signature forEach(BiConsumer) void forEach(BiConsumer<? super K, ? super V> action)
iterator(BiFunction) <U> Iterator<U> iterator(BiFunction<? super K, ? super V, ? extends U> mapper)
keysIterator() Iterator<K> keysIterator()
valuesIterator() Iterator<V> valuesIterator()
Transformation Signature bimap(Function, Function) <U, W> Map<U, W> bimap(Function<? super K, ? extends U> keyMapper, Function<? super V, ? extends W> valueMapper)
flatMap(BiFunction) <U, W> Map<U, W> flatMap(BiFunction<? super K, ? super V, ? extends java.lang.Iterable<? extends Tuple2<? extends U, ? extends W>>> mapper)
lift() Function1<K, Option<V>> lift()
map(BiFunction) <U, W> Map<U, W> map(BiFunction<? super K, ? super V, ? extends Tuple2<? extends U, ? extends W>> mapper)
mapKeys(Function) <U> Map<U, V> mapKeys(Function<? super K, ? extends U> keyMapper)
mapKeys(Function, BiFunction) <U> Map<U, V> mapKeys(Function<? super K, ? extends U> keyMapper, BiFunction<? super V, ? super V, ? extends V> mergeFunction)
mapValues(Function) <W> Map<K, W> mapValues(Function<? super V, ? extends W> valueMapper)
transform(Function) <U> U transform(Function<? super Map<K, V>, ? extends U> function)
unzip(BiFunction) <U, W> Tuple2<? extends List<U>, ? extends List<W>> unzip(BiFunction<? super K, ? super V, Tuple2<? extends U, ? extends W>> f)
unzip3(BiFunction) <U, W, Z> Tuple3<? extends List<U>, ? extends List<W>, ? extends List<Z>> unzip3(BiFunction<? super K, ? super V, Tuple3<? extends U, ? extends W, ? extends Z>> f)
withDefault(Function) Map<K, V> withDefault(Function<? super K, ? extends V> defaultValueFunction)
withDefaultValue(Object) Map<K, V> withDefaultValue(V value)
具体vavr怎么使用以及同一问题的不同实现,放到专栏后续再详细说吧。