疯狂Java讲义(八)----第二部分

1. Set集合

        前面已经介绍过Set集合,它类似于一个罐子,程序可以依次把多个对象“丢进”Set集合,而Set集合通常不能记住元素的添加顺序。Set集合与Collection 基本相同,没有提供任何额外的方法。实际上 Set就是Collection,只是行为略有不同(Set不允许包含重复元素)
        Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。
        上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet和 EnumSet三个实现类,只是三个实现类还各有特色。

  (1) HashSet类

        HashSet 是Set 接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet 按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
        HashSet具有以下特点。

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  • HashSet不是同步的如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步
  • 集合元素值可以是null

        当向HashSet集合中存入一个元素时,HashSet会调用该对象的 hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的 hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
        也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等
        下面程序分别提供了三个类A、B和C,它们分别重写了equals()、hashCode()两个方法的一个或全部,通过此程序可以让读者看到HashSet 判断集合元素相同的标准。

        上面程序中向books集合中分别添加了两个A对象、两个B对象和两个C对象,其中C类重写了equals()方法总是返回true,hashCode()方法总是返回2,这将导致HashSet 把两个C对象当成同一个对象。运行上面程序,看到如下运行结果:

        从上面程序可以看出,即使两个A对象通过equals()方法比较返回true,但HashSet依然把它们当成两个对象;即使两个B对象的 hashCode()返回相同值(都是1),但HashSet 依然把它们当成两个对象
        这里有一个注意点:当把一个对象放入HashSet中时,如果需要重写该对象对应类的 equals()方法,则也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同
        如果两个对象通过equals()方法比较返回true,但这两个对象的 hashCode()方法返回不同的 hashCode值时,这将导致HashSet 会把这两个对象保存在 Hash表的不同位置,从而使两个对象都可以添加成功,这就与Set集合的规则冲突
        如果两个对象的 hashCode()方法返回的 hashCode值相同,但它们通过 equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同, HashSet将试图把它们保存在同一个位置,但又不行(否则将只剩下一个对象),所以实际上会在这个位置用链式结构来保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet 中两个以上的元素具有相同的 hashCode值,将会导致性能下降。

        HashSet中每个能存储元素的“槽位”(slot)通常称为“桶”(bucket),如果有多个元素的hashCode值相同,但它们通过equals()方法比较返回false,就需要在一个“桶”里放多个元素,这样会导致性能下降。
        前面介绍了hashCode()方法对于HashSet的重要性(实际上,对象的hashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本规则。

  • 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值
  • 当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值
  • 对象中用作equals()方法比较标准的实例变量,都应该用于计算 hashCode值

        下面给出重写hashCode()方法的一般步骤。

  1. 把对象内每个有意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值。计算方式如表8.1所示。
  2.  用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回。例如如下代码:

        如果向HashSet 中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。下面程序演示了这种情况。

        上面程序中提供了R类,R类重写了equals(Objectobj)方法和 hashCode()方法,这两个方法都是根据R对象的count 实例变量来判断的。上面程序的①号粗体字代码处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。程序运行结果如图8.4所示。

        正如图8.4中所见到的,HashSet集合中的第1个元素和第2个元素完全相同,这表明两个元素已经重复。此时 HashSet 会比较混乱:当试图删除 count为-3的R对象时,HashSet 会计算出该对象的hashCode值,从而找出该对象在集合中的保存位置,然后把此处的对象与count为-3的R对象通过equals()方法进行比较,如果相等则删除该对象——HashSet 只有第2个元素才满足该条件(第1个元素实际上保存在count为-2的R对象对应的位置),所以第2个元素被删除。至于第一个count为-3的R对象,它保存在count为-2的R对象对应的位置,但使用equals()方法拿它和count为-2的R对象比较时又返回false——这将导致HashSet不可能准确访问该元素。
        由此可见,当程序把可变对象添加到HashSet中之后,尽量不要去修改该集合元素中参与计算hashCode()、equals()的实例变量,否则将会导致HashSet无法正确操作这些集合元素。

   LinkedHashSet类

        HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的 hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
        LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet 的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

  (2)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(Object fromElement, Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)
  • SortedSet headSet(Object toElement):返回此Set的子集,由小于toElement 的元素组成。
  • SortedSet tailSet(Object fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

 下面程序测试了TreeSet的通用用法。

        根据上面程序的运行结果即可看出,TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的
        与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet 采用红黑树的数据结构来存储集合元素。那么TreeSet进行排序的规则是怎样的呢?TreeSet支持两种排序方法:自然排序定制排序。在默认情况下,TreeSet采用自然排序。

  自然排序:

        TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序
        Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明obj1大于 obj2;如果该方法返回一个负整数,则表明objl小于obj2。
        Java的一些常用类已经实现了Comparable 接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。

  • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
  • Character:按字符的UNICODE值进行比较。
  • Boolean: true对应的包装类实例大于false对应的包装类实例。
  • String:按字符串中字符的UNICODE值进行比较。
  • Date、Time:后面的时间、日期比前面的时间、日期大。

        如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。如下程序示范了这个错误。

         上面程序试图向TreeSet集合中添加Err对象,在自然排序时,集合元素必须实现Comparable接口,否则将会引发运行时异常:ClassCastException——因此,TreeSet 要求自然排序的集合元素必须都实现该接口。

        还有一点必须指出:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。当试图把一个对象添加到TreeSet集合时,TreeSet 会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较—―这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException异常。如下程序示范了这个错误。

        上面程序先向TreeSet集合中添加了一个字符串对象,这个操作完全正常。当添加第二个Date对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——Date对象的compareTo(Object obj)方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常。
        如果向TreeSet中添加的对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,且实现compareTo(Object obj)方法没有进行强制类型转换当试图取出TreeSet里的集合元素时,不同类型的元素依然会发生ClassCastException异常
        总结:如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象。
        当把一个对象加入TreeSet集合中时,TreeSet 调用该对象的compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中
        对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0——如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等;否则就认为它们不相等。

        程序中①代码行把同一个对象再次添加到 TreeSet集合中,因为 z1 对象的compareTo(Object obj)方法总是返回1,虽然它的equals()方法总是返回true,但 TreeSet会认为z1 对象和它自己也不相等,因此TreeSet可以添加两个z1对象。图8.5显示了TreeSet及Z对象在内存中的存储示意图。


        从图8.5可以看到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中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet 不会再次调整它们的顺序,甚至可能导致TreeSet 中保存的这两个对象通过compareTo(Object obj)方法比较返回0。下面程序演示了这种情况。

        上面程序中的R对象对应的类正常重写了equals()方法和 compareTo()方法,这两个方法都以R对象的count 实例变量作为判断的依据。当程序执行①行代码时,看到程序输出的Set集合元素处于有序状态;因为R类是一个可变类,因此可以改变R对象的count实例变量的值,程序通过粗体字代码行改变了该集合里第一个元素和最后一个元素的count 实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。运行上面程序,看到如图8.6所示的结果。


        一旦改变了TreeSet集合里可变元素的实例变量,当再试图删除该对象时,TreeSet 也会删除失败(甚至集合中原有的、实例变量没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count为-2的R对象时,没有任何元素被删除;程序执行④代码时,可以看到删除了count为5的R对象,这表明TreeSet可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象。

   定制排序:


        TreeSet 的自然排序是根据集合元素的大小,TreeSet将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。
        如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。

        上面程序中粗体字部分使用了目标类型为Comparator 的 Lambda表达式,它负责ts集合的排序。所以当把M对象添加到ts 集合中时,无须M类实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较大小,而是由与TreeSet 关联的Lambda表达式来负责集合元素的排序。运行程序,看到如下运行结果:

  (3) EnumSet类

        EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet 的集合元素也是有序的,EnumSet以枚举值在 Enum类内的定义顺序来决定集合元素的顺序
        EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此 EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll()和retainAll()方法)时,如果其参数也是 EnumSet集合,则该批量操作的执行速度也非常快。
        EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出 NullPointerException异常。如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null 元素被删除。
        EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。EnumSet类它提供了如下常用的类方法来创建 EnumSet对象。

  • EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合。
  • EnumSet complementOf(EnumSet s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值)。
  • EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合。
  • EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。
  • EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet。
  • EnumSet of(E first, ... rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。
  • EnumSet range(E from,E to):创建一个包含从from枚举值到to 枚举值范围内所有枚举值的EnumSet集合。

下面程序示范了如何使用EnumSet来保存枚举类的多个枚举值。

        上面程序中粗体字标识的代码示范了EnumSet集合的常规用法。除此之外,还可以复制另一个EnumSet集合中的所有元素来创建新的EnumSet集合,或者复制另一个Collection集合中的所有元素来创建新的EnumSet集合。当复制Collection集合中的所有元素来创建新的EnumSet集合时,要求Collection集合中的所有元素必须是同一个枚举类的枚举值。下面程序示范了这个用法。

        上面程序中两处粗体字标识的代码没有任何区别,只是因为执行②行代码时,c集合中的元素不全是枚举值,而是包含了两个字符串对象,所以在②行代码处抛出ClassCastException异常

2. 各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集合的意外非同步访问。例如:

 关于Collections工具类的更进一步用法,可以参考8.8节的内容。

3. List集合

        List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引,例如第一次添加的元素索引为0,第二次添加的元素索引为1……

  (1)Java8改进的List接口和Listlterator接口

        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 tolndex):返回从索引fromIndex(包含)到索引toIndex (不包含)处所有集合元素组成的子集合

        所有的List实现类都可以调用这些方法来操作集合元素。与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法。除此之外,Java 8还为List接口添加了如下两个默认方法。

  • void replaceAll(UnaryOperator operator):根据operator 指定的计算规则重新设置List集合的所有元素。
  • void sort(Comparator c):根据Comparator参数对List集合的元素排序。

下面程序示范了List集合的常规用法。

        上面程序中粗体字代码示范了List集合的独特用法,List集合可以根据位置索引来访问集合中的元素,因此List增加了一种新的遍历集合元素的方法:使用普通的 for循环来遍历集合元素。运行上面程序,将看到如下运行结果:

        从上面运行结果清楚地看出List集合的用法。注意①行代码处,程序试图返回新字符串对象在List集合中的位置,实际上 List集合中并未包含该字符串对象。因为 List集合添加字符串对象时,添加的是通过new关键字创建的新字符串对象,.①行代码处也是通过new关键字创建的新字符串对象,两个字符串显然不是同一个对象,但List的 indexOf方法依然可以返回1。List判断两个对象相等的标准是什么呢? List判断两个对象相等只要通过equals()方法比较返回true即可。看下面程序。

        从上面运行结果可以看出,执行①行代码时,程序试图删除一个A对象,List将会调用该A对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素——A类重写了equals()方法,该方法总是返回true。所以每次从List集合中删除A对象时,总是删除List集合中的第一个元素

        Java 8为List集合增加了sort()和 replaceAll()两个常用的默认方法,其中 sort()方法需要一个Comparator对象来控制元素排序,程序可使用Lambda表达式来作为参数;而replaceAll()方法则需要一个UnaryOperator 来替换所有集合元素,UnaryOperator也是一个函数式接口,因此程序也可使用Lambda表达式作为参数。如下程序示范了List集合的两个默认方法的功能。

        上面程序中第一行粗体字代码控制对List集合进行排序,传给sort()方法的Lambda表达式指定的排序规则是:字符串长度越长,字符串越大,因此执行完第一行粗体字代码之后,List集合中的字符串会按由短到长的顺序排列
        程序中第二行粗体字代码传给replaceAll()方法的 Lambda表达式指定了替换集合元素的规则:直接用集合元素(字符串)的长度作为新的集合元素。执行该方法后,集合元素被替换为[7,8,11,16].
        与Set只提供了一个iterator()方法不同,List还额外提供了一个listlterator()方法,该方法返回一个Listlterator对象,ListIterator接口继承了lterator接口,提供了专门操作List 的方法。Listlterator接口在Iterator接口基础上增加了如下方法。

  • boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
  • Object previous():返回该迭代器的上一个元素。
  • void add(Object o):在指定位置插入一个元素。

        拿ListIterator 与普通的 lterator进行对比,不难发现 ListIterator增加了向前迭代的功能(Iterator 只能向后迭代),而且 ListIterator还可通过 add()方法向List集合中添加元素(Iterator 只能删除元素)。下面程序示范了ListIterator的用法。

        从上面程序中可以看出,使用ListIterator 迭代List集合时,开始也需要采用正向迭代,即先使用next()方法进行迭代,在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素。运行上面程序,看到如下结果:

  (2) ArrayList和Vector 实现类

        ArrayListVector作为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集合对象占用的存储空间。

        ArrayList和 Vector在用法上几乎完全相同,但由于Vector是一个古老的集合(从JDK 1.0就有了),那时候Java还没有提供系统的集合框架,所以Vector里提供了一些方法名很长的方法,例如addElement(Object obj),实际上这个方法与add (Object obj)没有任何区别。从JDK 1.2以后,Java提供了系统的集合框架,就将Vector改为实现 List接口,作为List的实现之一,从而导致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”进栈,最后一个进“栈”的元素总是位于“栈”顶。

        需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它同样是线程安全的、性能较差的,因此应该尽量少用Stack 类。如果程序需要使用“栈”这种数据结构,则可以考虑使用后面将要介绍的ArrayDeque

  (3) 固定长度的List

        前面讲数组时介绍了一个操作数组的工具类: Arrays,该工具类里提供了asList(Object. a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合这个 List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例
        Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。如下程序所示。

        上面程序中粗体字标识的两行代码对于普通的List集合完全正常,但如果试图通过这两个方法来增加、删除ArraysSArrayList集合里的元素,将会引发异常。所以上面程序在编译时完全正常,但会在运行第一行粗体字标识的代码行处引发UnsupportedOperationException异常。

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121044601
今日推荐