Java程序设计4——集合类
1 JAVA集合概述
Java集合封装了一系列数据结构比如链表、二叉树、栈、队列等,然后提供了针对这些数据结构的一系列算法比如查找、排序、替换,使编程难度大大降低。(这句话有可能是非法的,因为个人对算法目前不是太了解,并不了解Java有没有实现哪些数据结构。但是说在这里想给那些畏难算法与数据结构这门课程的人一丝信心,嫑以为非要懂算法和数据结构才能编程,不懂这门课程编也能编程,并不是所有的问题都要自己来实现一个数据结构。如果因为觉得编程一定要懂算法和数据结构,进而对编程产生一种心理阴影,没有信心,那是很错误的!你总需要先用一门语言来练练手,然后才能去学习算法和数据结构。"懂算法和数据结构=会编程",完全是那些所谓的专家来吓唬那些新手入门者的,找一本语言介绍书好好练练,编程并不是你想的那么难!)这样的话,只需要利用提供的数据结构存储需要用的数据,然后利用数据结构上提供的算法就可以对数据进行简单的处理。链表、二叉树、栈、队列在JAVA里面统称为集合,就是把数据统统塞进这些集合里面,这些集合的结构可能是不同的。Java集合将这些数据结构分成如下几类。
注意: 这些集合类里面存储的数据都是指向堆内存对象的对象栈内存变量
区别:
Java的集合类分为四种:Set,List、Map和Queue四种体系,即MSQL设计者可以根据需要使用这四种体系里面的数据结构,然后处理数据。
Map:代表有映射关系的集合,也就是key-value这样的数据集合,比如{001——张三,002——李四}
Set:代表无序、不可重复的集合,比如{1,2,3}。注意:无序、不可重复
Queue:代表一种队列集合的实现
List:代表添加时候有序存储可重复的集合{1,1,2,3}
1.1 Java集合继承树
在处理数据时候经常处理到元素个数不确定(数组长度是固定的,初始化后就无法改变),有关联的数据{001——张三,002——李四},为了解决这些问题,Java提供了集合类,集合类负责保存、盛装这些数据,因此集合类也被称为容器类,所有的集合类都存放在java.util包中
集合类和数组的区别:
数组可以保存基本数据类型或者对象,但是集合类智能保存指向堆内存数据的栈内存对象变量(不过很多书上会直接说集合类里面装的都是对象,这样可能会好理解一些)。
MSQL接口类的继承拓扑图:
提供的四大接口类有如下继承结构的拓扑图,Collection和Map是两个根类,由这两个根类派生出一系列的接口类。
其中Collection派生SQL,Map派生其他的接口类。MSQL(记法M-SQL,像一种编程语言一样):Map,Set,Queue,List这四种集合类最常用的是SML(记法:AI领域的一种语言),也就是Set,Map,List,这三种接口类最常用实现是,Set的HashSet,Map的HashMap,List的ArrayList,也就是HHA(2HA,2哈——啊哈)如下图的颜色重的标注出来是常用的
1.2 Collection和Iterator接口
1.2.1 Collection 接口
下面如果没有特别说明,SQL并不指数据库语言SQL,而是代表Set,Queue,List三个Collection的子接口类。
Collection接口下面定义了若干方法,这些方法可以用于SQL三种集合接口类。注意:集合接口模板类里面存储的是栈对象变量
boolean add(Object o):该方法用于像一个集合接口类添加一个对象变量o指向的对象,如果添加成功,也就是集合类被改变成功,返回true,否则返回false
boolean addAll(Collection c):假设调用该接口的是集合接口类a,调用该接口后,会将集合接口类c的对象全部添加到集合接口类a中,元素间用逗号隔开
void clear():清空集合里面的所有元素,使元素个数size为0
boolean contains(Object o):检测集合里面是否含有对象元素o,有true,无false
boolean containsAll(Collection c):假定调用该接口的是集合a,那么调用该接口后会检测集合a里面是否包含集合c的全部元素,有true,无false
boolean isEmpty():返回集合是否为空,相当于int size()方法返回值为0和不为0的情况
boolean remove(Object o):删除集合里面指定元素o,删除成功返回true,失败false
boolean removeAll(Collection c):假设调用该接口的是集合a,调用该接口后,会从集合a里面找出集合c里面没有的元素,也就是找出差集,如果删除了一个或一个以上的元素,该方法返回true
boolean retainAll(Collection c):假设调用该接口的是集合a,调用该接口后,删除a中c没有的元素,也就是把a编程a和c的交集,如果操作成功,返回true
int size():返回集合里面元素个数
Object[] toArray():把集合转换成一个数组,所有的集合元素变成对应的数组元素,这样就可以用数组的方法来访问元素
Iterator iterator():返回一个Iterator对象用于遍历集合里面的所有元素,也就是一个迭代器,这个迭代器可以用于查询集合类元素,利用System.out的println查询同样可以打印出集合的所有数据,但是这样的查询是不可控制的,也就是说要么查询出所有,要么不查询,使用迭代器可以根据需要进行查询。
1.2.2 Iterator接口
Iterator接口隐藏了Collection集合类的底层实现,提供了若干方法对Collection集合类进行处理。
方法:
boolean hasNext():如果被迭代的集合仍然有元素没有被遍历(迭代),则返回true,就是说如果集合里面迭代一次后剩余的元素不止一个,则返回true。
Object next():返回集合里下一个元素,注意迭代器每次只返回一个,不像println那样一次返回所有
void remove():删除集合里上一次next方法返回的元素,比如说如果等于某个值,就可以删除这个元素,这样就可以控制集合返回结果
看一段代码:
1 //创建一个集合 2 Collection books = new HashSet(); 3 books.add("轻量级J2EE企业应用实战"); 4 books.add("Struts2权威指南"); 5 books.add("基于J2EE的Ajax宝典"); 6 //获取books集合对应的迭代器 7 Iterator it = books.iterator(); 8 while(it.hasNext()){ 9 //it.next()方法返回的数据类型是Object类型,需要强制类型转换 10 String book = (String)it.next();//book代表的是每次返回的一个元素 11 // it.remove();//remove删除每次next方法返回的元素,按照此循环,如果每次返回后都进行删除,那么最后就不返回任何一个结果,因为返回一个删掉一个 12 //如果next方法返回的元素与Struts2权威指南一样,则删除 13 // if(book.equals("Struts2权威指南")){ 14 // 15 // it.remove(); 16 // } 17 //对book变量赋值,不会改变改变集合本身 18 // System.out.println(book); 19 //book = "测试字符串"; 20 } 21 System.out.println(books);
强烈注意
Iterator接口类仅用于遍历集合,Iterator本身并不提供盛装对象的能力,如果需要创建Iterator对象,则必须与有一个可以被它迭代的集合,没有集合的迭代器没有存在价值。也就是说Iterator必须依附于Collection对象,有一个Iterator对象,则必然有一个与之关联的Collection对象供其迭代。
代码倒数第二行有一个book赋值代码,但是输出集合books时候,输出结果没有任何改变,可以得到一个结论:当使用Iterator对集合进行迭代输出时候,Iterator并没有指向堆内存的集合,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素没有任何改变。
墙裂注意
1. 删除集合里面的元素只能通过迭代器的remove方法,也就是it.remove()才可以删除集合里面的元素,通过调用集合自身的方法来删除集合元素值也就是说books.remove(book)将会出错,引发的异常是java.util.ConcurrentModificationException异常,或者简单的说,迭代过程中,不能通过除了迭代器之外的方式来修改集合,也就是说在迭代过程中,只有迭代器有修改集合的权限,其他方式包括集合本身都没有修改集合自身元素的权限。(其实更本质的是集合类变量本身只是指向堆内存的数据,迭代时候,相当于堆内存的使用权交给了迭代器,集合类本身当然没有修改权限)迭代器采用的是快速失败机制,一旦在迭代过程中,检测到该集合已经被修改(通常是其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。
第二for循环遍历集合元素
前面已经介绍了for循环有两种方是,一种是标准的,另一种是被很多书称之为foreach循环(这种叫法很容易让人误解循环的关键字是foreach,但其实依然是for),这里我称之为第二for循环(表示第二种for循环),同样使用在用for循环迭代输出集合类“中”也不能修改集合元素。同样使用该种循环方式按照前面的语法
for(循环变量类型 循环变量:集合类)
代码如下:
1 //创建一个集合 2 Collection lan = new HashSet(); 3 //往集合里面装东西 4 lan.add("Englis"); 5 lan.add("Chinese"); 6 lan.add("Castellano"); 7 lan.add("Deutsch"); 8 //使用第二for循环输出集合里面的元素 9 for(Object lang:lan){ 10 String lgu = (String)lang;//集合里面存储的都是对象变量 11 System.out.print(lgu); 12 }
2 Set集合类
Set集合特点:Set集合接口类的元素特征是无序、不重复
Set集合与Collection基本完全一样,它没有提供任何额外的方法,实际上Set就是Collection,只是行为不同(Set不允许重复元素)。
Set不允许重复元素,如果相同元素加入同一个Set集合中则会添加失败,判断是否相同的标准是使用equals方法,而不是==方法,所以比较严格。
例如:
1 Set books = new HashSet(); 2 books.add(new String("English")); 3 //再次添加一个不同的对象 4 books.add(new String("English"));
由于Set是一个集合类,集合类里面应该装的是不同对象,上述代码里面明显装的是不同对象,但是Set集合判断是否是同一个对象的标准是equals方法,所以不能添加(有点怪怪的)
上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet、EnumSet三个实现类,只是三个实现类各有特色。
2.1 HashSet——LinkedHashSet类
2.1.1 HashSet接口
HashSet是Set接口的典型实现,大多数时候使用Set集合就是使用这个实现类,HashSet按Hash算法存储集合中的元素,因此具有很好的存取和查找性能。
HashSet特点
1. 不能保证元素的排列顺序,顺序有可能发送变化
2. HashSet不是同步的,如果多个线程同时访问一个Set集合或者一个HashSet,当2条或2条以上的线程同时修改了HashSet集合时,必须通过代码来保证同步。
3. 集合元素值可以是null
当向HashSet集合中存入一个元素,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值来决定该对象在HashSet中存储位置。如果有两个元素通过equals方法比较返回true,但他们的hashCode()方法返回值不相等,HashSet会把他们存储在不同位置,也就可以添加成功。
简单的说:HashSet集合通过两个条件来判断是否存储要插入的元素。一个是通过equals判断元素值是否相等,另一个是通过hashCode()方法判断Hash值是否相等。只有两个都相等了,才说明要插入的元素相等。
1 import java.util.*; 2 /** 3 * Description: 4 * <br/>Copyright (C), 2005-2008, Yeeku.H.Lee 5 * <br/>This program is protected by copyright laws. 6 * <br/>Program Name: 7 * <br/>Date: 8 * @author Yeeku.H.Lee kongyeeku@163.com 9 * @version 1.0 10 */ 11 12 //类A的equals方法总是返回true,但没有重写其hashCode()方法 13 class A{ 14 public boolean equals(Object obj){ 15 return true; 16 } 17 } 18 //类B的hashCode()方法总是返回1,但没有重写其equals()方法 19 class B{ 20 public int hashCode(){ 21 return 1; 22 } 23 } 24 //类C的hashCode()方法总是返回2,但没有重写其equals()方法 25 class C{ 26 public int hashCode(){ 27 return 2; 28 } 29 public boolean equals(Object obj){ 30 return true; 31 } 32 } 33 public class TestHashSet{ 34 public static void main(String[] args) { 35 HashSet books = new HashSet(); 36 //分别向books集合中添加2个A对象,2个B对象,2个C对象 37 books.add(new A()); 38 books.add(new A()); 39 books.add(new B()); 40 books.add(new B()); 41 books.add(new C()); 42 books.add(new C()); 43 System.out.println(books); 44 } 45 }
上面的程序中向books集合中添加了2个A对象、2个B对象、2个C对象,其中C类重写了equals()方法,总是返回true,hashCode()方法总是返回2,这将导致HashSet会把2个C对象当成同一个对象。运行上述可以得到
[B@1,B@1,C@2,A@343,A@kd33]
需要注意的是
如果需要把一个对象放入HashSet中时,如果重写该对象对应类的equals()方法,也应该重写起hashCode()方法,其规则是:如果2个对象通过equals方法比较返回true,这两个hashCode应该也相同,这样就不会添加两个同样的对象。
如果两个对象通过equals方法返回true,但是通过hashCode返回不同的,那么将会将两个对象保存在不同位置,从而都可以添加成功,这样会违反Set集合的特征(虽然可以添加成功)。
如果equals方法返回false,但hashCode值却是一样的,这样更加极品。因为hash值一样,HashSet试图把它们保存在同一个位置(但实际不能这样做,否认则将覆盖其中一个),所以处理比较复杂,而且HashSet访问元素根据hash值访问,如果HashSet中包含元素有相同的hashCode值,将导致性能下降。
HashSet的使用hashCode值去查询元素,hashCode就相当于数组索引,但为什么不使用数组呢?因为数组元素索引是连续的,并且长度固定,无法自由增加数组长度。而HashSet访问元素,可以先算出hashCode值,然后去对应位置取出元素。
重写hashCode方法原则:
当两个对象通过equals方法返回true时,对象的hashCode应该也要相等
对象中用作equals比较标准的熟悉,都应该用来计算hashCode值
重写hashCode()方式
第一步:
对象内每个有意义的熟悉f(即每个用作equals()比较标准的属性)计算出一个int类型的hashCode值,计算方法如下:
不同类型属性取得hashCode值方式
属性类型 计算方式
boolean hashCode=(f?0:1);
整数类型(byte,short,char,int) hashCode=(int)f;
long hashCode=(int)(f^(f>>>32))
float hashCode=Float.floatToIntBits(f)
double long l = Double.doubleTolongBits(f);
hashCode = (int)(l^(l>>>32));
普通引用类型 hashCode = f.hashCode();
第二步:
用第一步计算出来的对象的多个hashCode值组合计算出一个最终的hashCode,作为对象的hashCode值返回
return f2.hashCode() + (int)f2;
为了避免直接相加产生的偶然相等(两个对象的f1、f2不等,但他们的和刚好相等。例如6=2+4=3+3),可以通过为各属性乘以一个质数再相加:
return f1.hashCode()*17 + (int)f2*13;
当向HashSet中添加可变对象时,必须十分小心,如果修改HashSet集合中的对象,有可能导致该对象与集合中其他对象相等,从而导致HashSet无法准确访问对象。也就是说添加时候会做出检查,修改时候是不会检查元素之间是否相同的。所以如果修改导致了与其他对象相等,那么HashSet无法准确访问该对象。
2.1.2 LinkedHashSet接口
HashSet还有一个子类LinkedHashSet集合,LinkedHashSet集合也是根据元素hashCode值来决定元素的存储位置,但它同时使用链表维护元素次序,这样时元素看起来以插入的顺序保存的,也就是说,当遍历LinkedHashSet集合元素时,HashSet将会按元素添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet性能,但迭代输出时候性能将会很好。
注意:LinkedHashSet集合特点是有序的,并且顺序和添加时候顺序一致。
2.2 SortedSet——TreeSet接口
TreeSet是SortedSet接口的唯一实现,正如SortedSet名字所述,TreeSet可以确保元素处于排序状态。TreeSet提供了如下方法
方法:
Comparator comparator():返回当前Set使用的Comparator,或者null(表示自然方式排序)
Object first():返回集合中的第一个元素
Object last():返回集合中的最后一个元素
Object lower(Object e):返回集合中与e相比的最大元素,e不一定是TreeSet里面的元素,也就是下确界
Object higher(Object e):返回集合中与e相比的最小元素,也就是上确界,e不一定是TreeSet里面的元素
SortedSet subSet(fromElement,toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)
SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成
SortedSet tailSet(fromElement):返回此Set的子集,由大于等于fromElement的元素组成
总的来说:TreeSet提供了:访问第一个最后一个,上确界、下确界(前一个后一个),以及截取子TreeSet的方法
区别:
同样是提供了排序的存储,LinkedHashSet与TreeSet是有本质区别的。LinkedHashSet的顺序是插入元素时候的顺序,TreeSet提供的是元素值的顺序
TreeSet支持两种排序方法:自然排序和定制排序,默认是自然排序
2.2.1 自然排序
TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后集合元素按照升序排列,这种方式就是自然排序。
Java提供了一个Comparable接口,该接口定义一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较,例如obj1.compareTo(obj2),如果该方法返回0,则表示这两个对象相等;如果返回一个正整数,则表明obj1大于obj2,如果返回是一个负整数,则表示obj1小于obj2。
Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。
BigDecimal、BigInteger以及所有数值型对应的包装类:按它们对应的数值大小进行比较
Character:按字符的UNICODE值进行比较
Boolean:true对应的包装类实例大于false对应的包装类实例
String:按字符串字符的UNICODE值进行比较
Date、Time:后面的时间、日期比前面的时间、日期大。
如果试图把一个对象添加进TreeSet时,该对象的类必须实现Comparable接口,否则程序抛出异常。另外在实现compareTo(Object obj)方法时,需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的实例才会比较大小。比如日期和字符串就不能直接比较。
对TreeSet集合而言:判断两个对象不相等的标准是:两个对象通过equals方法比较false,或通过compareTo(Object obj)比较没有返回0——即使两个对象是同一个对象。
类似地:当需要把一个对象放入TreeSet中时,重写该对象对应类的equals()方法时,应该保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较返回0.
如果两个对象通过equals方法返回true,但是通过compareTo(Object obj)返回不同的,那么将会将两个对象保存在不同位置,从而都可以添加成功,这样会违反Set集合的特征(虽然可以添加成功)。
如果equals方法返回false,但compareTo(Object obj)值却是一样的,这样更加极品。因为比较相等,TreeSet试图把它们保存在同一个位置(但实际不能这样做,否认则将覆盖其中一个),所以处理比较复杂。
如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变 对象的属性,导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们的顺序,甚至可能导致TreeSet中保存这两个对象,它们通过equals方法比较返回true,通过compareTo(Object obj)方法比较返回0.
2.2.2 定制排序
可以对TreeSet集合类进行自定义排序,比如降序。可以使用Comparator接口帮助,该接口里面有一个int compare(T o1,T o2)方法用于比较o1和o2大小,比较原理痛compareTo()一样。
如果实现定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。
2.3 EnumSet类
EnumSet是一个专为枚举类设计的集合类,EnumSet中所有值都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet集合元素也是有序的,EnumSet以枚举值在Enum类定义顺序来决定集合元素的顺序。也就是不以元素值大小决定,根据添加顺序决定。
EnumSet在内部以为向量的形式存储,这种存储形式紧凑、高效,占用内存小。
EnumSet集合不允许加入null元素,如果插入null元素将抛出空指针异常。如果只是测试是否出现null元素或删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的static方法来创建EnumSet对象。它提供了如下常用static方法来创建EnumSet对象。
2.3.1 EnumSet创建方法
static EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合
static EnumSet complementOf(EnumSet s):假设调用该方法的是EnumSet a,调用后的新EnumSet的集合元素是a+s,结果放在a中
static EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合
static EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型、相同集合元素的EnumSet
static EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet
static EnumSet of(E first,E...rest):创建一个包含一个或多个枚举值的EnumSet,传入的多个枚举值必须属于同一个枚举类。
static EnumSet range(E from ,E to):创建包含从from枚举值,到to枚举值范围内所有枚举值的EnumSet集合
注意:
EnumSet复制另一个EnumSet集合中所有元素创建新的EnumSet,或复制另一个Collection集合中所有元素来创建新的EnumSet,当复制Collection集合中所有元素创建新的EnumSet时,要求Collection集合中所有元素必须是同一个枚举类型的枚举值
HashSet和TreeSet是Set的两个典型实现,那如何选择HashSet和TreeSet呢?HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet
HashSet还有一个子类:LinkedHashSet,对于普通插入、删除操作,LinkedHashSet比HashSet要略慢一点:这是有维护链表所带来的额外开销,不过有了链表,遍历LinkedHashSet会更快
EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的,如果有多条线程同时访问一个Set集合,并且有超过一条线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类synchronizedSortedSet方法来包装该Set集合。次操作最好在创建时进行,以防止对Set集合的意外非同步访问。
3.Queue集合类
3.1概述
Queue用于模拟了队列这种数据结构,队列是指"先进先出"(FIFO)的容器。队列的头部保存在队列中时间最长的元素,队列的尾部保存在队列中时间最短的元素。
3.2队列
队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
在队列这种数据结构中,最先插入的元素将是最先被删除的元素;反之最后插入的元素将是最后被删除的元素,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。
队列空的条件:front=rear
队列满的条件: rear = MAXSIZE
队列不允许随机访问队列中的元素。
3.3 Queue接口定义的方法
void add(Object e):将制定元素加入此队列的尾部。
Object element():获取队列头部的元素,但是不删除该元素。
boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
Object peek():获取队列头部的元素,但是不删除该元素,如果此队列为空,则返回null
Object poll():获取队列头部的元素,并删除该元素,如果队列为空,则返回为null
Object remove():获取队列头部的元素,并删除该元素。
3.4 Queue的实现类
Queue有两个常用的实现类:LinkedList和PriorityQueue,下面介绍着两个类。
3.4.1 LinkedList实现类
LinkedList类是一个奇怪的类,它是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,Deque有如下方法。
void addFirst(Object e):将制定元素插入该双向队列的开头
void addLast(Object e):将制定元素插入该双向队列的末尾
Iterator descendingIterator():返回以该双向队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
Object getFirst():获取、但不删除双向队列的第一个元素
Object getLast():获取、但不删除双向队列的最后一个元素
boolean offerFirst(Object e):将指定的元素插入该双向队列的开头
boolean offerLast(Object e):将指定的元素插入该双向队列的末尾
Object poolFirst():获取、并删除该双向队列的第一个元素;如果此双向队列为空,则返回null
Object poolLast():获取、并删除该双向队列的最后一个元素;如果此双向队列为空,则返回null
Object peekFirst():获取、但不删除该双向队列的第一个元素;如果此双向队列为空,则返回null
Object peekLast():获取、但不删除该双向队列的最后一个元素;如果此双向队列为空,则返回null
Object pop():pop出该双向队列所表示的栈中第一个元素
void push(Object e):将一个元素push进该双向队列所表示的栈中(即该双向队列的头部)
Object removeFirst():获取并删除该双向队列的第一个元素
Object removeFirstOccurrence(Object o):删除该双向队列的第一次出现元素o
removeLast():获取并删除该双向队列的最后一个元素
removeLastOccurrence(Object o):删除该双向队列的最后一次出现的元素o
LinkedList不仅可以当成双向队列使用,也可以当成"栈"(但其实不是栈)使用,因为该类里面有pop和push两个方法,除此之外,LinkedList实现了List接口,所以被当成List使用。
LinkedList有上述方法让它可以当做双向队列、栈和List集合用。LinkedList是个相当强大的集合类。
LinkedList与ArrayList、Vector的实现机制完全不同,ArrayList、Vector内部以数组形式保存集合中的元素,因此随机访问集合元素上性能较好,而LinkedList以链表形式保存集合,所以随机访问集合元素较差,但插入、删除元素时性能出色。
通常编程时候无需理会ArrayList和LinkedList性能差异,只需要知道LinkedList集合不仅提供了List功能,还有双向队列及栈功能,在一些性能比较敏感的地方,可能需要慎重选择哪个List实现。下面是三者性能差异:
类别 实现机制 随机访问排名 迭代排名 插入排名 删除排名
数组 连续内存区保存元素 1 不支持 不支持 不支持
ArrayList 内部以数组保存元素 2 2 2 2
Vector 内部以数组保存元素 3 3 3 3
LinkedList 内部以链表保存元素 4 1 1 1
从上表看出:因为数组以一块连续内存区保存所有数组元素,所以数组在随机访问时性能最好。所有内部数组作为底层实现的集合在随机访问时也有较好的性能;而内部以链表作为底层实现的集合在插入、删除操作时有很好的性能,以链表作为底层实现的集合也比数组作为底层实现的集合性能好。
1 package chapter3excercise; 2 3 import java.util.*; 4 5 public class TestPerformance { 6 public static void main(String[] args){ 7 //创建一个字符串数组 8 String[] test1 = new String[200000]; 9 //动态初始化数组 10 for(int i = 0;i < test1.length;i++){ 11 test1[i] = String.valueOf(i); 12 } 13 ArrayList al = new ArrayList(); 14 for(int i = 0;i < test1.length;i++){ 15 al.add(test1[i]); 16 } 17 LinkedList ll = new LinkedList(); 18 for(int i = 0;i < test1.length;i++){ 19 ll.add(test1[i]); 20 } 21 long start = System.currentTimeMillis(); 22 for(Iterator it = al.iterator();it.hasNext();){ 23 it.next(); 24 } 25 System.out.println("迭代ArrayList元素所需要的时间:" + 26 (System.currentTimeMillis() - start)); 27 start = System.currentTimeMillis(); 28 for(Iterator it = ll.iterator();it.hasNext();){ 29 it.next(); 30 } 31 System.out.println("迭代LinkedList元素所需要的时间:" + 32 (System.currentTimeMillis() - start)); 33 } 34 }
多次运行上面程序会发现,迭代ArrayList集合的时间略大于迭代LinkedList集合的时间。因此,关于使用List集合有如下建议:
如果需要遍历List集合元素,对于ArrayList、Vector集合,则应该使用随机访问方法(get)来遍历集合元素,这样性能更好。对于LinkedList集合,则应该采用迭代器来遍历集合元素
如果需要经常执行插入、删除操作来改变List集合的大小,则应该使用LinkedList集合,而不是ArrayList。使用ArrayList、Vector集合将需要经常重新分配内部数组的大小,其时间开销常常是使用LinkedList时时间开销的几十倍,效果很差。
如果有多条线程需要同时访问List集合中元素,可以考虑使用Vector这个同步实现。
3.4.2 PriorityQueue实现类
PriorityQueue是一个比较标准的队列实现类,之所以说它是比较标准的队列实现,而不是绝对标准的队列实现是因为:PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此因此当调用peek方法或者poll方法来取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列最小的元素。从这个意义上看,PriorityQueue已经违反了队列的最基本的规则:先进先出。
PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,队列元素有两种排序方式:
自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一类的多个实例,否则可能导致ClassCastException异常
定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中所有元素排序,采用定制排序时不要求队列元素实现Comparable接口
PriorityQueue队列对元素的要求与前面TreeSet对元素的要求基本一致,可以参考TreeSet处理
4.List接口和ListIterator接口
List作为Collection接口的子接口,可以使用Collection接口里的全部方法,而且List是有序集合,因此List集合里面增加了一些根据索引来操作集合元素的方法:
void add(int index,Object element):将元素element插入在List集合index处
boolean addAll(int index,Collection c):将集合c的所有元素都插入在List集合index处
Object get(int index):返回集合index索引处的元素
int indexOf(Object o):返回对象o在List集合中出现的位置索引
int lastIndexOf(Object o):返回对象o在List集合中最后一次出现的位置索引
Object remove(int index):删除并返回index索引处的元素
Object set(int index ,Object element):将index索引处的元素替换成element对象,返回新元素
List subList(int fromIndex,int toIndex):返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素的子集合。
所有List实现类都可以调用这些方法实现对集合元素的操作,相对于Set集合,List可以根据索引来插入、替换和删除集合元素。
与Set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。
ListIterator接口在Iterator接口基础上增加了如下方法:
boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素
Object previous():返回该迭代器的上一个元素
void add():在指定位置插入一个元素
拿ListIterator与普通Iterator进行对比,容易发现ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator还可以通过add方法向List集合添加元素(Iterator只能删除元素)。
正向迭代是从正向迭代输出元素,反向迭代是从反向迭代输出元素。
4.1 ArrayList和Vector实现类
ArrayList和Vector是List的两个典型实现类,完全支持List接口的全部功能。
ArrayList和Vector类是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态再分配的Object[]数组。每个ArrayList或Vector对象有一个capacity属性,这个capacity表示它们所封装的Object[]数组长度。当想ArrayList或Vector中添加元素时,其capacity会自动增加。
通常无需关心ArrayList或Vector的capacity属性,但如果想ArrayList集合或Vector集合中添加大量元素时,可以使用ensureCapacity方法一次性增加capacity,可以减少增加分配次数,提高性能。
如果开始就知道ArrayList集合或Vector集合需要保存多少个元素,可以创建时候就指定capacity大小。如果不指定,则capacity属性默认值为10
此外,ArrayList和Vector集合提供了两个方法操作capacity属性。
void ensureCapacity(int minCapacity):将ArrayList或Vector集合中的capacity增加minCapacity
void trimToSize():调整ArrayList或Vector集合的capacity为列表当前大小。程序可调用该方法来减少ArrayList或Vector集合对象存储空间
ArrayList或Vector在用法上几乎完全相同,但Vector是一个古老的集合最开始Java没有提供系统的集合框架,所以Vector提供了一些方法名很长的方法。 ArrayList开始就作为List的主要实现类,因此没有那些方法名很长的方法,实际上Vector有很多缺点,通常尽量少用Vector实现类。
ArrayList和Vector的显著区别是ArrayList是线程不安全的,当多线程访问同一个ArrayList集合时,如果超过一条修改了ArrayList集合,则需要手动保证集合的同步性,而Vector集合是线程安全的,无需保证集合的同步性。Vector性能低于ArrayList性能。
Vector还提供了Stack子类,用于模拟"栈"这种数据结构,栈通常是后进先出进栈出栈的都是Object,所以取出栈里的元素需要做类型转换。
Object peek():返回"栈"的第一个元素,但并不将该元素pop出栈
Object pop():返回栈的第一个元素,并将该元素pop出栈
void push(Object item):将一个元素push进栈,最后一个进栈的元素总是位于栈顶。
数组的工具类Arrays里面提供了asList(Object...a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除集合里的元素。
5.Map集合
5.1 Map集合特点
Map用于保存具有映射关系的数据,因此Map集合保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的value。也就是说Map保存的元素是键值对。如果把Map里的key放在一起看,它们就组成了一个Set集合(key是没有顺序,key与key之间不能重复),事实上Map确实包含了一个keySet()方法,用于返回Map所有key组成的Set集合。
不仅如此Map里key集合和Set集合里元素的存储形式也很像,Map子类和Set子类在名字上也几乎相似。Set接口下有HashSet、LinkedHashSet、SortedSet(接口)、TreeSet、EnumSet等实现类和子接口,而Map接口下则有HashMap、LinkedHashMap、SortedMap(接口)、TreeMap、EnumMap等实现类和子接口。正如名字暗示,Map的实现类和子接口中key集存储形式和对应的Set集合存储形式完全相同。如果把Map所有的value放在一起看,它们非常类似于一个List:元素与元素之间可以重复,每个元素通过索引来查找,只是Map中的索引不再使用整数值,而是另一个对象来作为索引。如果需要从List集合取出元素,需要提供该元素的数字索引,如果需要从Map中取出元素,需要提供该元素的key索引,因此Map也被称为字典或关联数组。
5.2 Map接口方法
Map中包括一个内部实现类:Entry。该类封装了一个key-value对,Entry包含三个方法:
Object getKey():返回该Entry里包含的key值
Object getValue():返回Entry里面包含的value值
Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value值
可以把Map理解成一个特殊的Set,只是该Set里包含的集合元素是Entry类对象,而不是普通对象
5.3Map接口的内部实现类
Map中包括一个内部实现类:Entry。该类封装了一个key-value对,Entry包含三个方法: Object getKey():返回该Entry里包含的key值 Object getValue():返回Entry里面包含的value值 Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value值 可以把Map理解成一个特殊的Set,只是该Set里包含的集合元素是Entry类对象,而不是普通对象
5.4 HashMap和Hashtable
HashMap和Hashtable是Map接口的典型实现类,它们之间的关系类似于ArrayList和Vector关系:Hashtable是个古老的Map实现类,它有两个繁琐的方法elements()和keys(),现在基本不用了。
Hashtable和HashMap的区别
Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable性能好,但多线程访问Map对象时,Hashtable实现类更好 Hashtable不允许使用null作为key和table,如果把null值放进Hashtable中,将会引发NullPointerException异常,但HashMap可以使用null作为key或value,但由于key不能重复,所以HashMap里最多只有一项key-value对的key为null,但可以有无数多项key-value对的value为null。key类似Set集合的,所以无序、禁止重复key,而value类似List所以可以重复,顺序和key是对应的。 代码:
1 package chapter7; 2 3 import java.util.HashMap; 4 5 public class NullInHashMapTest { 6 public static void main(String[] args){ 7 HashMap hm = new HashMap(); 8 //将两个key值为null的放在key-value键值对中 9 hm.put(null, null); 10 hm.put(null, null); 11 //将一个value值为null的放入key-value键值对中 12 hm.put('a', null); 13 System.out.println(hm); 14 } 15 } 16 17 输出结果:{null=null, a=null}
根据输出结果可以看出HashMap重写了toString()方法,实际所有Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式字符串 {key1=value1,key2=value2}
Hashtable从类名上就可以看出是一个古老的类,命名甚至都没有遵守Java的命名规范:每个类的单词首字母大写。后来也没有改成HashTable,否则将有大量程序需要改写。尽量少用Hashtable类,即使需要创建线程安全的Map实现类,可以通过Collections工具类,把HashMap变成线程安全的,无须使用Hashtable实现类。
为了成功在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode方法和equals方法。HashMap、Hashtable判断两个key相等的标准也是key通过equals方法返回true,两个key的hashCode相等。判断value相等的标准是equals返回true即可,不需要hashCode判断。
1 package chapter7; 2 3 import java.util.Hashtable; 4 5 //定义类A,该类根据A对象的count属性来判断两个对象是否相等,计算hashCode值 6 //只要两个A对象的count相等,则它们通过equals比较也相等,其hashCode值也相等 7 8 class AA{ 9 int count; 10 public AA(int count){ 11 this.count = count; 12 } 13 public boolean equals(Object obj){ 14 if (obj == this){ 15 return true; 16 } 17 if(obj!=null && obj.getClass() == AA.class){ 18 AA a = (AA)obj; 19 if(this.count == a.count){ 20 return true; 21 } 22 23 } 24 return false; 25 } 26 public int hashCode(){ 27 return this.count; 28 } 29 } 30 //定义类B,B对象与任何对象通过equals方法比较都相等 31 class BB{ 32 public boolean equals(Object obj){ 33 return true; 34 } 35 } 36 public class HashtableTest { 37 public static void main(String[] args){ 38 Hashtable ht = new Hashtable(); 39 ht.put(new AA(60000),"English"); 40 ht.put(new AA(87653),"Castellano"); 41 ht.put(new AA(1232),new B()); 42 System.out.println(ht); 43 //只要两个对象通过equals比较返回true,Hashtable就认为它们是相等的value。 44 //因为Hashtable中有一个B对象,它与任何对象通过equals比较都相等,所以下面输出true。 45 System.out.println(ht.containsValue("测试字符串")); 46 //只要两个A对象的count属性相等,它们通过equals比较返回true,且hashCode相等 47 //Hashtable即认为它们是相同的key,所以下面输出true。 48 System.out.println(ht.containsKey(new AA(87653))); 49 //下面语句可以删除最后一个key-value对 50 ht.remove(new AA(1232)); 51 for(Object key:ht.keySet()){ 52 System.out.print(key + "---->"); 53 System.out.print(ht.get(key) + "\n"); 54 } 55 } 56 }
程序最后展示了如何遍历Map中的全部key-value对:调用Map对象的keySet方法返回全部key组成的Set对象,通过遍历Set的元素(就是Map的全部key)就可以遍历Map中的所有键值对。
与HashSet类似的是,尽量不要使用可变对象作为HashMap、Hashtable的key,如果确实需要使用可变对象作为HashMap、Hashtable的key,则尽量不要在程序中修改作为key的可变对象。
HashSet有一个子类是LinkedHashSet,HashMap则有一个子类:LinkedHashMap;LinkedHashMap也使用双向链表来维护key-value对的次序,该链表定义了迭代次序,迭代顺序与插入顺序时候保持一致。
LinkedHashMap可以避免需要对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可)。同时可以避免使用TreeMap所增加的成本。LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。下面程序示范了LinkedHashMap的功能:迭代输出LinkedHashMap的元素时,将会按添加key-value对相同顺序输出。
Properties类
Properties类是Hashtable类的子类,该文件处理属性文件(ini文件就是一种属性文件)。Properties类可以把Map对象和属性文件关联起来,从而把Map对象中的key-value对写入属性文件,也可以把属性文件中的属性名=属性值加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型,该类提供了如下三个方法来修改Properties里的key、value值。
方法:
String getProperty(String key):获取Properties中指定属性名对应的属性值,类似于Map的get(Object key)方法
String getProperty(String key,String defaultValue):该方法与前一个方法基本类似,该方法多一个功能,如果Properties中不存在指定key时,该方法返回默认值
Object setProperty(String key,String value):设置属性值,类似Hashtable的put方法 除此之外,它还提供了两个读、写属性文件的方法
void load(InputStream inStream):从属性文件(以输入流表示)中加载属性名=属性值,把加载到的属性名=属性值对追加到Properties里(由于Properties是Hashtable的子类,它不保证key-value对之间的次序)。
void store(OutputStream out,String comments):将Properties中的key-value对写入指定属性文件(以输出流表示)。
5.5 SortedMap和TreeMap
正如Set接口派生出了SortedSet子接口,SortedSet接口有一个TreeSet实现类,Map接口也派生了一个SortedMap子接口,SortedMap也有一个TreeMap实现类。 与TreeSet类似的是,TreeMap也是基于红黑树对TreeMap中所有key进行排序,从而保证TreeMap中的键值对处于有序状态。类似也有两种排序方式。
排序方式
自然排序:TreeMap的所有key必须实现Comparable接口,而且所有key应该是同一个类对象,否则抛出ClassCastException异常 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中所有key进行排序,采用定制排序时不要求Map的key实现Comparable接口 类似地,TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准也是两个key通过equals比较返回true,而通过compareTo方法返回0,TreeMap即认为这两个key是相等的。 如果想使用自定义的类作为TreeMap的key,且想让TreeMap工作良好,重写该类的equals方法和compareTo方法时应该有一致的返回结果:即两个key通过equals方法比较返回true时,通过compareTo方法返回0,否则处理会有性能问题,具体可以参考TreeSet。
TreeMap方法
与TreeSet类似的是,TreeMap提供了系列根据key顺序访问Map中key-value对方法。
Map.Entry firstEntry():返回该Map中最小key所对应的键值对,如果该Map为空,则返回null
Object firstKey():返回该Map中的最小key值,如果Map为空,则返回null
Map.Entry lastEntry():返回该Map中最大key所对应的key-value对,如果该Map为空,或不存在这样的key-value则返回null
Object lastKey():返回该Map中的最大key值,如果Map为空,或不存在这样的key都返回null
Map.Entry higherEntry(Object key):返回该Map中key的上确界对应的键值对
Object higherKey(Object key):返回key的上确界
Map.Entry lowerEntry(Object key):返回该Map中key的下确界对应的键值对
Object lowerKey(Object key):返回key的下确界
NavigableMap subMap(Object fromKey,boolean fromInclusive,Object toKey,boolean toInclusive):返回该Map的子Map,其key的范围从fromKey(是否包括取决于第二个参数) 到toKey(是否包括取决于第四个参数)
NavigableMap tailMap(Object fromKey,boolean inclusive):返回该Map的子Map,其key的范围从fromKey(是否包括取决于第二个参数)
NavigableMap headMap(Object toKey,boolean inclusive):返回该Map的子Map,其key的范围小于toKey(是否包括取决于第二个参数)
SortedMap subMap(Object fromKey,Object toKey):返回该Map的子Map,其key的范围从fromKey(包括)到toKey(不包括)
SortedMap tailMap(Object fromKey):返回该Map的子Map,其key的范围从fromKey(不包括)
SortedMap headMap(Object toKey):返回该Map的子Map,其key的范围小于toKey(不包括)
上面的看起来方法挺多:也就是第一个、前一个、后一个、最后一个键值对方法,并提供了截取子TreeMap的方法
5.6 WeakHashMap实现类
WeakHashMap与HashMap用法基本类似,但与HashMap区别在于,HashMap的key保留对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap对象所有key所引用的对象不会被垃圾回收,HashMap也不会自动删除这些key所对应的key-value对象;但WeakHashMap的key只保留对实际对象的弱引用,这意味着如果该HashMap对象所有key所引用的对象没有被其他强引用变量所引用,key所引用的对象可能被垃圾回收,HashMap有可能自动删除这些key所对应的key-value对象 WeakHashMap中的每个key对象保存了实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后,WeakHashMap会自动删除key-value对。
1 package chapter7; 2 3 import java.util.*; 4 5 public class TestWeakHashMap { 6 public static void main(String[] args){ 7 //添加一个集合,并添加匿名对象(无变量引用) 8 WeakHashMap whm = new WeakHashMap(); 9 whm.put(new String("English"), "80"); 10 whm.put(new String("Chemistry"), "80"); 11 whm.put(new String("Java"), "80"); 12 System.out.println(whm); 13 //添加一个有变量引用的对象 14 whm.put("Deutsch", "99"); 15 System.out.println("垃圾回收前:" + whm); 16 //通知系统进行垃圾回收 17 System.gc(); 18 //输出垃圾回收后的结果 19 System.out.println("垃圾回收后:" + whm); 20 //对比HashMap垃圾处理机制 21 HashMap hm = new HashMap(); 22 hm.put(new String("English"), "80"); 23 hm.put(new String("Chemistry"), "80"); 24 hm.put(new String("Java"), "80"); 25 System.out.println(hm); 26 //添加一个有变量引用的对象 27 hm.put("Deutsch", "99"); 28 System.out.println("垃圾回收前:" + hm); 29 //通知系统进行垃圾回收 30 System.gc(); 31 //输出垃圾回收后的结果 32 System.out.println("垃圾回收后:" + hm); 33 34 } 35 } 36 执行结果如下: 37 {Java=80, English=80, Chemistry=80} 38 垃圾回收前:{Java=80, English=80, Deutsch=99, Chemistry=80} 39 垃圾回收后:{Deutsch=99} 40 {Chemistry=80, Java=80, English=80} 41 垃圾回收前:{Chemistry=80, Deutsch=99, Java=80, English=80} 42 垃圾回收后:{Chemistry=80, Deutsch=99, Java=80, English=80}
5.7 IdentityHashMap实现类
这个Map实现类的实现机制与HashMap基本相似,但它在处理两个key相等时比较独特:在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等,对于普通HashMap而言,只要key1和key2通过equals方法比较返回true,且hashCode值相同即可。 IdentityHashMap是一个特殊的Map实现,它有意违反Map的通常规范:在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等 IndetityHashMap提供了与HashMap基本类似的方法,null作为key和value,不保证键值对之间的顺序。
1 package chapter7; 2 3 import java.util.*; 4 /** 5 * Description: 6 * <br/>Copyright (C), 2005-2008, Yeeku.H.Lee 7 * <br/>This program is protected by copyright laws. 8 * <br/>Program Name: 9 * <br/>Date: 10 * @author Yeeku.H.Lee kongyeeku@163.com 11 * @version 1.0 12 */ 13 public class TestIdentityHashMap{ 14 public static void main(String[] args){ 15 IdentityHashMap ihm = new IdentityHashMap(); 16 //下面两行代码将会向IdentityHashMap对象中添加2个key-value对 17 ihm.put(new String("语文") , 89); 18 ihm.put(new String("语文") , 78); 19 ihm.put("java" , 93); 20 ihm.put("java" , 98); 21 System.out.println(ihm); 22 } 23 }
注意:最后添加的两个key为java的键值对是一样的,因为java采用的缓存机制,对于同一个字符串,不新建新的对象来浪费内存
5.8 EnumMap集合类
EnumMap是一个与枚举类一起使用的Map实现,EnumMap中所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效。
EnumMap根据key的定义时顺序来维护键值对的次序,当使用keySet()、entrySet()、values()等方法来遍历EnumMap时即可看到这种顺序与插入时候顺序是一致的。
EnumMap不允许使用null昨晚key值,但允许使用null作为value,如果试图使用null作为key将抛出NullPointerException异常,如果只是查询是否包含值为null的key,或者删除使用删除值为null的key都不会抛出异常。
与创建普通Map有区别的是,创建EnumMap时必须指定一个枚举类,从将该EnumMap和指定枚举类关联起来。
对于Map的常用实现类而言,HashMap和Hashtable的效率大致相同,因为实现机制几乎完全一样,但通常HashMap通常比Hashtable快一点,因为Hashtable额外实现同步操作。 TreeMap通常比HashMap、Hashtable要慢(尤其是插入、删除键值对时候更慢),因为TreeMap需要额外的红黑树操作来维护key之间的次序,但是用TreeMap有一个好处:TreeMap中的键值对总是有序的,无序专门进行排序操作。当TreeMap被填充后,可以调用keySet(),取得key组成的Set,然后是用toArray()生成key的数组,接下来是用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。当然,通常只有在某些情况下无法使用HashMap的时候才这么做,因为HashMap正是为快速查询而设计的,通常使用Map时候首选HashMap,除非需要一个总是排好序的Map时才使用TreeMap。 LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key的顺序。IndentityHashMap性能没有特别出色之处,EnumMap性能最好,但它只能使用同一个枚举类的枚举值作为key。
5.9 HashSet和HashMap的性能选择
对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来增加集合容积的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key Set容积的大小Hash表里可以存储元素的位置被称为"桶(bucket)",通常情况下,单个"桶"里存储一个元素,此时有最好的性能:hash算法可以根据hashCode值计算出"桶"的存储位置,接着从"桶"中取出元素,但hash表的状态为open:当发送hash冲突时候,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如下图
因为HashSet、HashMap、Hashtable都是用hash算法来决定其元素(对HashMap则是key)的存储,因此HashSet、HashMap的hash表包含如下属性:
Hash表属性
容量(capacity):hash表中桶的数量
初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet都允许在构造器中指定初始化容量
尺寸(size):当前hash表中记录的数量
负载因子(load factor):负载因子等于size/capacity。负载因子为0,表示空的hash表,0.5 表示半满的hash表,以此类推,轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时候会变慢)
除此之外,hash表里还有一个负载极限,负载极限在[0,1]的数值,负载极限决定了hash表中的最大填满程度。当hash表中的负载因子到达指定的负载极限时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,也被称为rehashing
HashSet与HashMap、Hashtable的构造器允许指定一个负载极限,HashSet与HashMap、Hashtable默认的负载极限为0.75, 表明当该hash表被填到3/4 时,hash表会发生rehashing
默认负载极限值0.75 是时间和空间成本上的一种折中:较高的负载极限可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作;较低的负载极限会增加查询数据性能,但会增加hash表所占用的内存空间。通常无须改变HashSet和HashMap的负载极限值。通常不需要将初始化容量设置太高。
5.10 操作集合工具类Collections
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。
5.10.1 排序操作
Collections提供了如下几个方法对List集合元素进行排序 static void reverse(List list):反转指定List集合中元素的顺序 static void shuffle(List list):对List集合元素进行随机排序(shuffle方法模拟了洗牌动作) static void sort(List list):根据元素的自然顺序对指定List集合的元素按升序进行排序 static void sort(List list,Comparator c):根据指定Comparator产生的顺序对List集合的元素进行排序 static void swap(List list,int i,int j):将指定List集合中i处元素和j处元素进行交换 static void rotate(List list,int distance):将指定集合中i处元素和list.length-i-1 处的元素进行交换
5.10.2查找,替换操作
Collections还提供了如下用于查找、替换集合元素的常用方法 static int binarySearch(List list,Object key):使用二分搜索法搜索指定List集合,以获得指定对象在List集合中的索引,如果要该方法可以正常工作,必须保证List中的元素已经处于有序状态
static Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素 static Object max(Collection coll,Comparator comp):根据指定Comparator产生的顺序,返回给定集合的最大元素 static Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素 static Object min(Collection coll,Comparator comp):根据指定Comparator产生的顺序,返回给定集合的最小元素 static void fill(List list,Object obj):使用指定元素obj替换指定List集合中的所有元素 static int frequency(Collection c,Object o):返回指定集合中等于指定对象的元素数量 static int indexOfSubList(List source,List target):返回子List对象在母List对象中第一次出现的位置索引;如果母List中没有这样的子List,则返回-1
static int lastIndexOfSubList(List source,List target):返回子List对象在母List对象中最后一次出现的位置索引;如果母List中没有这样的子List,则返回-1 static boolean replaceAll(List list,Object oldVal,Object newVal):使用一个新值newVal替换List对象所有的旧值oldVal
5.10.3 同步控制
Collections类中提供了多个synchronizedXxx方法(Xxx代表集合类名称),该方法返回指定集合对象对应的同步对象,从而可以解决多线程并发访问集合时的线程安全问题。 如前所述,Java常用的集合框架中推荐使用的三个实现类:HashSet、ArrayList和HashMap都是线程不安全的,如果有多条线程访问它们,而且有超过一条线程试图修改它们,则可能出现错误。Collections提供了多个静态方法用于创建同步集合。
5.10.4 设置不可变集合
Collections提供了如下三类方法来返回一个不可变的集合,Xxx代表集合类名称 emptyXxx():返回一个空的、不可变的集合对象,此处的集合可以是List、Set、Map singleonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合可以是List、Set、Map unmodifiableXxx:返回指定集合对象的不可变视图。此处的集合可以是List、Set、Map 通过这样的方法就可以创建"只读"版本的集合,对象不可修改
5.10.5 烦琐的:Enumeration
Enumeration接口是Iterator迭代器的古老版本,目前都使用Iterator,保留Enumeration主要是为了照顾那些古老的程序。
6.小结
本部分详细介绍了Java集合框架的相关知识。
本部分从Java集合框架体系开始讲起,概述了Java集合框架的三个主要体系:Set、List和Map,简述集合在编程中的重要性。详细讲述了Set、Queue、List、Map接口及各实现类的详细用法,深入分析了各实现类的机制差异,给出选择集合实现类的原则,最后给出了Collections工具类的基本用法。