映 射

基本映射操作

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。

键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用 put方法, 第二个值就会取代第一个值。实际上,put 将返回用这个键参数存储的上一个值。

remove 方法用于从映射中删除给定键对应的元素。size 方法用于返回映射中的元素数。 要迭代处理映射的键和值, 最容易的方法是使用 forEach 方法。可以提供一个接收键和 值的 lambda 表达式。映射中的每一项会依序调用这个表达式。

scores.forEach((k, v) -> System.out.println("key=" + k + ", value: " + v)); 

 

程序清单 9-6 显示了映射的操作过程。首先将键 / 值对添加到映射中。然后,从映射中 删除一个键, 同时与之对应的值也被删除了。接下来, 修改与某一个键对应的值, 并调用 get 方法查看这个值。最后, 迭代处理条目集。

//程序清单9-6 map/MapTest.java 
package map;
​
import java.util.*;
​
/**
 * This program demonstrates the use of a map with key type String and value type Employee.
 * @version 1.11 2012-01-26
 * @author Cay Horstmann
 */
public class MapTest
{
   public static void main(String[] args)
   {
      Map<String, Employee> staff = new HashMap<>();
      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"));
​
      // print all entries
​
      System.out.println(staff);
​
      // remove an entry
​
      staff.remove("567-24-2546");
​
      // replace an entry
​
      staff.put("456-62-5527", new Employee("Francesca Miller"));
​
      // look up a value
​
      System.out.println(staff.get("157-62-7935"));
​
      // iterate through all entries
for (Map.Entry<String, Employee> entry : staff.entrySet())
      {
         String key = entry.getKey();
         Employee value = entry.getValue();
         System.out.println("key=" + key + ", value=" + value);
      }
   }
}

 

更新映射项

处理映射时的一个难点就是更新映射项。正常情况下,可以得到与一个键关联的原值, 完成更新, 再放回更新后的值。不过,必须考虑一个特殊情况, 即键第一次出现。下面来看 一个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词(word) 时,我 们将计数器增 1,如下所示:

counts.put(word, counts.get(word)+ 1) ;

这是可以的, 不过有一种情况除外:就是第一次看到 word时。在这种情况下,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) ;

将把 word 与 1 关联,否则使用 Integer::sum 函数组合原值和 1 (也就是将原值与 1 求和)。

 

映射视图

集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键 / 值对 集合, 或者是由键索引的值集合。 )不过, 可以得到映射的视图(View)— —这是实现了 Collection 接口或某个子接口的对象。

有 3 种视图: 键集、 值集合(不是一个集)以及键 / 值对集。键和键 / 值对可以构成一个 集, 因为映射中一个键只能有一个副本。下面的方法:

Set<K> keySet()

Collection<V> values()

Set<Map.Entry<K, V>> entrySet()

 

会分别返回这 3 个视图。(条目集的元素是实现 Map.Entry 接口的类的对象。 )

需要说明的是,keySet 不是 HashSet 或 TreeSet, 而是实现了 Set 接口的另外某个类的对 象。Set 接口扩展了 Collection接口。因此, 可以像使用集合一样使用keySet。

弱散列映射

WeakHashMap 使用弱引用(weak references) 保存键。 WeakReference 对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的 对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象 已经没有他人引用了,就将其回收。然而, 如果某个对象只能由 WeakReference 引用, 垃圾 回收器仍然回收它,但要将引用这个对象的弱引用放人队列中。WeakHashMap将周期性地检 查队列, 以便找出新添加的弱引用。一个弱引用进人队列意味着这个键不再被他人使用, 并 且已经被收集起来。于是, WeakHashMap将删除对应的条目。

链接散列集与映射

LinkedHashSet 和 LinkedHashMap类用来记住插人元素项的顺序。这样就可以避免在散歹IJ表 中的项从表面上看是随机排列的。当条目插入到表中时,就会并人到双向链表中(见图 9-11 )。

链接散列映射将用访问顺序, 而不是插入顺序, 对映射条目进行迭代。每次调用 get 或 put, 受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位 置会受影响, 而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中) 。要项 构造这样一个的散列映射表,请调用

LinkedHashMapcK, V>(initialCapacity, loadFactor, true)

访问顺序对于实现高速缓存的“ 最近最少使用” 原则十分重要。例如, 可能希望将访问 频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项 且表又已经满时,可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最 少使用的几个元素。

枚举集与映射

EmimSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例, 所以 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.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY); 

 

可以使用 Set 接口的常用方法来修改 EnumSet。

[注]  在 EnumSet 的 API 文档中, 将会看到 E extends Enum<E> 这样奇怪的类型参数。 简单地说, 它的意思是 “ E 是一个枚举类型。 ” 所有的枚举类型都扩展于泛型 Enum 类。 例如,Weekday 4广展 Enum<Weekday〉 。

标识散列映射

类 IdentityHashMap 有特殊的作用。在这个类中, 键的散列值不是用 hashCode 函数计算 的, 而是用 System.identityHashCode 方法计算的。 这是 Object.hashCode 方法根据对象的内 存地址来计算散列码时所使用的方式。而且, 在对两个对象进行比较时, IdentityHashMap 类 使用 ==, 而不使用 equals。

也就是说, 不同的键对象, 即使内容相同, 也被视为是不同的对象。 在实现对象遍历算 法(如对象串行化)时, 这个类非常有用, 可以用来跟踪每个对象的遍历状况。

视图与包装器

用如此多的接口和抽象类来实现数量并不多的具 体集合类似乎没有太大必要。然而,这两张图并没有展示出全部的情况。通过使用视图 ( views) 可以获得其他的实现了 Collection接口和 Map 接口的对象。映射类的 keySet 方法就 是一个这样的示例。初看起来, 好像这个方法创建了一个新集, 并将映射中的所有键都填进 去,然后返回这个集。但是, 情况并非如此。取而代之的是:keySet方法返回一个实现 Set 接口的类对象, 这个类的方法对原映射进行操作。这种集合称为视图。

轻量级集合包装器

Arrays 类的静态方法 asList 将返回一个包装了普通 Java 数组的 List 包装器。这个方法可 以将数组传递给一个期望得到列表或集合参数的方法。例如:

Card[] cardOeck = new Card[52];

List<Card> cardList = Arrays.asList(cardDeck):

 

返回的对象不是 ArrayList。它是一个视图对象, 带有访问底层数组的 get 和 set方 法。改变数组大小的所有方法(例如,与迭代器相关的 add 和 remove 方法)都会抛出一个 UnsupportedOperationException 异常。

asList方法可以接收可变数目的参数。例如:

List<String> names = Arrays.asList("A«iy", "Bob", "Carl");

这个方法调用

Col1ections.nCopies(n, anObject)

将返回一个实现了 List 接口的不可修改的对象, 并给人一种包含n个元素, 每个元素都像是 一个 anObject 的错觉。

[注]  Collections 类包含很多实用方法, 这些方法的参数和返回值都是集合。 不要将它 与 Collection 接口混淆起来。

子范围

可以为很多集合建立子范围(subrange) 视图。例如, 假设有一个列表 staff, 想从中取出 第 10 个 ~ 第 19 个元素。可以使用 subList方法来获得一个列表的子范围视图。

List group2 = staff.subList(10, 20);

第一个索引包含在内, 第二个索引则不包含在内。这与 String类的 substring 操作中的参 数情况相同。

可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。例如, 可以删除 整个子范围:

group2.clear(); // staff reduction

现在, 元素自动地从 staff 列表中清除了, 并且 group2 为空。

对于有序集和映射, 可以使用排序顺序而不是元素位置建立子范围。SortedSet 接口声明 了 3 个方法:

SortedSet<E> subSet(E from, E to)

SortedSet<E> headSet(E to)

SortedSet<E> tailSet(E from)

 

这些方法将返回大于等于 from 且小于 to 的所有元素子集。有序映射也有类似的方法:

SortedMap<K, V> subMap(K from, K to)

SortedMap<K, V> headMap(K to)

SortedMap<K, V> tailMap(K from)

 

返回映射视图, 该映射包含键落在指定范围内的所有元素。

不可修改的视图

Collections 还有几个方法, 用于产生集合的不可修改视图 (unmodifiable views)。这些视图对现有集合增加了一个运行时的检查。如果发现视图对集合进行修改, 就抛出一个异常, 同时这个集合将保持未修改的状态。

可以使用下面 8 种方法获得不可修改视图:

Collections.unmodifiableCollection 
Collections.unmodifiableList 
Collections.unmodifiableSet 
Collections.unmodifiableSortedSet 
Collections.unmodifiableNavigableSet 
Collections.unmodifiableMap 
Collections.unmodifiableSortedMap 
Collections.unmodifiableNavigableMap 

 

每个方法都定义于一个接口。 例如, Collections.unmodifiableList 与 ArrayList、 LinkedList 或者任何实现了 List 接口的其他类一起协同工作。

例如, 假设想要查看某部分代码, 但又不触及某个集合的内容, 就可以进行下列操作:

List<String> staff = new LinkedListoO;

...

lookAt(Collections.unmodifiableList(staff));

Collections-unmodifiableList 方法将返回一个实现 List 接口的类对象。 其访问器方法将从 staff集合中获取值。当然,lookAt 方法可以调用 List 接口中的所有方法,而不只是访问器。但 是所有的更改器方法(例如,add) 已经被重新定义为抛出一个 UnsupportedOperationException 异常,而不是将调用传递给底层集合。

不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff) 对集合进行修改。并且仍然可以让集合的元素调用更改器方法。

由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例 如, LinkedList 类有一些非常方便的方法,addFirst 和 addLast,它们都不是 List 接口的方法, 不能通过不可修改视图进行访问。

同步视图

如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如, 如果一个线程试图 将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。

类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合 类。例如, Collections 类的静态 synchronizedMap方法可以将任何一个映射表转换成具有同 步访问方法的 Map:

Map<String, Employee〉map = Collections.synchronizedMap(new HashMap<String, Employee>0);

现在,就可以由多线程访问 map对象了。像 get 和 put 这类方法都是同步操作的,即在 另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成。

 

受查视图

“ 受査” 视图用来对泛型类型发生问题时提供调试支持。实际上将错 误类型的元素混人泛型集合中的问题极有可能发生。例如:

ArrayList<String> strings = new ArrayListoO;

ArrayList rawList = strings; // warning only, not an error, for compatibility with legacy code

rawList.add(new DateO); // now strings contains a Date object!

 

这个错误的 add命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用 get方法,并将结果转化为 String 时,这个类才会抛出异常。

受査视图可以探测到这类问题。下面定义了一个安全列表:

List<String> safestrings = Collections.checkedList(strings,String,class);

视图的 add 方法将检测插人的对象是否属于给定的类。如果不属于给定的类,就立即抛 出一个 ClassCastException。这样做的好处是错误可以在正确的位置得以报告:

ArrayList rawList = safestrings;

rawList.add(new DateO);// checked list throws a ClassCastException

[警告]   受查视图受限于虚拟机可以运行的运行时检查。 例如, 对于 ArrayList <Pair <String», 由于虚拟机有一个单独的“ 原始” Pair 类, 所以,无法阻止插入 Pair<Date>

 

算 法

排序与混排

Collections 类中的 sort方法可以对实现了 List 接口的集合进行排序。

List<String> staff = new LinkedListo();

fill collection

Collections,sort(staff);

 

这个方法假定列表元素实现了 Comparable接口。如果想采用其他方式对列表进行排序,可 以使用List接口的 sort方法并传入一个 Comparator对象。可以如下按工资对一个员工列表排序i:

staff_sort(Comparator.comparingDouble(Employee::getSalary));

如果想按照降序对列表进行排序, 可以使用一种非常方便的静态方法 Collections.reverseOrder()。这个方法将返回一个比较器, 比较器则返回 b.compareTo(a)。例如,

staff.sort(Comparator.reverseOrder())

这个方法将根据元素类型的 compareTo方法给定排序顺序, 按照逆序对列表 staff进行排 序。同样,

staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())

将按工资逆序排序。

程序清单 9-7中的程序用 1 ~ 49 之间的 49 个 Integer 对象填充数组。然后,随机地打乱 列表,并从打乱后的列表中选前 6 个值。最后再将选择的数值进行排序和打印。

//程序澝单 9-7 shuffle/ShuffleTest.java
package shuffle;
​
import java.util.*;
​
/**
 * This program demonstrates the random shuffle and sort algorithms.
 * @version 1.12 2018-04-10
 * @author Cay Horstmann
 */
public class ShuffleTest
{
   public static void main(String[] args)
   {
      var numbers = new ArrayList<Integer>();
      for (int i = 1; i <= 49; i++)
         numbers.add(i);
      Collections.shuffle(numbers);
      List<Integer> winningCombination = numbers.subList(0, 6);
      Collections.sort(winningCombination);
      System.out.println(winningCombination);
   }
}

 


二分查找

Collections类的 binarySearch方法实现了这个算法。注意, 集合必须是排好序的, 否则 算法将返回错误的答案。要想查找某个元素,必须提供集合(这个集合要实现 List 接口, 下 面还要更加详细地介绍这个问题)以及要查找的元素。如果集合没有采用 Comparable 接口的 compareTo方法进行排序, 就还要提供一个比较器对象。

i = Collections.binarySearch(c, element);

i = Collections.binarySearch(c, element, comparator);

如果 binarySearch方法返回的数值大于等于 0, 则表示匹配对象的索引。也就是说, c.get(i) 等于在这个比较顺序下的 element。如果返回负值, 则表示没有匹配的兀素。但是, 可以利用返回值计算应该将 element 插人到集合的哪个位置, 以保持集合的有序性。插人的 位置是

insertionPoint = -i -1 ;

这并不是简单的-i ,因为 0值是不确定的。也就是说, 下面这个操作:

if (i < 0)

  c.add(-i - 1 , element);

将把元素插人到正确的位置上。

只有采用随机访问,二分査找才有意义。如果必须利用迭代方式一次次地遍历链表的一 半元素来找到中间位置的元素,二分査找就完全失去了优势。因此,如果为 binarySearch 算 法提供一个链表,它将自动地变为线性查找。

简单算法

在 Collections 类中包含了几个简单且很有用的算法。前面介绍的查找集合中最大元素的 示例就在其中。另外还包括:将一个列表中的元素复制到另外一个列表中;用一个常量值填 充容器;逆置一个列表的元素顺序。

 

批操作

很多操作会“ 成批” 复制或删除元素。以下调用

coll1.removeAll(coll2);

将从 coll1中删除 coll2中出现的所有元素。与之相反,

coll1.retainAll(coll2); 会

从 coll1 中删除所有未在 C0112 中出现的元素。

 

集合与数组的转换

如果需要把一个数组转换为集合,Arrays.asList 包装器可以达到这个目的。例如:

String[] values = . . .;

HashSet<String> staff = new HashSeto(Arrays.asList(values));

从集合得到数组会更困难一些。当然,可以使用 toArray方法:

Object[] values = staff.toArray();

不过,这样做的结果是一个对象数组。尽管你知道集合中包含一个特定类型的对象,但 不能使用强制类型转换:

String[] values = (String[]) staff.toArray();// Error!

toArray方法返回的数组是一个 Object[] 数组, 不能改变它的类型。实际上,必须使用 toArray方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。这样一来, 返回的数 组就会创建为相同的数组类型:

String[] values = staff.toArray(new String[]);

如果愿意,可以构造一个指定大小的数组:

staff.toArray(new String[staff.size()]);

在这种情况下,不会创建新数组。

 

遗留的集合

Hashtable 类

Hashtable 类与 HashMap类的作用一样,实际上,它们拥有相同的接口。与 Vector 类的 方法一样。Hashtable 的方法也是同步的。如果对同步性或与遗留代码的兼容性没有任何要求, 就应该使用 HashMap。如果需要并发访问, 则要使用 ConcurrentHashMap。

枚举

遗留集合使用 Enumeration接口对元素序列进行遍历。Enumeration接口有两个方法, 即 hasMoreElements 和 nextElement。这两个方法与 Iterator 接口的 hasNext 方法和 next 方法十 分类似。 例如,Hashtable 类的 elements 方法将产生一个用于描述表中各个枚举值的对象:

Enuineration<Einployee> e = staff.elements(); 
​
whi1e (e.HasMoreElements()) {
​
 Eniployee e = e.nextElement();
​
...
​
}

 

有时还会遇到遗留的方法,其参数是枚举类型的。静态方法 Collections.enumeration将产 生一个枚举对象,枚举集合中的元素。例如:

List<InputStream> streams = . . .;

Sequencel叩utStream in = new SequencelnputStream(Collections.enumeration(streams));

// the SequencelnputStream constructor expects an enumeration

 

属性映射

属性映射(property map) 是一个类型非常特殊的映射结构。它有下面 3 个特性:

•键与值都是字符串。

•表可以保存到一个文件中,也可以从文件中加载。

•使用一个默认的辅助表。

实现属性映射的 Java 平台类称为 Properties。

属性映射通常用于程序的特殊配置选项。

 

从 1.0 版开始,标准类库中就包含了 Stack类,其中有大家熟悉的 push方法和 pop方法。 但是, Stack 类扩展为 Vector 类,从理论角度看,Vector 类并不太令人满意,它可以让栈使 用不属于栈操作的 insert 和 remove方法,即可以在任何地方进行插入或删除操作,而不仅仅 是在栈顶。

位集

java平台的 BitSet类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更 为合适)。如果需要高效地存储位序列(例如,标志)就可以使用位集。由于位集将位包装在 字节里,所以,使用位集要比使用 Boolean对象的 ArrayList 更加高效。

BitSet 类提供了一个便于读取、设置或清除各个位的接口。使用这个接口可以避免屏蔽 和其他麻烦的位操作。如果将这些位存储在 int 或丨ong变量中就必须进行这些繁琐的操作。

例如,对于一个名为 bucketOfBits 的 BitSet,

bucketOfBits.get( i)

如果第 i 位处于“ 开” 状态,就返回 true; 否则返回 false。同样地,

bucketOfBits.set(i)

将第 i 位置为“ 开” 状态。最后,

bucketOfBits.clear(i)

将第 i 位置为“ 关” 状态。

这里并不想深人程序的细节,关键是要遍历一个拥有 200万个位的位集。首先将所有的 位置为“ 开” 状态,然后,将已知素数的倍数所对应的位都置为“ 关” 状态。经过这个操作 保留下来的位对应的就是素数。 程序清单 9-8 是用 Java 程序设计语言实现的这个算法程序, 而程序清单 9-9mmC++ 实现的这个算法程序。

//程序清单 9-8 sieve/Sieve.java 
package sieve;
​
import java.util.*;
​
/**
 * This program runs the Sieve of Erathostenes benchmark. It computes all primes up to 2,000,000.
 * @version 1.21 2004-08-03
 * @author Cay Horstmann
 */
public class Sieve
{
   public static void main(String[] s)
   {
      int n = 2000000;
      long start = System.currentTimeMillis();
      BitSet b = new BitSet(n + 1);
      int count = 0;
      int i;
      for (i = 2; i <= n; i++)
         b.set(i);
      i = 2;
      while (i * i <= n)
      {
         if (b.get(i))
         {
            count++;
            int k = 2 * i;
            while (k <= n)
            {
               b.clear(k);
               k += i;
            }
         }
         i++;
      }
      while (i <= n)
      {
         if (b.get(i)) count++;
         i++;
      }
      long end = System.currentTimeMillis();
      System.out.println(count + " primes");
      System.out.println((end - start) + " milliseconds");
   }
}
​

 

//程序清单 9-9 sieve/Sieve.cpp
/**
   @version 1.21 2004-08-03
   @author Cay Horstmann
*/
​
#include <bitset>
#include <iostream>
#include <ctime>using namespace std;
​
int main()
{  
   const int N = 2000000;
   clock_t cstart = clock();
​
   bitset<N + 1> b;
   int count = 0;
   int i;
   for (i = 2; i <= N; i++)
      b.set(i);
   i = 2;
   while (i * i <= N)
   {  
      if (b.test(i))
      {  
         count++;
         int k = 2 * i;
         while (k <= N)
         {  
            b.reset(k);
            k += i;
         }
      }
      i++;
   }
   while (i <= N)
   {  
      if (b.test(i))
         count++;
      i++;
   }
​
   clock_t cend = clock();
   double millis = 1000.0 * (cend - cstart) / CLOCKS_PER_SEC;
​
   cout << count << " primes\n" << millis << " milliseconds\n";
​
   return 0;
}

 


 

posted on 2020-08-15 21:13  ♌南墙  阅读(211)  评论(0)    收藏  举报