在实现方法时,选择不同的数据结构会导致其实现风格以及性能存在很大差异;利用Java类库帮助我们在程序设计中实现传统的数据结构;介绍使用标准库中的集合类
1.集合接口
Java最初版本只为最常用的数据结构提供了很少的一组类:Vector、Stack、Hashtable、BitSet和Enumeration接口,其中的Enumeration接口提供了一种用于访问任意容器中各个元素的抽象机制
1.将集合的接口与实现分离
Java集合类库将接口(interface)和实现(implementation)分离,例如,当需要收集对象,并按照“先进先出”的规则检索对象时,就应该使用队列;一个队列的最小形式如下:
interface Queue<E>
{
void add(E element);
E remove();
int size();
}
这个接口并没有说明队列是如何实现的;队列通常有2中实现方式:
1.使用循环数组
2.使用链表
每一个实现都可以通过一个实现了Queue接口的类表示:
循环数组:
class CircularArrayQueue<E> implements Queue<E>//not an actual library class
{
CircularArrayQueue(int capacity){...}
public void add(E element){...}
public E remove(){...}
private E[] elements;
private int head;
private int tail;
}
链表:
class LinkedListQueue<E> implements Queue<E>//not an actual library class
{
LinkedListQueue(){...}
public void add(E element){...}
public E remove(){...}
public int size(){...}
private E[] elements;
private int head;
private int tail;
}
以上这2个在Java中并不存在,如果需要一个 循环数组队列,就可以使用ArrayDeque类,如果需要一个链表队列,就可以直接使用LinkedList类,这个类实现了Queue接口
使用接口类型存放集合的引用
Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));
利用这种方式,如果改变想法,只需要修改调用构造器的地方,也可以选择LinkedListQueue,如下:
Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));
循环数组比链表更高效,多数人优先选择循环数组;循环数组是一个有限集合,容量有限,如果程序中要收集的对象没有上限,最好使用链表实现
在API文档中存在着以Abstract开头的类,例如AbstractQueue,这些类是为类库实现者设计的
2.Java类库中的集合接口和迭代器接口
在Java类库中,集合类的基本接口是Collection接口,这个接口有2个基本的方法:
public interface Collection<E>
{
boolean add(E element);
Iterator<E> iterator();
...
}
1.add方法用于向集合中添加元素,如果添加元素确实改变了集合就返回true,如果集合没有发生变化,就返回false;例如,试图向集中添加一个对象,而这个对象在集中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象
2.iterator方法用于返回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素
1.迭代器
Iterator接口包含3个方法:
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
}
通过反复调用next方法,可以逐个访问集合中的每个元素,如果集合到达末尾,next方法会抛出一个NoSuchElementException,因此需要调用hasNext方法,如果迭代器对象还有可供访问的元素,就会返回true
Collection<String> c = ...;
Iterator<String> iter = .iterator();
while(iter.hasNext())
{
String element = iter.next();
do something with element
}
请求一个迭代器,并在hasNext方法返回true时反复调用next方法,来查看集合中的所有元素
上面代码可以简化为for each 循环
for(String element : c)
{
do something with element
}
编译器简单的将“for each”循环翻译为带有迭代器的循环,“for each”循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个方法:
public interface Iterable<E>
{
Iterable<E> iterator();
}
Collection接口扩展了Iterable接口,因此对于标准类库中的任何集合都有可以使用“for each”循环
元素被访问的顺序取决于集合的类型:
- 1.对ArrayList进行迭代,迭代器将从索引0开始,没迭代一次,索引值家1
- 2.对HashSet进行迭代,每个元素将会按照某种随机的次序出现(虽然可以确定在迭代的过程中,能够遍历到集合中的所有元素,但却无法预知元素被访问的次序)【对于与顺序无关的操作来说,没有问题】
Java的迭代器不能像操作数组那样进行操作,查找操作与位置变更紧密相连;查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动;因此将Java的迭代器认为是位于2个元素之间,当调用next时,迭代器就越过下一个元素,并返回刚才越过的那个元素的引用
【可以将Iterator.next与InputStream.read看作是等效的,从数据流中读取一个字节,就会自动的“消耗掉”这个字节,下次调用read将会消耗并返回输入的下一个字节,用同样的方式,反复地调用next就可以读取集合中所有元素】
2.删除元素
Iterator接口的remove方法将会删除上次调用next方法是返回的元素(在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有意义的),如果想要删除指定位置上的元素,仍然需要越过这个元素,例如:
Iterator<String> it = c.iterator();
it.next();//skip over first element
it.remove();//remove it
对于remove方法和next方法的调用具有相互依赖性,如果remove之前没有调用next将是不合法的,会抛出IllegalStateException,如果要删除相邻的2个元素,不能如下调用:
it.remove();
it.remove();
必须先调用next越过将要删除的元素:
it.remove();
it.next();
it.remove();
3.泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法,例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c , Object obj)
{
for(E element : c)
{
if(element.equals(obj))
{
return true;
}
return false;
}
}
Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法,如下列举了部分:
int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection<?> c)
boolean equals(Object other)
boolean addAll(Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll(Collection<?> c)
void clear()
boolean retainAll(Collection<?> c)
Object[] toArray()
<T> T[] toArray(T[] arrayToFill)
如果实现Collection接口的每一个类都要提供如此多的例行方法将是一件很烦人的事情,为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection,它将基础方法size和iterator抽象化了,但是在此提供了例行方法,如:
public abstract class AbstractCollection<E> implements Collection<E>
{
...
public abstract Iterator<E> iterator();
public boolean contains(Object obj)
{
for(E element : this)//calls iterator()
{
if(element.equals(obj))
{
return true;
}
return false;
}
...
}
此时,一个具体的集合类可以扩展AbstractCollection类了,现在要由具体的集合类提供iterator方法,而contains方法由AbstractCollection超类提供,如果子类有更加有效的方式实现contains方法,也可以由子类提供
集合类的用户可以使用泛型接口中一组更加丰富的方法,而实际的数据结构实现者并没有需要实现所有例行方法的负担
2.具体集合
Java类库中的集合(不包含线程安全集合),除Map结尾的类之外,其他类都实现了Collection接口;以Map结尾的类实现了Map接口:
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型值的集合 |
LinkedHashSet | 一种可以记住元素插入次序的集合 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键/值关联的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举型的映射表 |
LinkedHashMap | 一种可以记住键/值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityMap | 一种用==而不是equals比较键值的映射表 |
1.链表
数组和数组列表都有一个重大的缺陷,从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动(插入元素亦是如此)
【链表可以解决这个问题】
数组在连续的位置上存放对象的引用,链表却将每个对象存放在独立的节点中(每个节点总还存放着序列中下一个节点的引用)【在Java中,所以链表都是双向链表】
在链表中删除一个元素是很轻松的操作,只需要对被删除元素附近的节点进行更新即可
链表和泛型集合之间有一个重要的区别:链表是一个有序集合,每个对象的位置十分重要
LinkedList.add方法将对象添加到链表的尾部;由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将有迭代器负责;只有对自然有序的集合使用迭代器添加元素才有实际意义
集类型(Set),其中的元素完无序,因此在Iterator接口中没有add方法,集合类库提供了子接口ListIterator,其中包含add方法:
interface ListIterator<E> extends Iterator<E>
{
void add(E element);
...
}
与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总是会改变链表
ListIterator接口有2个方法,可以反向遍历链表:
- 1.E previous()
- 2.boolean hasPrevious()
与next方法一样previous方法返回越过的对象
LinkedList类的listIterator方法返回一个实现了ListIterator接口的迭代器对象
ListIterator<String> iter = staff.listIterator();
add方法在迭代器位置之前添加一个新对象;如果多次调用add方法,将按照提供的次序把元素添加到链表中,它们被依次添加到迭代器当前位置之前
当用一个刚刚由Iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头;当迭代器越过链表的最后一个元素时(即hasNext方法返回false),添加的元素将变为新表尾
如果链表有n个元素,有n+1个位置可以添加新的元素,这些位置与迭代器的n+1个可能的位置相对应
删除元素时要注意:
- 1.在调用next方法之后,remove方法将会删除迭代器左侧的元素(即迭代器刚越过的那个元素)
- 2.在调用previous方法之后,remove方法将会删除迭代器右侧的元素(即迭代器刚越过的那个元素)
- 3.不能再同一行中调用2次remove方法
- 4.add方法只依赖于迭代器的位置,remove方法依赖于迭代器的状态
- 5.set方法用一个新元素取代调用next方法或previous方法返回的上一个元素
一个迭代器指向另一个迭代器刚刚删除的元素前,现在这个迭代器就是无效的,链表迭代器可以检测出这种修改,如果迭代器发现它的集合被另一个迭代器修改了或被该集合自身的方法修改了,就会抛出ConcurrentModificationException
为了避免发生并发修改的的异常,请遵循以下原则:
- 1.可以根据需要给容器附加许多的迭代器,但这些迭代器只能读取列表
- 2.在单独附加一个既能读又能写的的迭代器
有一个简单的方法可以检测到并发修改的问题:
- 1.集合可以跟踪改写操作(如添加或删除元素)的次数
- 2.每个迭代器都维护一个独立的计数器
- 3.在每个迭代器的方法开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致;如果不一致,抛出ConcurrentModificationException
【链表只负责跟踪对列表的结构性修改(如添加或删除元素),set操作不被视为结构性修改】
链表不支持快速地随机访问,如果要查看链表中的第n个元素,必须从头开始越过n-1个元素,鉴于这个原因,在程序需要采用整数索引访问元素时,通常不采用链表;尽管如此,LinkedList还是提供了一个用来访问某个特定元素的get方法(get方法做了微小的优化,如果所有大于size()/2就从表尾开始搜索元素):
LinkedList<String> list = ...;
String obj = list.get(n);
这个方法效率不高,如果发现自己正在使用这个方法,说明有可能对于所要解决的问题使用了错误的数据结构(绝对不能使用这种让人误解的随机访问方法来遍历链表)如下代码效率极低:
for(int i = 0; i < list.size(); i++)
{
do something with list.get(i);
}
每次查找一个元素都要从列表的头部重新开始搜索,LinkedList对象根本不做任何缓存位信息的操作
由于Java迭代器指向两个元素之间的位置,所以可以同时产生2个索引,nextIndex方法返回下一次调用next方法时返回元素的整数索引,previousIndex方法返回下一次调用previous方法时返回元素的整数索引(这2个方法的效率非常高,因为迭代器保持着当前位置的计数器)
如果有1个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素的前一个位置,然后调用next和list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低
使用链表的唯一理由是尽可能地减少在链表中插入或删除元素所付出的代价,如果列表只有少数几个元素,就完全可以使用ArrayList
建议避免使用以整数索引表示链表中位置的所有方法,如果需要对集合进行随机的访问,就使用ArrayList或者数组,而不是链表
List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要,有2种访问元素的协议,一个是迭代器,另一个是用get和set方法随机地访问每个元素(不适用于链表,但对数组有用)
2.数组列表
ArrayList封装了一个动态再分配的对象数组,ArrayList方法不是同步的
在需要使用动态数组时,可以使用Vector类,因为Vector类的所有方法都是同步的,可以由2个线程安全的访问一个Vector对象,如果用一个线程访问Vector,代码要在同步操作上消耗大量的时间,
3.散列集
链表和数组按照人们的意愿排列元素的次序,如果要查找元素,又忘记了位置,需要访问所有的元素
如果不在意元素的顺序,可以有几种能够快速查找元素的数据结果,其缺点是无法控制元素出现的次序,它们将按照有利于其操作目的的原则组织数据
散列表可以快速查找所需要的对象,散列表为每个对象计算一个整数,称为散列码(由对象实例域产生的一个整数【具有不同数据域的对象将产生不同的散列码】)
字符串由hashCode方法产生散列码,如果自定义类,就要负责实现这个类的hashCode方法【自己实现的hashCode方法应该与equals方法兼容(即如果a.equals(b)为true,a与b必须具有相同的散列码)】
现在最重要的问题是,散列码能够快速的计算出来,并且这个计算只与要散列的对象的状态有关,与散列表中的其他对象无关
在Java中,散列表用链表数组实现,每个列表被称为桶,要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数(桶数是指用于收集具有相同散列值的桶的数目)取余,所得到的结果就是保存这个元素的桶的索引
- 1.有时候会遇到桶被占满的情况,这是不可避免的,这种现象称为散列冲突,这时需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在
- 2.如果大致知道最终会有多少个元素要插入到散列表中,可以设置桶数,通常桶数设置为预计元素个数的75%-150%,最好将桶数设置为一个素数,以防键的聚集
- 3.如果散列表太满就需要再散列,这需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原有的表【装填因子决定何时对散列表进行再散列,默认为0.75】
散列表可以用于实现几个重要的数据结构,其中最简单的是set类型,set是没有重复元素的元素集合;set的add方法首先在集合中查找要添加的对象,如果不存在,就将这个对象添加进去
Java集合类库提供了一个HashSet类,它实现了基于散列表的集:
- 1.可以用add方法添加元素
- 2.contains方法被重新定义,用来快速的查看是否某个元素已经出现在集中(只在某个桶中查找元素,不必查看集合中的所有元素)
散列集迭代器将以此访问所有的桶,由于散列将元素分散在各个位置,所有访问的顺序几乎是随机的,只有不关心集合中顺序时,才应该使用HashSet
在更改集中的元素时要格外小心,如果元素的散列码发生了变化,元素在数据结构中的位置也会发生变化
4.树集
树集是一个有序集合,可以以任意顺序将元素插入到集合中;在对集合进行遍历的时候,每个值将自动地按照排序后的顺序呈现
每次将一个元素添加在树中时,都被放置在正确的排序位置上,迭代器总是以排好的顺序访问每个元素;将一个元素添加到树中,要比添加到散列表中慢,但是,与元素添加到数组或链表的正确位置上相比要快很多
如果树中包含n个元素,查找新元素的正确位置平均需要log2n次比较
5.对象的比较
默认情况下,,树集假定插入的元素实现了Comparable接口,这个接口定义了一个方法:
public interface Comparable<T>
{
int compareTo(T other);
}
如果要插入自定义的对象,就必须通过实现Comparable接口自定义排列顺序,在Object类中没有提供任何compareTo接口的默认实现
也可以通过将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较器方法,Comparator接口声明了一个带有2个显示参数的compare方法:
public interface Comparator<T>
{
int compare(T a, T b);
}
注意比较器没有任何数据,它只是比较方法的持有器,有时候将这种对象称为函数对象,函数对象通常动态定义(即定义为匿名内部类的实例):
SortedSet<Item> sortByDescription = new TreeSet<>(
new Compatator<Item>()
{
public int compare(Item a, Item b)
{
String desceA = a.getDescription();
String desceB = b.getDescription();
return descrA.compareTo(descrB);
}
});
树的排序必须是全序,任何2个元素必须是可比的,并且只有在2个元素相等时结果才是0
6.队列与双端队列
- 1.队列可以有效地在尾部添加一个元素,在头部删除一个元素
- 2.双端队列可以让人们有效地在头部和尾部同时添加或删除元素,不支持在队列中间添加元素
在Java SE6 中引入了Deque接口,并由ArrayDeque和LinkedList类实现,这2个类都提供了双端队列,而且必要时可以增加队列的长度
7.优先级队列
优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索,也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素,然而优先级队列并没有对所有的元素进行排序
如果用迭代的方式处理这些元素,并不需要对它们进行排序,优先队列使用堆(heap),堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remove)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序
与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供比较器的对象
使用优先级队列的典型示例是任务调度,每一个任务都有一个优先级,任务以随机顺序添加到队列中,每当启动一个新任务时,都将优先级最高的任务从队列中删除(习惯将1设置为最高级,所以会将最小的元素删除)
优先级队列的迭代并不是按照元素的排列顺序访问的,而删除却总是删掉剩余元素中优先级数最小的那个元素
8.映射表
- 1.集是一个集合,它可以快速地查找现有的元素
- 2.映射表用来存放键/值对
Java类库为映射表提供了2个通用的实现:HashMap和TreeMap,这2个类都实现了Map接口
- 1.散列映射表对键进行散列
- 2.树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树
散列或比较函数只能作用于键,与键关联的值不能进行散列或比较
选择散列还是树呢?与集一样,散列稍微快一些,如果不需要按照排列属性访问键,最好选择散列
如果在映射表中没有与给定的键对应的信息,get方法返回null
键必须是唯一的,不能对同一个键存放2个值;如果对同一个键调用了2个put方法,第二个值就会取代第一个值,实际上,put方法将返回这个键参数存储的上一个值
- 1.remove方法用于从映射表中删除给定键对应的元素
- 2.size方法用于返回映射表中的元素数
集合框架并没将映射表本身视为一个集合(其他的数据结构框架则将映射表视为对(pairs)的集合,或视为用键作为索引的值的集合),然而,可以获得映射表的视图,这是一组实现了Collection接口对象,或它的子接口的视图
有3种试图:
- 1.键集:
Set<K> keySet()
- 2.值集合:
Collection<V> values()
- 3.键值对集:
Set<Map.Enty<k,v>> entrySet()
keySet既不是HashSet也不是TreeSet,而是实现了Set接口的某个其他的对象,Set接口扩展了Collection接口,可以与使用任何集合一样使用keySet
例如,枚举映射表中的所有键:
Set<String> keys = map.keySet();
for(String key : keys)
{
do somthing with key
}
如果同时查看键与值,可以通过枚举各个条目(entries)查看,以避免对值进行查找,可以使用如下框架:
for(Map.Entry<String, Employee> entry : staff.entrySet())
{
String key = entry.getKey();
Employee value = entry.getValue();
do something with key . value
}
如果调用迭代器remove方法,实际上就从映射表中删除了键以及对应的值;但是不能将元素添加到键集的视图中,如果调add方法,会抛出UnsupportedOperationException
9.专用集与映射表类
在集合类库中有几个专用的映射表集:
- 1.弱散列映射表(WeakHashMap)
设计WeakHashMap类是为了解决一个有趣的问题,如果有一个值,对应的键已经不在使用了,将会出现什么情况?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了,但是,由于在程序中的任何部分没有再出现这个键,所以这个键值对无法从映射表中删除
垃圾回收器跟踪活动的对象,只要映射表对象是活动的,其中的所有桶也都是活动的,它们不能被回收;因此需要程序负责从长期活动的映射表中删除那些无用的值,或者使用WeakHashMap完成这件事,当键值对的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键值对
WeakHashMap使用弱引用保存键,WeakReference对象将引用保存到另外一个对象中,在这里就是散列表键(对于这种类型的对象,垃圾回收器用一种特有的方式进行处理,通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收)
如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中,WeakHashMap将周期性的检查队列,以便找出新添加的弱引用,一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来,于是,WeakHashMap将删除对应的条目
- 2.链接散列集和链接映射表
Java SE 1.4增加了LinkedHashSet和LinkedHashMap用来记住插入元素项的顺序(这样可以避免在散列表中的项从表面上看是随机排列的),当条目插入到表中,就会并入到双向链表中
链接散列映射表将用访问顺序,而不是插入顺序,对映射表条目进行迭代;每次调用get或put,受到影响的条目将从当前位置删除,并放到条目链表尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受到影响,一个条目总位于与键散列码对应的桶中)
构造这样一个散列映射表,调用如下:
LinkedHashMap<K, V>(initialCapacity, loadFactor, true)
访问顺序对于实现高速缓存的“最近最少使用”原则十分重要
- 3.枚举集与映射表
EnumSet是一个枚举类型元素集的高效实现,由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现,如果对应的值在集中,则相应的位置被置为1
可以使用Set接口的常用方法来修改EnumSet
EnumMap是一个键类型为枚举类型的映射表,它可以直接且高效地用一个值数组实现,在使用时,需要在构造器中指定键类型:
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
- 4.标识散列映射表
Java SE 1.4 增加了IdentityHashMap,这个类中,键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的(这是Object.hashCode方法根据内存地址来计算散列码时所使用的方式)在对2个对象进行比较的时候IdentityHashMap类使用==而不是equals
不同的键对象,即使内容相同,也被视为不同的对象
3.集合框架
Java集合类库构成了集合类的框架,它为集合的实现定义了大量的接口和抽象类,并且对其中的某些机制给予了描述,集合框架的接口如下所示:
集合有2个基本接口:Collection和Map,使用下列方法向集合中插入元素:
boolean add(E element)
V put(K key, V value)
从集合中读取元素:
V get(K key)
List是一个有序集合,元素可以添加到容器中某个特定位置,将元素放置在某个位置上可以采用2中方式:
- 1.使用整数索引
- 2.使用列表迭代器
List接口定义了几个用于随机访问的方法:
void add(int index, E element)
E get(int index)
void remove(int index)
List在提供这些随机访问的方法时,并不管它们对某种特定实现是否高效
为了避免执行成本较高,Java SE 1.4引入了一个标记接口RandomAccess,这个接口没有任何方法,但可以用来检测一个特定的集合是否支持高效的随机访问:
if(c instanceof RandomAccess)
{
usr random access algorithm
}
else
{
use sequential access algorithm
}
ArrayList和Vector类都实现了RandomAccess接口
ListIterator接口定义了一个方法,用于将元素添加到迭代器所处位置的前面:
void add(E element)
要想获取和删除给定位置的元素,只需要调用Iterator接口中的next方法和remove方法即可
*Set接口与Collection接口是一样的,只是其方法的行为有着更加严谨 定义:
- 1.集的add方法拒绝添加重复的元素*
- 2.集的equals方法定义2个集相等的条件是它们包含相同的元素但顺序不必相同
- 3.hashCode方法定义应该保证具有相同元素的集将会得到相同的散列码
在Java第一版“遗留”下来的容器类:
Vector
Stack
HashTable
Properties
这些类已经被集成到集合框架中
1.视图与包装器
通过使用视图(views)可以获得其他的实现了集合接口和映射表接口的对象(映射表类的keySet方法就是一个这样的示例)
keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射表进行操作,这种集合称为视图
视图技术在集框架中有许多非常有用的应用
- 1.轻量级包装器
Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器,这个方法可以将数组传递给一个期望得到列表或集合变元的方法,如:
Card[] cardEdck = new Card[52];
...
List<Card> cardList = Arrays.asList(cardDeck);
返回的对象不是ArrayList,它是一个视图对象,带有访问底层数组的get和set方法;改变数组大小的所有方法(add或remove)都会抛出一个UnsupportedOperationException
在Java SE 5.0开始,asList方法声明为一个具有可变数量参数的方法,除了可以传递一个数组之外,还可以将各个元素直接传递给这个方法,如:
List<String> names = Arrays.asList("Amy","Bob","Carl");
这个方法调用Collections.nCopies(n, anObject),将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都想一个 anObject的错觉
Collections类包含很多实用方法,这些方法的参数和返回值都是集合,不要将它与Collection接口混淆
- 2.子范围
可以为很多集合建立子范围(subrange)视图,假设有一个列表staff,想从中去除第10-19个元素,可以使用subList得到来获取一个列表的子范围视图
List group2 = staff.subList(10, 20);
第一个索引包含在内,第二个索引则不包含在内
可以将任何操作应用于子范围,并且能够自动地反应整个列表的情况,例如删除整个子范围:
group2.clear();
元素自动从staff列表中清除了,并且group2为空
对于有序集合映射表,可以使用排序顺序而不是元素位置建立子范围,SortedSet接口和SortedMap接口声明了3个方法:
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)
SortedMap<K, V> subMap(K from, V to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)
这些方法将返回>=from
且<to
的所有元素子集
返回映射表视图,该映射表包含键落在指定范围内的所有元素
Java SE 6中引入了NavigableSet接口赋予子范围操作更多的控制能力,可以指定是否包含边界:
NavigableSet<E> subSet(E from, boolean fromInclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
NavigableSet<E> tailSet(E from, boolean fromInclusive)
- 3.不可修改的视图
Collections还有几个方法,用于产生集合的不可修改视图,可以使用下面6种方法获得不可修改视图:
Collections.unmodifiableCollection//equals方法只检测2个对象是否是同一个对象
Collections.unmodifiableList//使用底层集合的equals和hashCode方法
Collections.unmodifiableSet//使用底层集合的equals和hashCode方法
Collections.unmodifiableSortedSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
每个方法都定义了一个接口
不可修改视图并不是本身不可修改,仍然可以通过集合的原始引用对集合进行修改,并且仍然可以让集合的元素调用更改器方法
视图只是包装了接口而不是实际的集合对象。所以只能访问接口中定义的方法
- 4.同步视图
如果由多个线程访问集合,就必须确保不会被意外地破坏
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类,例如:Collections类的静态synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map:
Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());
现在就可以由多线程访问map对象了;像get和put这类方法都是同步操作
- 5.检查视图
Java SE 5.0增加了一组“检查”视图,用来对泛型类型发生问题时提供调试支持
List<String> safeString = Collections.checkList(String, String.class);
视图的add方法将检测插入的对象是否属于给定的类,如果不属于给定的类,就立即抛出一个ClassCastException,这样做的好处是错误可以在正确的位置得以报告
被检查视图受限于虚拟机可以运行的运行时检测
- 6.关于可选操作的说明
视图有一些局限性,即可能只可读、无法改变大小,只支持删除而不支持插入,这些与映射表的键视图情况相同,如果进行不恰当的操作,会抛出异常
2.批操作
绝大多数示例都使用迭代器遍历集合,一次遍历一个元素,然而可以使用类库中的批操作(bulk operation)避免频繁地使用迭代器
3.集合与数组之间的转换
如果有一个数组需要转换为集合,Arrays.aList的包装器就可以实现这个目的,例如:
String[] valus = ...;
HashSet<String> staff = new HashSet<>(Arrays.asList(values));
将集合转换为数组有点困难,可以使用toArray方法:
Object[] values = staff.toArrays();
这样将产生一个对象数组,即使知道集合中包含一个特定类型的对象,也不能进行类型转换:
String[] values = (String[]) staff.toArray();//error!
由toArray方法返回的数组是一个Object数组,无法改变其类型
应该采用如下方法:
String[] values = staff.toArray(new String[staff.size()]);
4.算法
泛型集合接口有一个很大的优点,即算法只需要实现一次,下面是找到数组中的最大元素的代码:
数组:
if(a.length == 0)throw new NoSuchElementException();
T largest = a[0];
for(int i = 1; i < a.length; i++)
{
if(largest.compareTo(a[i]) < 0)
{
largest = a[i];
}
}
数组列表:
if(v.size() == 0)throw new NoSuchElementException();
T largest = v.get();
for(int i = 1; i < v.size(); i++)
{
if(largest.compareTo(v.get(i)) < 0)
{
largest = v.get(i);
}
}
链表:
if(l.isEmpty())throw new NoSuchElementException();
Iterator<T> iter = l.iterator();
T largest = iter.next();
while(iter.hasNext())
{
T next = iter.next();
if(largest.compareTo(next) < 0)
{
largest = next;
}
}
可以将max方法实现为能够接受任何实现了Collection接口的对象:
public static <T extends Comparable> T max(Collection<T> c)
{
if(c.isEmpty()) throw new NoSuchElementException();
Iterator<T> iter = c.iterator();
T largest = iter.next();
while(iter.hasNext())
{
T next = iter.next();
if(largest.compareTo(next) < 0)
{
largest = next;
}
return largest;
}
}
现在就可以用一个方法计算链表、数组列表、数组中的最大元素
1.排序与混排
Collections类中的sort方法可以对实现了List接口的集合进行排序
List<String> staff = new LinkedList<>();
fill collection
Collections.sort(staff);
这个方法假定列表元素实现了Comparable接口,如果想采用其他方式进行排序,可以将Comparator对象作为第二个参数传递给sort方法
Comparator<Item> itemComparator = new Comparator<Item>()
{
public int compare(Item a, Item b)
{
return a.partumber - b.partNumber;
}
};
Collections.sort(items,itemComparator);
如果想按照降序排列,可以使用一个静态方法Collections.reverseOrder(),这个方法将返回一比较器,比较器则返回b.compareTo(a),例如:
Collections.sort(staff, Collections.reverseOrder())
这个方法将根据元素类型的compareTo方法给定排序顺序,按照逆序对列表staff进行排序,同样
Collections.sort(staff, Collections.reverseOrder(itemComparator))
将逆置itemComparator的次序
- 1.如果列表支持set方法,则是可修改的
- 2.如果列表支持add和remove方法,则是可改变大小的
Collections类有一个算法shuffle,随机的混排列表中的顺序:
ArrayList<Card> cards = ...;
Collections.shuffle(cards);
如果提供的列表没有实现RandomAccess接口,shuffle方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱的元素复制回列表
2.二分查找
Collections类的binarySearch方法实现了二分算法,注意,集合必须是排好序的,否则算法将返回错误的答案,想要查找某个元素,必须提供集合(这个集合要实现List接口)以及要查找的元素;如果集合没有采用Comparable接口的compareTo方法进行排序,就要提供比较器对象:
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
如果返回为>=0,表示匹配到了,否则没有匹配到,可以利用返回值计算要将element插入到集合的哪个位置,以保持有序性,插入位置应该是:insertionPoint = -i -1;其中i应该小于0
如果为binarySearch算法提供一个链表,它将自动地变为线性查找
binarySearch方法检查列表参数是否实现了RandomAccess接口,如果实现了这个接口,这个方法就采用二分查找,否则,将采用线性查找
3.简单算法
Collections类中包含很多实用的简单算法,查看API文档吧
4.编写自己的算法
如果编写自己的算法(实际上是以集合作为参数的任何方法),应该尽可能使用接口,而不是使用具体的实现
5.遗留的集合
1.Hashtable类
Hashtable类与HashMap类的作用一样,它们拥有相同的接口,与Vector类的方法一样,Hashtable的方法是同步的,如果对同步性和遗留代码的兼容性没有要求,可以使用HashMap
Java编译器对大小写敏感
2.枚举
遗留集合使用Enumeration接口对元素进行遍历,有2个方法,hasMoreElement和nextElement方法
3.属性映射表
属性映射表是一个类型非常特殊的映射表结构,有3个特性:
- 1.键与值都是字符串
- 2.便可以保存到一个文件中,也可以从文件中加载
- 3.使用一个默认的辅助表
实现属性映射表的Java平台称为Properties
4.栈
对Stack的操作,push和pop方法,stack类扩展了Vector,这样栈可以使用不属于栈操作的insert和remove方法,即可以在任何位置进行插入和删除 不仅仅是在栈顶
5.位集
Java平台的BitSet类用于存放一个位序列,由于位集将位包装在字节里,所以,使用位集要比使用Boolean对象的ArrayList更加高效
BitSet类提供了一个便于读取、设置和清除各个为的接口,使用这个接口可以避免屏蔽和其他麻烦的位操作