12. Java 集合

空~2022年7月12日
  • java
大约 82 分钟

12. Java 集合

Java 集合类是一种特别有用的工具类,可以用于存储数量不等的多个对象,并可以实现常用的数据结构,如栈、队列等。除此之外,Java 集合还可用于保存具有映射关系的关联数组。Java 集合大致可分为 SetListMap 三种体系,其中 Set 代表无序、不可重复的集合;List 代表有序、重复的集合;而 Map 则代表具有映射关系的集合。从 Java 5 以后,Java 又增加了 Queue 体系集合,代表一种队列集合实现。

Java 集合就像一种容器,我们可以把多个对象(实际上是对象的引用,但习惯上都称对象)“丢进”该容器中。在 Java 5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 Java 5 增加了泛型以后,Java 集合可以记住容器中对象的数据类型,从而可以编写出更简洁、健壮的代码。

Java 集合概述

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

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

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

Java 的集合类主要由两个接口派生而出:CollectionMapCollectionMap 是 Java 集合框架的根接口,这两个接口又包含了一些子接口或实现类。

img

粗线圈出的 SetList 接口是 Collection 接口派生的两个子接口,它们分别代表了无序集合和有序集合;Queue 是 Java 提供的队列实现,有点类似于 List

img

如图所示是 Map 体系的继承树,所有的 Map 实现类用于保存具有映射关系的数据(也就是前面介绍的关联数组)。

图中显示了 Map 接口的众多实现类,这些实现类在功能、用法上存在一定的差异,但它们都有一个功能特征:Map 保存的每项数据都是 key-value 对,也就是由 keyvalue 两个值组成。就像前面介绍的成绩单:语文-79,数学-80,每项成绩都由两个值组成,即科目名和成绩。

对于一张成绩表而言,科目通常不会重复,而成绩是可重复的,通常习惯根据科目来查阅成绩,而不会根据成绩来查阅科目。Map 与此类似,Map 里的 key 是不可重复的,key 用于标识集合里的每项数据,如果需要查阅 Map 中的数据时,总是根据 Mapkey 来获取。

根据以上俩图中粗线标识的 4 个接口,我们可以把 Java 的所有集合分成三大类,其中 Set 集合类似于一个罐子,把一个对象添加到 Set 集合时,Set 集合无法记住添加这个元素的顺序,所以 Set 里的元素不能重复(否则系统无法准确识别这个元素);List 集合非常像一个数组,它可以记住每次添加元素的顺序,只是 List 的长度可变。Map 集合也像一个罐子,只是它里面的每项数据都由两个值组成。

img

如果访问 List 集合中的元素,可以直接根据元素的索引来访问;如果访问 Map 集合中的元素,可以根据每项元素的 key 来访问其 value;如果访问 Set 集合中的元素,则只能根据元素本身来访问(这也是 Set 集合里元素不允许重复的原因)。

对于 SetListQueueMap 四种集合,最常用的实现类分别是 HashSetTreeSetArrayListArrayDequeLinkedListHashMapTreeMap 等实现类。

CollectionIterator

Collection 接口是 ListSetQueue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 ListQueue 集合。

  1. boolean add(Object o):该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回 true
  2. boolean addAll(Collection c):该方法把集合 c 里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回 true
  3. void clear():清除集合里的所有元素,将集合长度变为 0。
  4. boolean contains(Object o):返回集合里是否包含指定元素。
  5. boolean containsAll(Collection c):返回集合里是否包含集合 c 里的所有元素。
  6. boolean isEmpty():返回集合是否为空。当集合长度为 0 时返回 true,否则返回 false
  7. Iterator iterator():返回一个 Iterator 对象,用于遍历集合里的元素。
  8. boolean remove(Object o):删除集合中的指定元素 o,当集合中包含了一个或多个元素 o 时,这些元素将被删除,该方法将返回 true
  9. boolean removeAll(Collection c):从集合中删除集合 c 里包含的所有元素(相当于用调用该方法的集合减集合 c),如果删除了一个或一个以上的元素,则该方法返回 true
  10. boolean retainAll(Collection c):从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true
  11. int size():该方法返回集合里元素的个数。
  12. Object[] toArray():该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素。
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);
    }
}
/*
    c集合的元素个数为:2
    c集合的元素个数为:1
    c集合是否包含"孙悟空"字符串:true
    c集合的元素:[孙悟空, 轻量级Java EE企业应用实战]
    c集合是否完全包含books集合?false
    c集合的元素:[孙悟空]
    c集合的元素:[]
    books集合的元素:[]
*/

Iterator

Collection 系列集合、Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素,Iterator 对象也被称为迭代器。

Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。

  1. boolean hasNext():如果被迭代的集合元素还没有被遍历,则返回 true
  2. Object next():返回集合里的下一个元素。
  3. void remove():删除集合里上一次 next 方法返回的元素。
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 必须依附于 Collection 对象,若有一个 Iterator 对象,则必然有一个与之关联的 Collection 对象。

Iterator 提供了两个方法来迭代访问 Collection 集合里的元素,并可通过 remove() 方法来删除集合中上一次 next() 方法返回的集合元素。

当使用 Iterator 对集合元素进行迭代时,Iterator 并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响。

当使用 Iterator 迭代访问 Collection 集合元素时,Collection 集合里的元素不能被改变,只有通过 Iteratorremove 方法删除上一次 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 异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

foreach

除了可以使用 Iterator 接口迭代访问 Collection 集合里的元素之外,使用 Java 5 提供的 foreach 循环迭代访问集合元素更加便捷,如下程序示范了使用 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);
    }
}

foreach 循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在 foreach 循环中修改迭代变量的值也没有任何实际意义。

同样,当使用 foreach 循环迭代访问集合元素时,该集合也不能被改变,否则将引发 Concurrent ModificationException 异常。

Set 集合

Set 集合与 Collection 基本上完全一样,它没有提供任何额外的方法。实际上 Set 就是 Collection,只是行为略有不同(Set 不允许包含重复元素)。

Set 判断两个对象相同不是使用 == 运算符,而是根据 equals 方法。

也就是说,只要两个对象用 equals 方法比较返回 trueSet 就不会接受这两个对象;反之,只要两个对象用 equals 方法比较返回 falseSet 就会接受这两个对象(甚至这两个对象是同一个对象,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);
    }
}

HashSet

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

HashSet 具有以下特点。

  1. 不能保证元素的排列顺序,顺序有可能发生变化。
  2. HashSet 不是同步的,如果多个线程同时访问一个 HashSet,假设有两个或者两个以上线程同时修改了 HashSet 集合时,则必须通过代码来保证其同步。
  3. 集合元素值可以是 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);
    }
}
/*
    [CollectionDemo.B@1, CollectionDemo.B@1, CollectionDemo.C@2, CollectionDemo.A@4554617c, CollectionDemo.A@1b6d3586]
*/

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

提示

当把一个对象放入 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() 方法的基本规则。

  1. 在程序运行过程中,同一个对象多次调用 hashCode()方法应该返回相同的值。
  2. 当两个对象通过 equals()方法比较返回 true 时,这两个对象的 hashCode()方法应返回相等的值。
  3. 对象中用作 equals()方法比较标准的 Field,都应该用来计算 hashCode 值。

重写 hashCode() 方法的一般规则。

  • 把对象内每个有意义的 Field(即每个用做 equals() 方法比较标准的 Field)计算出一个 int 类型的 hashCode 值。

img

  • 用第 1 步计算出来的多个 hashCode 值组合计算出一个 hashCode 值返回。
return f1.hashCode() + (int)f2;

为了避免直接相加产生偶然相等(两个对象的 f1f2 Field 并不相等,但它们的和恰好相等),可以通过为各 Field 乘以任意一个质数后再相加。

return f1.hashCode() * 17 + (int)f2 * 13;

如果向 HashSet 中添加一个可变对象后,后面程序修改了该可变对象的 Field,则可能导致它与集合中的其他元素相同,这就有可能导致 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[count:-2], R[count:-3], R[count:5], R[count:9]]
    [R[count:-3], R[count:-3], R[count:5], R[count:9]]
    [R[count:-3], R[count:5], R[count:9]]
    hs是否包含count为-3的R对象?false
    hs是否包含count为5的R对象?true
*/

此时 HashSet 会比较混乱:

当试图删除 count 为-3 的 R 对象时,HashSet 会计算出该对象的 hashCode 值,从而找出该对象在集合中的保存位置,然后把此处的对象与 count 为-3 的 R 对象通过 equals() 方法进行比较,如果相等则删除该对象——HashSet 只有第三个元素才满足该条件(第一个元素实际上保存在 count 为 5 的 R 对象对应的位置),所以第三个元素被删除。

至于第一个 count 为-3 的 R 对象,它保存在 count5R 对象对应的位置,但使用 equals() 方法拿它和 count 为 5 的 R 对象比较时又返回 false——这将导致 HashSet 不可能准确访问该元素。

LinkedHashSet

HashSet 还有一个子类 LinkedHashSetLinkedHashSet 集合也是根据元素的 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 使用了链表记录集合元素的添加顺序,但 LinkedHashSet 依然是 HashSet,因此它依然不允许集合元素重复。

TreeSet

TreeSetSortedSet 接口的实现类,正如 SortedSet 名字所暗示的,TreeSet 可以确保集合元素处于排序状态。与 HashSet 集合相比,TreeSet 还提供了如下几个额外的方法。

  1. Comparator comparator():如果 TreeSet 采用了定制排序,则该方法返回定制排序所使用的 Comparator;如果 TreeSet 采用了自然排序,则返回 null
  2. Object first():返回集合中的第一个元素。
  3. Object last():返回集合中的最后一个元素。
  4. Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是 TreeSet 集合里的元素)。
  5. Object higher (Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是 TreeSet 集合里的元素)。
  6. SortedSet subSet(fromElement, toElement):返回此 Set 的子集合,范围从 fromElement(包含)到 toElement(不包含)。
  7. SortedSet headSet(toElement):返回此 Set 的子集,由小于 toElement 的元素组成。
  8. SortedSet tailSet(fromElement):返回此 Set 的子集,由大于或等于 fromElement 的元素组成。
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 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。

Java 提供了一个 Comparable 接口,该接口里定义了一个 compareTo(Object obj) 方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。

当一个对象调用该方法与另一个对象进行比较时,例如 obj1.compareTo(obj2),如果该方法返回 0,则表明这两个对象相等;如果该方法返回一个正整数,则表明 obj1 大于 obj2;如果该方法返回一个负整数,则表明 obj1 小于 obj2

Java 的一些常用类已经实现了 Comparable 接口,并提供了比较大小的标准。

  1. BigDecimalBigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
  2. Character:按字符的 UNICODE 值进行比较。
  3. Booleantrue 对应的包装类实例大于 false 对应的包装类实例。
  4. String:按字符串中字符的 UNICODE 值进行比较。
  5. DateTime:后面的时间、日期比前面的时间、日期大。

如果试图把一个对象添加到 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 就会调用该对象的 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 中添加的对象是程序员自定义类的对象,则可以向 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 对象。

img

从图中可以看到 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 R1 implements Comparable {
    int count;

    public R1(int count) {
        this.count = count;
    }

    public String toString() {
        return "R1[count:" + count + "]";
    }

    // 重写equals()方法,根据count来判断是否相等
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Z.class) {
            R1 r = (R1)obj;
            if (r.count == this.count) {
                return true;
            }
        }
        return false;
    }

    // 重写compareTo()方法,根据count来比较大小
    public int compareTo(Object obj) {
        R1 r = (R1)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 R1(5));
        ts.add(new R1(-3));
        ts.add(new R1(9));
        ts.add(new R1(-2));
        // ①打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);
        // 取出第一个元素
        R1 first = (R1)ts.first();
        // 对第一个元素的count赋值
        first.count = 20;
        // 取出最后一个元素
        R1 last = (R1)ts.last();
        // 对最后一个元素的count赋值,与第二个元素的count相同
        last.count = -2;
        // ②再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts);
        // ③删除Field被改变的元素,删除失败
        System.out.println(ts.remove(new R1(-2)));
        System.out.println(ts);
        // ④删除Field没有改变的元素,删除成功
        System.out.println(ts.remove(new R1(5)));
        System.out.println(ts);
    }
}
/*
    [R1[count:-3], R1[count:-2], R1[count:5], R1[count:9]]
    [R1[count:20], R1[count:-2], R1[count:5], R1[count:-2]]
    false
    [R1[count:20], R1[count:-2], R1[count:5], R1[count:-2]]
    true
    [R1[count:20], R1[count:-2], R1[count:-2]]
*/

上面程序中的 R 对象对应的类正常重写了 equals() 方法和 compareTo() 方法,这两个方法都以 R 对象的 count 实例变量作为判断的依据。

当程序执行 ① 行代码时,看到程序输出的 Set 集合元素处于有序状态;因为 R1 类是一个可变类,因此可以改变 R1 对象的 count 实例变量的值,程序改变了该集合里第一个元素和最后一个元素的 count 实例变量的值。

当程序执行 ② 行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。

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

提示

当执行了 ④ 代码后,TreeSet 会对集合中的元素重新索引(不是重新排序),接下来就可以删除 TreeSet 中的所有元素了,包括那些被修改过 Field 的元素。与 HashSet 类似的是,如果 TreeSet 中包含了可变对象,当可变对象的 Field 被修改时,TreeSet 在处理这些对象时将非常复杂,而且容易出错。

为了让程序更加健壮,推荐 HashSetTreeSet 集合中只放入不可变对象。

定制排序

TreeSet 的自然排序是根据集合元素的大小,TreeSet 将它们以升序排列。如果需要实现定制排序,例如以降序排列,则可以通过 Comparator 接口的帮助。

该接口里包含一个 int compare(T o1, T o2) 方法,该方法用于比较 o1o2 的大小:如果该方法返回正整数,则表明 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);
    }
}
/*
 [M[age:9], M[age:5], M[age:-3]]
*/

提示

当通过 Comparator 对象来实现 TreeSet 的定制排序时,依然不可以向 TreeSet 中添加类型不同的对象,否则会引发 ClassCastException 异常。

使用定制排序时,TreeSet 对集合元素排序不管集合元素本身的大小,而是由 Comparator 对象负责集合元素的排序规则。

TreeSet 判断两个集合元素相等的标准是:通过 Comparator 比较两个元素返回了 0,这样 TreeSet 不会把第二个元素添加到集合中。

EnumSet

EnumSet 是一个专为枚举类设计的集合类,EnumSet 中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建 EnumSet 时显式或隐式地指定。EnumSet 的集合元素也是有序的,EnumSet 以枚举值在 Enum 类内的定义顺序来决定集合元素的顺序。

EnumSet 在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此 EnumSet 对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用 containsAllretainAll 方法)时,如果其参数也是 EnumSet 集合,则该批量操作的执行速度也非常快。

EnumSet 集合不允许加入 null 元素,如果试图插入 null 元素,EnumSet 将抛出 NullPointerException 异常。如果只是想判断 EnumSet 是否包含 null 元素或试图删除 null 元素都不会抛出异常,只是删除操作将返回 false,因为没有任何 null 元素被删除。

EnumSet 类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的 static 方法来创建 EnumSet 对象。

EnumSet 类它提供了如下常用的 static 方法来创建 EnumSet 对象。

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

public class EnumSetTest {
    public static void main(String[] args) {
        // 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
        EnumSet es1 = EnumSet.allOf(Season.class);
        // 输出[SPRING,SUMMER,FALL,WINTER]
        System.out.println(es1);
        // 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值
        EnumSet es2 = EnumSet.noneOf(Season.class);
        // 输出[]
        System.out.println(es2);
        // 手动添加两个元素
        es2.add(Season.WINTER);
        es2.add(Season.SPRING);
        // 输出[SPRING,WINTER]
        System.out.println(es2);
        // 以指定枚举值创建EnumSet集合
        EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
        // 输出[SUMMER,WINTER]
        System.out.println(es3);
        EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        // 输出[SUMMER,FALL,WINTER]
        System.out.println(es4);
        // 新创建的EnumSet集合元素和es4集合元素有相同的类型
        // es5集合元素 + es4集合元素=Season枚举类的全部枚举值
        EnumSet es5 = EnumSet.complementOf(es4);
        // 输出[SPRING]
        System.out.println(es5);
    }
}

上面程序示范了 EnumSet 集合的常规用法。除此之外,还可以复制另一个 EnumSet 集合中的所有元素来创建新的 EnumSet 集合,或者复制另一个 Collection 集合中的所有元素来创建新的 EnumSet 集合。

当复制 Collection 集合中的所有元素来创建新的 EnumSet 集合时,要求 Collection 集合中的所有元素必须是同一个枚举类的枚举值。

public class EnumSetTest2 {
    public static void main(String[] args) {
        Collection c = new HashSet();
        c.clear();
        c.add(Season.FALL);
        c.add(Season.SPRING);
        // ①复制Collection集合中的所有元素来创建EnumSet集合
        EnumSet enumSet = EnumSet.copyOf(c);
        // 输出[SPRING,FALL]
        System.out.println(enumSet);
        c.add("疯狂Java讲义");
        c.add("轻量级Java EE企业应用实战");
        // ②下面代码出现异常:因为c集合里的元素不是全部都为枚举值
        enumSet = EnumSet.copyOf(c);
    }
}

Set 性能分析

HashSetTreeSetSet 的两个典型实现,到底如何选择 HashSetTreeSet 呢?

HashSet 的性能总是比 TreeSet 好(特别是最常用的添加、查询元素等操作),因为 TreeSet 需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的 Set 时,才应该使用 TreeSet,否则都应该使用 HashSet

HashSet 还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSetHashSet 要略微慢一点,这是由维护链表所带来的额外开销造成的;不过,因为有了链表,遍历 LinkedHashSet 会更快。

EnumSet 是所有 Set 实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。

必须指出的是,Set 的三个实现类 HashSetTreeSetEnumSet 都是线程不安全的。

如果有多个线程同时访问一个 Set 集合,并且有超过一个线程修改了该 Set 集合,则必须手动保证该 Set 集合的同步性。通常可以通过 Collections 工具类的 synchronizedSortedSet 方法来“包装”该 Set 集合。

此操作最好在创建时进行,以防止对 Set 集合的意外非同步访问。

SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

List 集合

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

ListListIterator

List 作为 Collection 接口的子接口,当然可以使用 Collection 接口里的全部方法。而且由于 List 是有序集合,因此 List 集合里增加了一些根据索引来操作集合元素的方法。

  1. void add(int index, Object element):将元素 element 插入到 List 集合的 index 处。
  2. boolean addAll(int index, Collection c):将集合 c 所包含的所有元素都插入到 List 集合的 index 处。
  3. Object get(int index):返回集合 index 索引处的元素。
  4. int indexOf(Object o):返回对象 o 在 List 集合中第一次出现的位置索引。
  5. int lastIndexOf(Object o):返回对象 o 在 List 集合中最后一次出现的位置索引。
  6. Object remove(int index):删除并返回 index 索引处的元素。
  7. Object set(int index, Object element):将 index 索引处的元素替换成 element 对象,返回新元素。
  8. List subList(int fromIndex, int toIndex):返回从索引 fromIndex(包含)到索引 toIndex(不包含)处所有集合元素组成的子集合。

所有的 List 实现类都可以调用这些方法来操作集合元素。

Set 集合相比,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));
    }
}
/*
    [轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
    轻量级Java EE企业应用实战
    疯狂Ajax讲义
    疯狂Java讲义
    疯狂Android讲义
    [轻量级Java EE企业应用实战, 疯狂Ajax讲义, 疯狂Android讲义]
    1
    [轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
    [疯狂Java讲义]
*/

注意 ① 行代码处,程序试图返回新字符串对象在 List 集合中的位置,实际上 List 集合中并未包含该字符串对象。

因为 List 集合添加字符串对象时,添加的是通过 new 关键字创建的新字符串对象,① 行代码处也是通过 new 关键字创建的新字符串对象,两个字符串显然不是同一个对象,但 ListindexOf 方法依然可以返回 1。

List 判断两个对象相等的标准是什么呢?

List 判断两个对象相等只要通过 equals()方法比较返回 true 即可。

class A2 {
    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 A2());
        System.out.println(books);
        // ②删除集合中的A对象,再次删除集合中的第一个元素
        books.remove(new A2());
        System.out.println(books);
    }
}
/*
    [轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
    [疯狂Java讲义, 疯狂Android讲义]
    [疯狂Android讲义]
*/

Set 只提供了一个 iterator() 方法不同,List 还额外提供了一个 listIterator() 方法,该方法返回一个 ListIterator 对象,ListIterator 接口继承了 Iterator 接口,提供了专门操作 List 的方法。

ListIterator 接口在 Iterator 接口基础上增加了如下方法。

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

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

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());
        }
    }
}
/*
    疯狂Java讲义
    轻量级Java EE企业应用实战
    =======下面开始反向迭代=======
    -------分隔符-------
    轻量级Java EE企业应用实战
    -------分隔符-------
    疯狂Java讲义
*/

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

ArrayListVector

ArrayListVector 作为 List 类的两个典型实现,完全支持前面介绍的 List 接口的全部功能。

ArrayListVector 类都是基于数组实现的 List 类,所以 ArrayListVector 类封装了一个动态的、允许再分配的 Object[] 数组。

ArrayListVector 对象使用 initialCapacity 参数来设置该数组的长度,当向 ArrayListVector 中添加元素超出了该数组的长度时,它们的 initialCapacity 会自动增加。

对于通常的编程场景,程序员无须关心 ArrayList 或 Vector 的 initialCapacity。但如果向 ArrayListVector 集合中添加大量元素时,可使用 ensureCapacity(int minCapacity) 方法一次性地增加 initialCapacity。这可以减少重分配的次数,从而提高性能。

如果开始就知道 ArrayListVector 集合需要保存多少个元素,则可以在创建它们时就指定 initialCapacity 大小。如果创建空的 ArrayListVector 集合时不指定 initialCapacity 参数,则 Object[] 数组的长度默认为 10。

除此之外,ArrayListVector 还提供了如下两个方法来重新分配 Object[] 数组。

  1. void ensureCapacity(int minCapacity):将 ArrayListVector 集合的 Object[] 数组长度增加 minCapacity
  2. void trimToSize():调整 ArrayListVector 集合的 Object[] 数组长度为当前元素的个数。程序可调用该方法来减少 ArrayListVector 集合对象占用的存储空间。

ArrayListVector 在用法上几乎完全相同,但由于 Vector 是一个古老的集合(从 JDK 1.0 就有了),那时候 Java 还没有提供系统的集合框架,所以 Vector 里提供了一些方法名很长的方法,例如 addElement(Object obj),实际上这个方法与 add(Object obj) 没有任何区别。

从 JDK 1.2 以后,Java 提供了系统的集合框架,就将 Vector 改为实现 List 接口,作为 List 的实现之一,从而导致 Vector 里有一些功能重复的方法。

ArrayListVector 的显著区别是:ArrayList 是线程不安全的,当多个线程访问同一个 ArrayList 集合时,如果有超过一个线程修改了 ArrayList 集合,则程序必须手动保证该集合的同步性;但 Vector 集合则是线程安全的,无须程序保证该集合的同步性。

因为 Vector 是线程安全的,所以 Vector 的性能比 ArrayList 的性能要低。实际上,即使需要保证 List 集合线程安全,也同样不推荐使用 Vector 实现类。

Vector 还提供了一个 Stack 子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。

与 Java 中的其他集合一样,进栈出栈的都是 Object,因此从栈中取出元素后必须进行类型转换,除非你只是使用 Object 具有的操作。

所以 Stack 类里提供了如下几个方法。

  1. Object peek():返回“栈”的第一个元素,但并不将该元素“pop”出栈。
  2. Object pop():返回“栈”的第一个元素,并将该元素“pop”出栈。
  3. 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 接口,因此可以作为栈来使用。

固定长度的 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讲义");
    }
}

Queue 集合

Queue 用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。

Queue 接口中定义了如下几个方法。

  1. void add(Object e):将指定元素加入此队列的尾部。
  2. Object element():获取队列头部的元素,但是不删除该元素。
  3. boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比 add(Object e) 方法更好。
  4. Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回 null。
  5. Object poll():获取队列头部的元素,并删除该元素。如果此队列为空,则返回 null。
  6. Object remove():获取队列头部的元素,并删除该元素。

PriorityQueue 实现类

riorityQueue 是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为 PriorityQueue 保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。

因此当调用 peek() 方法或者 poll() 方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。

从这个意义上来看,PriorityQueue 已经违反了队列的最基本规则:先进先出(FIFO)。

public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue pq = new PriorityQueue();
        // 下面代码依次向pq中加入四个元素
        pq.offer(6);
        pq.offer(-3);
        pq.offer(9);
        pq.offer(0);
        // 输出pq队列,并不是按元素的加入顺序排列
        // 而是按元素的大小顺序排列,输出[-3, 0, 9, 6]
        System.out.println(pq);
        // 访问队列的第一个元素,其实就是队列中最小的元素:-3
        System.out.println(pq.poll());
    }
}

运行上面程序直接输出 PriorityQueue 集合时,可能看到该队列里的元素并没有很好地按大小进行排序,但这只是受到 PriorityQueuetoString() 方法的返回值的影响。

实际上,程序多次调用 PriorityQueue 集合对象的 poll() 方法,即可看到元素按从小到大的顺序“移出队列”。

PriorityQueue 不允许插入 null 元素,它还需要对队列元素进行排序,PriorityQueue 的元素有两种排序方式。

  1. 自然排序:采用自然顺序的 PriorityQueue 集合中的元素必须实现了 Comparable 接口,而且应该是同一个类的多个实例,否则可能导致 ClassCastException 异常。
  2. 定制排序:创建 PriorityQueue 队列时,传入一个 Comparator 对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现 Comparable 接口。

PriorityQueue 队列对元素的要求与 TreeSet 对元素的要求基本一致。

DequeArrayDeque

Deque 接口是 Queue 接口的子接口,它代表一个双端队列,Deque 接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

  1. void addFirst(Object e):将指定元素插入该双端队列的开头。
  2. void addLast(Object e):将指定元素插入该双端队列的末尾。
  3. Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
  4. Object getFirst():获取但不删除双端队列的第一个元素。
  5. Object getLast():获取但不删除双端队列的最后一个元素。
  6. boolean offerFirst(Object e):将指定元素插入该双端队列的开头。
  7. boolean offerLast(Object e):将指定元素插入该双端队列的末尾。
  8. Object peekFirst():获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回 null。
  9. Object peekLast():获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回 null。
  10. Object pollFirst():获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回 null。
  11. Object pollLast():获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回 null。
  12. Object pop()(栈方法):pop 出该双端队列所表示的栈的栈顶元素。相当于 removeFirst()
  13. void push(Object e)(栈方法):将一个元素 push 进该双端队列所表示的栈的栈顶。相当于 addFirst(e)
  14. Object removeFirst():获取并删除该双端队列的第一个元素。
  15. Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素 o。
  16. removeLast():获取并删除该双端队列的最后一个元素。
  17. removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素 o。

Deque 不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了 pop(出栈)、push(入栈)两个方法。

Deque 的方法与 Queue 的方法对照表。

img

Deque 的方法与 Stack 的方法对照表。

img

Deque 接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建 Deque 时同样可指定一个 numElements 参数,该参数用于指定 Object[] 数组的长度;如果不指定 numElements 参数,Deque 底层数组的长度为 16。

提示

ArrayListArrayDeque 两个集合类的实现机制基本相似,它们的底层都采用一个动态的、可重分配的 Object[] 数组来存储集合元素,当集合元素超出了该数组的容量时,系统会在底层重新分配一个 Object[] 数组来存储集合元素。

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 的性能会更加出色,因此现在的程序中需要使用“栈”这种数据结构时,推荐使用 ArrayDequeLinkedList,而不是 Stack

LinkedList 实现类

LinkedList 类是 List 接口的实现类——这意味着它是一个 List 集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList 还实现了 Deque 接口,因此它可以被当成双端队列来使用,自然也可以被当成“栈”来使用了。

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);
    }
}

LinkedListArrayListArrayDeque 的实现机制完全不同,ArrayListArrayDeque 内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而 LinkedList 内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能非常出色(只需改变指针所指的地址即可)。

需要指出的是,虽然 Vector 也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能,所以各方面性能都有所下降。

提示

对于所有的内部基于数组的集合实现,例如 ArrayListArrayDeque 等,使用随机访问的性能比使用 Iterator 迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。

各种线性表的性能分析

Java 提供的 List 就是一个线性表接口,而 ArrayListLinkedList 又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。

Queue 代表了队列,Deque 代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。

初学者可以无须理会 ArrayListLinkedList 之间的性能差异,只需要知道 LinkedList 集合不仅提供了 List 的功能,还提供了双端队列、栈的功能就行。

但在一些性能非常敏感的地方,可能需要慎重选择哪个 List 实现,下表列出了数组、ArrayList/ArrayDequeVectorLinkedList 的性能差异。

img

因为数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好。

所有的内部以数组作为底层实现的集合在随机访问时性能较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有很好的性能;进行迭代操作时,以链表作为底层实现的集合比以数组作为底层实现的集合性能好。

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));
    }
}

上面程序中的 currentTimeMillis 用以获取系统当前时间——这个时间以 long 整数的形式返回,通常取得这种时间用于统计程序中一段代码的执行时间。

多次运行上面程序会发现,迭代 ArrayList 集合的时间略大于迭代 LinkedList 集合的时间。

因此,关于使用 List 集合有如下建议。

  1. 如果需要遍历 List 集合元素,对于 ArrayListVector 集合,应该使用随机访问方法 get 来遍历集合元素,这样性能更好;对于 LinkedList 集合,则应该采用迭代器 Iterator 来遍历集合元素。
  2. 如果需要经常执行插入、删除操作来改变 List 集合的大小,则应该使用 LinkedList 集合,而不是 ArrayList。使用 ArrayListVector 集合需要经常重新分配内部数组的大小,其时间开销常常是使用 LinkedList 的时间开销的几十倍,效果很差。
  3. 如果有多个线程需要同时访问 List 集合中的元素,开发者可考虑使用 Collections 将集合包装成线程安全的集合。

Map

Map 用于保存具有映射关系的数据,因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 valuekeyvalue 都可以是任何引用类型的数据。Mapkey 不允许重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较总是返回 false

keyvalue 之间存在单向一对一关系,即通过指定的 key,总能找到唯一的、确定的 value。从 Map 中取出数据时,只要给出指定的 key,就可以取出对应的 value

如果把 Map 的两组值拆开来看,Map 里的数据有如图所示的结构。

img

如果把 Map 里的所有 key 放在一起来看,它们就组成了一个 Set 集合(所有的 key 没有顺序,keykey 之间不能重复),实际上 Map 确实包含了一个 keySet() 方法,用于返回 Map 里所有 key 组成的 Set 集合。

不仅如此,Mapkey 集和 Set 集合里元素的存储形式也很像,Map 子类和 Set 子类在名字上也惊人地相似,比如 Set 接口下有 HashSetLinkedHashSetSortedSet(接口)、TreeSetEnumSet 等子接口和实现类,而 Map 接口下则有 HashMapLinkedHashMapSortedMap(接口)、TreeMapEnumMap 等子接口和实现类。

正如它们的名字所暗示的,Map 的这些实现类和子接口中 key 集的存储形式和对应 Set 集合中元素的存储形式完全相同。

相关信息

SetMap 之间的关系非常密切。

虽然 Map 中放的元素是 key-value 对,Set 集合中放的元素是单个对象,但如果我们把 key-value 对中的 value 当成 key 的附庸:key 在哪里,value 就跟在哪里。这样就可以像对待 Set 一样来对待 Map 了。

事实上,Map 提供了一个 Entry 内部类来封装 key-value 对,而计算 Entry 存储时则只考虑 Entry 封装的 key

从 Java 源码来看, Java 是先实现了 Map,然后通过包装一个所有 value 都为 nullMap 就实现了 Set 集合。

如果把 Map 里的所有 value 放在一起来看,它们又非常类似于一个 List

元素与元素之间可以重复,每个元素可以根据索引来查找,只是 Map 中的索引不再使用整数值,而是以另一个对象作为索引。

如果需要从 List 集合中取出元素,则需要提供该元素的数字索引;如果需要从 Map 中取出元素,则需要提供该元素的 key 索引。

因此,Map 有时也被称为字典,或关联数组。

Map 接口中定义了如下常用的方法。

  1. void clear():删除该 Map 对象中的所有 key-value 对。
  2. boolean containsKey(Object key):查询 Map 中是否包含指定的 key,如果包含则返回 true
  3. boolean containsValue(Object value):查询 Map 中是否包含一个或多个 value,如果包含则返回 true
  4. Set entrySet():返回 Map 中包含的 key-value 对所组成的 Set 集合,每个集合元素都是 Map.EntryEntryMap 的内部类)对象。
  5. Object get(Object key):返回指定 key 所对应的 value;如果此 Map 中不包含该 key,则返回 null
  6. boolean isEmpty():查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true
  7. Set keySet():返回该 Map 中所有 key 组成的 Set 集合。
  8. Object put(Object key, Object value):添加一个 key-value 对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。
  9. void putAll(Map m):将指定 Map 中的 key-value 对复制到本 Map 中。
  10. Object remove(Object key):删除指定 key 所对应的 key-value 对,返回被删除 key 所关联的 value,如果该 key 不存在,则返回 null
  11. int size():返回该 Map 里的 key-value 对的个数。
  12. Collection values():返回该 Map 里所有 value 组成的 Collection

Map 接口提供了大量的实现类,典型实现如 HashMapHashtable 等、HashMap 的子类 LinkedHashMap,还有 SortedMap 子接口及该接口的实现类 TreeMap,以及 WeakHashMapIdentityHashMap 等。

下面将详细介绍 Map 接口实现类。

Map 中包括一个内部类 Entry,该类封装了一个 key-value 对。

Entry 包含如下三个方法。

  1. Object getKey():返回该 Entry 里包含的 key 值。
  2. Object getValue():返回该 Entry 里包含的 value 值。
  3. Object setValue(V value):设置该 Entry 里包含的 value 值,并返回新设置的 value 值。

HashMapHashtable

HashMapHashtable 都是 Map 接口的典型实现类,它们之间的关系完全类似于 ArrayListVector 的关系:Hashtable 是一个古老的 Map 实现类,它从 JDK 1.0 起就已经出现了,当它出现时,Java 还没有提供 Map 接口,所以它包含了两个烦琐的方法,即 elements()(类似于 Map 接口定义的 values() 方法)和 keys()(类似于 Map 接口定义的 keySet() 方法),现在很少使用这两个方法。

除此之外,HashtableHashMap 存在两点典型区别。

  1. Hashtable 是一个线程安全的 Map 实现,但 HashMap 是线程不安全的实现,所以 HashMapHashtable 的性能高一点;但如果有多个线程访问同一个 Map 对象时,使用 Hashtable 实现类会更好。
  2. Hashtable 不允许使用 null 作为 keyvalue,如果试图把 null 值放进 Hashtable 中,将会引发 NullPointerException 异常;但 HashMap 可以使用 null 作为 keyvalue

由于 HashMap 里的 key 不能重复,所以 HashMap 里最多只有一个 key-value 对的 keynull,但可以有无数多个 key-value 对的 valuenull

public class NullInHashMap {
    public static void main(String[] args) {
        HashMap hm = new HashMap();
        // ①试图将两个key为null值的key-value对放入HashMap中
        hm.put(null, null);
        hm.put(null, null);
        // ②将一个value为null值的key-value对放入HashMap中
        hm.put("a", null);
        //输出Map对象
        System.out.println(hm);
    }
}
/*
 {null=null, a=null}
*/

所有的 Map 实现类都重写了 toString() 方法,调用 Map 对象的 toString() 方法总是返回如下格式的字符串:{key1=value1,key2=value2...}

相关信息

Hashtable 的类名上就可以看出它是一个古老的类,它的命名甚至没有遵守 Java 的命名规范:每个单词的首字母都应该大写。

也许当初开发 Hashtable 的工程师也没有注意到这一点,后来大量 Java 程序中使用了 Hashtable 类,所以这个类名也就不能改为 HashTable 了,否则将导致大量程序需要改写。

Vector 类似的是,尽量少用 Hashtable 实现类,即使需要创建线程安全的 Map 实现类,也无须使用 Hashtable 实现类

为了成功地在 HashMapHashtable 中存储、获取对象,用作 key 的对象必须实现 hashCode() 方法和 equals() 方法。

HashSet 集合不能保证元素的顺序一样,HashMapHashtable 也不能保证其中 key-value 对的顺序。类似于 HashSetHashMapHashtable 判断两个 key 相等的标准也是:两个 key 通过 equals() 方法比较返回 true,两个 keyhashCode 值也相等。

除此之外,HashMapHashtable 中还包含一个 containsValue() 方法,用于判断是否包含指定的 value

那么 HashMapHashtable 如何判断两个 value 相等呢?

HashMapHashtable 判断两个 value 相等的标准更简单:只要两个对象通过 equals() 方法比较返回 true 即可。

class A3 {
    int count;

    public A3(int count) {
        this.count = count;
    }

    // 根据count的值来判断两个对象是否相等
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (obj != null && obj.getClass() == A3.class) {
            A3 a3 = (A3)obj;
            return this.count == a3.count;
        }
        return false;
    }

    // 根据count来计算hashCode值
    public int hashCode() {
        return this.count;
    }
}

class B3 {
    // 重写equals()方法,B对象与任何对象通过equals()方法比较都相等
    public boolean equals(Object obj) {
        return true;
    }
}

public class HashtableTest {
    public static void main(String[] args) {
        Hashtable ht = new Hashtable();
        ht.put(new A3(60000), "疯狂Java讲义");
        ht.put(new A3(87563), "轻量级Java EE企业应用实战");
        ht.put(new A3(1232), new B3());
        System.out.println(ht);
        // 只要两个对象通过equals()方法比较返回true
        // Hashtable就认为它们是相等的value
        // 由于Hashtable中有一个B对象
        // ①它与任何对象通过equals()方法比较都相等,所以下面输出true
        System.out.println(ht.containsValue("测试字符串"));
        // 只要两个A对象的count相等,它们通过equals()方法比较返回true,且hashCode值相等
        // ②Hashtable即认为它们是相同的key,所以下面输出true
        System.out.println(ht.containsKey(new A3(87563)));
        // ③下面语句可以删除最后一个key-value对
        ht.remove(new A3(1232));
        // 通过返回Hashtable的所有key组成的Set集合
        // 从而遍历Hashtable的每个key-value对
        for (Object key : ht.keySet()) {
            System.out.print(key + "---->");
            System.out.print(ht.get(key) + "\n");
        }
    }
}

提示

当使用自定义类作为 HashMapHashtablekey 时,如果重写该类的 equals(Object obj)hashCode() 方法,则应该保证两个方法的判断标准一致——当两个 key 通过 equals() 方法比较返回 true 时,两个 keyhashCode() 返回值也应该相同。

因为 HashMapHashtable 保存 key 的方式与 HashSet 保存集合元素的方式完全相同,所以 HashMapHashtablekey 的要求与 HashSet 对集合元素的要求完全相同。

HashSet 类似的是,如果使用可变对象作为 HashMapHashtablekey,并且程序修改了作为 key 的可变对象,则也可能出现与 HashSet 类似的情形:程序再也无法准确访问到 Map 中被修改过的 key

public class HashtableErrorTest {
    public static void main(String[] args) {
        Hashtable ht = new Hashtable();
        // 此处的A类与前一个程序的A类是同一个类
        ht.put(new A3(60000), "疯狂Java讲义");
        ht.put(new A3(87563), "轻量级Java EE企业应用实战");
        // 获得Hashtable的key Set集合对应的Iterator迭代器
        Iterator it = ht.keySet().iterator();
        // ①取出Map中第一个key
        A3 first = (A3)it.next();
        first.count = 87563;
        // 输出{A@1560b=疯狂Java讲义, A@1560b=轻量级Java EE企业应用实战}
        System.out.println(ht);
        // 只能删除没有被修改过的key所对应的key-value对
        ht.remove(new A3(87563));
        System.out.println(ht);
        // ②无法获取剩下的value,下面两行代码都将输出null
        System.out.println(ht.get(new A3(87563)));
        System.out.println(ht.get(new A3(60000)));
    }
}

提示

HashSet 类似的是,尽量不要使用可变对象作为 HashMapHashtablekey,如果确实需要使用可变对象作为 HashMapHashtablekey,则尽量不要在程序中修改作为 key 的可变对象。

LinkedHashMap 实现类

HashSet 有一个子类是 LinkedHashSetHashMap 也有一个 LinkedHashMap 子类;LinkedHashMap 也使用双向链表来维护 key-value 对的次序(其实只需要考虑 key 的次序),该链表负责维护 Map 的迭代顺序,迭代顺序与 key-value 对的插入顺序保持一致。

LinkedHashMap 可以避免对 HashMapHashtable 里的 key-value 对进行排序(只要插入 key-value 对时保持顺序即可),同时又可避免使用 TreeMap 所增加的成本。

LinkedHashMap 需要维护元素的插入顺序,因此性能略低于 HashMap 的性能;但因为它以链表来维护内部顺序,所以在迭代访问 Map 里的全部元素时将有较好的性能。

下面程序示范了 LinkedHashMap 的功能:

迭代输出 LinkedHashMap 的元素时,将会按添加 key-value 对的顺序输出。

public class LinkedHashMapTest {
    public static void main(String[] args) {
        LinkedHashMap scores = new LinkedHashMap();
        scores.put("语文", 80);
        scores.put("英文", 82);
        scores.put("数学", 76);
        // 遍历scores里的所有key-value对
        for (Object key : scores.keySet()) {
            System.out.println(key + "------>" + scores.get(key));
        }
    }
}

Properties

Properties 类是 Hashtable 类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便(Windows 操作平台上的 ini 文件就是一种属性文件)。

Properties 类可以把 Map 对象和属性文件关联起来,从而可以把 Map 对象中的 key-value 对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到 Map 对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以 Properties 里的 keyvalue 都是字符串类型。

该类提供了如下三个方法来修改 Properties 里的 keyvalue 值。

  1. String getProperty(String key):获取 Properties 中指定属性名对应的属性值,类似于 Mapget(Object key) 方法。
  2. String getProperty(String key, String defaultValue):该方法与前一个方法基本相似。该方法多一个功能,如果 Properties 中不存在指定的 key 时,则该方法指定默认值。
  3. Object setProperty(String key, String value):设置属性值,类似于 Hashtableput() 方法。

除此之外,它还提供了两个读写 Field 文件的方法。

  1. void load(InputStream inStream):从属性文件(以输入流表示)中加载 key-value 对,把加载到的 key-value 对追加到 Properties 里(PropertiesHashtable 的子类,它不保证 key-value 对之间的次序)。
  2. void store(OutputStream out, String comments):将 Properties 中的 key-value 对输出到指定的属性文件(以输出流表示)中。

提示

上面两个方法中使用了 InputStream 类和 OutputStream 类,它们是 Java IO 体系中的两个基类。

public class PropertiesTest {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        //向Properties中添加属性
        props.setProperty("username", "yeeku");
        props.setProperty("password", "123456");
        // ① 将Properties中的key-value对保存到a.ini文件中
        props.store(new FileOutputStream("a.ini"), "comment line");
        //新建一个Properties对象
        Properties props2 = new Properties();
        //向Properties中添加属性
        props2.setProperty("gender", "male");
        // ②将a.ini文件中的key-value对追加到props2中
        props2.load(new FileInputStream("a.ini"));
        System.out.println(props2);
    }
}
/*
 {password=123456, gender=male, username=yeeku}
*/

上面程序还在当前项目路径下生成了一个 a.ini 文件,该文件的内容如下:

#comment line
#Thu Aug 11 14:51:58 CST 2022
password=123456
username=yeeku

SortedMapTreeMap

正如 Set 接口派生出 SortedSet 子接口,SortedSet 接口有一个 TreeSet 实现类一样,Map 接口也派生出一个 SortedMap 子接口,SortedMap 接口也有一个 TreeMap 实现类。

TreeMap 就是一个红黑树数据结构,每个 key-value 对即作为红黑树的一个节点。TreeMap 存储 key-value 对(节点)时,需要根据 key 对节点进行排序。TreeMap 可以保证所有的 key-value 对处于有序状态。TreeMap 也有两种排序方式。

  1. 自然排序:TreeMap 的所有 key 必须实现 Comparable 接口,而且所有的 key 应该是同一个类的对象,否则将会抛出 ClassCastException 异常。
  2. 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。采用定制排序时不要求 Mapkey 实现 Comparable 接口。

类似于 TreeSet 中判断两个元素相等的标准,TreeMap 中判断两个 key 相等的标准是:两个 key 通过 compareTo() 方法返回 0,TreeMap 即认为这两个 key 是相等的。

如果使用自定义类作为 TreeMapkey,且想让 TreeMap 良好地工作,则重写该类的 equals() 方法和 compareTo() 方法时应保持一致的返回结果:两个 key 通过 equals() 方法比较返回 true 时,它们通过 compareTo() 方法比较应该返回 0。如果 equals() 方法与 compareTo() 方法的返回结果不一致, TreeMapMap 接口的规则就会冲突。

提示

SetMap 的关系十分密切,Java 源码就是先实现了 HashMapTreeMap 等集合,然后通过包装一个所有的 value 都为 nullMap 集合实现了 Set 集合类。

TreeSet 类似的是,TreeMap 中也提供了一系列根据 key 顺序访问 key-value 对的方法。

  1. Map.Entry firstEntry():返回该 Map 中最小 key 所对应的 key-value 对,如果该 Map 为空,则返回 null
  2. Object firstKey():返回该 Map 中的最小 key 值,如果该 Map 为空,则返回 null
  3. Map.Entry lastEntry():返回该 Map 中最大 key 所对应的 key-value 对,如果该 Map 为空或不存在这样的 key-value 对,则都返回 null
  4. Object lastKey():返回该 Map 中的最大 key 值,如果该 Map 为空或不存在这样的 key,则都返回 null
  5. Map.Entry higherEntry(Object key):返回该 Map 中位于 key 后一位的 key-value 对(即大于指定 key 的最小 key 所对应的 key-value 对)。如果该 Map 为空,则返回 null
  6. Object higherKey(Object key):返回该 Map 中位于 key 后一位的 key 值(即大于指定 key 的最小 key 值)。如果该 Map 为空或不存在这样的 key-value 对,则都返回 null
  7. Map.Entry lowerEntry(Object key):返回该 Map 中位于 key 前一位的 key-value 对(即小于指定 key 的最大 key 所对应的 key-value 对)。如果该 Map 为空或不存在这样的 key-value 对,则都返回 null
  8. Object lowerKey(Object key):返回该 Map 中位于 key 前一位的 key 值(即小于指定 key 的最大 key 值)。如果该 Map 为空或不存在这样的 key,则都返回 null
  9. NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回该 Map 的子 Map,其 key 的范围是从 fromKey(是否包括取决于第二个参数)到 toKey(是否包括取决于第四个参数)。
  10. SortedMap subMap(Object fromKey, Object toKey):返回该 Map 的子 Map,其 key 的范围是从 fromKey(包括)到 toKey(不包括)。
  11. SortedMap tailMap(Object fromKey):返回该 Map 的子 Map,其 key 的范围是大于 fromKey(包括)的所有 key
  12. NavigableMap tailMap(Object fromKey, boolean inclusive):返回该 Map 的子 Map,其 key 的范围是大于 fromKey(是否包括取决于第二个参数)的所有 key
  13. SortedMap headMap(Object toKey):返回该 Map 的子 Map,其 key 的范围是小于 toKey(不包括)的所有 key。
  14. NavigableMap headMap(Object toKey, boolean inclusive):返回该 Map 的子 Map,其 key 的范围是小于 toKey(是否包括取决于第二个参数)的所有 key

以自然排序为例,介绍 TreeMap 的基本用法。

class R3 implements Comparable {
    int count;

    public R3(int count) {
        this.count = count;
    }

    public String toString() {
        return "R[count:" + count + "]";
    }

    // 根据count来判断两个对象是否相等
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == R3.class) {
            R3 r3 = (R3)obj;
            return r3.count == this.count;
        }
        return false;
    }

    // 根据count属性值来判断两个对象的大小
    public int compareTo(Object obj) {
        R3 r3 = (R3)obj;
        return count > r3.count ? 1 : count < r3.count - 1 ? -1 : 0;
    }
}

public class TreeMapTest {
    public static void main(String[] args) {
        TreeMap tm = new TreeMap();
        tm.put(new R3(3), "轻量级Java EE企业应用实战");
        tm.put(new R3(-5), "疯狂Java讲义");
        tm.put(new R3(9), "疯狂Android讲义");
        System.out.println(tm);
        // 返回该TreeMap的第一个Entry对象
        System.out.println(tm.firstEntry());
        // 返回该TreeMap的最后一个key值
        System.out.println(tm.lastKey());
        // 返回该TreeMap的比new R(2)大的最小key值
        System.out.println(tm.higherKey(new R3(2)));
        // 返回该TreeMap的比new R(2)小的最大的key-value对
        System.out.println(tm.lowerEntry(new R3(2)));
        // 返回该TreeMap的子TreeMap
        System.out.println(tm.subMap(new R3(-1), new R3(4)));
    }
}
/*
    {R[count:-5]=疯狂Java讲义, R[count:3]=轻量级Java EE企业应用实战, R[count:9]=疯狂Android讲义}
    R[count:-5]=疯狂Java讲义
    R[count:9]
    R[count:9]
    R[count:-5]=疯狂Java讲义
    {}
*/

WeakHashMap 实现类

WeakHashMapHashMap 的用法基本相似。与 HashMap 的区别在于,HashMapkey 保留了对实际对象的强引用,这意味着只要该 HashMap 对象不被销毁,该 HashMap 的所有 key 所引用的对象就不会被垃圾回收,HashMap 也不会自动删除这些 key 所对应的 key-value 对;但 WeakHashMapkey 只保留了对实际对象的弱引用,这意味着如果 WeakHashMap 对象的 key 所引用的对象没有被其他强引用变量所引用,则这些 key 所引用的对象可能被垃圾回收,WeakHashMap 也可能自动删除这些 key 所对应的 key-value 对。

WeakHashMap 中的每个 key 对象只持有对实际对象的弱引用,因此,当垃圾回收了该 key 所对应的实际对象之后,WeakHashMap 会自动删除该 key 对应的 key-value 对。

public class WeakHashMapTest {
    public static void main(String[] args) {
        WeakHashMap whm = new WeakHashMap();
        // 向WeakHashMap中添加三个key-value对
        // 三个key都是匿名字符串对象(没有其他引用)
        whm.put(new String("语文"), new String("良好"));
        whm.put(new String("数学"), new String("及格"));
        whm.put(new String("英文"), new String("中等"));
        // 向WeakHashMap中添加一个key-value对
        // ①该key是一个系统缓存的字符串对象
        whm.put("java", new String("中等"));
        // 输出whm对象,将看到4个key-value对
        System.out.println(whm);
        // 通知系统立即进行垃圾回收
        System.gc();
        System.runFinalization();
        // 在通常情况下,将只看到一个key-value对
        System.out.println(whm);
    }
}
/*
    {英文=中等, java=中等, 数学=及格, 语文=良好}
    {java=中等}
*/

当系统进行垃圾回收时,删除了 WeakHashMap 对象的前三个 key-value 对。这是因为添加前三个 key-value 对时,这三个 key 都是匿名的字符串对象, WeakHashMap 只保留了对它们的弱引用,这样垃圾回收时会自动删除这三个 key-value 对。

WeakHashMap 对象中第 4 个组 key-value 对的 key 是一个字符串直接量,(系统会自动保留对该字符串对象的强引用),所以垃圾回收时不会回收它。

提示

如果需要使用 WeakHashMapkey 来保留对象的弱引用,则不要让该 key 所引用的对象具有任何强引用,否则将失去使用 WeakHashMap 的意义。

IdentityHashMap 实现类

这个 Map 实现类的实现机制与 HashMap 基本相似,但它在处理两个 key 相等时比较独特:在 IdentityHashMap 中,当且仅当两个 key 严格相等(key1==key2)时,IdentityHashMap 才认为两个 key 相等;对于普通的 HashMap 而言,只要 key1key2 通过 equals() 方法比较返回 true,且它们的 hashCode 值相等即可。

提示

IdentityHashMap 是一个特殊的 Map 实现!

此类实现 Map 接口时,它有意违反 Map 的通常规范:IdentityHashMap 要求两个 key 严格相等时才认为两个 key 相等。

IdentityHashMap 提供了与 HashMap 基本相似的方法,也允许使用 null 作为 keyvalue。与 HashMap 相似:IdentityHashMap 也不保证 key-value 对之间的顺序,更不能保证它们的顺序随时间的推移保持不变。

public class IdentityHashMapTest {
    public static void main(String[] args) {
        IdentityHashMap ihm = new IdentityHashMap();
        // 下面两行代码将会向IdentityHashMap对象中添加两个key-value对
        ihm.put(new String("语文"), 89);
        ihm.put(new String("语文"), 78);
        // 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
        ihm.put("java", 93);
        ihm.put("java", 98);
        System.out.println(ihm);
    }
}
/*
    {java=98, 语文=89, 语文=78}
*/

EnumMap 实现类

EnumMap 是一个与枚举类一起使用的 Map 实现,EnumMap 中的所有 key 都必须是单个枚举类的枚举值。创建 EnumMap 时必须显式或隐式指定它对应的枚举类。

EnumMap 在内部以数组形式保存,所以这种实现形式非常紧凑、高效。

EnumMap 根据 key 的自然顺序(即枚举值在枚举类中的定义顺序)来维护 key-value 对的顺序。当程序通过 keySet()entrySet()values() 等方法遍历 EnumMap 时可以看到这种顺序。

EnumMap 不允许使用 null 作为 key,但允许使用 null 作为 value。如果试图使用 null 作为 key 时将抛出 NullPointerException 异常。如果只是查询是否包含值为 nullkey,或只是删除值为 nullkey,都不会抛出异常。

与创建普通的 Map 有所区别的是,创建 EnumMap 时必须指定一个枚举类,从而将该 EnumMap 和指定枚举类关联起来。

enum Season1 {
    SPRING, SUMMER, FALL, WINTER
}

public class EnumMapTest {
    public static void main(String[] args) {
        // 创建一个EnumMap对象,该EnumMap的所有key
        // 必须是Season枚举类的枚举值
        EnumMap enumMap = new EnumMap(Season1.class);
        enumMap.put(Season1.SUMMER, "夏日炎炎");
        enumMap.put(Season1.SPRING, "春暖花开");
        System.out.println(enumMap);
    }
}
/*
 {SPRING=春暖花开, SUMMER=夏日炎炎}
*/

Map 性能分析

对于 Map 的常用实现类而言,HashMapHashtable 的效率大致相同,因为它们的实现机制几乎完全一样;但 HashMap 通常比 Hashtable 要快一点,因为 Hashtable 需要额外的线程同步控制。

TreeMap 通常比 HashMapHashtable 要慢(尤其在插入、删除 key-value 对时更慢),因为 TreeMap 底层采用红黑树来管理 key-value 对(红黑树的每个节点就是一个 key-value 对)。

使用 TreeMap 有一个好处:TreeMap 中的 key-value 对总是处于有序状态,无须专门进行排序操作。当 TreeMap 被填充之后,就可以调用 keySet(),取得由 key 组成的 Set,然后使用 toArray() 方法生成 key 的数组,接下来使用 ArraysbinarySearch() 方法在已排序的数组中快速地查询对象。

对于一般的应用场景,程序应该多考虑使用 HashMap,因为 HashMap 正是为快速查询设计的(HashMap 底层其实也是采用数组来存储 key-value 对)。但如果程序需要一个总是排好序的 Map 时,则可以考虑使用 TreeMap

LinkedHashMapHashMap 慢一点,因为它需要维护链表来保持 Mapkey-value 时的添加顺序。IdentityHashMap 性能没有特别出色之处,因为它采用与 HashMap 基本相似的实现,只是它使用 == 而不是 equals() 方法来判断元素相等。EnumMap 的性能最好,但它只能使用同一个枚举类的枚举值作为 key

HashSetHashMap

对于 HashSet 及其子类而言,它们采用 hash 算法来决定集合中元素的存储位置,并通过 hash 算法来控制集合的大小;对于 HashMapHashtable 及其子类而言,它们采用 hash 算法来决定 Mapkey 的存储,并通过 hash 算法来增加 key 集合的大小。

hash 表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能:hash 算法可以根据 hashCode 值计算出“桶”的存储位置,接着从“桶”中取出元素。

hash 表的状态为 open:在发生“hash 冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。

如图所示是 hash 表保存各元素,且发生“hash 冲突”的示意图。

img

因为 HashSetHashMapHashtable 都使用 hash 算法来决定其元素(HashMap 则只考虑 key)的存储,因此 HashSetHashMaphash 表包含如下属性。

  1. 容量(capacity):hash 表中桶的数量。
  2. 初始化容量(initial capacity):创建 hash 表时桶的数量。HashMapHashSet 都允许在构造器中指定初始化容量。
  3. 尺寸(size):当前 hash 表中记录的数量。
  4. 负载因子(load factor):负载因子等于“size/capacity”。负载因子为 0,表示空的 hash 表,0.5 表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用 Iterator 迭代元素时比较慢)。

除此之外,hash 表里还有一个“负载极限”,“负载极限”是一个 0~1 的数值,“负载极限”决定了 hash 表的最大填满程度。当 hash 表中的负载因子达到指定的“负载极限”时,hash 表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为 rehashing

HashSetHashMapHashtable 的构造器允许指定一个负载极限,HashSetHashMapHashtable 默认的“负载极限”为 0.75,这表明当该 hash 表的 3/4 已经被填满时,hash 表会发生 rehashing

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低 hash 表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMapget()put() 方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加 hash 表所占用的内存开销。

程序员可以根据实际情况来调整 HashSetHashMap 的“负载极限”值。

如果开始就知道 HashSetHashMapHashtable 会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于 HashSetHashMapHashtable 所包含的最大记录数除以负载极限,就不会发生 rehashing

使用足够大的初始化容量创建 HashSetHashMapHashtable 时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得太高。

工具类:Collections

Java 提供了一个操作 SetListMap 等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。

排序操作

Collections 提供了如下几个方法用于对 List 集合元素进行排序。

  1. static void reverse(List list):反转指定 List 集合中元素的顺序。
  2. static void shuffle(List list):对 List 集合元素进行随机排序(shuffle 方法模拟了“洗牌”动作)。
  3. static void sort(List list):根据元素的自然顺序对指定 List 集合的元素按升序进行排序。
  4. static void sort(List list, Comparator c):根据指定 Comparator 产生的顺序对 List 集合元素进行排序。
  5. static void swap(List list, int i, int j):将指定 List 集合中的 i 处元素和 j 处元素进行交换。
  6. static void rotate(List list , int distance):当 distance 为正数时,将 list 集合的后 distance 个元素“整体”移到前面;当 distance 为负数时,将 list 集合的前 distance 个元素“整体”移到后面。该方法不会改变集合的长度。
public class SortTest {
    public static void main(String[] args) {
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(-5);
        nums.add(3);
        nums.add(0);
        // 输出:[2, -5, 3, 0]
        System.out.println(nums);
        // 将List集合元素的次序反转
        Collections.reverse(nums);
        // 输出:[0, 3, -5, 2]
        System.out.println(nums);
        // 将List集合元素按自然顺序排序
        Collections.sort(nums);
        // 输出:[-5, 0, 2, 3]
        System.out.println(nums);
        // 将List集合元素按随机顺序排序
        Collections.shuffle(nums);
        // 每次输出的次序不固定
        System.out.println(nums);
    }
}

查找、替换操作

Collections 还提供了如下用于查找、替换集合元素的常用方法。

  1. static int binarySearch(List list, Object key):使用二分搜索法搜索指定的 List 集合,以获得指定对象在 List 集合中的索引。如果要使该方法可以正常工作,则必须保证 List 中的元素已经处于有序状态。
  2. static Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
  3. static Object max(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最大元素。
  4. static Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
  5. static Object min(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最小元素。
  6. static Object min(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最小元素。
  7. static void fill(List list, Object obj):使用指定元素 obj 替换指定 List 集合中的所有元素。
  8. static int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
  9. static int indexOfSubList(List source, List target):返回子 List 对象在父 List 对象中第一次出现的位置索引;如果父 List 中没有出现这样的子 List,则返回-1。
  10. static int lastIndexOfSubList(List source, List target):返回子 List 对象在父 List 对象中最后一次出现的位置索引;如果父 List 中没有出现这样的子 List,则返回-1。
  11. static boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值 newVal 替换 List 对象的所有旧值 oldVal
public class SearchTest {
    public static void main(String[] args) {
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(-5);
        nums.add(3);
        nums.add(0);
        // 输出:[2, -5, 3, 0]
        System.out.println(nums);
        // 输出最大元素,将输出3
        System.out.println(Collections.max(nums));
        // 输出最小元素,将输出-5
        System.out.println(Collections.min(nums));
        // 将nums中的0使用1来代替
        Collections.replaceAll(nums, 0, 1);
        // 输出:[2, -5, 3, 1]
        System.out.println(nums);
        // 判断-5 在List集合中出现的次数,返回1
        System.out.println(Collections.frequency(nums, -5));
        // 对nums集合排序
        Collections.sort(nums);
        // 输出:[-5, 1, 2, 3]
        System.out.println(nums);
        // 只有排序后的List集合才可用二分法查询,输出3
        System.out.println(Collections.binarySearch(nums, 3));
    }
}

同步控制

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

Java 中常用的集合框架中的实现类 HashSetTreeSetArrayListArrayDequeLinkedListHashMapTreeMap 都是线程不安全的。

如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则可能出现错误。

Collections 提供了多个静态方法可以把它们包装成线程同步的集合。

public class SynchronizedTest {
    public static void main(String[] args) {
        // 下面程序创建了4个同步的集合对象
        Collection c = Collections.synchronizedCollection(new ArrayList());
        List list = Collections.synchronizedList(new ArrayList());
        Set s = Collections.synchronizedSet(new HashSet());
        Map m = Collections.synchronizedMap(new HashMap());
    }
}

设置不可变集合

Collections 提供了如下三类方法来返回一个不可变的集合。

  1. emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是 List,也可以是 Set,还可以是 Map
  2. singletonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合既可以是 List,也可以是 Set,还可以是 Map
  3. unmodifiableXxx:返回指定集合对象的不可变视图,此处的集合既可以是 List,也可以是 Set,还可以是 Map

三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。

通过 Collections 提供的三类方法,可以生成“只读”的 CollectionMap

public class UnmodifiableTest {
    public static void main(String[] args) {
        //创建一个空的、不可改变的List对象
        List unmodifiableList = Collections.emptyList();
        //创建一个只有一个元素,且不可改变的Set对象
        Set unmodifiableSet = Collections.singleton("疯狂Java讲义");
        //创建一个普通的Map对象
        Map scores = new HashMap();
        scores.put("语文", 80);
        scores.put("Java", 82);
        //返回普通的Map对象对应的不可变版本
        Map unmodifiableMap = Collections.unmodifiableMap(scores);
        //下面任意一行代码都将引发UnsupportedOperationException异常
        unmodifiableList.add("测试元素");
        unmodifiableSet.add("测试元素");
        unmodifiableMap.put("语文", 90);
    }
}

Enumeration

Enumeration 接口是 Iterator 迭代器的“古老版本”,从 JDK 1.0 开始,Enumeration 接口就已经存在了(Iterator 从 JDK 1.2 才出现)。Enumeration 接口只有两个名字很长的方法。

  1. boolean hasMoreElements():如果此迭代器还有剩下的元素,则返回 true
  2. Object nextElement():返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。

Enumeration 接口中的方法名称冗长,难以记忆,而且没有提供 Iteratorremove() 方法。如果现在编写 Java 程序,应该尽量采用 Iterator 迭代器,而不是用 Enumeration 迭代器。

相关信息

Java 之所以保留 Enumeration 接口,主要是为了照顾以前那些“古老”的程序,那些程序里大量使用了 Enumeration 接口,如果新版本的 Java 里直接删除 Enumeration 接口,将会导致那些程序全部出错。

在计算机行业有一条规则:加入任何规则都必须慎之又慎,因为以后无法删除规则。

实际上,前面介绍的 Vector(包括其子类 Stack)、Hashtable 两个集合类,以及另一个极少使用的 BitSet,都是从 JDK 1.0 遗留下来的集合类,而 Enumeration 接口可用于遍历这些“古老”的集合类。

对于 ArrayListHashMap 等集合类,不再支持使用 Enumeration 迭代器。

public class EnumerationTest {
    public static void main(String[] args) {
        Vector v = new Vector();
        v.add("疯狂Java讲义");
        v.add("轻量级Java EE企业应用实战");
        Hashtable scores = new Hashtable();
        scores.put("语文", 78);
        scores.put("数学", 88);
        Enumeration em = v.elements();
        while (em.hasMoreElements()) {
            System.out.println(em.nextElement());
        }
        Enumeration keyEm = scores.keys();
        while (keyEm.hasMoreElements()) {
            Object key = keyEm.nextElement();
            System.out.println(key + "--->" + scores.get(key));
        }
    }
}