九、集合

一、JAVA集合框架

  与现代的数据结构类库的常见情况一样, Java 集合类库也将接口( interface) 与 实 现 (implementation) 分离。

 队列通常有两种实现方式: 一种是使用循环数组。另一种是使用链表。

 当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此, 只 有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。

Queue<Customer> expressLane=new CircularArrrayQueue<>(100);
expressLane.add(new Customer("Harry"));

利用这种方式,一旦改变了想法, 可以轻松地使用另外一种不同的实现。只需要对程序 的一个地方做出修改, 即调用构造器的地方。

循环数组要比链表更高效,因此多数人优先选择循环数组。然而, 通常这样做也需要 付出一定的代价。 循环数组是一个有界集合, 即容量有限。如果程序中要收集的对象数量没有上限, 就最 好使用链表来实现。

Collection接口

在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法:

public interface Collection<E>
{
    boolean add(E element);
    Iterator<E> iterator();
}

迭代器

Iterator 接口包含 4 个方法:

public interface Iterator<E>
{
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

“ for each” 循环可以与任何实现了 Iterable 接口的对象一起工作

Collection 接口扩展了 Iterable 接口。

  因此, 对于标准类库中的任何集合都可以使用“ for each” 循环。 在 Java SE 8中,甚至不用写循环。可以调用 forEachRemaining 方法并提供一 lambda 表达式(它会处理一个元素)。 将对迭代器的每一个元素调用这个 lambda 表达式,直到再没有元素为止。

iterator.forEachRemaining(element->do something with element)

元素被访问的顺序取决于集合类型。 如果对 ArrayList 进行迭代, 迭代器将从索引 0开 始,每迭代一次,索引值加 1 然而,如果访问 HashSet 中的元素, 每个元素将会按照某种 随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知 元素被访问的次序。这对于计算总和或统计符合某个条件的元素个数这类与顺序无关的操作 来说,并不是什么问题。

  因此,应该将 Java 迭代器认为是位于两个元素之间。 当调用 next 时,迭代器就越过下 一个元素,并返回刚刚越过的那个元素的引用.

 Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素.

 更重要的是,对 next 方法和 remove 方法的调用具有互相依赖性。如果调用 remove 之前 没有调用 next 将是不合法的。

泛型使用方法

 当然, 如果实现 Collection 接口的每一个类都要提供如此多的例行方法将是一件很烦人的 事情。为了能够让实现者更容易地实现这个接口,Java 类库提供了一个类 AbstractCollection。

集合框架中的接口

集合有两个基本接口:Collection 和 Map。

List 是一个有序集合(  元 素 会 增 加 到 容 器 中 的 特 定 位 置。可 以 采 用 两种方式访问元素:使用迭代器访问, 或者使用一个整数索引来访问。后一种方法称为随机访问(random access ), 因为这样可以按任意顺序访问元素。与之不同, 使用迭代器访问时,必须顺序地访问元素。

List 接口定义了多个用于随机访问的方法:

void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)

Listlterator 接口是 Iterator 的一个子接口。

实际中有两种有序集合,其性能开销有很大差异。由数组支持的有序集合可以快速地随机访问,因此适合使用 List 方法并提供一个 整数索引来访问。与之不同, 链表尽管也是有序的, 但是随机访问很慢,所以最好使用迭代 器来遍历。如果原先提供两个接口就会容易一些了。

为了避免对链表完成随机访问操作, Java SE 1.4 引入了一个标记接口 RandomAccess。 这个接口不包含任何方法, 不过可以用它来测试一个特定的集合是否支持高效的随机访问

Set 接口等同于 Collection 接口,不过其方法的行为有更严谨的定义。集(set) 的 add方 法不允许增加重复的元素。要适当地定义集的 equals 方法:只要两个集包含同样的元素就认 为是相等的,而不要求这些元素有同样的顺序。hashCode 方法的定义要保证包含相同元素的 两个集会得到相同的散列码。

SortedSet 和 SortedMap 接口会提供用于排序的比较器对象,这两个接口定义了可以得到 集合子集视图的方法。

二、具体的集合

 

链表

 在 Java 程序设计语言中,所有链表实际上都是双向链接的 (doubly linked) —即每个结点还存放着指向前驱结点的引用。

链 表 是 一 个 有 序 集 合。每个对象的位置十分重要。

LinkedList.add 方法将对象添加到链表的尾部。但是, 常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的, 所以这种依赖于位置 的 add 方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例 如, 下一节将要讨论的集(set) 类型,其中的元素完全无序。因此, 在 Iterator 接口中就没有 add 方法。相反地,集合类库提供了子接口 Listlterator, 其 中 包 含 add 方 法:

interface ListIterator<E> extends Iterator<E>
{
void add(E element);
}

与 Collection.add 不同, 这个方法不返回 boolean 类型的值, 它假定添加操作总会改变 链表

另外, Listlterator 接口有两个方法, 可以用来反向遍历链表。

E previous()
boolean hasPrevious()

LinkedList 类的 listlterator 方法返回一个实现了 Listlterator 接口的迭代器对象。Add 方法在迭代器位置之前添加一个新对象

List<String> staff=new LinkedList<>();
ListIterator<String> iter=staff.listIterator();
staff.add("Bob");

add 方法只依赖于迭代器的位置, 而 remove 方法依赖于迭代器的状态。

set 方法用一个新元素取代调用 next 或 previous 方法返回的上一个元素。

如果迭代 器发现它的集合被另一个迭代器修改了, 或是被该集合自身的方法修改了, 就会抛出一个 ConcurrentModificationException 异常。

为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的 迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。

有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删 除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致。如果不一致, 抛出一个 Concurrent ModificationException 异常。

对于并发修改列表的检测肴一个奇怪的例外。链表只负责跟踪对列表的结构性修 改,例如,添加元素、 删除元素。set 方法不被视为结构性修改。可以将多个迭代器附加 给一个链表,所有的迭代器都调用 set 方法对现有结点的内容进行修改

Collection的 toString 方法调用了 所有元素的 toString, 并产生了一个很长的格式为[A,B,C] 的字符串。链表不支持快速地随机访问。在程序需要采用整数索引访问元素时,程序员通常不选用链表。 尽管如此, LinkedList 类还是提供了一个用来访问某个特定元素的 get 方法.

for (int i = 0; i < list.sizeO;i++)
do something with list.get(i);

每次査找一个元素都要从列表的头部重新开始搜索。LinkedList 对象根本不做任何缓存 位置信息的操作。

get 方法做了微小的优化:如果索引大于 size() / 2 就从列表尾端开始搜索元素。

列表迭代器接口还有一个方法,可以告之当前位置的索引。实际上,从概念上讲, 由于 Java 迭代器指向两个元素之间的位置, 所以可以同时产生两个索引:nextlndex 方法返回下一 次调用 next 方法时返回元素的整数索引;previouslndex 方法返回下一次调用 previous 方法时 返回元素的整数索引。当然, 这个索引只比 nextlndex 返回的索引值小 1。这两个方法的效率 非常高,这是因为迭代器保持着当前位置的计数值。最后需要说一下,如果有一个整数索引 n,ist.listlterator(n) 将返回一个迭代器, 这个迭代器指向索引为 n 的元素前面的位置。也就 是说, 调用 next 与调用 Hst.get(n) 会产生同一个元素, 只是获得这个迭代器的效率比较低。

使用链表的唯一理由是尽可能地减少在列表中间插人或删除 元素所付出的代价。如果列表只有少数几个元素, 就完全可以使用 ArrayList。

数组列表

有两种访问元素的协议:一种是用迭代 器, 另一种是用 get 和 set 方法随机地访问每个元素。后者不适用于链表, 但对数组却很有 用。集合类库提供了一种大家熟悉的 ArrayList 类, 这个类也实现了 List 接口。ArrayList 封 装了一个动态再分配的对象数组。Vector 类的所有方法都是同步的。 可 以由两个线程安全地访问一个 Vector 对象。但是, 如果由一个线程访问 Vector, 代码要 在同步操作上耗费大量的时间。这种情况还是很常见的。而 ArrayList 方法不是同步的, 因此,建议在不需要同步时使用 ArrayList, 而不要使用 Vector

散列集

链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要査看某个指定的元 素, 却又忘记了它的位置, 就需要访问所有元素, 直到找到为止。如果集合中包含的元 素很多, 将会消耗很多时间。如果不在意元素的顺序,可以有几种能够快速査找元素的数 据结构。其缺点是无法控制元素出现的次序。它们将按照有利于其操作目的的原则组织 数据。 

有一种众所周知的数据结构, 可以快速地査找所需要的对象, 这就是散列表(hash table)。 由 hashCode 函数得出的散列码。散列表为每个对象计算一个整数, 称为散列码(hashcode)。散列码是由对象的实例 域产生的一个整数。更准确地说, 具有不同数据域的对象将产生不同的散列码。

自己实现的 hashCode方法应该与 equals方法兼 容,即如果 a.equals(b) 为true, a 与 b 必须具有相同的 散列码.

在 Java中,散列表用链表数组实现。每个列表 被称为桶(bucket) (参看图 9-10 )。要想査找表中对 象的位置, 就要先计算它的散列码, 然后与桶的总 数取余, 所得到的结果就是保存这个元素的桶的索 引。例如, 如果某个对象的散列码为 76268, 并且 有 128 个桶, 对象应该保存在第 108号桶中(76268 除以 128余 108 )。或许会很幸运, 在这个桶中没有 其他元素,此时将元素直接插人到桶中就可以了。当然,有时候会遇到桶被占满的情况, 这也是不可避免的。这种现象被称为散列冲突(hash collision)o这时, 需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。 如果散列码是合理且随机分布的, 桶的数目也足够大, 需要比较的次数就会很少.

在 JavaSE 8 中, 桶满时会从链表变为平衡二叉树。

如果想更多地控制散列表的运行性能, 就要指定一个初始的桶数。桶数是指用于收集具 有相同散列值的桶的数目.

行性能。 如果大致知道最终会有多少个元素要插人到散列表中, 就可以设置桶数。通常, 将桶数 设置为预计元素个数的 75% ~ 150%。有些研究人员认为:尽管还没有确凿的证据,但最好 将桶数设置为一个素数, 以防键的集聚。标准类库使用的桶数是 2 的幂, 默认值为 16 。如果散列 表太满, 就需要再散列 (rehashed)。如果要对散列表再散列, 就需要创建一个桶数更多的表, 并将所有元素插入到这个新表中 . ,然后丢弃原来的表。装填因子(load factor) 决定何时对散 列表进行再散列。例如, 如果装填因子为 0.75 (默认值), 而表中超过 75%的位置已经填人 元素, 这个表就会用双倍的桶数自动地进行再散列.

散列表可以用于实现几个重要的数据结构。其中最简单的是 set 类型。set 是没有重复元 素的元素集合。set 的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添 加进去。 

散列集迭代器将依次访问所有的桶。 由于散列将元素分散在表的各个位置上,所以访问 它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用 HashSet.

 在更改集中的元素时要格外小心。如果元素的散列码发生了改变, 元素在数据结 构中的位置也会发生变化。

树集

 树集(implements SortedSet)是一个有序集合 ( sorted collection)o 可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将 自动地按照排序后的顺序呈现.排序是用 树结构完成的(当前实现使用的是红黑树). 每次将一个元素添加到树中时,都被放置在正确的排序 位置上。因此,迭代器总是以排好序的顺序访问每个元素。将一个元素添加到树中要比添加到散列表中慢. 

是否总是应该用树集取代散列集。毕竟, 添加一个元 素所花费的时间看上去并不很长,而且元素是自动排序的。到底应该怎样做将取决于所要收 集的数据。如果不需要对数据进行排序, 就没有必要付出排序的开销。

 TreeSet 类实现了 NavigableSet 接口

package treeSet;

import java.util.*;

/**
 * An item with a description and a part number.
 */
public class Item implements Comparable<Item>
{
   private String description;
   private int partNumber;

   /**
    * Constructs an item.
    * 
    * @param aDescription
    *           the item's description
    * @param aPartNumber
    *           the item's part number
    */
   public Item(String aDescription, int aPartNumber)
   {
      description = aDescription;
      partNumber = aPartNumber;
   }

   /**
    * Gets the description of this item.
    * 
    * @return the description
    */
   public String getDescription()
   {
      return description;
   }

   public String toString()
   {
      return "[description=" + description + ", partNumber=" + partNumber + "]";
   }

   public boolean equals(Object otherObject)
   {
      if (this == otherObject) return true;
      if (otherObject == null) return false;
      if (getClass() != otherObject.getClass()) return false;
      Item other = (Item) otherObject;
      return Objects.equals(description, other.description) && partNumber == other.partNumber;
   }

   public int hashCode()
   {
      return Objects.hash(description, partNumber);
   }

   public int compareTo(Item other)
   {
      int diff = Integer.compare(partNumber, other.partNumber);
      return diff != 0 ? diff : description.compareTo(other.description);
   }
}

package treeSet;

import java.util.*;

/**
 * This program sorts a set of item by comparing their descriptions.
 * @version 1.12 2015-06-21
 * @author Cay Horstmann
 */
public class TreeSetTest
{
   public static void main(String[] args)
   {
      SortedSet<Item> parts = new TreeSet<>();
      parts.add(new Item("Toaster", 1234));
      parts.add(new Item("Widget", 4562));
      parts.add(new Item("Modem", 9912));
      System.out.println(parts);

      NavigableSet<Item> sortByDescription = new TreeSet<>(
            Comparator.comparing(Item::getDescription));

      sortByDescription.addAll(parts);
      System.out.println(sortByDescription);
   }
}

队列与双端队列

有两个端头的队列, 即双端队列,可以让人们有效地在头部和尾部同时添加或删除元 素。 Deque 接口,并由 ArrayDeque 和 LinkedList 类实现。

优先级队列

优先级队列(priority queue) 中的元素可以按照任意的顺序插人,却总是按照排序的顺序 进行检索。也就是说,无论何时调用 remove方法,总会获得当前优先级队列中最小的元素。 然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需 要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一 个可以自我调整的二叉树,对树执行添加(add) 和删除(remore) 操作, 可以让最小的元素 移动到根,而不必花费时间对元素进行排序。

与 TreeSet—样,一个优先级队列既可以保存实现了 Comparable 接口的类对象, 也可以 保存在构造器中提供的 Comparator 对象。 

使用优先级队列的典型示例是任务调度。

三、映射

通常, 我们知道某些键的信息,并想要 查找与之对应的元素。 映射(map) 数据结构就是为此设计的。

基本映射操作

Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了 Map接口。 散列映射对键进行散列, 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索 树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。

应该选择散列映射还是树映射呢? 与集一样, 散列稍微快一些, 如果不需要按照排列顺 序访问键, 就最好选择散列

例:


Map<String,String> map=new HashMap<>();
        map.put("a","b");
        map.get("c");
map.getOrDefault("a","0");

如果在映射中没有与给定键对应的信息,get 将返回 null。 null 返回值可能并不方便。有时可以有一个好的默认值, 用作为映射中不存在的键。然 后使用 getOrDefault方法.

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

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

map.forEach((k,v)->{
            System.out.println("k="+k);
        });

更新映射项

下面来看 一个例子,使用一个映射统计一个单词在文件中出现的频度。看到一个单词(word) 时,我 们将计数器增 1,如下所示

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

这是可以的, 不过有一种情况除外:就是第一次看到 word时。在这种情况下,get 会返 回 null, 因此会出现一个 NullPointerException 异常。 作为一个简单的补救, 可以使用 getOrDefault方法: 

counts.put(word,getOrDefault(word,0)+1);

另一种方法是首先调用 putlfAbsent 方法。只有当键原先不存在时才会放入一个值。 

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

不过还可以做得更好。merge方法可以简化这个常见的操作。如果键原先不存在,下面 的调用: 

counts.merge(word,1,Integer::sum);

映射视图

集合框架不认为映射本身是一个集合。不过, 可以得到映射的视图(View)— —这是实现了 Collection 接口或某个子接口的对象.有 3 种视图: 键集、 值集合(不是一个集)以及键 / 值对集。键和键 / 值对可以构成一个 集, 因为映射中一个键只能有一个副本。下面的方法: 

        Map<String,String> map=new HashMap<>();
        map.put("a","b");
        Set<String> set=map.keySet();
        Collection<String> collection=map.values();
        Set<Map.Entry<String, String>> set2=map.entrySet();
        Iterator itr=set2.iterator();
        while(itr.hasNext()) {
        	System.out.println(itr.next());
        }
        for(Map.Entry<String, String> entry:set2) {
        	String a=entry.getValue();
        	String b=entry.getKey();
        }

如果在键集视图上调用迭代器的 remove方法, 实际上会从映射中删除这个键和与它关 联的值。不过,不能向键集视图增加元素。另外, 如果增加一个键而没有同时增加值也是没 有意义的。如果试图调用 add方法, 它会抛出一个 UnsupportedOperationException。条目集 视图有同样的限制,尽管理论上增加一个新的键 / 值对好像是有意义的。

弱散列映射

如果有一个值,对应的键已经不再 使用了.垃圾回收器跟踪活动的对象。只要映射对象是活动的, 其中的所有桶也是活动的, 它们不能被回收。因此,需要由程序负责从长期存活的映射表中 删除那些无用的值。 或者使用 WeakHashMap完成这件事情。当对键的唯一引用来自散列条目时, 这一数据结构将与垃圾回收器协同工作一起删除键 / 值对.如果对应的key被回收,则这个key指向的对象会被从Map容器中移除.

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

链接散列集与映射

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

链接散列映射将用访问顺序(谁访问的多), 而不是插入顺序, 对映射条目进行迭代。每次调用 get 或 put, 受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位 置会受影响, 而散列表中的桶不会受影响)。

枚举集与映射

EmimSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例, 所以 EnumSet 内部用位序列实现。如果对应的值在集中, 则相应的位被置为1.EnumSet 类没有公共的构造器。可以使用静态工厂方法构造这个集。EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。 在使用时,需要在构造器中指定键类型:

EnumMap<Weekday, Employee〉personlnCharge = new EnumMap<>(Weekday.class); 

表示散列映射

类 IdentityHashMap 有特殊的作用。在这个类中, 键的散列值不是用 hashCode 函数计算 的, 而是用 System.identityHashCode 方法计算的。 这是 Object.hashCode 方法根据对象的内 存地址来计算散列码时所使用的方式。而且, 在对两个对象进行比较时, IdentityHashMap 类 使用 ==, 而不使用 equals。 也就是说, 不同的键对象, 即使内容相同, 也被视为是不同的对象。 在实现对象遍历算 法(如对象串行化)时, 这个类非常有用, 可以用来跟踪每个对象的遍历状况。 

四、视图与包装器

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

轻量级集合包装器

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

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

子范围

不可修改的视图

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 LinkedList<>(); lookAt(Collections.unmodifiableList(staff)); 由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例 如, LinkedList 类有一些非常方便的方法,addFirst 和 addLast,它们都不是 List 接口的方法, 不能通过不可修改视图进行访问。 

 unmodifiableCollection 方法(与本节稍后讨论的 synchronizedCollection 和 checked Collection 方法一样)将返回一个集合, 它的 equals 方法不调用底层集合的 equals 方法。 相反, 它继承了 Object 类的 equals 方法, 这个方法只是检测两个对象是否是同一个对 象。如果将集或列表转换成集合, 就再也无法检测其内容是否相同了。 • 视图就是以这种 方式运行的, 因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以 同样的方式处理 hashCode 方法。 然而,unmodifiableSet 类和 unmodifiableList 类 却 使 用 底 层 集 合 的 equals 方 法 和 hashCode 方 法。


同步视图

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

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

受查视图

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

ArrayList<String> strings = new ArrayList<>();

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

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

这个错误的 add命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用 get方法,并将结果转化为 String 时,这个类才会抛出异常。 受査视图可以探测到这类问题。下面定义了一个安全列表:

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

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

关于可选操作的说明

通常,视图有一些局限性, 即可能只可以读、无法改变大小、只支持删除而不支持插 人,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一 个 UnsupportedOperationException。 在集合和迭代器接口的 API 文档中,许多方法描述为“ 可选操作

五、算法

排序与混排

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

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

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

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

staff.sort(Comparator.reverseOrder()) .同样:

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

 例如,显然不能将 unmodifiableList 列表传递给排序算法。可 以传递什么类型的列表呢? 根据文档说明,列表必须是可修改的,但不必是可以改变大小的。 

下面是有关的术语定义:

•如果列表支持 set 方法,则是可修改的。

•如果列表支持 add 和 remove方法,则是可改变大小的。 

Collections 类有一个算法 shuffle, 其功能与排序刚好相反, 即随机地混排列表中元素的 顺序。

ArrayList<Card> cards = . . .; 
Collections.shuffle(cards);

如果提供的列表没有实现 RandomAccess 接口,shuffle 方法将元素复制到数组中,然后 打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表.

二分查找

Collections类的 binarySearch方法实现了这个算法。注意, 集合必须是排好序的, 否则 算法将返回错误的答案。要想查找某个元素,必须提供集合(这个集合要实现 List 接口)。如果集合没有采用 Comparable 接口的 compareTo方法进行排序, 就还要提供一个比较器对象。如果为 binarySearch 算 法提供一个链表,它将自动地变为线性查找。

简单的算法

批操作

很多操作会“ 成批” 复制或删除元素。通过使用一个子范围视图,可以把批操作限制在子列表和子集上。这个子范围还可以完成更改操作。 staff.subList(0,10).clear(); 

集合与数组的转换

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

数组---->集合

List<String> list=new ArrayList<>();
    	String[] a=new String[10];
    	HashSet<String> set=new HashSet<>(Arrays.asList(a));

集合-->数组

    	String[] b=set.toArray(new String[0]);
    	String[] b=set.toArray(new String[10]);

六、遗留的集合

hashtable类

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

枚举

遗留集合使用 Enumeration接口对元素序列进行遍历。Enumeration接口有两个方法, 即 hasMoreElements 和 nextElement。这两个方法与 Iterator 接口的 hasNext 方法和 next 方法十 分类似。有时还会遇到遗留的方法,其参数是枚举类型的。静态方法 Collections.enumeration将产 生一个枚举对象,枚举集合中的元素:

List<InputStream> streams=...;
SequenceInputStream in=new SequenceInputStream(Collections.enumeration(streams));

属性映射

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

•键与值都是字符串。

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

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

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

栈(stack)

位集

由于位集将位包装在 字节里,所以,使用位集要比使用 Boolean对象的 ArrayList 更加高效。 

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");
   }
}

有序:ArrayList,LinkedList,TreeSet,TreeMap

无序:HashSet,HashMap

无重复:set,map

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值