容器--Map和AbstractMap
一、前言
前面我们介绍了Collection接口的定义及一系列实现,并重点介绍了List体系下的一些实现,对于Collection来说,其实还有Set系列同样很重要,但由于Set是依赖于Map实现的,所以我们在这里先介绍Map.
Collection的特点是存储一类元素的集合,而Map则描述了一组映射关系的集合,即我们常说的key-value结构。这类容器的特点是容器中的每一项都是一个键值对,键的值不能重复,但允许有null键和null值的存在,一个键只能对应一个值,但多个不同的键可以对应于同一个值。
由于键值的惟一性,所以其key集合就天然是一个Set, 因此,JDK的set实现也正是基于这样的思想,使用了Map中的key集合来实现。
二、主要方法
虽然存储的元素不一样,但同为集合,Map和Collection之间还是有很多功能相似甚至是相同的功能,下面一个表格进行对比如下:
方法描述 | Map方法名 | Collection方法名 |
容器中元素个数 | size() | size() |
判断容器是否为空 | isEmpty() | isEmpty() |
添加元素 | put(K key, V value) | add(E e) |
批量添加元素 |
putAll(Map<K, V> map) |
addAll(Collection<E> collection) |
删除元素 | remove(Object key) |
remove(Object obj) removeAll(Collection<E> collection) retainAll(Collection<E> collection) |
清空列表 | clear() | clear() |
判断元素是否存在 |
containsKey(Object key) containsValue(Object value) |
contains(Object e) containsAll(Collection<E> e) |
遍历 |
keySet() entrySet() values() |
iterator() |
比较 | equals(Object obj) | equals(Object o) |
求hash | hashCode() | hashCode() |
根据key查找value | V get(Object key) | 无对应方法 |
转化为数组 | 无对应方法 |
toArray() toArray(T[] t) |
从上表我们可以看到,这两个容器都提供了对容器的增加,修改,删除及遍历的功能,对于Map来说,由于其特有的映射结构提供了根据key查找value的功能,而Collection由于是一组类型相同的元素,所以提供了转化为toArray的方法。
三、AbstractMap的实现原理
AbstractMap作为Map的抽象实现类,提供了绝大多数方法的实现。其中的惟一个未实现的方法是entrySet()方法,返回一个元素类型为Map.Entry的Set.
1)由于Set属于Collection,通过上表的比较我们可以知道Collection和Map的绝大部分功能是相似的,所以对于这些功能的实现就可以根据entrySet的相应方法来实现。比如对于删除方法,JDK是这样实现的:
1 public V remove(Object key) { 2 Iterator<Entry<K,V>> i = entrySet().iterator(); 3 Entry<K,V> correctEntry = null; 4 if (key==null) { 5 while (correctEntry==null && i.hasNext()) { 6 Entry<K,V> e = i.next(); 7 if (e.getKey()==null) 8 correctEntry = e; 9 } 10 } else { 11 while (correctEntry==null && i.hasNext()) { 12 Entry<K,V> e = i.next(); 13 if (key.equals(e.getKey())) 14 correctEntry = e; 15 } 16 } 17 18 V oldValue = null; 19 if (correctEntry !=null) { 20 oldValue = correctEntry.getValue(); 21 i.remove(); 22 } 23 return oldValue; 24 }
2)不支持put方法的实现,相应的,putAll方法由于调用了put,所以这个方法也不支持。
3)根据定义,map中的key和value都有可能为null,所以判断一个map中是否包含某个key,不应该根据get(key) == null来判断 ,而应该调用containsKey(key)来判断。
4)keySet()和values()使用了匿名内部类的方式来实现,主要的实现逻辑是重写了抽象容器类的iterator方法,将其遍历转化为对于key和值的遍历。
三、总结
AbstractMap通过将entrySet定义为抽象方法的方式,巧妙的将具体的实现和存储逻辑交给子类实现,这样实现者可以专注于元素的存储,另外,通过学习AbstractMap中的方法实现,我们可以借鉴其map的遍历方式,正确的应该是先找到entrySet,就像下面这样:
1 public boolean containsKey(Object key) { 2 Iterator<Map.Entry<K,V>> i = entrySet().iterator(); 3 if (key==null) { 4 while (i.hasNext()) { 5 Entry<K,V> e = i.next(); 6 if (e.getKey()==null) 7 return true; 8 } 9 } else { 10 while (i.hasNext()) { 11 Entry<K,V> e = i.next(); 12 if (key.equals(e.getKey())) 13 return true; 14 } 15 } 16 return false; 17 }
在遍历时,我们使用iterator(), 而不是使用for(String key : map.keySet())的方式。因为这种方式的问题在于,如果按抽象类的实现方式,map.keySet()相当于是需要对整个map进行遍历,而在for循环中, 对于map.get(key)来说,实际上又需要遍历整个map,循环多少次,就遍历多少次,这确实是非常低效率的。
当然,也许HashMap并不会在get(key)时遍历整个map,但这并不能保证其它的实现不是这样,所以,iterator才是正理。
明天会继续学习HashMap.