映射
集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素, 需要有要查找元素的精确副本。这不是一种非常通用的査找方式。通常, 我们知道某些键的信息,并想要查找与之对应的元素。 映射(map) 数据结构就是为此设计的。映射用来存放键 / 值对。如果提供了键, 就能够查找到值。
基本映射操作
Java 类库为映射提供了两个通用的实现:HashMap 和 TreeMap。这两个类都实现了Map 接口。
散列映射对键进行散列, 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。应该选择散列映射还是树映射呢? 与集一样, 散列稍微快一些, 如果不需要按照排列顺序访问键,就最好选择散列。
Map<String, Employee> staff = new HashMap<>();// HashMap implements Map
Employee harry = new Employee('Harry Hacker");
staff.put(”987-98-9996", harry);
每当往映射中添加对象时, 必须同时提供一个键。在这里,键是一个字符串,对应的值是 Employee 对象。要想检索一个对象, 必须使用(因而,必须记住)一个键
String id = "987-98-9996";
e = staff,get(id);// gets harry
如果在映射中没有与给定键对应的信息, get 将返回 null。
null 返回值可能并不方便。有时可以有一个好的默认值, 用作为映射中不存在的键。然后使用 getOrDefault 方法
键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用 put 方法, 第二个值就会取代第一个值。实际上,put 将返回用这个键参数存储的上一个值。
remove 方法用于从映射中删除给定键对应的元素。size 方法用于返回映射中的元素数。要迭代处理映射的键和值, 最容易的方法是使用 forEach 方法。可以提供一个接收键和值的 lambda 表达式。
scores.forEach((k, v) ->
System.out.println("key=" + k + ", value:" + v));
更新映射项
处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值,完成更新, 再放回更新后的值。不过,必须考虑一个特殊情况, 即键第一次出现。
键第一次出现时,调用get方法会返回 null, 因此会出现一个 NullPointerException 异常。
作为一个简单的补救, 可以使用 getOrDefault 方法:
counts,put(word, counts.getOrDefault(word, 0)+ 1);
另一种方法是首先调用 putlfAbsent 方法。只有当键原先存在时才会放入一个值。
counts.putlfAbsent(word, 0);
counts.put(word, counts.get(word)+ 1); // Now we know that get will succeed
不过还可以做得更好。merge 方法可以简化这个常见的操作。如果键原先不存在,下面的调用:
counts.merge(word, 1, Integer::sum);
映射视图
集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键 / 值对
集合, 或者是由键索引的值集合。) 不过, 可以得到映射的视图(View )—这是实现了Collection 接口或某个子接口的对象。
有 3 种视图: 键集、 值集合(不是一个集) 以及键 / 值对集。键和键 / 值对可以构成一个集, 因为映射中一个键只能有一个副本。 下面的方法:
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
需要说明的是, keySet 不是 HashSet 或 TreeSet, 而是实现了 Set 接口的另外某个类的对象。Set 接口扩展了 Collection 接口。因此, 可以像使用集合一样使用 keySet。
Set<String> keys = map.keySet();
for (String key : keys)
{
do something with key
}
想同时查看键和值,可以通过枚举条目来避免查找值。
f
or (Map.Entry<String, Employee> entry : staff.entrySet())
{
String k = entry.getKey();
Employee v = entry.getValue();
do something with k, v
}
原先这是访问所有映射条目的最高效的方法。如今,只需要使用 forEach 方法
counts.forEach((k,v) -> {
do somethingwith k, v
});
如果在键集视图上调用迭代器的 remove方法, 实际上会从映射中删除这个键和与它关联的值。不过,不能向键集视图增加元素。
链接散列集与映射
LinkedHashSet 和 LinkedHashMap类用来记住插人元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并人到双向链表中。
链接散列映射将用访问顺序, 而不是插入顺序, 对映射条目进行迭代。每次调用 get 或put, 受到影响的条目将从当前的位置删除, 并放到条目链表的尾部(只有条目在链表中的位置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。要项构造这样一个的散列映射表, 请调用
LinkedHashMap<K, V>(initialCapacity, loadFactor, true)
访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。例如, 可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时, 可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
下面的高速缓存可以存放 100 个元素:
Map<K, V> cache = new
LinkedHashMapo(128, 0.75F, true)
{
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
{
return size() > 100;
}
}();
另外,还可以对 eldest 条目进行评估,以此决定是否应该将它删除。例如,可以检査与这个条目一起存在的时间戳。
标识散列映射
类 IdentityHashMap 有特殊的作用。在这个类中, 键的散列值不是用 hashCode 函数计算的, 而是用 System.identityHashCode 方法计算的。 这是 Object.hashCode 方法根据对象的内地址来计算散列码时所使用的方式。而且, 在对两个对象进行比较时, IdentityHashMap 类使用 ==, 而不使用 equals。
也就是说, 不同的键对象, 即使内容相同, 也被视为是不同的对象。 在实现对象遍历算法(如对象串行化)时, 这个类非常有用, 可以用来跟踪每个对象的遍历状况。