java集合(Set、List、Queue)

Java集合概述

在编程时,常常需要集中存放多个数据,当然我们可以使用数组来保存多个对象,但数组长度不可变化,一旦在初始化数组时指定了数组长度,这个数组长度就是不可变的,如果需要保存数量变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:语文—79,数学—80,这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent包下提供了一些多线程支持的集合类。

集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。

Java的集合类主要由两个接口派生而出:CollectionMap,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、SetQueue接口的父接口,该接口里定义的方法既可用于操作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类

TreeSetSortedSet接口的实现类,正如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实现类的性能分析

HashSetTreeSet是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

除此之外,ArrayListVector还提供了如下两个方法来重新分配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提供了ArrayDequeLinkedList两个实现类。

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将集合包装成线程安全的集合。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值