Java 集合类“二王”中的另一个,就是 Map 接口。
Map,意为映射表,表的元素是一个又一个的 Entry
对象。
每个 Entry 对象为一键值对(Key-Value pair),key 与 value 可以是任意的 Object,Map 提供接口方法通过 key 去查找 value。
由于要根据 key 来区分不同的 Entry:key 不能重复,不能对同一个 key 存放两个值,而 value 可以重复(包括 null
)。
如对同一个 key 两次调用 put(),第二次的值会取代第一个值,put() 将返回用这个键参数存储的上一个值。
Map 不继承 Collection 接口:Map 不是集合,如继承自 Collection:键值对何去何从?作为一组对象存放于 collection?这样子会加大映射表的负担,增加操作的复杂度。
接口方法
JDK 为映射表的 Key 和 Value 规定了两个范型:
1 | package java.util; |
集合框架 JCF 并没有将映射表本身视为一个集合;而其他数据结构框架会将其视为对(pair)集合,或用键作为索引的值的集合。
可以通过一些已经实现的方法获得映射表的视图,这些视图实现了 Collection 接口或者它的子接口:
1 | public interface Map<K,V> { |
在这里,我们需要说一下 hashCode()
和 equals()
这两个重要的方法。
1 | { |
hashCode() & equals()
如果两个对象相等(equals() 返回 true
),则散列值 hash code 一定相同;如两个对象 hash code 值相等,对象则不一定相等。
- hash code 相等即键值对的 hash value 相等;
- hash value 相等并不一定能得出键值对相等,不同的键值对可能会得出一样的散列值,这就是哈希冲突,又称散列冲突。
equals(),表示某个类逻辑上的相等,不同于 ==
判断内存地址是否相等。
有需要的话,类需要重写 equals() 实现该类对象的逻辑比较判断。
现在,假设有某一个类:
如果在散列表数据结构(HashSet, HashTable, HashMap 等)中用到该类,hashCode() 与 equals() 有关系,否则两者没关系。
该类作为散列表的 key 的话,必须重写 hashCode()。
如果有必要重写 hashCode() 或 equals(),最好两个一起重写;如果重写了 equals(),也应重写 hashCode() 方法:
- 重写 hashCode() 之后,返回值可能相同,但 identityHashCode() 不会相同:因为 identityHashCode() 返回对象物理地址产生的 hash 值
重写 equals() 后应检查是否符合:
- 对称性
- 传递性
- 一致性
- 自反性
- 非空性
注:散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。
如不需要按照排序访问键,就最好选择散列。如果不能通过 get() 得到,返回的则是 null。
New from JDK 1.8
1 | public interface Map<K,V> { |
由于从 Java 8 开始引入了函数式编程,因此 Map 也实现了与此相关的新接口:
1 | public interface Map<K,V> { |
内部接口 Map.Entry
Entry 沿用了 Map 接口的范型
1 | package java.util; |
Map 中的 keySet,values,entrySet 与其息息相关。
子类及接口
AbstractMap
映射表抽象类,为 Map 的实现类预先实现了许多 Map 接口声明的方法。
不过,AbstractMap 所实现的方法大多依赖于 entrySet();而 AbstractMap 并未实现 entrySet(),它选择交给子类实现。
AbstractMap 提供了两个实现了 Map.Entry 接口的静态内部类:
1 | public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable { |
1 | public static class SimpleImmutableEntry<K,V> implements Entry<K,V>, java.io.Serializable { |
SortedMap 接口
继承自 Map
- 接口暴露了用于排序的比较器对象,且定义的方法可以获得集合的子集视图
- 要求其实现类必须保证内部元素可以按自然或 Comparator 的顺序排列
NavigableMap 接口
扩展自 SortedMap,包含用于在映射表中查找和遍历的方法。
1 | public interface NavigableMap<K,V> extends SortedMap<K,V> { |
实现类
概括一下常用的非线程安全的 Map 实现类:
HashMap
:存储 k-v 关联的数据结构,对 key 进行散列
TreeMap
TreeMap 键值有序排列的映射表,元素整体以红黑树形式排序。
TreeMap 实现了 NavigableMap 接口,内部 Entry 为静态类 TreeMap.Entry。
LinkedHashMap
可记住 k-v 项添加次序(有序)的映射表。
LinkedHashMap 使用访问顺序对映射表条目进行迭代,而非插入顺序。
1 | LinkedHashMap<K, V>(initialCapacity, // 给定的初始容量 |
当 entry 插入到表中时,会被并入双向链表中。每次调用 get() 或 put() 的时候,受影响(被改动过的)的 entry 会从当前位置被删除,并放到 entry 链表尾部。
只有 entry 在链表中的位置受影响,散列表中桶的位置不受影响。
访问顺序对于实现高速缓存的“最近最少使用”(LRU)原则十分重要
- 在实际应用中,访问频繁的元素会被置于内存,不频繁的被放入数据库
- 在表中找不到元素项且表已满时,可将迭代器加入表中,并将枚举的前几个元素(近期最少使用的元素)删除
根据 LinkedHashMap 的属性,可以实现 LRU:
- 覆盖 LinkedHashMap 的
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
- 每当方法返回 true 时,添加一个新条目,从而删除 eldest 条目
- 另还可对 eldest 进行评估,以此决定是否应该将它删除
EnumMap
键值属于枚举类型的映射表,底层是数组,直接且高效。
WeakHashMap
对于弱引用对象来说,垃圾回收器将其回收,要先将引用这个对象的弱引用放入队列中。
当弱引用进入队列,意味着该引用不再被他人使用。
WeakHashMap 使用弱引用保存键,并周期性检查队列,以便找出新添加的弱引用,保证垃圾回收时 key 能被回收,随即回收 value,从而使对应的 entry 消失,一定程度避免内存溢出。
IdentityHashMap
IdentityHashMap 的键的散列值是使用 System.IdentityHashCode()
计算的,根据对象的内存地址来计算散列码。
因此比较两个对象的时候,IdentityHashMap 使用 ==
,而不是 equals()。
也就是说,不同的 key 即使内容相同,也被视为不同的对象。
所以 IdentityHashMap 允许重复的 key。