jdk源码解析-Map

概述

java.util.Map是一个泛型接口。

1
public interface Map<K,V> {}

Map 接口可以将键映射到值,不能包含重复的键,每个键最多可以映射到一个值。

与之相似的是Dictionary抽象类,不过接口使用起来更灵活一些。每天被面试官问的HashMap实现了Map接口,而HashTable也实现了Map接口,并且继承了Dictionary抽象类。

Map接口是用来代替Dictionary抽象类的,接口更适合定义行为规范。

内部类

Entry

Map接口有一个内部接口Interface Map.Entry<K,V>

此接口主要定义了对key和value的读写操作。

遍历

Map提供了3种集合视图(键集,值集,键值对集),可以通过下面的方法获取。Map接口对于集合顺序没有要求,不过像TreeMap 的集合是有序的,而HashMap的集合则无序。

1
2
3
Set<K> keySet();				// 不可重复
Collection<V> values(); // 可重复
Set<Map.Entry<K, V>> entrySet();// 不可重复

下面用HashMap举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HashMap<String, String> hashMap = new HashMap<String, String>() {{
put("blog", "hjwblog.com");
put("name", "hjw");
put("time", "2019/09/11");
}};

System.out.println("键集:");
Set<String> keySet = hashMap.keySet();
for (String key : keySet) {
System.out.println(key);
}


System.out.println("键集:");
Collection<String> values = hashMap.values();
for (String value : values) {
System.out.println(value);
}

System.out.println("键值对集");
Set<Map.Entry<String, String>> entrySet = hashMap.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
键集:
name
time
blog
键集:
hjw
2019/09/11
hjwblog.com
键值对集
name:hjw
time:2019/09/11
blog:hjwblog.com

方法

size

1
int size();

此方法返回Map中键值对的数量,如果数量超过Integer.MAX_VALUE,返回Integer.MAX_VALUE

isEmpty

1
boolean isEmpty();

此方法返回Map里面是否有键值对。

containsKey

1
boolean containsKey(Object key);

判断是否包含对应的key。

此接口是可以把null当作键的,如果实现上不允许键为null,可以抛出NullPointerException

当key的类型是map实现类不支持的时候,则抛出ClassCastException异常。

containsValue

1
boolean containsValue(Object value);

判断是否包含对应的value值。

这个操作复杂度应该是O(n)的。

get

1
V get(Object key);

此方法返回指定key映射到的 value,如果该 Map 不包含该 key 的映射,则返回null。

如果 value 可以为 null ,此方法不能区分是否包含该键,都可能返回 null 。需要用containsKey方法区分。

put

1
V put(K key, V value);

插入一个键值对。

如果存在此键了,会替换该键对应的值,顺便看了一下HashMap的实现中,是会返回旧值的。

此处对于熟悉 C++ STL 的要注意了,C++ STL里面的 map 插入键值对如果键值对已存在,是不会更改旧值的,要判断返回值才知道是否插入成功,之前我在这里遇到了坑。

remove

1
V remove(Object key);

移除 key 对应的键值对。

如果移除成功返回值,没有找到返回 null 。

putAll

1
void putAll(Map<? extends K, ? extends V> m);

此方法可以将一个 Map 的全部键值对添加到本 Map 中。

作用相当于遍历 m ,对每一个键值对调用一次 put 方法。

这里? extends K指得是 K 或者 K 的子类。

相对的? super K指得是 K 或者 K 的父类。

clear

1
void clear();

清空Map

keySet

1
Set<K> keySet();

返回此Map中包含的键的集合视图。

如果此时Map被改变,则此Set也会变,反之亦然,不过只支持移除操作,不能执行插入操作。

1
2
Set<String> keySet = hashMap.keySet();
keySet.remove("name");

如上,对Set执行remove方法,会导致Map里面的键值对被删除。

这里有个注意事项,在遍历过程中对Map执行remove是会报错的。(虽然没人会这样写,但是这对ArrayList之类的也一样)

1
2
3
4
5
6
7
Iterator<String> it = keySet.iterator();
while (it.hasNext()){
String key = it.next();
if ("name".equals(key)){
keySet.remove(key);
}
}

上面的代码会抛出java.util.ConcurrentModificationException异常。

正确写法:

1
2
3
4
5
6
7
Iterator<String> it = keySet.iterator();
while (it.hasNext()){
String key = it.next();
if ("name".equals(key)){
it.remove();
}
}

虽然对于Map删除元素只要调用remove方法,但是这里写这个例子主要是说明迭代过程中修改Map的正确方法。

values

1
Collection<V> values();

返回此Map中包含的值的集合视图。

类似keySet,如果此时Map被改变,Set也会变,反之亦然,不过只支持移除操作,不能执行插入操作。

其他方面和keySet方法差不多。

entrySet

1
Set<Map.Entry<K, V>> entrySet();

返回此Map中包含的映射的集合视图。

其他方面和keySet方法差不多。

equals

1
boolean equals(Object o);

就是比较两个Map是否相等的。

Map相等的定义是m1.entrySet().equals(m2.entrySet())

hashCode

1
int hashCode();

返回Map的hash码。

Map的哈希码定义为``entrySet()的所有entry的哈希码之和。

保证如果m1.equals(m2)m1.hashCode()==m2.hashCode()

getOrDefault

1
2
3
4
5
6
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}

这里使用了jdk8的新特性——default。

可以在接口实现一个方法。

此方法可以通过key获取value,如果key不存在,则返回指定的默认值。

在某些特定时刻,此方法可以精简代码。

forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}

Map中的每个entry执行给定的操作,直到处理完所有entry或操作引发异常为止。

相当于:

1
2
3
for (Map.Entry<K, V> entry : map.entrySet())
action.accept(entry.getKey(), entry.getValue());
Objects.requireNonNull(action);

Objects 类是jdk7出现的,提供了一些对对象的常用操作静态方法。

requireNonNull如果判断参数是null,则会抛出NullPointerException

1
2
3
4
5
6
hashMap.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
System.out.println(key + ":" + value);
}
});

同样的,不要尝试在遍历时修改Map。

1
2
3
4
5
6
hashMap.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
hashMap.remove(key);
}
});

这样会抛出ConcurrentModificationException

replaceAll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}

// ise thrown from function is not a cme.
v = function.apply(k, v);

try {
entry.setValue(v);
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}

这个方法会对所有元素调用apply方法,并将keyvalue作为参数,将返回值作为新value。

等价于:

1
2
for (Map.Entry<K, V> entry : map.entrySet())
entry.setValue(function.apply(entry.getKey(), entry.getValue()));

例子:

1
2
3
4
5
6
7
8
9
hashMap.replaceAll(new BiFunction<String, String, String>() {
@Override
public String apply(String key, String value) {
if ("hjw".equals(value)){
return "hjw1";
}
return value;
}
});

BiFunction泛型接口第一个参数是key的类型,第二个参数是value的类型,第三个参数是新value的类型,即为apply方法的返回值。

putIfAbsent

1
2
3
4
5
6
7
8
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}

return v;
}

如果此key对应的value为null,则插入value,否则不变。

remove(Object key, Object value)

1
2
3
4
5
6
7
8
9
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}

这个方法只有keyvalue都匹配时才会移除键值对。

------ 本文结束 ------