集合
版本号:1.0
作者:huangdos 日期:2006年6月06日 摘 要
摘要内容 Java里面最重要,最常用也就是集会一部分了。能够用好集合和理解好集合对于做Java程序的开发拥有无比的好处。本文详细解释了关于Java中的集合是如何实现的,以及他们的实现原理。
关键字: Collection , List ,Set , Map , 集合,框架。 目 录
集合 1 集合框架1.1 集合框架概述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 就是映射的一种表现形式。 1.1.2 容器的分类既然您已经具备了一些集的理论,您应该能够更轻松的理解“集合框架”。 “集合框架”由 一组用来操作对象的接口组成。不同接口描述不同类型的组。在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集 合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。框架接口层次结构如下图所示。
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 框架还引入了六个集合实现,如下表所示。
这里没有
这张图看起来有点吓人,熟悉之后就会发现其实只有三种容器:Map,List和Set ,它们各自有两个三个实现版本。常用的容器用黑色粗线框表示。 点线方框代表“接口”,虚线方框代表抽象类,而实线方框代表普通类(即具体类,而非抽象类)。虚线箭头指出一个特定的类实现了一个接口(在抽象类的情况下,则是“部分”实现了那个接口)。实线箭头指出一个类可生成箭头指向的那个类的对象。例如任何集合( Collection )都能产生一个迭代器( Iterator ),而一个List 除了能生成一个ListIterator (列表迭代器)外,还能生成一个普通迭代器,因为List 正是从集合继承来的.
1.2 Collection1.2.1 常用方法
注意: 集合必须只有对象,集合中的元素不能是基本数据类型。 Collection接口支持如添加和除去等基本操作。设法除去一个元素时,如果这个元素存在,除去的仅仅是集合中此元素的一个实例。 u boolean add(Object element) u boolean remove(Object element) Collection 接口还支持查询操作: u int size() u boolean isEmpty() u boolean contains(Object element) u Iterator iterator() 组操作 :Collection 接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于整个集合的任务。 u boolean containsAll(Collection collection) u boolean addAll(Collection collection) u void clear() u void removeAll(Collection collection) u void retainAll(Collection collection)
我们看一个简单的例子,来了解一下集合类的基本方法的使用: import java.util.*; public class CollectionToArray { public static void main(String[] args) { Collection collection1=new ArrayList();//创建一个集合对象 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=new ArrayList(); 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(int i=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 应该是集合中所有存放的对象的类的父类。
1.2.2 迭代器任何容器类,都必须有某种方式可以将东西放进去,然后由某种方式将东西取出来。毕竟,存放事物是容器最基本的工作。对于ArrayList,add()是插入对象的方法,而get()是取出元素的方式之一。ArrayList很灵活,可以随时选取任意的元素,或使用不同的下标一次选取多个元素。 如果从更高层的角度思考,会发现这里有一个缺点:要使用容器,必须知道其中元素的确切类型。初看起来这没有什么不好的,但是考虑如下情况:如果原本是ArrayList ,但是后来考虑到容器的特点,你想换用Set ,应该怎么做?或者你打算写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就可以应用于不同类型的容器? 所以迭代器(Iterator)的概念,也是出于一种设计模式就是为达成此目的而形成的。所以Collection不提供get()方法。如果要遍历Collectin中的元素,就必须用Iterator。 迭代器(Iterator)本身就是一个对象,它的工作就是遍历并选择集合序列中的对象,而客户端的程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为“轻量级”对象,创建它的代价小。但是,它也有一些限制,例如,某些迭代器只能单向移动。
下面,我们看一个对于迭代器的简单使用:
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator;
public class IteratorDemo { public static void 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 List1.3.1 概述前面我们讲述的Collection接口实际上并没有直接的实现类。而List是容器的一种,表示列表的意思。当我们不知道存储的数据有多少的情况,我们就可以使用List 来完成存储数据的工作。例如前面提到的一种场景。我们想要在保存一个应用系统当前的在线用户的信息。我们就可以使用一个List来存储。因为List的最大的特点就是能够自动的根据插入的数据量来动态改变容器的大小。下面我们先看看List接口的一些常用方法。 1.3.2 常用方法List 就是列表的意思,它是Collection 的一种, 面向位置的操作包括插入某个元素或 Collection 的功能,还包括获取、除去或更改元素的功能。在 List 中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置。 u void add(int index, Object element) :添加对象element到位置index上 u boolean addAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素 u Object get(int index) :取出下标为index的位置的元素 u int indexOf(Object element) :查找对象element 在List中第一次出现的位置 u int lastIndexOf(Object element) :查找对象element 在List中最后出现的位置 u Object remove(int index) :删除index位置上的元素 u Object set(int index, Object element) :将index位置上的对象替换为element 并返回老的元素。
先看一下下面表格:
在“集合框架”中有两种常规的
我们以ArrayList 为例,先看一个简单的例子:
例子中,我们把12个月份存放到ArrayList 中,然后用一个循环,并使用get()方法将列表中的对象都取出来。
使用这些新方法,您就可以轻松的把 我们再来看另外一个使用 import java.util.*;
public class ListExample { public static void 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); } } 运行程序产生了以下输出。请注意,与
该的程序演示了具体
u ListIterator listIterator() :返回一个ListIterator 跌代器,默认开始位置为0 u ListIterator listIterator(int startIndex) :返回一个ListIterator 跌代器,开始位置为startIndex u List subList(int fromIndex, int toIndex) :返回一个子列表List ,元素存放为从 fromIndex 到toIndex之前的一个元素。 处理
此外,我们还应该提醒的是:对子列表的更改(如 ListIterator 接口
以下源代码演示了列表中的反向循环。请注意
正常情况下,不用 我们看一个List的例子:
import java.util.*;
public class ListIteratorTest {
public static void 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 我们还需要稍微再解释一下
对于List 的基本用法我们学会了,下面我们来进一步了解一下List的实现原理,以便价升我们对于集合的理解。 1.3.3 实现原理前面已经提了一下Collection的实现基础都是基于数组的。下面我们就已ArrayList 为例,简单分析一下ArrayList 列表的实现方式。首先,先看下它的构造函数。 下列表格是在SUN提供的API中的描述:
其中第一个构造函数 第3个构造函数:
其中数组:: Keys:transient 表示被修饰的属性不是对象持久状态的一部分,不会自动的序列化。 第2个属性:
1.4 Map1.4.1 概述数学中的映射关系在Java中就是通过Map来实现的。它表示,里面存储的元素是一个对(pair),我们通过一个对象,可以在这个映射关系中找到另外一个和这个对象相关的东西。 前面提到的我们对于根据账号名得到对应的人员的信息,就属于这种情况的应用。我们讲一个人员的帐户名和这人员的信息作了一个映射关系,也就是说,我们把帐户名和人员信息当成了一个“键值对”,“键”就是帐户名,“值”就是人员信息。下面我们先看看Map 接口的常用方法。 1.4.2 常用方法
我们可以把这个接口方法分成三组操作:改变、查询和提供可选视图。 改变操作允许您从映射中添加和除去键-值对。键和值都可以为 null。但是,您不能把 Map 作为一个键或值添加给自身。
查询操作允许您检查映射内容:
最后一组方法允许您把键或值的组作为集合来处理。
因为映射中键的集合必须是唯一的,就使用 Set 来支持。因为映射中值的集合可能不唯一,就使用 Collection 来支持。最后一个方法返回一个实现 Map.Entry 接口的元素 Set。 我们看看Map的常用实现类的比较,如下表:
下面我们看一个简单的例子:
import java.util.*;
public class MapTest { public static void main(String[] args) { Map map1 = new HashMap(); Map map2 = new HashMap(); map1.put("1","aaa1"); map1.put("2","bbb2"); map2.put("10","aaaa10"); map2.put("11","bbbb11"); //根据键 "1" 取得值:"aaa1" System.out.println("map1.get(\"1\")="+map1.get("1")); // 根据键 "1" 移除键值对"1"-"aaa1" System.out.println("map1.remove(\"1\")="+map1.remove("1")); System.out.println("map1.get(\"1\")="+map1.get("1")); map1.putAll(map2);//将map2全部元素放入map1中 map2.clear();//清空map2 System.out.println("map1 IsEmpty?="+map1.isEmpty()); System.out.println("map2 IsEmpty?="+map2.isEmpty()); System.out.println("map1 中的键值对的个数size = "+map1.size()); System.out.println("KeySet="+map1.keySet());//set System.out.println("values="+map1.values());//Collection System.out.println("entrySet="+map1.entrySet()); System.out.println("map1 是否包含键:11 = "+map1.containsKey("11")); System.out.println("map1 是否包含值:aaa1 = "+map1.containsValue("aaa1")); } } 运行输出结果为: map1.get("1")=aaa1 map1.remove("1")=aaa1 map1.get("1")=null map1 IsEmpty?=false map2 IsEmpty?=true map1 中的键值对的个数size = 3 KeySet=[10, 2, 11] values=[aaaa10, bbb2, bbbb11] entrySet=[10=aaaa10, 2=bbb2, 11=bbbb11] map1 是否包含键:11 = true map1 是否包含值:aaa1 = false
在该例子中,我们创建一个HashMap,并使用了一下Map接口中的各个方法。 其中Map中的
import java.util.*;
public class MapSortExample { public static void main(String args[]) { Map map1 = new HashMap(); Map map2 = new LinkedHashMap(); for(int i=0;i<10;i++){ double s=Math.random()*100;//产生一个随机数,并将其放入Map中 map1.put(new Integer((int) s),"第 "+i+" 个放入的元素:"+s+"\n"); map2.put(new Integer((int) s),"第 "+i+" 个放入的元素:"+s+"\n"); }
System.out.println("未排序前HashMap:"+map1); System.out.println("未排序前LinkedHashMap:"+map2); //使用TreeMap来对另外的Map进行重构和排序 Map sortedMap = new TreeMap(map1); System.out.println("排序后:"+sortedMap); System.out.println("排序后:"+new TreeMap(map2)); } }
未排序前HashMap:{64=第 1 个放入的元素:64.05341725531845 , 15=第 9 个放入的元素:15.249165766266382 , 2=第 4 个放入的元素:2.66794706854534 , 77=第 0 个放入的元素:77.28814965781416 , 97=第 5 个放入的元素:97.32893518378948 , 99=第 2 个放入的元素:99.99412014935982 , 60=第 8 个放入的元素:60.91451419025399 , 6=第 3 个放入的元素:6.286974058646977 , 1=第 7 个放入的元素:1.8261658496439903 , 48=第 6 个放入的元素:48.736039522423106 } 未排序前LinkedHashMap:{77=第 0 个放入的元素:77.28814965781416 , 64=第 1 个放入的元素:64.05341725531845 , 99=第 2 个放入的元素:99.99412014935982 , 6=第 3 个放入的元素:6.286974058646977 , 2=第 4 个放入的元素:2.66794706854534 , 97=第 5 个放入的元素:97.32893518378948 , 48=第 6 个放入的元素:48.736039522423106 , 1=第 7 个放入的元素:1.8261658496439903 , 60=第 8 个放入的元素:60.91451419025399 , 15=第 9 个放入的元素:15.249165766266382 } 排序后:{1=第 7 个放入的元素:1.8261658496439903 , 2=第 4 个放入的元素:2.66794706854534 , 6=第 3 个放入的元素:6.286974058646977 , 15=第 9 个放入的元素:15.249165766266382 , 48=第 6 个放入的元素:48.736039522423106 , 60=第 8 个放入的元素:60.91451419025399 , 64=第 1 个放入的元素:64.05341725531845 , 77=第 0 个放入的元素:77.28814965781416 , 97=第 5 个放入的元素:97.32893518378948 , 99=第 2 个放入的元素:99.99412014935982 } 排序后:{1=第 7 个放入的元素:1.8261658496439903 , 2=第 4 个放入的元素:2.66794706854534 , 6=第 3 个放入的元素:6.286974058646977 , 15=第 9 个放入的元素:15.249165766266382 , 48=第 6 个放入的元素:48.736039522423106 , 60=第 8 个放入的元素:60.91451419025399 , 64=第 1 个放入的元素:64.05341725531845 , 77=第 0 个放入的元素:77.28814965781416 , 97=第 5 个放入的元素:97.32893518378948 , 99=第 2 个放入的元素:99.99412014935982 }
我们简单介绍一下这个接口: 1.4.3 Comparable 接口在
可以看到compareTo 方法里面通过判断当前的 在 Java 2 SDK,版本 1.2 中有十四个类实现 Comparable 接口。下表展示了它们的自然排序。虽然一些类共享同一种自然排序,但只有相互可比的类才能排序。
1.4.4 实现原理有的人可能会认为
下面我们以HashMap为例,对Map的实现机制作一下更加深入一点的理解。 因为HashMap里面使用Hash算法,所以在理解HashMap之前,我们需要先了解一下Hash算法和Hash表。 Hash,一般翻译做“散列”,也有直接音译为"哈希"的,就是把任意长度的输入(又叫做 预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能 会散列成相同的输出,而不可能从散列值来唯一的确定输入值。 说的通俗一点,Hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系,(每一个真实值只能有一个键值,但是一个键值可以对应多个真实值),这样可以快速在数组等里面存取数据。 看下图:
我们建立一个HashTable(哈希表),该表的长度为N,然后我们分别在该表中的格子中存放不同的元素。每个格子下面存放的元素又是以链表的方式存放元素。 u 当添加一个新的元素Entry 的时候,首先我们通过一个Hash函数计算出这个Entry元素的Hash值hashcode。通过该hashcode值,就可以直接定位出我们应该把这个Entry元素存入到Hash表的哪个格子中,如果该格子中已经存在元素了,那么只要把新的Entry元存放到这个链表中即可。 u 如果要查找一个元素Entry的时候,也同样的方式,通过Hash函数计算出这个Entry元素的Hash值hashcode。然后通过该hashcode值,就可以直接找到这个Entry是存放到哪个格子中的。接下来就对该格子存放的链表元素进行逐个的比较查找就可以了。 举一个比较简单的例子来说明这个算法的运算方式: 假定我们有一个长度为8的Hash表(可以理解为一个长度为8的数组)。在这个Hash表中存放数字:如下表
假定我们的Hash函数为: Hashcode = X%8 -------- 对8 取余数 其中X就是我们需要放入Hash表中的数字,而这个函数返回的Hashcode就是Hash码。 假定我们有下面10个数字需要依次存入到这个Hash表中: 11 , 23 , 44 , 9 , 6 , 32 , 12 , 45 , 57 , 89 通过上面的Hash函数,我们可以得到分别对应的Hash码: 11――3 ; 23――7 ;44――4 ;9――1;6――6;32――0;12――4;45――5;57――1;89――1; 计算出来的Hash码分别代表,该数字应该存放到Hash表中的哪个对应数字的格子中。如果改格子中已经有数字存在了,那么就以链表的方式将数字依次存放在该格子中,如下表:
Hash表和Hash算法的特点就是它的存取速度比数组差一些,但是比起单纯的链表,在查找和存储方面却要好很多。同时数组也不利于数据的重构而排序等方面的要求。 更具体的说明,读者可以参考数据结构相关方面的书籍。
简单的了解了一下Hash算法后,我们就来看看HashMap的属性有哪些:
里面最重要的3个属性: n transient Entry[] table: 用来存放键值对的对象Entry数组,也就是Hash表 n transient int size:当前Map中存放的键值对的个数 n final float loadFactor:负载因子,用来决定什么情况下应该对Entry进行扩容 我们Entry 对象是Map接口中的一个内部接口。即是使用它来保存我们的键值对的。 我们看看这个Entry 内部接口在HashMap中的实现:
通过查看源码,我们可以看到Entry类有点类似一个单向链表。其中: final Object key 和 Object value;存放的就是我们放入Map中的键值对。 而属性Entry next;表示当前键值对的下一个键值对是哪个Entry。 接下来,我们看看HashMap的主要的构造函数:
我们主要看看 public HashMap(int initialCapacity, float loadFactor) 因为,另外两个构造函数实行也是同样的方式进行构建一个HashMap 的。 该构造函数: 1. 首先是判断参数int initialCapacity和float loadFactor是否合法 2. 然后确定Hash表的初始化长度。确定的策略是:通过传进来的参数initialCapacity 来找出第一个大于它的2的次方的数。比如说我们传了18这样的一个initialCapacity 参数,那么真实的table数组的长度为2的5次方,即32。之所以采用这种策略来构建Hash表的长度,是因为2的次方的运算对于现代的处理器来说,可以通过一些方法得到更加好的执行效率。 3. 接下来就是得到重构因子(threshold)了,这个属性也是HashMap中的一个比较重要的属性,它表示,当Hash表中的元素被存放了多少个之后,我们就需要对该Hash表进行重构。 4. 最后就是使用得到的初始化参数capacity 来构建Hash表:Entry[] table。
下面我们看看一个键值对是如何添加到HashMap中的。
该put方法是用来添加一个键值对(key-value)到Map中,如果Map中已经存在相同的键的键值对的话,那么就把新的值覆盖老的值,并把老的值返回给方法的调用者。如果不存在一样的键,那么就返回null 。我们看看方法的具体实现: 1. 首先我们判断如果key为null则使用一个常量来代替该key值,该行为在方法maskNull()终将key替换为一个非null 的对象k。 2. 计算key值的Hash码:hash 3. 通过使用Hash码来定位,我们应该把当前的键值对存放到Hash表中的哪个格子中。indexFor()方法计算出的结果:i 就是Hash表(table)中的下标。 4. 然后遍历当前的Hash表中table[i]格中的链表。从中判断已否已经存在一样的键(Key)的键值对。如果存在一样的key的键,那么就用新的value覆写老的value,并把老的value返回 5. 如果遍历后发现没有存在同样的键值对,那么就增加当前键值对到Hash表中的第i个格子中的链表中。并返回null 。 最后我们看看一个键值对是如何添加到各个格子中的链表中的:
我们先看void addEntry(int hash, Object key, Object value, int bucketIndex)方法,该方法的作用就用来添加一个键值对到Hash表的第bucketIndex个格子中的链表中去。这个方法作的工作就是: 1. 创建一个Entry对象用来存放键值对。 2. 添加该键值对 ---- Entry对象到链表中 3. 最后在size属性加一,并判断是否需要对当前的Hash表进行重构。如果需要就在void resize(int newCapacity)方法中进行重构。 之所以需要重构,也是基于性能考虑。大家可以考虑这样一种情况,假定我们的Hash表只有4个格子,那么我们所有的数据都是放到这4个格子中。如果存储的数据量比较大的话,例如100。这个时候,我们就会发现,在这个Hash表中的4个格子存放的4个长长的链表。而我们每次查找元素的时候,其实相当于就是遍历链表了。这种情况下,我们用这个Hash表来存取数据的性能实际上和使用链表差不多了。 但是如果我们对这个Hash表进行重构,换为使用Hash表长度为200的表来存储这100个数据,那么平均2个格子里面才会存放一个数据。这个时候我们查找的数据的速度就会非常的快。因为基本上每个格子中存放的链表都不会很长,所以我们遍历链表的次数也就很少,这样也就加快了查找速度。但是这个时候又存在了另外的一个问题。我们使用了至少200个数据的空间来存放100个数据,这样就造成至少100个数据空间的浪费。 在速度和空间上面,我们需要找到一个适合自己的中间值。在HashMap中我们通过负载因子(loadFactor)来决定应该什么时候应该重构我们的Hash表,以达到比较好的性能状态。 我们再看看重构Hash表的方法:void resize(int newCapacity)是如何实现的:
它的实现方式比较简单: 1. 首先判断如果Hash表的长度已经达到最大值,那么就不进行重构了。因为这个时候Hash表的长度已经达到上限,已经没有必要重构了。 2. 然后就是构建新的Hash表 3. 把老的Hash表中的对象数据全部转移到新的Hash表newTable中,并设置新的重构因子threshold
对于HashMap中的实现原理,我们就分析到这里。大家可能会发现,HashCode的计算,是用来定位我们的键值对应该放到Hash表中哪个格子中的关键属性。而这个HashCode的计算方法是调用的各个对象自己的实现的hashCode()方法。而这个方法是在Object对象中定义的,所以我们自己定义的类如果要在集合中使用的话,就需要正确的覆写hashCode() 方法。下面就介绍一下应该如何正确覆写hashCode()方法。 1.4.5 覆写hashCode()在明白了HashMap具有哪些功能,以及实现原理后,了解如何写一个hashCode()方法就更有意义了。当然,在HashMap中存取一个键值对涉及到的另外一个方法为equals (),因为该方法的覆写在高级特性已经讲解了。这里就不做过多的描述。 设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该生成同样的值。如果在将一个对象用put()方法添加进HashMap时产生一个hashCode()值,而用get()取出时却产生了另外一个hashCode()值,那么就无法重新取得该对象了。所以,如果你的hashCode()方法依赖于对象中易变的数据,那用户就要小心了,因为此数据发生变化时,hashCode()就会产生一个不同的hash码,相当于产生了一个不同的“键”。 此外,也不应该使hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值,这只能产生很糟糕的hashCode()。因为这样做无法生成一个新的“键”,使之与put()种原始的“键值对”中的“键”相同。例如,如果我们不覆写Object的hashCode()方法,那么调用该方法的时候,就会调用Object的hashCode()方法的默认实现。Object的hashCode()方法,返回的是当前对象的内存地址。下次如果我们需要取一个一样的“键”对应的键值对的时候,我们就无法得到一样的hashCode值了。因为我们后来创建的“键”对象已经不是存入HashMap中的那个内存地址的对象了。 我们看一个简单的例子,就能更加清楚的理解上面的意思。假定我们写了一个类:Person (人),我们判断一个对象“人”是否指向同一个人,只要知道这个人的身份证号一直就可以了。 先看我们没有实现hashCode的情况: package c08.hashEx;
import java.util.*; //身份证类 class Code{ final int id;//身份证号码已经确认,不能改变 Code(int i){ id=i; } //身份号号码相同,则身份证相同 public boolean equals(Object anObject) { if (anObject instanceof Code){ Code other=(Code) anObject; return this.id==other.id; } return false; } public String toString() { return "身份证:"+id; } } //人员信息类 class Person { Code id;// 身份证 String name;// 姓名 public Person(String name, Code id) { this.id=id; this.name=name; } //如果身份证号相同,就表示两个人是同一个人 public boolean equals(Object anObject) { if (anObject instanceof Person){ Person other=(Person) anObject; return this.id.equals(other.id); } return false; } public String toString() { return "姓名:"+name+" 身份证:"+id.id+"\n"; } }
public class HashCodeEx { public static void main(String[] args) { HashMap map=new HashMap(); Person p1=new Person("张三",new Code(123)); map.put(p1.id,p1);//我们根据身份证来作为key值存放到Map中 Person p2=new Person("李四",new Code(456)); map.put(p2.id,p2); Person p3=new Person("王二",new Code(789)); map.put(p3.id,p3); System.out.println("HashMap 中存放的人员信息:\n"+map); // 张三 改名为:张山 但是还是同一个人。 Person p4=new Person("张山",new Code(123)); map.put(p4.id,p4); System.out.println("张三改名后 HashMap 中存放的人员信息:\n"+map); //查找身份证为:123 的人员信息 System.out.println("查找身份证为:123 的人员信息:"+map.get(new Code(123))); } } 运行结果为: HashMap 中存放的人员信息: {身份证:456=姓名:李四 身份证:456 , 身份证:123=姓名:张三 身份证:123 , 身份证:789=姓名:王二 身份证:789 } 张三改名后 HashMap 中存放的人员信息: {身份证:456=姓名:李四 身份证:456 , 身份证:123=姓名:张三 身份证:123 , 身份证:123=姓名:张山 身份证:123 , 身份证:789=姓名:王二 身份证:789 } 查找身份证为:123 的人员信息:null
上面的例子的演示的是,我们在一个HashMap中存放了一些人员的信息。并以这些人员的身份证最为人员的“键”。当有的人员的姓名修改了的情况下,我们需要更新这个HashMap。同时假如我们知道某个身份证号,想了解这个身份证号对应的人员信息如何,我们也可以根据这个身份证号在HashMap中得到对应的信息。 而例子的输出结果表示,我们所做的更新和查找操作都失败了。失败的原因就是我们的身份证类:Code 没有覆写hashCode()方法。这个时候,当查找一样的身份证号码的键值对的时候,使用的是默认的对象的内存地址来进行定位。这样,后面的所有的身份证号对象new Code(123) 产生的hashCode()值都是不一样的。所以导致操作失败。 下面,我们给Code类加上hashCode()方法,然后再运行一下程序看看: //身份证类 class Code{ final int id;//身份证号码已经确认,不能改变 Code(int i){ id=i; } //身份号号码相同,则身份证相同 public boolean equals(Object anObject) { if (anObject instanceof Code){ Code other=(Code) anObject; return this.id==other.id; } return false; } public String toString() { return "身份证:"+id; } //覆写hashCode方法,并使用身份证号作为hash值 public int hashCode(){ return id; } } 再次执行上面的HashCodeEx 的结果就为: HashMap 中存放的人员信息: {身份证:456=姓名:李四 身份证:456 , 身份证:789=姓名:王二 身份证:789 , 身份证:123=姓名:张三 身份证:123 } 张三改名后 HashMap 中存放的人员信息: {身份证:456=姓名:李四 身份证:456 , 身份证:789=姓名:王二 身份证:789 , 身份证:123=姓名:张山 身份证:123 } 查找身份证为:123 的人员信息:姓名:张山 身份证:123
这个时候,我们发现。我们想要做的更新和查找操作都成功了。 对于Map部分的使用和实现,主要就是需要注意存放“键值对”中的对象的equals()方法和hashCode()方法的覆写。如果需要使用到排序的话,那么还需要实现Comparable 接口中的
1.5 Set1.5.1 概述Java 中的Set和正好和数学上直观的集(set)的概念是相同的。Set最大的特性就是不允许在其中存放的元素是重复的。根据这个特点,我们就可以使用Set 这个接口来实现前面提到的关于商品种类的存储需求。Set 可以被用来过滤在其他集合中存放的元素,从而得到一个没有包含重复新的集合。 1.5.2 常用方法按照定义,
我们简单的描述一下各个方法的作用: u public int size() :返回set中元素的数目,如果set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE u public boolean isEmpty() :如果set中不含元素,返回true u public boolean contains(Object o) :如果set包含指定元素,返回true u public Iterator iterator() l 返回set中元素的迭代器 l 元素返回没有特定的顺序,除非set是提高了该保证的某些类的实例 u public Object[] toArray() :返回包含set中所有元素的数组 u public Object[] toArray(Object[] a) :返回包含set中所有元素的数组,返回数组的运行时类型是指定数组的运行时类型 u public boolean add(Object o) :如果set中不存在指定元素,则向set加入 u public boolean remove(Object o) :如果set中存在指定元素,则从set中删除 u public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素 u public boolean containsAll(Collection c) :如果set包含指定集合的所有元素,返回true。如果指定集合也是一个set,只有是当前set的子集时,方法返回true u public boolean addAll(Collection c) :如果set中中不存在指定集合的元素,则向set中加入所有元素 u public boolean retainAll(Collection c) :只保留set中所含的指定集合的元素(可选操作)。换言之,从set中删除所有指定集合不包含的元素。 如果指定集合也是一个set,那么该操作修改set的效果是使它的值为两个set的交集 u public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素 u public void clear() :从set中删除所有元素
“集合框架” 支持
在更多情况下,您会使用 HashSet 存储重复自由的集合。同时HashSet中也是采用了Hash算法的方式进行存取对象元素的。所以添加到 HashSet 的对象对应的类也需要采用恰当方式来实现 hashCode() 方法。虽然大多数系统类覆盖了 Object 中缺省的 hashCode() 实现,但创建您自己的要添加到 HashSet 的类时,别忘了覆盖 hashCode()。 对于Set的使用,我们先以一个简单的例子来说明: import java.util.*;
public class HashSetDemo { public static void main(String[] args) { Set set1 = new HashSet(); if (set1.add("a")) {//添加成功 System.out.println("1 add true"); } if (set1.add("a")) {//添加失败 System.out.println("2 add true"); } set1.add("000");//添加对象到Set集合中 set1.add("111"); set1.add("222"); System.out.println("集合set1的大小:"+set1.size()); System.out.println("集合set1的内容:"+set1); set1.remove("000");//从集合set1中移除掉 "000" 这个对象 System.out.println("集合set1移除 000 后的内容:"+set1); System.out.println("集合set1中是否包含000 :"+set1.contains("000")); System.out.println("集合set1中是否包含111 :"+set1.contains("111")); Set set2=new HashSet(); set2.add("111"); set2.addAll(set1);//将set1 集合中的元素全部都加到set2中 System.out.println("集合set2的内容:"+set2); set2.clear();//清空集合 set1 中的元素 System.out.println("集合set2是否为空 :"+set2.isEmpty()); Iterator iterator = set1.iterator();//得到一个迭代器 while (iterator.hasNext()) {//遍历 Object element = iterator.next(); System.out.println("iterator = " + element); } //将集合set1转化为数组 Object s[]= set1.toArray(); for(int i=0;i<s.length;i++){ System.out.println(s[i]); } } } 程序执行的结果为: 1 add true 集合set1的大小:4 集合set1的内容:[222, a, 000, 111] 集合set1移除 000 后的内容:[222, a, 111] 集合set1中是否包含000 :false 集合set1中是否包含111 :true 集合set2的内容:[222, a, 111] 集合set2是否为空 :true iterator = 222 iterator = a iterator = 111 222 a 111 从上面的这个简单的例子中,我们可以发现,Set中的方法与直接使用Collection中的方法一样。唯一需要注意的就是Set中存放的元素不能重复。 我们再看一个例子,来了解一下其它的Set的实现类的特性: package c08;
import java.util.*;
public class SetSortExample { public static void main(String args[]) { Set set1 = new HashSet(); Set set2 = new LinkedHashSet(); for(int i=0;i<5;i++){ //产生一个随机数,并将其放入Set中 int s=(int) (Math.random()*100); set1.add(new Integer( s)); set2.add(new Integer( s)); System.out.println("第 "+i+" 次随机数产生为:"+s); } System.out.println("未排序前HashSet:"+set1); System.out.println("未排序前LinkedHashSet:"+set2); //使用TreeSet来对另外的Set进行重构和排序 Set sortedSet = new TreeSet(set1); System.out.println("排序后 TreeSet :"+sortedSet); } } 该程序的一次执行结果为: 第 0 次随机数产生为:96 第 1 次随机数产生为:64 第 2 次随机数产生为:14 第 3 次随机数产生为:95 第 4 次随机数产生为:57 未排序前HashSet:[64, 96, 95, 57, 14] 未排序前LinkedHashSet:[96, 64, 14, 95, 57] 排序后 TreeSet :[14, 57, 64, 95, 96] 从这个例子中,我们可以知道HashSet的元素存放顺序和我们添加进去时候的顺序没有任何关系,而LinkedHashSet 则保持元素的添加顺序。TreeSet则是对我们的Set中的元素进行排序存放。 一般来说,当您要从集合中以有序的方式抽取元素时,TreeSet 实现就会有用处。为了能顺利进行,添加到 TreeSet 的元素必须是可排序的。 而您同样需要对添加到TreeSet中的类对象实现 Comparable 接口的支持。对于Comparable接口的实现,在前一小节的Map中已经简单的介绍了一下。我们暂且假定一棵树知道如何保持 java.lang 包装程序器类元素的有序状态。一般说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快。这点和HashMap的使用非常的类似。 其实Set的实现原理是基于Map上面的。通过下面我们对Set的进一步分析大家就能更加清楚的了解这点了。 1.5.3 实现原理Java中Set的概念和数学中的集合(set)一致,都表示一个集内可以存放的元素是不能重复的。 前面我们会发现,Set中很多实现类和Map中的一些实现类的使用上非常的相似。而且前面再讲解Map的时候,我们也提到:Map中的“键值对”,其中的“键”是不能重复的。这个和Set中的元素不能重复一致。我们以HashSet为例来分析一下,会发现其实Set利用的就是Map中“键”不能重复的特性来实现的。 先看看HashSet中的有哪些属性:
再结合构造函数来看看:
通过这些方法,我们可以发现,其实HashSet的实现,全部的操作都是基于HashMap来进行的。我们看看是如何通过HashMap来保证我们的HashSet的元素不重复性的:
看到这个操作我们可以发现HashSet的巧妙实现:就是建立一个“键值对”,“键”就是我们要存入的对象,“值”则是一个常量。这样可以确保,我们所需要的存储的信息之是“键”。而“键”在Map中是不能重复的,这就保证了我们存入Set中的所有的元素都不重复。而判断是否添加元素成功,则是通过判断我们向Map中存入的“键值对”是否已经存在,如果存在的话,那么返回值肯定是常量:PRESENT ,表示添加失败。如果不存在,返回值就为null 表示添加成功。 我们再看看其他的方法实现:
了解了这些后,我们就不难理解,为什么HashMap中需要注意的地方,在HashSet中也同样的需要注意。其他的Set的实现类也是差不多的原理。 至此对于Set我们就应该能够比较好的理解了。 1.6 总结:集合框架中常用类比较用“集合框架”设计软件时,记住该框架四个基本接口的下列层次结构关系会有用处:
我们以下面这个图表来描述一下常用的集合的实现类之间的区别:
2 练习撰写一个Person class,表示一个人员的信息。令该类具备多辆Car的信息,表示一个人可以拥有的车子的数据,以及: Certificate code: 身份证对象 name: 姓名 cash: 现金 List car; 拥有的汽车,其中存放的是Car对象 boolean buycar(car); 买车子 boolean sellcar(Person p);//把自己全部的车子卖给别人 boolean buyCar(Car car,Person p);//自动查找卖车的人p是否有买主想要买的车car,如果有就买,并返回true ,否则返回false viod addCar(car);//把某辆车送给方法的调用者。 String toString();//得到人的信息
并撰写第二个Car class 具备的属性: String ID; //ID车牌号 cost //价格 color //颜色 Person owner; //车子的拥有者 toString();//得到汽车的信息 equals();//比较车子是否同一俩汽车,ID相同则认为相同 在另外一个Market类里面,进行车子的买卖。并保留所有交易人员的的信息到一个HashMap中,我们可以通过身份证号来查找到对应的人员的信息。同时所有的车子种类都在市场中进行注册,即车子的信息使用一个Set来保存 属性: HashMap people;//存放交易人员的信息。Key为身份证号,value为Person对象 方法: static boolean sellCar(Person p1 ,Car car1, Person p2); //p1 将car1卖给 p2 。并在该方法中记录效益人的信息到people 中。 撰写类Certificate 表示身份证: 属性: Id;//号码 方法: equals();//比较两个身份证是否同一个,ID相同则认为相同 hashCode();//正确编写hashCode 方法
场景: 一个叫Bob的人:身份证:310 现金:30000。 有一辆车子:ID:001,红色,价格:50000的车子; 一个叫Tom的人:身份证:210 现金:70000, 有一辆车子: 颜色:白色,ID:003, 价格:25000。 一个叫King 的人:身份证:245 现金:60000, 有2辆车子: 颜色:白色,ID:005, 价格:18000。 颜色:红色,ID:045, 价格:58000。
Tom 买了Bob的车子.他就拥有了2辆汽车 King 把ID=005的车子买给了Bob 最后各人的信息如何?
3 附录:排序为了用“集合框架”的额外部分把排序支持添加到 Java 2 SDK,版本 1.2,核心 Java 库作了许多更改。像 String 和 Integer 类如今实现 Comparable 接口以提供自然排序顺序。对于那些没有自然顺序的类、或者当您想要一个不同于自然顺序的顺序时,您可以实现 Comparator 接口来定义您自己的。 为了利用排序功能,“集合框架”提供了两种使用该功能的接口:SortedSet 和 SortedMap。 Comparable 接口 在 java.lang 包中,Comparable 接口适用于一个类有自然顺序的时候。假定对象集合是同一类型,该接口允许您把集合排序成自然顺序。
compareTo() 方法比较当前实例和作为参数传入的元素。如果排序过程中当前实例出现在参数前,就返回某个负值。如果当前实例出现在参数后,则返回正值。否则,返回零。这里不要求零返回值表示元素相等。零返回值只是表示两个对象排在同一个位置。 在 Java 2 SDK,版本 1.2 中有十四个类实现 Comparable 接口。下表展示了它们的自然排序。虽然一些类共享同一种自然排序,但只有相互可比的类才能排序。
创建您自己的类 Comparable 只是个实现 compareTo() 方法的问题。通常就是依赖几个数据成员的自然排序。您自己的类也应该覆盖 equals() 和 hashCode() 以确保两个相等的对象返回同一个散列码。 Comparator 接口 若一个类不能用于实现 java.lang.Comparable,您可以提供自己的 java.util.Comparator 行为。如果您不喜欢缺省的 Comparable 行为,您照样可以提供自己的 Comparator。
Comparator 的 compare() 方法的返回值和 Comparable 的 compareTo() 方法的返回值相似。在此情况下,如果排序时第一个元素出现在第二个元素之前,则返回一个负值。如果第一个元素出现在后,那么返回一个正值。否则,返回零。与 Comparable 相似,零返回值不表示元素相等。一个零返回值只是表示两个对象排在同一位置。由 Comparator 用户决定如何处理。如果两个不相等的元素比较的结果为零,您首先应该确信那就是您要的结果,然后记录行为。 为了演示,您会发现编写一个新的忽略大小写的 Comparator,代替使用 Collator 进行语言环境特定、忽略大小写的比较会更容易。这样的一种实现如下所示: class CaseInsensitiveComparator implements Comparator { public int compare(Object element1, Object element2) { String lowerE1 = ((String)element1).toLowerCase(); String lowerE2 = ((String)element2).toLowerCase(); return lowerE1.compareTo(lowerE2); } } 因为每个类在某些地方都建立了 Object 子类,所以这不是您实现 equals() 方法的必要条件。实际上大多数情况下您不会去这样做。切记该 equals() 方法检查的是 Comparator 实现的等同性,不是处于比较状态下的对象。 Collections 类有个预定义的 Comparator 用于重用。调用 Collections.reverseOrder() 返回一个 Comparator,它对逆序实现 Comparable 接口的对象进行排序。
|
|