分享一下我老师大神的人工智能教程。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.youkuaiyun.com/jiangjunshow
Java集合排序及java集合类详解
(Collection, List, Set, Map)
摘要内容
集合是Java里面最常用的,也是最重要的一部分。能够用好集合和理解好集合对于做Java程序的开发拥有无比的好处。本文详细解释了关于Java中的集合是如何实现的,以及他们的实现原理。
目录
1.1 集合框架概述.....................................................................................2
1.1.1 容器简介...................................................................................2
1.1.2 容器的分类.................................................................................5
1.2.1 常用方法....................................................................................6
1.2.2 迭代器........................................................................................9
1.3.2 常用方法...................................................................................12
1.3.3 实现原理..................................................................................17
1.4.2 常用方法...................................................................................21
1.4.3 Comparable接口.....................................................................27
1.4.4 实现原理..................................................................................29
1.4.5 覆写hashCode()........................................................................34
1.5.2 常用方法.....................................................................................39
1.5.3 实现原理..................................................................................45
1.6 总结:集合框架中常用类比较..............................................................46
1 集合框架
1.1 集合框架概述
到目前为止,我们已经学习了如何创建多个不同的对象,定义了这些对象以后,我们就可以利用它们来做一些有意义的事情。
举例来说,假设要存储许多雇员,不同的雇员的区别仅在于雇员的身份证号。我们可以通过身份证号来顺序存储每个雇员,但是在内存中实现呢?是不是要准备足够的内存来存储1000个雇员,然后再将这些雇员逐一插入?如果已经插入了500条记录,这时需要插入一个身份证号较低的新雇员,该怎么办呢?是在内存中将500条记录全部下移后,再从开头插入新的记录?还是创建一个映射来记住每个对象的位置?当决定如何存储对象的集合时,必须考虑如下问题。
对于对象集合,必须执行的操作主要以下三种:
u 添加新的对象
u 删除对象
u 查找对象
我们必须确定如何将新的对象添加到集合中。可以将对象添加到集合的末尾、开头或者中间的某个逻辑位置。
从集合中删除一个对象后,对象集合中现有对象会有什么影响呢?可能必须将内存移来移去,或者就在现有对象所驻留的内存位置下一个“洞”。
在内存中建立对象集合后,必须确定如何定位特定对象。可建立一种机制,利用该机制可根据某些搜索条件(例如身份证号)直接定位到目标对象;否则,便需要遍历集合中的每个对象,直到找到要查找的对象为止。
前面大家已经学习过了数组。数组的作用是可以存取一组数据。但是它却存在一些缺点,使得无法使用它来比较方便快捷的完成上述应用场景的要求。
1. 首先,在很多数情况下面,我们需要能够存储一组数据的容器,这一点虽然数组可以实现,但是如果我们需要存储的数据的个数多少并不确定。比如说:我们需要在容器里面存储某个应用系统的当前的所有的在线用户信息,而当前的在线用户信息是时刻都可能在变化的。也就是说,我们需要一种存储数据的容器,它能够自动的改变这个容器的所能存放的数据数量的大小。这一点上,如果使用数组来存储的话,就显得十分的笨拙。
2. 我们再假设这样一种场景:假定一个购物网站,经过一段时间的运行,我们已经存储了一系列的购物清单了,购物清单中有商品信息。如果我们想要知道这段时间里面有多少种商品被销售出去了。那么我们就需要一个容器能够自动的过滤掉购物清单中的关于商品的重复信息。如果使用数组,这也是很难实现的。
3. 最后再想想,我们经常会遇到这种情况,我知道某个人的帐号名称,希望能够进一步了解这个人的其他的一些信息。也就是说,我们在一个地方存放一些用户信息,我们希望能够通过用户的帐号来查找到对应的该用户的其他的一些信息。再举个查字典例子:假设我们希望使用一个容器来存放单词以及对于这个单词的解释,而当我们想要查找某个单词的意思的时候,能够根据提供的单词在这个容器中找到对应的单词的解释。如果使用数组来实现的话,就更加的困难了。
为解决这些问题,Java里面就设计了容器集合,不同的容器集合以不同的格式保存对象。
数学背景
在常见用法中,集合(collection)和数学上直观的集(set)的概念是相同的。集是一个唯一项组,也就是说组中没有重复项。实际上,“集合框架”包含了一个Set接口和许多具体的Set类。但正式的集概念却比 Java 技术提前了一个世纪,那时英国数学家 George Boole按逻辑正式的定义了集的概念。大部分人在小学时通过我们熟悉的维恩图引入的“集的交”和“集的并”学到过一些集的理论。
集的基本属性如下:
u 集内只包含每项的一个实例
u 集可以是有限的,也可以是无限的
u 可以定义抽象概念
集不仅是逻辑学、数学和计算机科学的基础,对于商业和系统的日常应用来说,它也很实用。“连接池”这一概念就是数据库服务器的一个开放连接集。Web服务器必须管理客户机和连接集。文件描述符提供了操作系统中另一个集的示例。
映射是一种特别的集。它是一种对(pair)集,每个对表示一个元素到另一元素的单向映射。一些映射示例有:
u IP 地址到域名(DNS)的映射
u 关键字到数据库记录的映射
u 字典(词到含义的映射)
u 2 进制到 10进制转换的映射
就像集一样,映射背后的思想比 Java编程语言早的多,甚至比计算机科学还早。而Java中的Map就是映射的一种表现形式。
既然您已经具备了一些集的理论,您应该能够更轻松的理解“集合框架”。 “集合框架”由一组用来操作对象的接口组成。不同接口描述不同类型的组。在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。框架接口层次结构如下图所示。
Java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:
1) Collection 。一组对立的元素,通常这些元素都服从某种规则。List必须保持元素特定的顺序,而Set不能有重复元素。
2) Map 。一组成对的“键值对”对象。初看起来这似乎应该是一个Collection,其元素是成对的对象,但是这样的设计实现起来太笨拙了,于是我们将Map明确的提取出来形成一个独立的概念。另一方面,如果使用Collection表示Map的部分内容,会便于查看此部分内容。因此Map一样容易扩展成多维Map,无需增加新的概念,只要让Map中的键值对的每个“值”也是一个Map即可。
Collection和Map的区别在于容器中每个位置保存的元素个数。Collection每个位置只能保存一个元素(对象)。此类容器包括:List,它以特定的顺序保存一组元素;Set则是元素不能重复。
Map保存的是“键值对”,就像一个小型数据库。我们可以通过“键”找到该键对应的“值”。
u Collection –对象之间没有指定的顺序,允许重复元素。
u Set – 对象之间没有指定的顺序,不允许重复元素
u List– 对象之间有指定的顺序,允许重复元素,并引入位置下标。
u Map – 接口用于保存关键字(Key)和数值(Value)的集合,集合中的每个对象加入时都提供数值和关键字。Map接口既不继承 Set也不继承 Collection。
List、Set、Map共同的实现基础是Object数组
除了四个历史集合类外,Java 2框架还引入了六个集合实现,如下表所示。
接口 | 实现 | 历史集合类 |
Set | HashSet |
|
| TreeSet |
|
List | ArrayList | Vector |
| LinkedList | Stack |
Map | HashMap | Hashtable |
| TreeMap | Properties |
这里没有Collection接口的实现,接下来我们再来看一下下面的这张关于集合框架的大图:
这张图看起来有点吓人,熟悉之后就会发现其实只有三种容器:Map,List和Set,它们各自有两个三个实现版本。常用的容器用黑色粗线框表示。
点线方框代表“接口”,虚线方框代表抽象类,而实线方框代表普通类(即具体类,而非抽象类)。虚线箭头指出一个特定的类实现了一个接口(在抽象类的情况下,则是“部分”实现了那个接口)。实线箭头指出一个类可生成箭头指向的那个类的对象。例如任何集合( Collection )都能产生一个迭代器( Iterator ),而一个List除了能生成一个ListIterator(列表迭代器)外,还能生成一个普通迭代器,因为List正是从集合继承来的.
1.2 Collection
1.2.1 常用方法
Collection
接口用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素时,就使用这一接口。Collection在前面的大图也可以看出,它是List和Set的父类。并且它本身也是一个接口。它定义了作为集合所应该拥有的一些方法。如下:
注意:
集合必须只有对象,集合中的元素不能是基本数据类型。
Collection接口支持如添加和除去等基本操作。设法除去一个元素时,如果这个元素存在,除去的仅仅是集合中此元素的一个实例。
u booleanadd(Object element)
u booleanremove(Object element)
Collection 接口还支持查询操作:
u int size()
u booleanisEmpty()
u booleancontains(Object element)
u Iteratoriterator()
组操作:Collection接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于整个集合的任务。
u boolean containsAll(Collectioncollection)
u boolean addAll(Collection collection)
u void clear()
u void removeAll(Collection collection)
u void retainAll(Collection collection)
containsAll() 方法允许您查找当前集合是否包含了另一个集合的所有元素,即另一个集合是否是当前集合的子集。其余方法是可选的,因为特定的集合可能不支持集合更改。 addAll()方法确保另一个集合中的所有元素都被添加到当前的集合中,通常称为并。 clear()方法从当前集合中除去所有元素。 removeAll() 方法类似于 clear() ,但只除去了元素的一个子集。 retainAll()方法类似于 removeAll() 方法,不过可能感到它所做的与前面正好相反:它从当前集合中除去不属于另一个集合的元素,即交。
我们看一个简单的例子,来了解一下集合类的基本方法的使用:
import java.util.*;
public class CollectionToArray {
public static voidmain(String[] args) {
Collection collection1=newArrayList();//创建一个集合对象
collection1.add("000");//添加对象到Collection集合中
collection1.add("111");
collection1.add("222");
System.out.println("集合collection1的大小:"+collection1.size());
System.out.println("集合collection1的内容:"+collection1);
collection1.remove("000");//从集合collection1中移除掉 "000"这个对象
System.out.println("集合collection1移除 000后的内容:"+collection1);
System.out.println("集合collection1中是否包含000:"+collection1.contains("000"));
System.out.println("集合collection1中是否包含111:"+collection1.contains("111"));
Collection collection2=newArrayList();
collection2.addAll(collection1);//将collection1集合中的元素全部都加到collection2中
System.out.println("集合collection2的内容:"+collection2);
collection2.clear();//清空集合 collection1中的元素
System.out.println("集合collection2是否为空:"+collection2.isEmpty());
//将集合collection1转化为数组
Object s[]= collection1.toArray();
for(inti=0;i<s.length;i++){
System.out.println(s[i]);
}
}
}
运行结果为:
集合collection1的大小:3
集合collection1的内容:[000, 111, 222]
集合collection1移除 000后的内容:[111,222]
集合collection1中是否包含000:false
集合collection1中是否包含111:true
集合collection2的内容:[111, 222]
集合collection2是否为空:true
111
222
这里需要注意的是,Collection它仅仅只是一个接口,而我们真正使用的时候,确是创建该接口的一个实现类。做为集合的接口,它定义了所有属于集合的类所都应该具有的一些方法。
而ArrayList(列表)类是集合类的一种实现方式。
这里需要一提的是,因为Collection的实现基础是数组,所以有转换为Object数组的方法:
u Object[] toArray()
u Object[] toArray(Object[] a)
其中第二个方法Object[] toArray(Object[] a)的参数 a应该是集合中所有存放的对象的类的父类。
任何容器类,都必须有某种方式可以将东西放进去,然后由某种方式将东西取出来。毕竟,存放事物是容器最基本的工作。对于ArrayList,add()是插入对象的方法,而get()是取出元素的方式之一。ArrayList很灵活,可以随时选取任意的元素,或使用不同的下标一次选取多个元素。
如果从更高层的角度思考,会发现这里有一个缺点:要使用容器,必须知道其中元素的确切类型。初看起来这没有什么不好的,但是考虑如下情况:如果原本是ArrayList,但是后来考虑到容器的特点,你想换用Set,应该怎么做?或者你打算写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就可以应用于不同类型的容器?
所以迭代器(Iterator)的概念,也是出于一种设计模式就是为达成此目的而形成的。所以Collection不提供get()方法。如果要遍历Collectin中的元素,就必须用Iterator。
迭代器(Iterator)本身就是一个对象,它的工作就是遍历并选择集合序列中的对象,而客户端的程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为“轻量级”对象,创建它的代价小。但是,它也有一些限制,例如,某些迭代器只能单向移动。
Collection接口的iterator()方法返回一个Iterator。Iterator和您可能已经熟悉的Enumeration接口类似。使用 Iterator接口方法,您可以从头至尾遍历集合,并安全的从底层Collection中除去元素。
下面,我们看一个对于迭代器的简单使用:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
publicclass IteratorDemo {
publicstaticvoid main(String[] args) {
Collection collection =new ArrayList();
collection.add("s1");
collection.add("s2");
collection.add("s3");
Iterator iterator =collection.iterator();//得到一个迭代器
while (iterator.hasNext()) {//遍历
Object element =iterator.next();
System.out.println("iterator = " + element);
}
if(collection.isEmpty())
System.out.println("collection is Empty!");
else
System.out.println("collection is not Empty! size="+collection.size());
Iterator iterator2 =collection.iterator();
while (iterator2.hasNext()) {//移除元素
Object element =iterator2.next();
System.out.println("remove: "+element);
iterator2.remove();
}
Iterator iterator3 =collection.iterator();
if (!iterator3.hasNext()) {//察看是否还有元素
System.out.println("还有元素");
}
if(collection.isEmpty())
System.out.println("collection is Empty!");
//使用collection.isEmpty()方法来判断
}
}
程序的运行结果为:
iterator = s1
iterator = s2
iterator = s3
collection is not Empty! size=3
remove: s1
remove: s2
remove: s3
还有元素
collection is Empty!
可以看到,Java的Collection的Iterator能够用来,:
1) 使用方法 iterator()要求容器返回一个Iterator .第一次调用Iterator的next()方法时,它返回集合序列的第一个元素。
2) 使用next()获得集合序列的中的下一个元素。
3) 使用hasNext()检查序列中是否元素。
4) 使用remove()将迭代器新返回的元素删除。
需要注意的是:方法删除由next方法返回的最后一个元素,在每次调用next时,remove方法只能被调用一次。
大家看,Java实现的这个迭代器的使用就是如此的简单。Iterator(跌代器)虽然功能简单,但仍然可以帮助我们解决许多问题,同时针对List还有一个更复杂更高级的ListIterator。您可以在下面的List讲解中得到进一步的介绍。
1.3 List
1.3.1 概述
前面我们讲述的Collection[i1] 接口实际上并没有直接的实现类。而List是容器的一种,表示列表的意思。当我们不知道存储的数据有多少的情况,我们就可以使用List来完成存储数据的工作。例如前面提到的一种场景。我们想要在保存一个应用系统当前的在线用户的信息。我们就可以使用一个List来存储。因为List的最大的特点就是能够自动的根据插入的数据量来动态改变容器的大小。下面我们先看看List接口的一些常用方法。
1.3.2 常用方法
List就是列表的意思,它是Collection的一种,即
继承了Collection
接口,以定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。List是按对象的进入顺序进行保存对象,而不做排序或编辑操作。它除了拥有Collection接口的所有的方法外还拥有一些其他的方法。
面向位置的操作包括插入某个元素或Collection的功能,还包括获取、除去或更改元素的功能。在List中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置。
u void add(intindex, Object element):添加对象element到位置index上
u booleanaddAll(int index, Collection collection):在index位置后添加容器collection中所有的元素
u Object get(intindex):取出下标为index的位置的元素
u intindexOf(Object element):查找对象element在List中第一次出现的位置
u intlastIndexOf(Object element):查找对象element在List中最后出现的位置
u Objectremove(int index):删除index位置上的元素
u Object set(intindex, Object element):将index位置上的对象替换为element并返回老的元素。
先看一下下面表格:
| 简述 | 实现 | 操作特性 | 成员要求 |
List | 提供基于索引的对成员的随机访问 | ArrayList | 提供快速的基于索引的成员访问,对尾部成员的增加和删除支持较好 | 成员可为任意Object子类的对象 |
LinkedList | 对列表中任何位置的成员的增加和删除支持较好,但对基于索引的成员访问支持性能较差 | 成员可为任意Object子类的对象 |
在“集合框架”中有两种常规的List实现:ArrayList和LinkedList。使用两种List实现的哪一种取决于您特定的需要。如果要支持随机访问,而不必在除尾部的任何位置插入或除去元素,那么,ArrayList提供了可选的集合。但如果,您要频繁的从列表的中间位置添加和除去元素,而只要顺序的访问列表元素,那么,LinkedList实现更好。
我们以ArrayList为例,先看一个简单的例子:
例子中,我们把12个月份存放到ArrayList中,然后用一个循环,并使用get()方法将列表中的对象都取出来。
而LinkedList添加了一些处理列表两端元素的方法(下图只显示了新方法):
使用这些新方法,您就可以轻松的把LinkedList当作一个堆栈、队列或其它面向端点的数据结构。
我们再来看另外一个使用LinkedList来实现一个简单的队列的例子:
import java.util.*;
publicclass ListExample {
publicstaticvoid main(String args[]) {
LinkedList queue =new LinkedList();
queue.addFirst("Bernadine");
queue.addFirst("Elizabeth");
queue.addFirst("Gene");
queue.addFirst("Elizabeth");
queue.addFirst("Clara");
System.out.println(queue);
queue.removeLast();
queue.removeLast();
System.out.println(queue);
}
}
运行程序产生了以下输出。请注意,与Set不同的是List允许重复。
[Clara, Elizabeth, Gene,Elizabeth, Bernadine]
[Clara, Elizabeth, Gene]
该的程序演示了具体List类的使用。第一部分,创建一个由ArrayList支持的List。填充完列表以后,特定条目就得到了。示例的LinkedList部分把LinkedList当作一个队列,从队列头部添加东西,从尾部除去。
List接口不但以位置友好的方式遍历整个列表,还能处理集合的子集:
u ListIterator listIterator():返回一个ListIterator跌代器,默认开始位置为0
u ListIterator listIterator(int startIndex):返回一个ListIterator跌代器,开始位置为startIndex
u List subList(int fromIndex, int toIndex):返回一个子列表List,元素存放为从 fromIndex到toIndex之前的一个元素。
处理subList()时,位于fromIndex的元素在子列表中,而位于toIndex的元素则不是,提醒这一点很重要。以下for-loop测试案例大致反映了这一点:
for (int i=fromIndex;i<toIndex; i++) {
// process element at position i
}
此外,我们还应该提醒的是:对子列表的更改(如add()、remove()和set()调用)对底层List也有影响。
ListIterator 接口
ListIterator
接口继承Iterator
接口以支持添加或更改底层集合中的元素,还支持双向访问。
以下源代码演示了列表中的反向循环。请注意ListIterator
最初位于列表尾之后(list.size()
),因为第一个元素的下标是0。
List list = ...;
ListIterator iterator = list.listIterator(list.size());
while (iterator.hasPrevious()) {
Object element = iterator.previous();
// Process element
}
正常情况下,不用ListIterator
改变某次遍历集合元素的方向 —向前或者向后。虽然在技术上可能实现时,但在previous()
后立刻调用next()
,返回的是同一个元素。把调用 next() 和 previous()的顺序颠倒一下,结果相同。
我们看一个List的例子:
import java.util.*;
publicclass ListIteratorTest {
publicstaticvoid main(String[] args) {
List list =new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
System.out.println("下标0开始:"+list.listIterator(0).next());//next()
System.out.println("下标1开始:"+list.listIterator(1).next());
System.out.println("子List 1-3:"+list.subList(1,3));//子列表
ListIterator it =list.listIterator();//默认从下标0开始
//隐式光标属性add操作 ,插入到当前的下标的前面
it.add("sss");
while(it.hasNext()){
System.out.println("next Index="+it.nextIndex()+",Object="+it.next());
}
//set属性
ListIterator it1 =list.listIterator();
it1.next();
it1.set("ooo");
ListIterator it2 =list.listIterator(list.size());//下标
while(it2.hasPrevious()){
System.out.println("previous Index="+it2.previousIndex()+",Object="+it2.previous());
}
}
}
程序的执行结果为:
下标0开始:aaa
下标1开始:bbb
子List 1-3:[bbb, ccc]
next Index=1,Object=aaa
next Index=2,Object=bbb
next Index=3,Object=ccc
next Index=4,Object=ddd
previous Index=4,Object=ddd
previous Index=3,Object=ccc
previous Index=2,Object=bbb
previous Index=1,Object=aaa
previous Index=0,Object=ooo
我们还需要稍微再解释一下add()
操作。添加一个元素会导致新元素立刻被添加到隐式光标的前面。因此,添加元素后调用previous()
会返回新元素,而调用next()
则不起作用,返回添加操作之前的下一个元素。下标的显示方式,如下图所示:
对于List的基本用法我们学会了,下面我们来进一步了解一下List的实现原理,以便价升我们对于集合的理解。
前面已经提了一下Collection的实现基础都是基于数组的。下面我们就已ArrayList为例,简单分析一下ArrayList列表的实现方式。首先,先看下它的构造函数。
下列表格是在SUN提供的API中的描述:
ArrayList() Constructs an empty list with an initial capacity of ten. |
ArrayList(Collection c) Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator. |
ArrayList(int initialCapacity) Constructs an empty list with the specified initial capacity. |
其中第一个构造函数ArrayList()和第二构造函数ArrayList(Collection c)是按照Collection接口文档所述,所应该提供两个构造函数,一个无参数,一个接受另一个Collection。
第3个构造函数:
ArrayList(int initialCapacity)是ArrayList实现的比较重要的构造函数,虽然,我们不常用它,但是某认的构造函数正是调用的该带参数: