通常,Map是一种数据结构,用来存储键值对的,每个key只能在map中出现1次。这篇文章总结了9个 Map 和其子类很常见的问题。简单起见,例子中我将会使用泛型。因此,我将只考虑常规的map,而不是特定的map。但是你可以假设K和V都可以比较,那意味着K和V都继承了Comparable接口。

1. 把Map转换成List

在Java中,Map提供了3中集合视图:key set,value set,和 key-value set。他们都能使用addAll()转换成为List。下面的代码片段展示了如何从map构建一个ArrayList。

1
2
3
4
5
6
// key list
List keyList = new ArrayList(map.keySet());
// value list
List valueList = new ArrayList(map.valueSet());
// key-value list
List entryList = new ArrayList(map.entrySet());

2.遍历map的记录

对于map来说遍历键值对是很基础的操作,这种键值对在map中被叫做Map.Entry。Map.Entry将会返回键值的集合,因此这是遍历map最有效的方法。

1
2
3
4
5
6
for(Entry entry: map.entrySet()) {
// get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}

对于JDK1.5以前的可以这样:

1
2
3
4
5
6
7
8
Iterator itr = map.entrySet().iterator();
while(itr.hasNext()) {
Entry entry = itr.next();
// get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}

3. 根据map的key来排序

另一种常见的操作是针对map的key排序。方法1是把Map.Entry 放入list然后使用一个comparator 来进行排序。

1
2
3
4
5
6
7
8
9
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {
@Override
public int compare(Entry e1, Entry e2) {
return e1.getKey().compareTo(e2.getKey());
}
});

另外一个方法是使用SortedMap,它提供了按照key排序的功能。但是所有的key都必须实现 Comparable或者可以比较操作。

TreeMap是一个SortedMap的实现。它的构造函数可以接受一个比较器。下面的代码显示了如何把一个普通map转换到有序map。

1
2
3
4
5
6
7
8
9
SortedMap sortedMap = new TreeMap(new Comparator() {
@Override
public int compare(K k1, K k2) {
return k1.compareTo(k2);
}
});
sortedMap.putAll(map);

4. 根据map的值来排序

把map放入list然后排序也可以,但是这次我们比较的是Entry.getValue()。代码几乎和以前一样。

1
2
3
4
5
6
7
8
9
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {
@Override
public int compare(Entry e1, Entry e2) {
return e1.getValue().compareTo(e2.getValue());
}
});

这个问题我们也可以使用有序map1来解决,但是只有当值是唯一的。在这样的情况下你可以反转key=value 变成 value=key。这个解决方法有很强的局限性,因此我不推荐使用。

5. 初始化一个static的/不可变的Map

当你希望map保持不变,最好的方法是把它复制到一个不可变map中。这样的防御技术会帮助你创建不仅使用安全而且线程安全的map.

为了初始一个static的/不可变的map,我们可以使用static来初始化(如下文代码)。但是即使我们使用了static final来修饰map,我们仍然可以在初始化后操作map,比如:Test.map.put(3,”three”);因此它不是一个实际不可变的。为了创建一个不可变的map使用static初始化话,我们需要一个匿名类,然后将其复制到定义的地方,必须放在初始化的最后(因为不可以修改了嘛)。看第二个代码,如果你调用: Test.map.put(3,”three”);那么会抛出UnsupportedOperationException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
private static final Map map;
static {
map = new HashMap();
map.put(1, "one");
map.put(2, "two");
}
}
public class Test {
private static final Map map;
static {
Map aMap = new HashMap();
aMap.put(1, "one");
aMap.put(2, "two");
map = Collections.unmodifiableMap(aMap);
}
}

Guava库同样支持不同的方式创建static或者不可变的集合。了解怎么使用

6. HashMap, TreeMap, 和 Hashtable的不同

在java中Map接口的实现主要有3种HashMap, TreeMap, 和 Hashtable。最主要的区别如下:

  • 遍历的顺序。HashMap和Hashtable是不保证遍历的顺序的。主要是在恒定的时间不能保证不变。但是TreeMap 是有序的,它的排序是“自然排序”。
  • 键值权限。HashMap可以有null 键和null值,但是 Hashtable不允许有null键或null值。TreeMap 如果是使用自然排序的,那么使用null值会报NullPointerException异常。
  • 同步。只有Hashtable 是同步的,别的都不同步。然后如果是线程安全的,我们就不需要同步的,可以使用HashMap 来代替Hashtable 。

一个比较完整的比较:

HashMap Hashtable TreeMap
遍历有序 no no yes
null key-value yes-yes no-no no-yes
synchronized no yes no
时间性能 O(1) O(1) O(log n)
实现方式 buckets buckets red-black tree

{: rules=”groups”}

了解更多HashMap vs. TreeMap vs. Hashtable vs. LinkedHashMap

7. Map的反向查看/查找

有后时候我们需要 key-key 的集合。这就意味着map中的值也是和key一样是唯一(one-to-one map)。这时候就可以考虑一下创建“Map反向查看/查找”。所以我们可以根据值找到key。如:双向map,JDK默认是没有支持的。

Apache Common Collections 和 Guava 都提供了双向map的实现。它们分别叫BidiMapBiMap,它们都强制限制了key和value 的1:1 的关系。

8. Map的浅拷贝

在java中很多map的实现提供了一个方式来拷贝map。但是拷贝是不同步的。那就意味着,如果一个线程在拷贝map,另一个线程也可以修改map的数据。为了防止不同步的线程拷贝,一个方式是使用Collections.synchronizedMap()来防止。

1
Map copiedMap = Collections.synchronizedMap(map);

另一种比较有趣的方式是使用clone()方法来进行浅拷贝。但是这个方式不是java集合设计者(Josh Bloch)的推荐方式.Copy constructor versus cloning
,他说:

我常常在具体的类中提供一个公共的方法,因为大家都很喜欢使用。 ……当克隆性不管用的时候这将引起麻烦。而且这情况确实会发生 ……可克隆性是一个微弱的点,我想大家应该意识到它的局限性。

出于这个原因,我就不告诉你如何使用clone()方法复制map了。

9. 创建一个空map

如果Map是不可变的,这样用:

1
map = Collections.emptyMap();

否则使用构造函数,比如:

1
map = new HashMap();

参考文档