Java集合概述
在编程时,常常需要集中存放多个数据,当然我们可以使用数组来保存多个对象,但数组长度不可变化,一旦在初始化数组时指定了数组长度,这个数组长度
就是不可变
的,如果需要保存数量变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:语文—79,数学—80,这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。
为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent包下提供了一些多线程支持的集合类。
集合类和数组不一样,数组元素既可以是基本类型的值
,也可以是对象
(实际上保存的是对象的引用变量);而集合里只能保存对象
(实际上只是保存对象的引用变量
,但通常习惯上认为集合里保存的是对象)。
Java的集合类主要由两个接口派生而出:Collection
和Map
,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。
我们可以把Java的所有集合分成三大类,其中Set集合类似于一个罐子
,把一个对象添加到Set集合时,Set集合无法记住添加这个元素的顺序
,所以Set里的元素不能重复
(否则系统无法准确识别这个元素);List集合非常像一个数组,它可以记住每次添加元素的顺序,只是List的长度可变。Map集合也像一个罐子,只是它里面的每项数据都由两个值组成。图8.3显示了这三种集合的示意图。
如果访问List集合中的元素,可以直接根据元素的索引
来访问;如果访问Map集合中的元素,可以根据每项元素的key
来访问其value;如果访问Set集合中的元素,则只能根据元素本身
来访问(这也是Set集合里元素不允许重复的原因)。
最常用的实现类在图中以灰色区域覆盖,分别是HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList和HashMap、TreeMap
等实现类。
2 Collection和Iterator接口
Collection接口是List、Set
和Queue
接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。Collection接口里定义了如下操作集合元素的方法。
- boolean
add(Object o)
:该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回true。 - boolean
addAll(Collection c)
:该方法把集合c里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回true。 - void
clear()
:清除集合里的所有元素,将集合长度变为0。 - boolean
contains(Object o)
:返回集合里是否包含指定元素。 - boolean
containsAll(Collection c)
:返回集合里是否包含集合c里的所有元素。 - boolean
isEmpty()
:返回集合是否为空。当集合长度为0时返回true,否则返回false。 - Iterator
iterator()
:返回一个Iterator对象,用于遍历集合里的元素。 - boolean
remove(Object o)
:删除集合中的指定元素o,当集合中包含了一个或多个元素o时,这些元素将被删除,该方法将返回true。 - boolean
removeAll(Collection c)
:从集合中删除集合c里包含的所有元素(相当于用调用该方法
public class CollectionTest
{
public static void main(String[] args)
{
Collection c=new ArrayList();
//添加元素
c.add("孙悟空");
//虽然集合里不能放基本类型的值,但Java支持自动装箱
c.add(6);
System.out.println("c集合的元素个数为:" + c.size());
//删除指定元素
c.remove(6);
System.out.println("c集合的元素个数为:" + c.size());
//判断是否包含指定字符串
System.out.println("c集合是否包含\"孙悟空\"字符串:"
+ c.contains("孙悟空"));
c.add("轻量级Java EE企业应用实战");
System.out.println("c集合的元素:" + c);
Collection books=new HashSet();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
System.out.println("c集合是否完全包含books集合?"
+ c.containsAll(books));
//用c集合减去books集合里的元素
c.removeAll(books);
System.out.println("c集合的元素:" + c);
//删除c集合里的所有元素
c.clear();
System.out.println("c集合的元素:" + c);
//books集合里只剩下c集合里也包含的元素
books.retainAll(c);
System.out.println("books集合的元素:" + books);
}
}
上面程序中创建了两个Collection对象,一个是c集合,一个是books集合,其中c集合是ArrayList,而books集合是HashSet。虽然它们使用的实现类不同,但当把它们当成Collection来使用时,使用add、remove、clear等方法来操作集合元素时没有任何区别。
当使用System.out的println方法来输出集合对象时,将输出[ele1,ele2,…]的形式,这显然是因为所有的Collection实现类
都重写了toString()
方法,该方法可以一次性地输出集合中的所有元素。
如果想依次访问集合里的每一个元素,则需要使用某种方式来遍历集合元素,下面介绍遍历集合元素的两种方法。
注意:
在普通情况下,当我们把一个对象“丢进”集合中后,集合会忘记这个对象的类型——也就是说,系统把所有的集合元素都当成Object类的实例进行处理。从JDK 1.5以后,这种状态得到了改进:可以 使用泛型来限制集合里元素的类型
,并让集合记住所有集合元素的类型。
2.1 使用Iterator接口遍历集合元素
Iterator接口也是Java集合框架的成员,但它与Collection系列、Map系列的集合不一样:Collection系列集合、Map系列集合主要用于盛装其他对象,而Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器
Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口。Iterator接口里定义了如下三个方法。
- boolean
hasNext()
:如果被迭代的集合元素还没有被遍历,则返回true。 - Object
next()
:返回集合里的下一个元素。 - void
remove()
:删除集合里上一次next方法返回的元素。下面程序示范了通过Iterator接口来遍历集合元素。
public class IteratorTest
{
public static void main(String[] args)
{
//创建一个集合
Collection books=new HashSet();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//获取books集合对应的迭代器
Iterator it=books.iterator();
while(it.hasNext())
{
//it.next()方法返回的数据类型是Object类型
//需要强制类型转换
String book=(String)it.next();
System.out.println(book);
if (book.equals("疯狂Java讲义"))
{
//从集合中删除上一次next方法返回的元素
it.remove();
}
//对book变量赋值,不会改变集合元素本身
book="测试字符串"; //①
}
System.out.println(books);
}
}
当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量
,所以修改迭代变量的值对集合元素本身没有任何影响。
当使用Iterator迭代访问Collection集合元素时,Collection集合里的元素不能被改变,只有通过Iterator的remove方法删除上一次next方法返回的集合元素才可以
;否则将会引发java.util.Concurrent ModificationException异常。下面程序示范了这一点。
public class IteratorErrorTest
{
public static void main(String[] args)
{
//创建一个集合
Collection books=new HashSet();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//获取books集合对应的迭代器
Iterator it=books.iterator();
while(it.hasNext())
{
String book=(String)it.next();
System.out.println(book);
if (book.equals("疯狂Android讲义"))
{
//使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常
books.remove(book);
}
}
}
}
Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题
**注意:**上面程序如果改为删除“疯狂Java讲义”字符串,则不会引发异常,这样可能有些读者会“心存侥幸”地想:在迭代时好像也可以删除集合元素啊。实际上这是一种危险的行为:对于HashSet以及后面的ArrayList等,迭代时删除元素都会导致异常——只有在删除集合中的某个特定元素时才不会抛出异常,这是由集合类的实现代码决定的,程序员不应该这么做。
2.2 使用foreach循环遍历集合元素
public class ForeachTest
{
public static void main(String[] args)
{
//创建一个集合
Collection books=new HashSet();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
for (Object obj : books)
{
//此处的book变量也不是集合元素本身
String book=(String)obj;
System.out.println(book);
if (book.equals("疯狂Android讲义"))
{
//下面代码会引发ConcurrentModificationException异常
books.remove(book); //①
}
}
System.out.println(books);
}
}
与使用Iterator接口迭代访问集合元素类似的是,foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量
,因此在foreach循环中修改迭代变量的值也没有任何实际意义。
3 Set集合
Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add方法返回false,且新元素不会被加入。
Set判断两个对象相同不是使用==运算符,而是根据 equals
方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象;反之,只要两个对象用equals方法比较返回false,Set就会接受这两个对象(甚至这两个对象是同一个对象,Set也可把它们当成两个对象处理,在后面程序中可以看到这种极端的情况)。
public class SetTest
{
public static void main(String[] args)
{
Set books=new HashSet();
//添加一个字符串对象
books.add(new String("疯狂Java讲义"));
//再次添加一个字符串对象
//因为两个字符串对象通过equals方法比较相等
//所以添加失败,返回false
boolean result=books.add(new String("疯狂Java讲义"));
//从下面输出看到集合只有一个元素
System.out.println(result + "-->" + books);
}
}
3.1 HashSet类
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
- 不能保证元素的排列顺序,顺序有可能发生变化。
- HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
- 集合元素值可以是null。
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()
方法来得到该对象的hashCode值,然后根据该HashCode
值决定该对象在HashSet中的存储位置
。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且
两个对象的hashCode()方法返回值也相等。
//类A的equals()方法总是返回true,但没有重写其hashCode()方法
class A
{
public boolean equals(Object obj)
{
return true;
}
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
public int hashCode()
{
return 1;
}
}
//类C的hashCode()方法总是返回2,且重写了其equals()方法
class C
{
public int hashCode()
{
return 2;
}
public boolean equals(Object obj)
{
return true;
}
}
public class HashSetTest
{
public static void main(String[] args)
{
HashSet books=new HashSet();
//分别向books集合中添加两个A对象、两个B对象、两个C对象
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
上面程序中向books集合中分别添加了两个A对象、两个B对象和两个C对象,其中C类重写了equals()方法总是返回true,hashCode()方法总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行上面程序,看到如下运行结果:
[B@1, B@1, C@2, A@5483cd, A@9931f5]
当把一个对象放入HashSet中时,如果需要重写
该对象对应类的equals()
方法,则也应该重写其hashCode()
方法。其规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同。
如果两个对象通过equals()方法比较返回true,但这两个对象的hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两个对象保存在Hash表的不同位置
,从而使两个对象都可以添加成功,这就与Set集合的规则有些出入了。
如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同,HashSet将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构
来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。
hashCode()方法对于HashSet是不是十分重要?
我们先要理解hash(也被翻译为哈希、散列)算法的功能——它能保证通过一个对象快速查找到另一个对象。hash算法的价值在于速度,它可以保证查询被快速执行。当需要查询集合中某个元素时,hash算法可以直接根据该元素的值计算出该元素的存储位置,从而可以让程序快速定位该元素。为了理解这个概念,我们先看数组(数组是所有能存储一组元素里最快的数据结构)。数组可以包含多个元素,每个元素也有索引,如果需要访问某个数组元素,只需提供该元素的索引,该索引即指出了该元素在数组内存区里的存储位置。
表面上看起来,HashSet集合里的元素都没有索引,实际上当程序向HashSet集合中添加元素时,HashSet会根据该元素的hashCode值来计算它的存储位置——也就是说,每个元素的hashCode值就可以决定它的存储“索引”。
**为什么不直接使用数组,还需要使用HashSet呢?**因为数组元素的索引是连续的,而且数组的长度是固定的,无法自由增加数组的长度
。而HashSet就不一样了, HashSet采用每个元素的hashCode值来计算其索引,从而可以自由增加HashSet的长度,并可以根据元素的hashCode值来访问元素。因此,当从HashSet中访问元素时,HashSet先计算该元素的hashCode值(也就是调用该对象的hashCode()方法的返回值),然后直接到该hashCode值对应的位置去取出该元素——这就是HashSet速度很快的原因。
重写hashCode()方法的基本规则。
- 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值。
- 对象中用作equals()方法比较标准的Field,都应该用来计算hashCode值。
下面给出重写hashCode()方法的一般规则。
(1)把对象内每个有意义的Field(即每个用做equals()方法比较标准的Field)计算出一个int类型的hashCode值。
(2)用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回。例如如下代码:
return f1.hashCode() + (int)f2;
为了避免直接相加产生偶然相等(两个对象的f1、f2 Field并不相等,但它们的和恰好相等),可以通过为各Field乘以任意一个质数后再相加。例如如下代码:
return f1.hashCode() * 17+ (int)f2 * 13;
如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的Field,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。下面程序演示了这种情况。
class R
{
int count;
public R(int count)
{
this.count=count;
}
public String toString()
{
return "R[count:" + count + "]";
}
public boolean equals(Object obj)
{
if(this==obj)
return true;
if (obj !=null && obj.getClass()==R.class)
{
R r=(R)obj;
if (r.count==this.count)
{
return true;
}
}
return false;
}
public int hashCode()
{
return this.count;
}
}
public class HashSetTest2
{
public static void main(String[] args)
{
HashSet hs=new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
//打印HashSet集合,集合元素没有重复
System.out.println(hs);
//取出第一个元素
Iterator it=hs.iterator();
R first=(R)it.next();
//为第一个元素的count实例变量赋值
first.count=-3; //①
//再次输出HashSet集合,集合元素有重复元素
System.out.println(hs);
//删除count为-3的R对象
hs.remove(new R(-3)); //②
//可以看到被删除了一个R元素
System.out.println(hs);
//输出false
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3)));
//输出false
System.out.println("hs是否包含count为5的R对象?"
+ hs.contains(new R(5)));
}
}
上面程序中提供了R类,R类重写了equals(Object obj)方法和hashCode()方法,这两个方法都是根据R对象的count实例变量来判断的。上面程序的①号粗体字代码处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。运行结果:
正如图中所见到的,HashSet集合中的第一个元素和第三个元素完全相同,这表明两个元素已经重复,但因为HashSet把它们添加到了不同的地方,所以HashSet完全可以容纳两个相同的元素。
此时HashSet会比较混乱:当试图删除count为-3的R对象时,HashSet会计算
出该对象的hashCode
值,从而找出
该对象在集合中的保存位置
,然后把此处的对象与count为-3的R对象通过equals()方法进行比较,如果相等则删除该对象——HashSet只有第三个元素才满足该条件(第一个元素实际上保存在count为5的R对象对应的位置),所以第三个元素被删除。至于第一个count为-3的R对象,它保存在count为5的R对象对应的位置,但使用equals()方法拿它和count为5的R对象比较时又返回false——这将导致HashSet不可能准确访问该元素。
**注意:**当向HashSet中添加可变对象时,必须十分小心。如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。
3.2 LinkedHashSet类
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值
来决定元素的存储位置
,但它同时使用链表
维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序
来访问集合里元素
。
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set
里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
public class LinkedHashSetTest
{
public static void main(String[] args)
{
LinkedHashSet books=new LinkedHashSet();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
System.out.println(books);
//删除 疯狂Java讲义
books.remove("疯狂Java讲义");
//重新添加 疯狂Java讲义
books.add("疯狂Java讲义");
System.out.println(books);
}
}
编译、运行上面程序,看到如下输出:
[疯狂Java讲义, 轻量级Java EE企业应用实战]
[轻量级Java EE企业应用实战, 疯狂Java讲义]
输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致
。
3.3 TreeSet类
TreeSet是SortedSet接口
的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与HashSet集合相比,TreeSet还提供了如下几个额外的方法。
- Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null。
- Object first():返回集合中的第一个元素。
- Object last():返回集合中的最后一个元素。
- Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。
- Object higher (Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。
- SortedSet subSet(fromElement, toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。
- SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。
- SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。
注意: 表面上看起来这些方法很复杂,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。
public class TreeSetTest
{
public static void main(String[] args)
{
TreeSet nums=new TreeSet();
//向TreeSet中添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
//输出集合元素,看到集合元素已经处于排序状态
System.out.println(nums);
//输出集合里的第一个元素
System.out.println(nums.first());
//输出集合里的最后一个元素
System.out.println(nums.last());
//返回小于4的子集,不包含4
System.out.println(nums.headSet(4));
//返回大于5的子集,如果Set中包含5,子集中也包含5
System.out.println(nums.tailSet(5));
//返回大于等于-3、小于4的子集
System.out.println(nums.subSet(-3 , 4));
}
}
运行结果:
[-9, 2, 5, 10]
-9
10
[-9, 2]
[5, 10]
[2]
与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树
的数据结构来存储集合元素。那么TreeSet进行排序的规则是怎样的呢?TreeSet支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet采用自然排序
。
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接口,否则程序将会抛出异常。
class Err
{
}
public class TreeSetErrorTest
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
//向TreeSet集合中添加两个Err对象
ts.add(new Err());
ts.add(new Err()); //①
}
}
上面程序试图向TreeSet集合中添加两个Err对象,添加第一个对象时,TreeSet里没有任何元素,所以不会出现任何问题;当添加第二个Err对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——如果其对应的类没有实现Comparable接口,则会引发ClassCastException异常。
**注意:**向TreeSet集合中添加元素时,只有第一个元素无须实现Comparable接口,后面添加的所有元素都必须实现Comparable接口。当然这也不是一种好做法,当试图从TreeSet中取出元素时,依然会引发ClassCastException异常。
还有一点必须指出:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。 当试图把一个对象添加到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException异常
public class TreeSetErrorTest2
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
//向TreeSet集合中添加两个对象
ts.add(new String("Struts权威指南"));
ts.add(new Date()); //①
}
}
上面程序先向TreeSet集合中添加了一个字符串对象,这个操作完全正常。当添加第二个Date对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——Date对象的compareTo(Object obj)方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常。
如果向TreeSet中添加的对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,实现该接口时实现的compareTo(Object obj)方法没有进行强制类型转换。但当试图取出TreeSet里的集合数据时,不同类型的元素依然会发生ClassCastException异常。
当把一个对象加入TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。
对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0——如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等;否则就认为它们不相等。
class Z implements Comparable
{
int age;
public Z(int age)
{
this.age=age;
}
// 重写equals()方法,总是返回true
public boolean equals(Object obj)
{
return true;
}
//重写了compareTo(Object obj)方法,总是返回正整数
public int compareTo(Object obj)
{
return 1;
}
}
public class TreeSetTest2
{
public static void main(String[] args)
{
TreeSet set=new TreeSet();
Z z1=new Z(6);
set.add(z1);
//输出true,表明添加成功
System.out.println(set.add(z1)); //①
//下面输出set集合,将看到有两个元素
System.out.println(set);
//修改set集合的第一个元素的age变量
((Z)(set.first())).age=9;
//输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age);
}
}
程序中①代码行把同一个对象再次添加到TreeSet集合中,因为z1对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但TreeSet会认为z1对象和它自己也不相等,因此TreeSet可以添加两个z1对象。下图显示了TreeSet及Z对象在内存中的存储示意图。
可以看到TreeSet对象保存的两个元素(集合里的元素总是引用,但我们习惯上把被引用的对象称为集合元素),实际上是同一个元素。所以当修改TreeSet集合里第一个元素的age变量后,该TreeSet集合里最后一个元素的age变量也随之改变了。
当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0。
如果两个对象通过compareTo(Object obj)方法比较返回0时,但它们通过equals()方法比较返回false将很麻烦,因为两个对象通过compareTo(Object obj)方法比较相等,TreeSet不会让第二个元素添加进去,这就会与Set集合的规则产生冲突。
如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的Field,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们的顺序,甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0。下面程序演示了这种情况。
class R implements Comparable
{
int count;
public R(int count)
{
this.count=count;
}
public String toString()
{
return "R[count:" + count + "]";
}
//重写equals()方法,根据count来判断是否相等
public boolean equals(Object obj)
{
if (this==obj)
{
return true;
}
if(obj !=null && obj.getClass()==Z.class)
{
R r=(R)obj;
if (r.count==this.count)
{
return true;
}
}
return false;
}
//重写compareTo()方法,根据count来比较大小
public int compareTo(Object obj)
{
R r=(R)obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeSetTest3
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet();
ts.add(new R(5));
ts.add(new R(-3));
ts.add(new R(9));
ts.add(new R(-2));
//打印TreeSet集合,集合元素是有序排列的
System.out.println(ts); //①
//取出第一个元素
R first=(R)ts.first();
//对第一个元素的count赋值
first.count=20;
//取出最后一个元素
R last=(R)ts.last();
//对最后一个元素的count赋值,与第二个元素的count相同
last.count=-2;
//再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
System.out.println(ts); //②
//删除Field被改变的元素,删除失败
System.out.println(ts.remove(new R(-2))); //③
System.out.println(ts);
//删除Field没有改变的元素,删除成功
System.out.println(ts.remove(new R(5))); //④
System.out.println(ts);
}
}
运行结果:
一旦改变了TreeSet集合里可变元素的Field,当再试图删除该对象时,TreeSet也会删除失败(甚至集合中原有的、Field没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count为-2的R对象时,没有任何元素被删除;程序执行④代码时,可以看到删除了count为5的R对象,这表明TreeSet可以删除没有被修改Field,且不与其他被修改Field的对象重复的对象。
注意:当执行了④代码后,TreeSet会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet中的所有元素了,包括那些被修改过Field的元素。与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的Field被修改时,TreeSet在处理这些对象时将非常复杂,而且容易出错。为了让程序更加健壮,推荐HashSet和TreeSet集合中只放入不可变对象。至于如何创建不可变对象,请参考final修饰符。
2.定制排序
TreeSet的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口里包含一个**int compare(T o1, T o2)**方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。
如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。
class M
{
int age;
public M(int age)
{
this.age=age;
}
public String toString()
{
return "M[age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
TreeSet ts=new TreeSet(new Comparator()
{
//根据M对象的age属性来决定大小
public int compare(Object o1, Object o2)
{
M m1=(M)o1;
M m2=(M)o2;
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
}
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}
上面程序中粗体字部分创建了一个Comparator接口的匿名内部类对象,该对象负责ts集合的排序。所以当我们把M对象添加到ts集合中时,无须M类实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较大小,而是由与TreeSet关联的Comparator对象来负责集合元素的排序。
[M对象(age:9), M对象(age:5), M对象(age:-3)]
注意:
当通过Comparator对象来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。使用定制排序时,TreeSet对集合元素排序不管集合元素本身的大小,而是由Comparator对象负责集合元素的排序规则。TreeSet判断两个集合元素相等的标准是:通过Comparator比较两个元素返回了0,这样TreeSet不会把第二个元素添加到集合中。
3.4 EnumSet类
EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll和retainAll方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常。 如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。
3.5 各Set实现类的性能分析
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集合的意外非同步访问。例如:
SortedSet s=Collections.synchronizedSortedSet(new TreeSet(...));
4 List集合
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引,例如第一次添加的元素索引为0,第二次添加的元素索引为1……
4.1 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增加了根据索引来插入、替换和删除集合元素的方法。**下面程序示范了List集合的常规用法。
public class ListTest
{
public static void main(String[] args)
{
List books=new ArrayList();
//向books集合中添加三个元素
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
//将新字符串对象插入在第二个位置
books.add(1 , new String("疯狂Ajax讲义"));
for (int i=0 ; i < books.size() ; i++ )
{
System.out.println(books.get(i));
}
//删除第三个元素
books.remove(2);
System.out.println(books);
//判断指定元素在List集合中的位置:输出1,表明位于第二位
System.out.println(
books.indexOf(new String("疯狂Ajax讲义"))); //①
//将第二个元素替换成新的字符串对象
books.set(1, new String("疯狂Java讲义"));
System.out.println(books);
//将books集合的第二个元素(包括)
//到第三个元素(不包括)截取成子集合
System.out.println(books.subList(1 , 2));
}
}
上面程序中粗体字代码示范了List集合的独特用法,List集合可以根据位置索引来访问集合中的元素,因此List增加了一种新的遍历集合元素的方法:使用普通的for循环来遍历集合元素。运行上面程序,将看到如下运行结果:
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
轻量级Java EE企业应用实战
疯狂Ajax讲义
疯狂Java讲义
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Ajax讲义, 疯狂Android讲义]
1
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义]
从上面运行结果清楚地看出List集合的用法。注意①行代码处,程序试图返回新字符串对象在List集合中的位置,实际上List集合中并未包含该字符串对象。因为List集合添加字符串对象时,添加的是通过new关键字创建的新字符串对象,①行代码处也是通过new关键字创建的新字符串对象,两个字符串显然不是同一个对象,但List的indexOf方法依然可以返回1。List判断两个对象相等的标准是什么呢?List判断两个对象相等只要通过equals()
方法比较返回true
即可。看下面程序。
class A
{
public boolean equals(Object obj)
{
return true;
}
}
public class ListTest2
{
public static void main(String[] args)
{
List books=new ArrayList();
books.add(new String("轻量级Java EE企业应用实战"));
books.add(new String("疯狂Java讲义"));
books.add(new String("疯狂Android讲义"));
System.out.println(books);
//删除集合中的A对象,将导致第一个元素被删除
books.remove(new A()); //①
System.out.println(books);
//删除集合中的A对象,再次删除集合中的第一个元素
books.remove(new A()); //②
System.out.println(books);
}
}
编译、运行上面程序,看到如下运行结果:
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义, 疯狂Android讲义]
[疯狂Android讲义]
从上面运行结果可以看出,执行①行代码时,程序试图删除一个A对象,List将会调用该A对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素——A类重写了equals()方法,该方法总是返回true。所以我们看到每次从List集合中删除A对象,总是删除List集合中的第一个元素。
注意:
当调用List的set(int index, Object element)
方法来改变List集合指定索引处的元素时,指定的索引必须是List集合的有效索引
。例如集合长度是4,就不能指定替换索引为4处的元素——也就是说,set(int index, Object element)方法不会改变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只能删除元素)。下面程序示范了ListIterator的用法。
public class ListIteratorTest
{
public static void main(String[] args)
{
String[] books={
"疯狂Java讲义",
"轻量级Java EE企业应用实战"
};
List bookList=new ArrayList();
for (int i=0; i < books.length ; i++ )
{
bookList.add(books[i]);
}
ListIterator lit=bookList.listIterator();
while (lit.hasNext())
{
System.out.println(lit.next());
lit.add("-------分隔符-------");
}
System.out.println("=======下面开始反向迭代=======");
while(lit.hasPrevious())
{
System.out.println(lit.previous());
}
}
}
从上面程序中可以看出,使用ListIterator迭代List集合时,开始也需要采用正向迭代,即先使用next()方法进行迭代,在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素。
4.2 ArrayList和Vector实现类
ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能
ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出了该数组的长度时,它们的initialCapacity会自动增加。
对于通常的编程场景,程序员无须关心ArrayList或Vector的initialCapacity。但如果向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)
方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。
如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。如果创建空的ArrayList或Vector集合时不指定initialCapacity参数,则Object[]数组的长度默认
为10
。
除此之外,ArrayList和Vector还提供了如下两个方法来重新分配Object[]数组。
- void
ensureCapacity(int minCapacity)
:将ArrayList或Vector集合的Object[]数组长度增加minCapacity。 - void
trimToSize()
:调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。程序可调用该方法来减少ArrayList或Vector集合对象占用的存储空间。
提示:
Vector里有一些功能重复的方法,这些方法中方法名更短的方法属于后来新增的方法,方法名更长的方法则是Vector原有的方法。Java改写了Vector原有的方法,将其方法名缩短是为了简化编程。而ArrayList开始就作为List的主要实现类,因此没有那些方法名很长的方法。实际上,Vector具有很多缺点,通常尽量少用Vector实现类。
除此之外,ArrayList和Vector的显著区别是:ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但Vector集合则是线程安全的,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也同样不推荐使用Vector实现类。后面会介绍一个Collections工具类,它可以将一个ArrayList变成线程安全的。
Vector还提供了一个Stack子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。与Java中的其他集合一样,进栈出栈的都是Object,因此从栈中取出元素后必须进行类型转换
,除非你只是使用Object具有
的操作。所以Stack类里提供了如下几个方法。
- Object
peek()
:返回“栈”的第一个元素,但并不将该元素“pop”出栈。 - Object
pop()
:返回“栈”的第一个元素,并将该元素“pop”出栈。 - void
push(Object item)
:将一个元素“push”进栈,最后一个进“栈”的元素总是位于“栈”顶。
public class VectorTest
{
public static void main(String[] args)
{
Stack v=new Stack();
//依次将三个元素“push”入栈
v.push("疯狂Java讲义");
v.push("轻量级Java EE企业应用实战");
v.push("疯狂Android讲义");
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(v);
//访问第一个元素,但并不将其“pop”出栈,输出:疯狂Android讲义
System.out.println(v.peek());
//依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(v);
//“pop”出栈第一个元素,输出:疯狂Android讲义
System.out.println(v.pop());
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(v);
}
}
需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它是线程安全的,性能比较差,因此现在的程序中一般较少使用Stack类。如果程序需要使用“栈”这种数据结构,则可以考虑使用LinkedList。
LinkedList也是List的实现类
,它是一个基于链表实现的List类,对于顺序访问集合中的元素进行了优化,特别是插入、删除元素时速度非常快。LinkedList既实现了List
接口,也实现了Deque
接口,由于实现了Deque接口,因此可以作为栈来使用。
4.3 固定长度的List
前面讲数组时介绍了一个操作数组的工具类
:Arrays
,该工具类里提供了asList(Object... a)
方法,该方法可以把一个数组
或指定个数的对象
转换成一个List集合
,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类
ArrayList的实例。
Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问
该集合里的元素,不可增加、删除该集合里的元素。如下程序所示。
public class FixedSizeList
{
public static void main(String[] args)
{
List fixedList=Arrays.asList("疯狂Java讲义"
, "轻量级Java EE企业应用实战");
//获取fixedList的实现类,将输出Arrays$ArrayList
System.out.println(fixedList.getClass());
//遍历fixedList的集合元素
for (int i=0; i < fixedList.size() ; i++)
{
System.out.println(fixedList.get(i));
}
//试图增加、删除元素都会引发UnsupportedOperationException异常
fixedList.add("疯狂Android讲义");
fixedList.remove("疯狂Java讲义");
}
}
5 Queue集合
Queue用于模拟队列这种数据结构,队列通常是指“先进先出
”(FIFO)的容器。队列的头部
保存在队列中存放时间最长
的元素,队列的尾部
保存在队列中存放时间最短的元素
。新元素插入(offer
)到队列的尾部,访问元素(poll)
操作会返回队列头部
的元素。通常,队列不允许随机访问队列中的元素。
Queue接口中定义了如下几个方法。
- void add(Object e):将指定元素加入此队列的尾部。
- Object element():获取队列头部的元素,但是不删除该元素。
- boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
- Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。
- Object poll():获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。
- Object remove():获取队列头部的元素,并删除该元素。
Queue接口有一个PriorityQueue
实现类。除此之外,Queue还有一个Deque接口,Deque代表一个“双端队列
”,双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列
使用,也可当成栈
使用。Java为Deque提供了ArrayDeque
和LinkedList
两个实现类。
5.1 PriorityQueue实现类
PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue保存队列元素的顺序并不是按加入队列
的顺序,而是按队列元素的大小
进行重新排序。因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。
PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,PriorityQueue的元素有两种排序方式。
- 自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
- 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。
5.2 Deque接口与ArrayDeque实现类
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 peekFirst():获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
-
Object peekLast():获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
-
Object pollFirst():获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
-
Object pollLast():获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
-
Object pop()(栈方法):pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。
-
void push(Object e)(栈方法):将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。
-
Object removeFirst():获取并删除该双端队列的第一个元素。
-
Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素o。
-
removeLast():获取并删除该双端队列的最后一个元素。
-
removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素o。
从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。
Deque的方法与Queue的方法对照表
Deque的方法与Stack的方法对照表
Deque接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
**提示:**ArrayList和ArrayDeque两个集合类的实现机制基本相似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个Object[]数组来存储集合元素。
下面程序示范了把ArrayDeque当成“栈”来使用。
public class ArrayDequeTest
{
public static void main(String[] args)
{
ArrayDeque stack=new ArrayDeque();
//依次将三个元素push入“栈”
stack.push("疯狂Java讲义");
stack.push("轻量级Java EE企业应用实战");
stack.push("疯狂Android讲义");
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(stack);
//访问第一个元素,但并不将其pop出“栈”,输出:疯狂Android讲义
System.out.println(stack.peek());
//依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战 , 疯狂Android讲义]
System.out.println(stack);
//pop出第一个元素,输出:疯狂Android讲义
System.out.println(stack.pop());
//输出:[疯狂Java讲义, 轻量级Java EE企业应用实战]
System.out.println(stack);
}
}
上面程序的运行结果与前面使用Stack的运行结果相似,不过使用ArrayDeque的性能会更加出色,因此现在的程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque或LinkedList,而不是Stack。
5.3 LinkedList实现类
LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引
来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,因此它可以被当成双端队列
来使用,自然也可以被当成“栈
”来使用了。下面程序简单示范了LinkedList集合的用法。
public class LinkedListTest
{
public static void main(String[] args)
{
LinkedList books=new LinkedList();
//将字符串元素加入队列的尾部
books.offer("疯狂Java讲义");
//将一个字符串元素加入栈的顶部
books.push("轻量级Java EE企业应用实战");
//将字符串元素添加到队列的头部(相当于栈的顶部)
books.offerFirst("疯狂Android讲义");
for (int i=0; i < books.size() ; i++ )
{
System.out.println(books.get(i));
}
//访问但不删除栈顶的元素
System.out.println(books.peekFirst());
//访问但不删除队列的最后一个元素
System.out.println(books.peekLast());
//将栈顶的元素弹出“栈”
System.out.println(books.pop());
//下面输出将看到队列中第一个元素被删除
System.out.println(books);
//访问并删除队列的最后一个元素
System.out.println(books.pollLast());
//下面输出将看到队列中只剩下中间一个元素:
//轻量级Java EE企业应用实战
System.out.println(books);
}
}
LinkedList与ArrayList、ArrayDeque的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问
集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除
元素时性能非常出色(只需改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能,所以各方面性能都有所下降。
注意: 对于所有的内部基于数组的集合实现,例如ArrayList、ArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。
5.4 各种线性表的性能分析
public class PerformanceTest
{
public static void main(String[] args)
{
//创建一个字符串数组
String[] tst1=new String[900000];
//动态初始化数组元素
for (int i=0; i < 900000; i++)
{
tst1[i]=String.valueOf(i);
}
ArrayList al=new ArrayList();
//将所有的数组元素加入ArrayList集合中
for (int i=0; i < 900000 ; i++)
{
al.add(tst1[i]);
}
LinkedList ll=new LinkedList();
//将所有的数组元素加入LinkedList集合中
for (int i=0; i < 900000 ; i++)
{
ll.add(tst1[i]);
}
//迭代访问ArrayList集合的所有元素,并输出迭代时间
long start=System.currentTimeMillis();
for (Iterator it=al.iterator();it.hasNext() ; )
{
it.next();
}
System.out.println("迭代ArrayList集合元素的时间:"
+ (System.currentTimeMillis() - start));
//迭代访问LinkedList集合的所有元素,并输出迭代时间
start=System.currentTimeMillis();
for (Iterator it=ll.iterator();it.hasNext() ; )
{
it.next();
}
System.out.println("迭代LinedList集合元素的时间:"
+ (System.currentTimeMillis() - start));
}
}
由于上面程序创建了一个长度为900000的字符串数组,需要很大的内存空间,JVM默认的内存空间不足以运行上面程序,所以应该采用如下命令来运行上面程序:
java -Xms128m -Xmx512m PerformanceTest
- -Xms是设置JVM的堆内存初始大小。
- -Xmx是设置JVM的堆内存最大大小(最好不要超过物理内存大小)。
多次运行上面程序会发现,迭代ArrayList集合的时间略大于迭代LinkedList集合的时间。因此,关于使用List集合有如下建议。
- 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素。
- 如果需要经常执行插入、删除操作来改变List集合的大小,则应该使用LinkedList集合,而不是ArrayList。使用ArrayList、Vector集合需要经常重新分配内部数组的大小,其时间开销常常是使用LinkedList的时间开销的几十倍,效果很差。
- 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。