Java集合框架完全解析.docx
《Java集合框架完全解析.docx》由会员分享,可在线阅读,更多相关《Java集合框架完全解析.docx(22页珍藏版)》请在冰豆网上搜索。
Java集合框架完全解析
Java集合框架完全解析
一、概述
现实生活中集合:
很多事物凑在一起。
数学中的集合:
具有共同属性的事物的总体。
Java中的集合类:
是一种工具类,就像是容器,储存任意数量的具有共同属性的对象。
在编程时,常常需要集中存放多个数据,当然我们可以使用数组来保存多个对象。
但数组长度不可变化,一旦初始化数组时指定了数组长度,则这个数组长度是不可变的,如果需要保存个数变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:
语文—79,数学—80,这种数据看上去像两个数组,但这个两个数组元素之间有一定的关联关系。
为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供集合类。
集合类主要负责保存其他数据,因此集合类也被称为容器类。
所有容器类都位于Java.util包下。
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用);而集合里只能保存对象(实际上也是保存对象的引用,但通常习惯上认为集合里保存的是对象)。
Java集合框架由Java类库的一系列接口、抽象类以及具体实现类组成。
我们这里所说的集合就是把一组对象组织到一起,然后再根据不同的需求操纵这些数据。
集合类型就是容纳这些对象的一个容器。
也就是说,最基本的集合特性就是把一组对象放一起集中管理。
根据集合中是否允许有重复的对象、对象组织在一起是否按某种顺序等标准来划分的话,集合类型又可以细分为许多种不同的子类型。
Java集合框架为我们提供了一组基本机制以及这些机制的参考实现,其中基本的集合接口是Collection接口,其他相关的接口还有Iterator接口、RandomAccess接口等。
这些集合框架中的接口定义了一个集合类型应该实现的基本机制,Java类库为我们提供了一些具体集合类型的参考实现,根据对数据组织及使用的不同需求,只需要实现不同的接口即可。
Java类库还为我们提供了一些抽象类,提供了集合类型功能的部分实现,我们也可以在这个基础上去进一步实现自己的集合类型。
Java集合框架的优势有以下几点:
1)这种框架是高性能的。
对基本类集(动态数组,链接表,树和散列表)的实现是高效率的。
一般很少需要人工去对这些“数据引擎”编写代码(如果有的话)。
2)框架允许不同类型的类集以相同的方式和高度互操作方式工作。
3)类集是容易扩展和/或修改的。
为了实现这一目标,类集框架被设计成包含一组标准的接口。
对这些接口,提供了几个标准的实现工具(例如LinkedList,HashSet和TreeSet),通常就是这样使用的。
如果你愿意的话,也可以实现你自己的类集。
为了方便起见,创建用于各种特殊目的的实现工具。
一部分工具可以使你自己的类集实现更加容易。
4)增加了允许将标准数组融合到类集框架中的机制。
二、集合接口与迭代器接口
Java的容器类主要由两个接口派生而出:
Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。
通过Collection与Map接口导出其他子接口及实现类的框架示意图如下图所示:
查看jdk中Collection类的源码后会发现如下内容:
[java]viewplaincopy
publicinterfaceCollectionextendsIterable{
//实现Collection接口的通用方法
intsize();
booleanisEmpty();
booleancontains(Objecto);
Iterableiterable();
Object[]toArray();
T[]toArray(T[]a);
booleanadd(Ee);
booleanremove(Objecto);
booleancontainsAll(Collection
>c);
booleanaddAll(Collection
extendsE>c);
booleanremoveAll(Collection
>c);
booleanretainAll(Collection
>c);
voidclear();
booleanequals(Objecto);
inthashCode();
}
通过源码发现Collection是一个接口类,其继承了Java迭代接口Iterable。
Collection接口有三个主要的子接口:
List、Set和Queue,注意Map不是Collection的子接口,这个要牢记。
Collection中可以存储的元素间无序,可以重复组各自独立的元素,即其内的每个位置仅持有一个元素,同时允许有多个null元素对象。
JDK不提供Collection接口的具体实现,而是提供了更加具体的子接口(如Set、List和Queue)实现。
那么Collection接口的存在有何作用呢?
存在即是道理。
原因在于:
为所有容器的实现类(如ArrayList实现了List接口,HashSet实现了Set接口)提供了两个“标准”的构造函数来实现:
一个无参的构造方法;一个带有Collection类型参数的单参数构造方法。
实际上:
因为所有通用的容器类都遵从Collection接口,用第二种构造方法允许容器之间相互的复制。
Collection接口中的方法如下:
其中,iterator方法用于返回一个实现了Iterator接口的对象,可以使用这个对象依次访问集合中的元素。
Iterator接口包含3个方法:
[java]viewplaincopy
publicinterfaceIterator{
Enext();
booleanhasNext();
voidremove();
}
通过反复调用next方法,可以逐个访问集合中的每个元素。
但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException异常。
因此,需要在调用next之前调用hasNext方法。
如果迭代器对象还有多个供访问的元素,这个方法就返回true。
如果要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true后反复调用next方法。
例如:
[java]viewplaincopy
Collectionc=...;
Iteratoriter=c.iterator();
while(iter.hasNext()){
Stringelement=iter.next();
dosomethingwithelement
}
从JavaSE5.0起,这个循环可以采用一种更优雅的缩写方式。
用foreach循环可以更加简练地表示同样的循环操作:
[java]viewplaincopy
for(Stringelement:
c){
dosomethingwithelement
}
Collectioin接口实现了Iterable接口,我们再来简单看下Iterable接口的定义:
[java]viewplaincopy
publicinterfaceIterable{
Iteratoriterator();
}
上面我们一共提到了两个和迭代器相关的接口:
Iterable接口和Iterator接口,从字面意义上来看,前者的意思是“可迭代的”,后者的意思是“迭代器”。
所以我们可以这么理解这两个接口:
实现了Iterable接口的类是可迭代的;实现了Iterator接口的类是一个迭代器。
迭代器就是一个我们用来遍历集合中的对象的东西。
也就是说,对于集合,我们不是像对原始类型数组那样通过直接访问元素来迭代,而是通过迭代器来遍历对象。
这么做的好处是将对于集合类型的遍历行为与被遍历的集合对象分离,这样一来我们无需关心该集合类型的具体实现是怎样的。
只要获取这个集合对象的迭代器,便可以遍历这个集合中的对象了。
而像遍历对象的顺序这些细节,全部由它的迭代器来处理。
现在我们来梳理一下前面提到的这些东西:
首先,Collection接口实现了Iterable接口,这意味着所有实现了Collection接口的具体集合类都是可迭代的。
那么既然要迭代,我们就需要一个迭代器来遍历相应集合中的对象,所以Iterable接口要求我们实现iterator方法,这个方法要返回一个迭代器对象。
一个迭代器对象也就是实现了Iterator接口的对象,这个接口要求我们实现hasNext()、next()、remove()这三个方法。
其中hasNext方法判断是否还有下一个元素(即是否遍历完对象了),next方法会返回下一个元素(若已经没有下一个元素了,调用它会抛出一个NoSuchElementException异常),remove方法用于移除最近一次调用next方法返回的元素(若没有调用next方法而直接调用remove方法会报错)。
我们可以想象在开始对集合进行迭代前,有一个指针指向集合第一个元素的前面,第一次调用next方法后,这个指针会“扫过”第一个元素并返回它,调用hasNext方法就是看这个指针后面还有没有元素了。
也就是说这个指针始终指向刚遍历过的元素和下一个待遍历的元素之间。
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。
Java类库的设计者认为:
这些实用方法中的某些方法非常有用,应该将它们提供给用户使用,这样,类库的使用者就不必自己重构这些方法了。
当然如果实现Collection接口的每一个类都提供如此多的例行方法将是一件很烦人的事情,为了能够让实现者更容易地实现这个接口,Java类库提供了一个抽象类AbstractCollection,它将基础方法size和iterator抽象化了,但是提供了通用例行方法。
例如:
[java]viewplaincopy
publicabstractclassAbstractCollectionimplementsCollection{
publicabstractIteratoriterator();
publicabstractintsize();
publicbooleanisEmpty(){
returnsize()==0;
}
publicbooleancontains(Objecto){
Iteratore=iterator();
if(o==null){
while(e.hasNext()){
if(e.next()==null)
returntrue;
}
}
else{
while(e.hasNext()){
if(o.equals(e.next()))
returntrue;
}
}
returnfalse;
}
publicObject[]toArray(){
Object[]result=newObject[size()];
Iteratore=iterator();
for(inti=0;e.hasNext();i++){
result[i]=e.next();
}
returnresult;
}
publicbooleanremove(Objecto){
Iteratore=iterator();
if(o==null){
while(e.hasNext()){
if(e.next()==null){
e.remove();
returntrue;
}
}
}
else{
while(e.hasNext()){
if(o.equals(e.next())){
e.remove();
returntrue;
}
}
}
returnfalse;
}
......
}
AbstractCollection的直接子类有:
AbstractList、AbstractQueue、AbstractSet。
三、Collection接口层次结构
下面的图是关于Collection的类的层次结构:
Set接口:
一个不包括重复元素(包括可变对象)的Collection,是一种无序的集合。
Set不包含满足a.equals(b)的元素对a和b,并且最多有一个null。
实现Set接口的类有:
EnumSet、HashSet、TreeSet等。
有一种众所周知的数据结构,可以快速查找所需要的对象,这就是散列表(hashtable)。
散列表为每个对象计算一个整数,称为散列码(hashcode)。
散列码由对象的实例域产生的一个整数。
准确地说,具有不同数据域的对象将产生不同的散列码。
如果自定义类,就要负责这个类的hashCode方法,注意,自己实现的hashCode方法应该与equals方法兼容,即若a.equals(b)为true,a与b必须具有相同的散列码。
散列码可以是任何整数,包括整数或负数。
在Java中,散列表用链表数组实现,每个列表称为桶。
计算对象的散列码并对桶数取余,得到的结果就是保存这个对象的桶的索引。
如果想要更多地控制散列表的性能,就要指定一个初始的桶数。
通常,将桶数设置为预计元素个数的75%~150%。
有些研究人员认为:
尽管还没有确凿的证据,但最好将桶数设置为素数,以防键的集聚。
当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。
如果散列表太满,就需要再散列(rehashed)。
如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入新表中,废弃旧表。
装填因子(loadfactor)决定何时对散列表进行再散列。
例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。
Java集合类库提供了一个HashSet类,它实现了基于散列表的集。
可以用add方法添加元素。
contains方法已经被重新定义,用来快速查看是否某个元素已经出现在集中,它只在某个桶中查找元素,而不必查看集合中的所有元素。
树集(TreeSet)是一个有序集合,当前使用的数据结构是红黑树。
将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。
TreeSet中的对象需要实现Comparable接口,这样才能进行元素之间的比较,如果一个类的创建者没有实现Comparable接口,可以创建一个Comparator类来规定原类对象之间的比较规则,将一个Comparator对象实例传递给TreeSet的构造器即可。
List接口:
一个有序的Collection(也称序列),元素可以重复。
列表通常允许满足e1.equals(e2)的元素对e1和e2,并且列表允许多个null元素。
实现List的类有:
ArrayList、LinkedList、Vector、Stack等。
其中,最常用的是ArrayList和LinkedList。
List接口是有序集合、元素可以重复,次序是List接口最重要的特点,它是以元素的添加的顺序作为集合的顺序,因此List的实现类中有可以通过来操作集合元素的方法。
其中ArrayList底层是通过数组实现的,数组的初始长度为10,可以扩展数组。
LinkedList底层是通过双向链表实现的,因此LinkedList可以在首尾添加删减元素,因此可以作为栈、队列、双端队列使用。
Queue接口:
JavaSE6中引入了Deque接口,它是Queue接口的子接口,Deque的实现类为ArrayDeque,可以实现双端队列,底层是通过数组实现,ArrayDeque有两个标志位分别指向数组的头与尾,因此才可以实现双端队列。
Queue接口的实现类在使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。
它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。
offer()方法向队列中加入元素,不成功时返回false。
而直接使用add()方法插入,若队列已满则抛出异常。
remove()和poll()方法删除并返回队列一端的元素。
前者队列为空时抛出异常,后者返回null。
当需要使用LIFO(后进先出)堆栈时。
应优先使用Deque接口而不是遗留Stack类。
在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。
堆栈的方法完全可以等效成Deque的某些方法,对应关系如下:
[java]wplaincopy
push(e)---addFirst(e)
pop()---removeFrist()
top()-----peekFirst()
优先级队列(priorityqueue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。
也就是说,无论何时调用remove方法,总会获得当前的优先级队列中最小的元素。
优先级队列并没有对所有的元素进行排序。
优先级队列使用了堆,堆是一个可以自我调整的二叉村,对树执行添加和删除操作时,总能保证最小的元素移动到根,而不必花费时间对元素进行排序。
与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器提供比较器的类对象。
优先级队列的典型示例是任务调度。
每一个任务有一个优先级,将优先级最高的任务从队列中删除(由于习惯上将1设为“最高”优先级,所以会将最小的元素删除)。
与TreeSet迭代不同,这里的迭代并不是按照元素的排列顺序迭代的。
删除时总是删掉剩余元素中优先级数最小的元素。
还有一种队列是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要实现类包括:
ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。
虽然接口并未定义阻塞方法,但是实现类扩展了父接口,实现了阻塞方法。
四、Map接口的层次结构
下面的图是Map接口的层次结构图
Map是一个键值对的集合。
也就是说,一个映射不能包含重复的键,每个键最多映射到一个值。
该接口取代了Dictionary抽象类。
实现map接口的类有:
HashMap、TreeMap、HashTable、Properties、EnumMap。
散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索村。
散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。
如何选择散列映射表与树映射表。
与集一样,散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择散列。
键必须是唯一的,不能对同一个键存放两个值。
如果对同一个键两次调用put方法,第二个值就会取代第一个值。
实际上,put方法会返回用这个键参数存储的上一个值。
集合框架并没有将映射表本身视为一个集合(其他的数据结构框架则将映射表视为对pairs的集合,或者视为用键作为索引的值的集合)。
然而,可以获得映射表的视图,这是一组实现了Collection接口或者它的子接口的视图。
有3个视图,它们分别是:
键集、值集和键/值对集,键与键/值对形成了一个集,这是因为在映射表中一个键只能有一个副本。
下列方法将返回这3个视图(条目集的元素是静态内部类Map.Entry的对象)。
[java]viewplaincopy
Setkeyset();
Collectionvalues();
Set>entrySet();
注意,keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个其他类的对象。
初看起来,keyset方法创建了一个新集,并将映射表中的所有键都填进去,然后返回这个集。
但是,情况并非如此。
取而代之的是:
keySet方法返回一个实现了Set接口的某个类的对象,这个类的方法对原映射表进行操作。
Set接口扩展了Collection接口,因此可以与使用任何集合一样使用keySet。
例如,可以杖举映射表中的所有键:
[java]viewplaincopy
Setkeys=map.keySet();
for(Stringkey:
keys){
System.out.println(key);
}
如果想要同时查看键与值,可以通过杖举各个条目(entries)查看,以避免对值进行查找。
[java]viewplaincopy
for(Map.Entryentry:
staff.entrySet()){
Stringkey=entry.getKey();
Employeevalue=entry.getValue();
System.out.println("key="+key+",value="+value);
}
五、数组与集合之间的转换
由于Java平台API中大部分内容都是在集合框架创建之前设计的,所以,有时候需要在传统的数组与现代的集合之间进行转换。
如果数组要转换为集合,Arrays.asList的包装器就可以实现这个目的。
例如:
[java]viewplaincopy
String[]values=...;
HashSetstaff=newHashSet(Arrays.asList(values));
反过来,将集合转换为数组采用toArray()方法。
转化为Object[]类型数组方法如下:
[java]viewplaincopy
Object[]listArray=list.toArray();
Object[]setArray=set.toArray();
转化为具体类型数组:
[java]viewplaincopy
String[]array1=(String[])list.toArray(newString[list.size()]);
String[]array2=(String[])list.toArray(newString[0]);
在转化为其它类型的数组时需要强制类型转换,并且要使用带参数的toArray方法,参数为对象数组,将list中的内容放入参数数组中,当参数数组的长度小于list的元素个数时,会自动扩充数组的长度以适应list的长度。
六、List接口实现类
List接口继承了Collection接口,并对父接口进行了简单的扩充:
同时List接口又有三个常用的实现类ArrayList、LinkedList和Vector。
1)ArrayList(数组线性表)
ArrayList数组线性表的特点为:
用类似数组的形式进行存储,因此它的随机访问