集合(一)

一.集合接口

★Enumeration接口提供了一种用于访问任意容器中各个元素的抽象机制。

1.将集合的接口与实现分离

(1)队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列。

(2)队列通常有两种实现方式:
◆使用循环数组
◆使用链表

(3)如果需要一个循环数组队列,就可以用ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList类,这个类实现了Queue接口。

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

(5)循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。

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

(1)在Java类库中,集合类的基本接口是Collection接口。该接口有两个基本方法:
boolean add(E element);
add方法用于向集合中添加元素,如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。

Iterator<E> iterator();
iterator方法用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。

(2)迭代器

①Iterator接口包含3个方法:
E next();
boolean hasNext();
void remove();
通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到了集合的末尾,next方法将抛出一个NoSuchElementException异常。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。

②“for each”循环可以与任何实现 Iterator 接口的对象一起工作,这个接口只包含一个方法:Iterator<E> iterator();Collection接口扩展了Iterator接口。因此,对于标准类库中的任何集合都可以使用”for each”循环。

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

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

(3)删除元素

◆Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该先看一下这个元素时很具有实际意义的。然而,如果想要删除指定位置上的元素,仍然需要越过这个元素。

(4)泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。

二.具体的接口

Java库中的具体集合:

这里写图片描述

1.链表

(1)数组和数组列表都有一个重大缺陷,就是从数组的中间位置删除一个元素要付出很大的代价,原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。栽数组的中间位置插入一个元素也很麻烦。

(2)链表解决了(1)中的问题,尽管数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。

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

(4)从链表中间删除一个元素很简单,即需要对被删除元素附近的结点更新一下即可。

(5)链表与泛型集合有一个重要的区别。链表是一个有序集合,每个对象的位置都十分重要。LinkedList.add方法将对象添加到列表的尾部。但是,常常需要将元素添加到列表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。

(6)对于集(Set)类型,其中的元素是无序的。因此,在Iterator接口中就没有add方法。

◆集合类库提供了子接口ListIterator,其中包含add方法,与Collection.add方法不同,这个方法不返回boolean类型的值。它假定添加操作总会改变链表。

◆Iterator接口有两个方法,可以用来反向遍历链表:
E previous()
boolean hasPrevious()
与next方法一样,previous返回越过的对象。

◆LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象。
ListIterator<String> iter = staff.listIterator();

◆add方法在迭代器位置之前添加一个新对象。当多次调用add方法,将按照提供的次序把元素添加到链表中。它们被依次添加到迭代器当前位置之前。

◆当用一个刚刚由Iterator方法返回,并且指向链表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。如果链表有n个元素,有n+1个位置可以添加元素。这些位置与迭代器的n+1个可能的位置相对应。

◆set方法用一个新元素取代调用next或previous方法返回的上一个元素。如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改,如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。

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

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

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

(7)可以使用ListIterator类从前后两个方向遍历链表中的元素,并可以添加、删除元素。

(8)Collection接口中声明了很多用于对链表操作的方法。大部分方法都是在LinkedList类的超类AbstractCollection中实现的。

(9)在Java类库中,还提供了许多在理论上存在一定争议的方法。链表不支持快速地随机访问。如果要查看链表中的第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,程序员通常不选用链表。尽管如此,LinkedList还是提供了一个用来访问某个特定元素的get方法:

LinkedList<String> list = ...;
String obj = list.get(n);

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

(11)如果链表中只有很少几个元素,就完全没有必要为get方法和set方法的开销而烦恼。但是,使用链表的唯一理由是尽可能低减少在列表中间插入或删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList。

(12)我们建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayList,而不要使用链表。

2.数组列表

(1)List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是用迭代器,另一种是用get和set方法随机地访问数组中的每个元素。后者不适用于链表,但对数组却很有用。

(2)在需要动态数组时,可能会使用Vector类。由于Vector类中的所有方法都是同步的。可以由两个线程安全的访问一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。这种情况还是很常见的。而ArrayList方法不是同步的,因此,建议在不需要同步的时使用ArrayList,而不要使用Vector。

3.散列集

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

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

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

(4)在Java中,散列表用链表数组实现,每个列表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。有时候,会遇到桶被占满的情况,这也是不可避免的,这种现象被称为散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是随机且合理分配的,桶的数目也足够大,需要比较的次数就会很少。

(5)如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素数目太多,就会增加冲突的可能性,降低运行性能。

(6)如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%—150%。有些研究人员认为:最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是2的幂,默认值为16。

(7)当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计太低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。
装载因子(load factor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说,装载因子为75%是比较合理的。

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

(9)Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素,contains方法已经被重新定义,用来快速查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。

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

4.树集

(1)TreeSet类与散列集十分相似。不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。

(2)迭代器总是以排好的顺序访问每个元素。

(3)将一个元素添加到树中要比添加到散列表中要慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。如果树中包含n个元素,查找新元素的正确位置平均需要log2的n次比较。例如,一棵树包含了1000个元素,添加一个新元素大概需要比较10次。

(4)因此,将一个元素添加到TreeSet中要比添加到HashSet中慢。不过,TreeSet可以自动地对元素进行排序。

5.队列与双端队列

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

6.优先级队列

(1)优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。

(2)然而,优先级队列中并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。

(3)优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)时,可以让最小的元素移动到根,而不必花费时间对元素排序。

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

(5)使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。

7.映射表

(1)集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有要查找元素的副本。这不是一种非常通用的查找方式。通常,我们知道某些键的信息,并想要查找与之对应的元素。映射表结构就是为此设计的。映射表用来存放键值对。如果提供了键,就能查找到值。

(2)Java类库为映射表提供了两个通用的类库:HashMapTreeMap。这两个类都实现了Map接口。

(3)散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能用于键。与键关联的值不能进行散列或比较。

(4)与集一样,散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择散列。

(5)如果在映射表中没有与给定键对应的信息,get将返回null。

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

(7)remove方法用于从映射表中删除给定键对应的元素。size方法用于返回映射表中的元素数。

(8)集合的框架并没有将映射表本身视为一个集合(其他的数据结构框架则将映射视为对(pairs)的集合,或者视为用键作为索引的值的集合)。然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或者它的子接口的视图。

(9)映射表有3个视图:键集、值集合(不是集)和键值对集。键与键值对形成了一个集,这是因为在映射表中的一个键只能有一个副本。

(10)KeySet既不是HashSet也不是TreeSet,而是实现了Set接口的某个其他类的对象。Set接口扩展了Collection接口。因此,可以与使用任何集合一样使用KeySet。

(11)如果调用了迭代器中的remove方法,实际上就是删除了映射表中的键以及对应的值。但是,不能将元素添加到键集的视图中。如果只添加键而不添加值是没有意义的。如果试图调用add方法,将会抛出一个UnsupportedOperationException异常。条目集视图也有同样的限制,不过,从概念上讲,添加新的键值对是有意义的。

(12)映射表的操作过程:首先将键值对添加到映射表中,然后从映射表中删除一个键,同时与之对应的值也被删掉了。接下类,修改与某一个键对应的值,并调用get方法查看这个值。最后,对条目集进行迭代。

8.专用集与映射表类

在集合类库中,有几个专用的映射表类:

(1)弱散列映射表(WeakHashMap)

◆弱散列映射表是为了解决一个问题,如果有一个值,对应的键已经不再使用了。假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现的这个键,所以这个键值对无法从映射表中删除。

◆垃圾回收器跟踪活动的对象。只要映射表中的对象是活动的,其中所有的桶都是活动的,它们不能被回收。因此,需要程序员从长期存活的映射表中删除那些无用的值。或者使用WeakHashMap完成这件事情。当对键的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键值对。

◆WeakHashMap使用弱引用(Weak Reference)保存键。WeakReference对象将引用保存到另外一个对象中,在这里,就是散列表键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。

◆通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。

◆如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入到队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。

◆一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来,然后,WeakHashMap将删除对应的条目。

(2)链接散列集和链接映射表

◆Java SE 1.4增加了两个类:LinkedHashSetLinkedHashMap,用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。

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

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

(3)枚举集与枚举映射表

◆EnumSet是一个枚举类型的元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。

◆EnumSet类没有公共的构造器。可以使用静态工厂方法构造这个集。可以使用Set接口的常用方法来修改EnumSet。

◆EnumMap是一个键类型为枚举类型的映射表。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personInCharge = new EnumMap<Weekday, Employee>(Weekday.class);

(4)标识散列映射表

◆Java SE 1.4还为另外一个特殊目的增加了另一个类IdentityHashMap。在这个类中,键的散列值不是用HashCode函数计算的,而是用System.identityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不使用equals。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值