Java核心技术 卷1-总结-14
- 映射
- 更新映射项
- 弱散列映射
- 链接散列集与映射
- 枚举集与映射
- 视图与包装器
- 轻量级集合包装器
映射
更新映射项
处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个特殊情况,即键第一次出现。下面这个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词(word)时,将计数器加1,如下所示:
counts.put(word, counts.get(word) + 1);
这是可以的,不过有一种情况除外:就是第一次看到word时。在这种情况下,get
会返回 null
,因此会出现一个 NullPointerException
异常。作为一个简单的补救,可以使用getOrDefault
方法:
counts.put(word, counts.getOrDefault(word, 0) + 1);
另一种方法是首先调用putlfAbsent
方法。只有当键原先存在时才会放入一个值。
counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1); // Now we know that get will succeed
merge
方法可以简化这个常见的操作。如果键原先不存在,下面的调用:
counts.merge(word,1, Integer::sum);
将把word与1关联,否则使用Integer:sum
函数组合原值和1(也就是将原值与1求和)。
弱散列映射
假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键/值对无法从映射中删除。垃圾回收器也不能够删除它。WeakHashMap
类可以解决这个问题。
垃圾回收器跟踪活动的对象,只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此,需要由程序负责从长期存活的映射表中删除那些无用的值。或者使用WeakHashMap
完成这件事情。当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
这种机制的内部运行情况如下:WeakHashMap
使用弱引用(weak references)保存键。弱引用对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由弱引用引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap
将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap
将删除对应的条目。
链接散列集与映射
LinkedHashSet
和LinkedHashMap
类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。
例如,在程序清单9-6中包含下列映射表插入的处理:
例如:
Map<String,Employee>staff = new LinkedHashMap<>();
staff.put("144-25-5464", new Employee("Amy Lee"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
然后,staff.keySet().iterator()
以下面的次序枚举键:
144-25-5464
567-24-2546
157-62-7935
456-62-5527
并且staff.values().iterator()
以下列顺序枚举这些值:
Amy Lee
Harry Hacker
Gary Cooper
Francesca Cruz
链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。要项构造这样一个的散列映射表,需要调用:
LinkedHashMap<K, V>(initialCapacity, loadFactor, true);
访问顺序对于实现高速缓存的"最近最少使用"原则十分重要。 例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
可以让这一过程自动化。即构造一个LinkedHashMap的子类,然后覆盖下面这个方法:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
每当方法返回true 时,就添加一个新条目,从而导致删除 eldest条目。例如,下面的高速缓存可以存放100个元素:
Map<K,V> cache = new LinkedHashMap<>(128,0.75F,true) {
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size()>100;
}
}();
另外,还可以对eldest条目进行评估,以此决定是否应该将它删除。例如,可以检查与这个条目一起存在的时间戳。
枚举集与映射
EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。
EnumSet类没有公共的构造器。可以使用静态工厂方法构造这个集:
enum Weekday [ MONDAY,TUESDAY,wEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY };
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY,Weekday.FRIDAY);EnumSet<Weekday>
mwf = EnumSet.of(Weekday.M(NDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
可以使用Set接口的常用方法来修改EnumSet。
EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
视图与包装器
通过使用视图(views)可以获得其他的实现了Collection接口和Map接口的对象。 映射类的keySet方法就是一个这样的示例。初看起来,好像这个方法创建了一个新集,并将映射中的所有键都填进去,然后返回这个集。但是,情况并非如此。取而代之的是:keySet方法返回一个实现Set 接口的类对象,这个类的方法对原映射进行操作。这种集合称为视图。视图技术在集框架中有许多非常有用的应用。
轻量级集合包装器
Arrays类的静态方法asList
将返回一个包装了普通Java数组的List包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。例如:
Card[] cardDeck = new Card[52];
List<Card> cardList = Arrays.asList(cardDeck);
返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的get
和set
方法。改变数组大小的所有方法(例如,与迭代器相关的add和remove方法)都会抛出一个Unsupported OperationException
异常。
asList方法可以接收可变数目的参数。例如:
List<String> names = Arrays.asList("Amy","Bob","Carl");
这个方法调用
Collections.nCopies(n,anObject)
将返回一个实现了List
接口的不可修改的对象,并给人一种包含n
个元素,每个元素都像是一个anObject
的错觉。
例如,下面的调用将创建一个包含100
个字符串的List,每个串都被设置为“DEFAULT”
:
List<String> settings = Collections.nCopies(100, "DEFAULT");
存储代价很小。这是视图技术的一种巧妙应用。
注意:Collections类包含很多实用方法,这些方法的参数和返回值都是集合。不要与Collection接口混淆。
如果调用下列方法
Collections.singleton(anObject)
则将返回一个视图对象。这个对象实现了Set
接口(与产生Lis
t的ncopies
方法不同)。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。singletonList
方法与singletonMap
方法类似。
类似地,对于集合框架中的每一个接口,还有一些方法可以生成空集、列表、映射,等等。特别是,集的类型可以推导得出:
Set<String> deepThoughts = Collections.emptySet();
子范围
可以为很多集合建立子范围(subrange)视图。例如,假设有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得一个列表的子范围视图。
List group2=staff.subList(10,20);