集合(Java核心技术卷Ⅰ)

来源:互联网 发布:java发布webservice 编辑:程序博客网 时间:2024/06/05 01:49

    • 集合
      • 1 集合接口
      • 2 具体的集合
      • 3 集合框架
      • 4 算法
      • 5 遗留的集合

12. 集合

12.1. 集合接口

将集合的接口与实现分离

  • Java集合类库将接口与实现分离。首先,看一下人们熟悉的数据结构——队列(queue)是如何分离的。
  • 队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列。
  • 一个队列接口的最小形式可能类似下面这样:
interface Queue<E>{    void add(E element);    E remove();    int size();}
  • 这个接口并没有说明队列是如何实现的。队列通常有两种实现方式:一种是使用循环数组(ArrayDeque);另一种是使用链表(LinkedList)。
  • 当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了那种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。Queue<Customer> expressLane=new ArrayDeque<>(100);
  • 利用这种方式,一旦改变了想法,可以轻松地使用另外一种不同的实现。只需要对程序的一个地方进行修改,即调用构造器的地方。如果觉得LinkedList是个更好的选择,就将代码修改为:Queue<Customer> expressLane=new LinkedList<>();
  • 循环数组是一个有界集合,即容量有限。但循环数组比链表高效。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。

Java类库中的集合接口和迭代器接口

在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:add方法和iterator方法。add方法用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器一次访问集合中的元素。

  • 迭代器
    Iterator接口包含next、hasNext、remove三个方法。通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。如:
Collection<String> c=...;Iterator<String> iter=c.iterator();while(iter.hasNext){    String element=iter.next();    do something with element}

从Java SE 5.0开始,用for each循环可以更加简练地表示同样的循环操作。for(String element:c){ do something with element } for each循环可以与任何实现了Iterable接口的对象一起工作。这个接口只包含了一个iterator方法。Collection接口扩展了iterable接口。因此,对于标准类库中的任何集合都可以使用for each循环。
元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1.然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。
Java迭代器中,查找操作和位置变更是紧密相连的。查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动。因此,应该将Java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

  • 删除元素
    Iterator接口的remove方法将会删除上次调用next方法时返回的元素。如果想要删除指定位置上的元素,仍然需要越过这个元素。
    更重要的是,对next方法和remove方法的调用具有相互依赖性。如果调用remove之前没有调用next将是不合法的,会抛出一个IllegalStateException异常。
    如果想删除两个相邻的元素,不能直接调用:it.remove(); it.remove();必须先调用next越过将要删除的元素。it.remove(); it.next(); it.remove();
  • 泛型实用方法
    由于Collection和Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。

12.2. 具体的集合

集合类型 描述 ArrayList 可以动态增长和缩减的索引序列 LinkedList 可以在任何位置进行高效地插入和删除操作的有序序列 ArrayDeque 用循环数组实现的双端队列 HashSet 没有重复元素的无序集合 TreeSet 有序集 EnumSet 包含枚举类型值的集 LinkedHashSet 可以记住元素插入次序的集 PriorityQueue 允许高效删除最小元素的集合 HashMap 存储键/值关联的数据结构 TreeMap 键值有序排列的映射表 EnumMap 键值属于枚举类型的映射表 LinkedHashMap 可以记住键/值添加次序的映射表 WeakHashMap 其值无用后可以被垃圾回收器回收的映射表 IdentityHashMap 用==而不是用equals比较键值的映射表

链表

  • 数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。在数组中间的位置上插入一个元素也是如此。
  • 链表解决了这个问题。数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。在Java中,所有链表实际上都是双向链接的(double linked),即每个结点还存放着指向前驱结点的引用。
  • 链表与泛型集合之间有一个重要的区别。链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法由迭代器负责。
  • 只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,集(set)其中的元素完全无序,在Iterator接口中就没有add方法。
  • 集合类库提供了子接口ListIterator,其中包含add方法。与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表。
  • 另外,ListIterator接口有previous、hasPrevious两个方法,可以用来反向遍历链表。
  • set方法用一个新元素取代调用next或previous方法返回的上一个元素。如,下面的代码将用一个新值取代链表的第一个元素:ListIterator<String> iter=list.listIterator(); String oldValue=iter.next(); iter.set(newValue);
  • 如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。如果迭代器发现它的集合被另一个迭代器修改了,或者被该集合自身的方法修改了,就会抛出一个Concurrent ModificationException异常。为了避免发生并发修改的异常,可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。
  • 有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致。如果不一致,抛出一个Concurrent ModificationException异常。
  • 链表不支持快速地随机访问。如果要查看链表中第n个元素,就必须从头开始,越过n-1个元素。鉴于这个原因,在程序需要采用整数索引访问元素时,通常不选用链表。尽管如此,LinkedList类还是提供了一个用来访问特定元素的get方法,但是这个方法的效率并不太高。如果正在使用这个方法,说明有可能对于要解决的问题使用了错误的数据结构。
  • 绝对不应该使用这个随机访问的方法来遍历链表,下面这段代码的效率极低:for(int i=0;i<list.size();i++) do something with list.get(i); 每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。
  • 列表迭代器接口还有一个方法,可以告知当前位置的索引,nextIndex方法和previousIndex方法。这两个方法的效率非常高,这是因为迭代器保持着当前位置的计数值。
  • 建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayList,而不要使用链表。

数组列表

  • ArrayList封装了一个动态再分配的对象数组。
  • 可以使用get和set方法随机地访问每个元素。
  • Vector类也可以实现动态数组。但是Vector类中的所有方法都是同步的,可以由两个线程安全地访问一个 Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。而ArrayList不是同步的。因此,建议在不需要同步时使用ArrayList,而不要使用Vector.

散列集

  • 散列表(hash table)可以快速地查找所需要的对象。散列表为每个对象计算一个整数,成为散列码(hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。
  • 在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。如,某个对象的散列码为76268,并且有128个桶,对象就该保存在第108号桶中(76268除以128余108)。或许会很幸运,在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
  • 如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入散列表中的元素太多,就会增加冲突的可能性,降低运行性能。如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%-150%。
  • 如果散列表太满,就需要再散列(rehashed)。如果对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。
  • 散列表可以实现几个重要的数据结构,其中最简单的是set。set是没有重复元素的集合。set的add方法首先在集中查找要添加的元素,如果不存在,就将这个对象添加进去。
  • HashSet实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。
  • 更改集中的元素时要格外小心。如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。

树集

  • TreeSet类与HashSet类十分类似。树集是一个有序集合(sorted collection)。可以以任何顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
  • TreeSet类中的排序是用树结构完成的(当前使用的是红黑树(red-black tree)),每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
  • 将一个元素添加到树中要比添加到散列表中慢,但是与将元素添加到数组或链表的正确位置上相比还是快很多的。如果树中包含n个元素,查找新元素的正确位置平均需要log2n次比较。因此,将一个元素添加到TreeSet要比添加到HashSet中慢。不过,TreeSet可以自动地对元素进行排序。

对象的比较
- 在默认情况时,树集假定插入的元素实现了Comparable接口。这个接口定义了一个compareTo方法。如果a和b相等,调用a.compareTo(b)一定返回0,;如果排序后a位于b之前,则返回负值;如果a位于b之后,则返回正值。
- 如果要插入自定义的对象,就必须通过实现Comparable接口自定义排列顺序。在Object类中,没有提供任何compareTo接口的默认实现。下面的代码展示了如何用部件编号对Item对象进行排序:public int compareTo(Item other) { return partNumber-other.partNumber; }
- 可以通过将Comparator对象传递给TreeSet构造器来使用不同的比较方法。Comparator接口声明了一个带有两个显式参数的compare方法。如,如果按照描述信息进行排序,就直接定义一个实现Comparator接口的类,再将这个类的对象传递给树集的构造器。ItemComparator comp=new ItemComparator(); SortedSet<Item> sortByDescription=new TreeSet<>(comp);如果构造了带比较器的树,就可以在需要比较两个元素时使用这个对象。
- 注意,这个比较器没有任何数据,只是比较方法的持有器。有时将这种对象称为函数对象(function object)。函数对象通常动态定义,即定义为匿名内部类的实例。

队列与双端队列

  • 有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在Java SE 6中引入了Deque接口,并由ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

优先级队列

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

映射表

  • 通常,我们知道某些键的信息,并想要查找与之对应的元素。映射表(map)数据结构就是为此设计的。映射表用来存放键/值对。Java类库为映射表提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。
  • 散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
  • 每当往映射表中添加对象时,必须同时提供一个键。staff.put("987-98-9996","Harry"); 要想检索一个对象,必须提供一个键。e=staff.get("987-98-9996"); 如果在映射表中没有与给定键对应的信息,get将返回null。
  • 键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值就会取代第一个值。实际上,put将返回这个键参数存储的上一个值。
  • 集合框架没有将映射表本身视为一个集合(其他的数据结构框架将映射表视为对(pairs)的集合,或者视为用键作为索引的值的集合)。然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或者它的子接口的视图。有3个视图,它们分别是:键集(keySet)、值集合(不是集,values)和键/值对集(entrySet)。keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个其他类的对象。Set接口扩展了Collection接口。因此,可以与使用任何集合一样使用keySet。

专用集与映射表类

  • 弱散列映射表
    设计WeakHashMap类是为了解决一个键对应值不再使用,又不能被删除的问题。
    垃圾回收器跟踪活动的对象。只要映射表对象是活动的,其中的所有桶也是活动的,它们不能被回收。使用WeakHashMap来完成。当对键的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
    这种机制的内部运行:WeakHashMap使用弱引用保存键。弱引用对象将引用保存到另一个对象中,在这里,就是散列表键。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器依然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应的条目。
  • 链接散列集和链接映射表
    LinkedHashSet和LinkedHashMap用来记住插入元素的顺序。
    LinkedHashMap将用访问顺序,而不是插入顺序对映射表条目进行迭代。每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部。要构造这样一个散列映射表,调用LinkedHashMap<K,V>(initialCapacity,loadFactor,true)。访问顺序对于实现高速缓存的“最近最少使用”原则十分重要。例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素则从数据库中读取。当在表中找不到元素而表又已经满时,可以将迭代器加入到表中,并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
  • 枚举集与映射表
    EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的为被置为1.
    EnumSet没有公共的构造器,可以使用静态工厂方法构造这个集:enum Weekday{MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY}; EnumSet<Weekday> always=EnumSet.allOf(Weekday.class); EnumSet<Weekday> workday=EnumSet.range(Weekday.MONDAY,Weekday.FRIDAY);
    EnumMap是一个键类型为枚举类型的映射表。它可以直接高效低用一个值数组实现。在使用时,需要在构造器中指定键类型:EnumMap<Weekday,Employee> personInCharge=new EnumMap<>(Weekday.class);
  • 标识散列映射表
    在IdentityHashMap中,键的散列值不是用hashCode函数计算的,而是用System.indentityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不是equals。
    也就是说,不同的键对象,即使内容相同,也被视为不同的对象。在实现对象遍历算法(如对象序列化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。

12.3. 集合框架

框架(framework)是一个类的集。框架包含很多超类,这些超类拥有非常有用的功能、策略和机制。
集合框架的接口

  • 集合有两个基本的接口:Collection和Map。用add方法向集合中插入元素,但映射表使用put方法。使用迭代器从集合中读取元素。也可以用get方法从映射表读取值。
  • List是一个有序集合。元素可以添加到容器中某个特定位置。将对象放置在某个位置上有两种方式:使用整数索引或列表迭代器。
  • 为了避免执行成本较高的随机访问操作,Java引入了标记接口RandomAccess。这个接口没有任何方法,但可以用来检测一个特定的集合是否支持高效的随机访问。ArrayList类和Vector类都实现了RandomAccess接口。
  • Set接口和Collection接口时一样的。集的add方法拒绝添加重复的元素。集的equals方法定义两个集相等的条件是它们包含相同的元素但顺序不必相同。
  • 接口NavigableSet和NavigableMap包含了用于在有序集和映射表中查找和遍历的方法。TreeSet和TreeMap类实现这几个接口。
  • 抽象类提供了集合接口的中的基本方法。如果要实现自己的类,就可能要扩展某个抽象类,以便可以选择例行操作的实现。
    集合框架中的类

视图与包装器

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

  • 轻量级集包装器
    Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器。这个方法可以将数组传递给一个期望得到列表或集合变元的方法。如:Card[] cardDeck=new Card[52]; List<Card> cardList=Arrays.asList(cardDeck); 返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的get和set方法。改变数组大小的的所有方法都会抛出一个UnsupportedOperationException异常。
    从Java SE 5.0开始,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.singleton(anObject)将返回一个视图对象。这个对象实现了Set接口。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。

  • 子范围
    可以为很多集合建立子范围(subrange)视图。可以使用subList方法获得一个列表的子范围视图。List group2=staff.subList(10,20);第一个索引包含在内,第二个索引不包含在内。
    可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。如,删除子范围:group2.clear(); 元素自动地从staff列表中清除了,并且group2为空。
    对于有序集和映射表,可以使用排序顺序而不是元素位置建立子范围。NavigableSet接口赋予子范围操作更多的控制能力,可以指定是否包括边界。

  • 不可修改的视图
    不可修改视图(unmodifiable views)对现有集合增加了一个运行时的检查。如果发现视图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态。
    可以通过Collections.unmodifiableSortedSet等方法获得不可修改视图。每个方法都定义于一个接口。如,Collections.unmodifiableList与ArrayList、LinkedList或者任何实现了List接口的其他类一起协同工作。
    想要查看某部分代码,但又不触及某个集合的内容,可以进行下列操作:List<String> staff=new LinkedList<>(); lookAt(Collections.unmodifiableList(staff)); 可以返回一个实现List接口的类对象,其访问器方法将从staff集合中获取值。当然,lookAt方法可以调用List接口中的所有方法,而不只是访问器。但是所有的更改器方法已经被重新定义为抛出异常,而不是将调用传递给底层集合。
    不可修改视图不是集合本身不可修改。仍然可以通过集合的原始引用对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
    视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的方法。

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

  • 检查视图
    检查视图用来对泛型类型发生问题时提供调试支持。
    将错误类型的元素私自带到泛型集合中的问题极有可能发生。如:ArrayList<String> strings=new ArrayList<>(); ArrayList rawList=strings; rawList.add(new Date()); 这个错误的add命令在运行时检测不到。只有在调用get方法,并将结果转为String时,这个类才会抛出异常。
    检测视图可以探测到这类问题。下面定义了一个安全列表:List<String> safeStrings=Collections.checkedList(strings,String.class);视图的add方法将检测插入的对象是否属于所给的类。如果不属于给定的类,就立即抛出一个ClassCastException异常。

  • 关于可选操作的说明
    通常,视图有一些局限性,即可能只可以读、无法改变大小、只支持删除而不支持插入,这些与映射表的键视图情况相同。如果试图进行不恰当的操作,受限制的视图会抛出UnsupportedOperationException。

批操作

  • 采用迭代器遍历集合,一次遍历一个元素。可以使用批操作(bulk operation)避免频繁地使用迭代器。
  • 如果希望找出两个集的交(intersection),即两个集中共有的元素。首先建立一个新集,用于存放结果:Set<String> result=new HashSet<>(a);接着调用retainAll方法:result.retainAll(b);result中保存了既在a中出现,也在b中出现的元素。这时已经构成了交集,而且没有使用循环。
  • 可以将批操作应用于视图。如用键集直接删除终止聘用关系的所有员工ID:staff.keySet().removeAll(terminatedIDs); 由于键集是映射表的一个视图,所以,键与对应的员工名将会从映射表中自动删除。(键集可以删除元素,但不能添加元素)

集合与数组之间的转换

  • 数组转为集合:使用Arrays.asList。String[] values=...; HashSet<String> staff=new HashSet<>(Arrays.asList(values));
  • 集合转为数组:String[] values=staff.toArray(new String[0]);staff.toArray(new String[staff.size()]); 此时,没有创建任何新数组。(直接使用staff.toArray();会产生一个Object数组,而且不能强制转换。)

12.4. 算法

泛型集合接口有一个很大的优点,即算法只需要实现一次。如需要找出数组、数组列表和链表中最大元素,可能会用循环实现这个算法。但因为不同数据结构的方法不一样(访问元素方法、获取长度方法),要编写不同的代码。而实际上,采用get和set方法进行随机访问要比直接迭代层次高。而且计算最大元素并不需要随机访问。因此,可以直接用迭代器遍历每个元素计算最大元素。所以可以将max方法实现为能够接收任何实现了Collection接口的对象。这样就可以使用一个方法计算链表、数组列表或数组中最大元素了。

排序与混排

  • Collections类中的sort方法可以对实现了List接口的集合进行排序:Collections.sort(staff); 这个方法假定列表元素实现了Comparable接口。如果想采用其他方式对列表进行排序,可以将Comparator对象作为第二个参数传递给sort方法。
  • 按照降序对列表进行排序可以使用静态方法Collections.reverseOrder()。这个方法返回一个比较器,比较器则返回b.compareTo(a):Collection.sort(staff,Collections.reverseOrder());Collection.sort(staff,Collections.reverseOrderiItemComparator)); 将逆置itemComparator的次序。
  • Java中的sort方法使用归并排序。调用方法时,直接将所有元素转入一个数组,并使用一种归并排序的变体对数组进行排序,然后再将排序后的序列复制回列表。集合类库中使用的归并排序比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是归并排序有一个主要的优点:稳定,即不需要交换相同的元素。(如有一个已经按姓名排序的员工列表,再按工资排序的时候,对于工资相同的人,就可以按之前的姓名排序。)
  • 可以传递给排序算法的列表:必须是可修改的,但不必是可改变大小的。(如果列表支持set方法,则是可修改的;如果列表支持add和remove方法,则是可改变大小的。)
  • Collections类有一个算法shuffle,可以随机地混排列表中元素的顺序:Collections.shuffle(cards); 如果提供的列表没有实现RandomAccess接口,shuffle方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。

二分查找

  • 在有序的集合中通过二分法查找元素。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; 即采用if(i<0) c.add(-i-1,element);将元素插入到正确位置。
  • 只有采用随机访问,二分查找才有意义。如果必须利用迭代方式一次次遍历链表中的一般元素来找到中间位置的元素,二分查找就完全失去了优势。因此,如果为binarySearch算法提供一个链表,它将自动变为线性查找。

简单算法
Collections类包含一些简单且很有用的算法。

方法 算法 max/min 返回集合中最大或最小的元素 copy 将原列表中所有元素复制到目标列表的相应位置上。目标列表的长度至少与原列表一样 fill 将列表中所有位置设为相同的值 replaceAll 用newValue渠道所有值为oldValue的元素 indexOfSubList/lastIndexOfSubList 返回列表中第一个或最后一个等于子列表的索引 swap 交换两个元素 reverse 逆置列表中元素的顺序。时间复杂度为O(n) rotate 旋转列表中的元素 frequency 返回集合中与对象相同的元素个数 disjoint 如果两个集合没有共同的元素,则返回true

编写自己的算法

  • 如果编写自己的算法,应该尽可能地使用接口,而不要使用具体的实现。将集合接口作为方法参数,可以使方法更为通用。
  • 如果编写了一个返回集合的方法,可能还想要一个返回接口,而不是返回类的方法,因为这样做可以在日后改变方法,并用另一个集合重新实现这个方法。如,编写一个返回所有菜单项的方法getAllItems。
List<JMenuItem> getAllItems(JMenu menu){    List<JMenuItem> items=new ArrayList<>();    for(int i=0;i<menu.getItemCount();i++)        items.add(menu.getItem(i));    return items;}

12.5. 遗留的集合

Hashtable类

Hashtable类和HashMap类的作用一样,实际上,它们拥有相同的接口。与Vector类一样,Hashtable的方法也是同步的。如果对同步性没有任何要求,就应该使用HashMap。

枚举

  • 遗留集合使用Enumeration接口对元素序列进行遍历。这个接口有两个方法,hasMoreElements和nextElement。这两个方法与Iterator接口的hasNext方法和next方法十分类似。如,Hashtable类的element方法将产生一个用于描述表中各个枚举值的对象。
Enumeration<Employee> e=staff.elements();while(e.hasMoreElements()){    Employee em=e.nextElement();    ...}
  • 静态方法Collections.enumeration将产生一个枚举对象,枚举集合中的元素。

属性映射表
属性映射表(property map)是一个类型非常特殊的映射表结构。有三个特性:

  • 键与值都是字符串。
  • 表可以保存到一个文件中,也可以从文件中加载。
  • 使用一个默认的辅助表。
    实现属性映射表的Java平台类成为Properties。属性映射表通常用于程序的特殊配置选项。


包含push方法和pop方法。但Stack类扩展为Vector类,使栈使用不属于栈操作的insert和remove方法,即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。

位集

  • BitSet类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。如果需要高效地存储位序列(如标志)就可以使用位集。由于位集将位包装在字节里,所以使用位集要比使用Boolean对象的ArrayList更加高效。
  • BitSet类提供了一个便于读取、设置或清除各个位的接口。使用这个接口可以避免屏蔽和其他麻烦的位操作。读取状态:bucketOfBits.get(i); 设为“开”状态:bucketOfBits.set(i); 设为“关”状态:bucketOfBits.clear(i);
0 0