集合主题

示例数据

创建一些样本数据用于集合示例。以下数据将颜色名称与 HTML 颜色的 RGB 值相关联。请注意,每个键和值都是唯一的:

public class HTMLColors {
    public static final Object[][] ARRAY = {
        { 0xF0F8FF, "AliceBlue" },
        { 0xFAEBD7, "AntiqueWhite" },
        { 0x7FFFD4, "Aquamarine" },
        { 0xF0FFFF, "Azure" },
        { 0xF5F5DC, "Beige" },
        { 0xFFE4C4, "Bisque" },
        { 0x000000, "Black" },
        { 0xFFEBCD, "BlanchedAlmond" },
        { 0x0000FF, "Blue" },
        { 0x8A2BE2, "BlueViolet" },
        { 0xA52A2A, "Brown" },
        { 0xDEB887, "BurlyWood" },
        { 0x5F9EA0, "CadetBlue" },
        { 0x7FFF00, "Chartreuse" },
        { 0xD2691E, "Chocolate" },
        { 0xFF7F50, "Coral" },
        { 0x6495ED, "CornflowerBlue" },
        { 0xFFF8DC, "Cornsilk" },
        { 0xDC143C, "Crimson" },
        { 0x00FFFF, "Cyan" },
        { 0x00008B, "DarkBlue" },
        { 0x008B8B, "DarkCyan" },
        { 0xB8860B, "DarkGoldenRod" },
        { 0xA9A9A9, "DarkGray" },
        { 0x006400, "DarkGreen" },
        { 0xBDB76B, "DarkKhaki" },
        { 0x8B008B, "DarkMagenta" },
        { 0x556B2F, "DarkOliveGreen" },
        { 0xFF8C00, "DarkOrange" },
        { 0x9932CC, "DarkOrchid" },
        { 0x8B0000, "DarkRed" },
        { 0xE9967A, "DarkSalmon" },
        { 0x8FBC8F, "DarkSeaGreen" },
        { 0x483D8B, "DarkSlateBlue" },
        { 0x2F4F4F, "DarkSlateGray" },
        { 0x00CED1, "DarkTurquoise" },
        { 0x9400D3, "DarkViolet" },
        { 0xFF1493, "DeepPink" },
        { 0x00BFFF, "DeepSkyBlue" },
        { 0x696969, "DimGray" },
        { 0x1E90FF, "DodgerBlue" },
        { 0xB22222, "FireBrick" },
        { 0xFFFAF0, "FloralWhite" },
        { 0x228B22, "ForestGreen" },
        { 0xDCDCDC, "Gainsboro" },
        { 0xF8F8FF, "GhostWhite" },
        { 0xFFD700, "Gold" },
        { 0xDAA520, "GoldenRod" },
        { 0x808080, "Gray" },
        { 0x008000, "Green" },
        { 0xADFF2F, "GreenYellow" },
        { 0xF0FFF0, "HoneyDew" },
        { 0xFF69B4, "HotPink" },
        { 0xCD5C5C, "IndianRed" },
        { 0x4B0082, "Indigo" },
        { 0xFFFFF0, "Ivory" },
        { 0xF0E68C, "Khaki" },
        { 0xE6E6FA, "Lavender" },
        { 0xFFF0F5, "LavenderBlush" },
        { 0x7CFC00, "LawnGreen" },
        { 0xFFFACD, "LemonChiffon" },
        { 0xADD8E6, "LightBlue" },
        { 0xF08080, "LightCoral" },
        { 0xE0FFFF, "LightCyan" },
        { 0xFAFAD2, "LightGoldenRodYellow" },
        { 0xD3D3D3, "LightGray" },
        { 0x90EE90, "LightGreen" },
        { 0xFFB6C1, "LightPink" },
        { 0xFFA07A, "LightSalmon" },
        { 0x20B2AA, "LightSeaGreen" },
        { 0x87CEFA, "LightSkyBlue" },
        { 0x778899, "LightSlateGray" },
        { 0xB0C4DE, "LightSteelBlue" },
        { 0xFFFFE0, "LightYellow" },
        { 0x00FF00, "Lime" },
        { 0x32CD32, "LimeGreen" },
        { 0xFAF0E6, "Linen" },
        { 0xFF00FF, "Magenta" },
        { 0x800000, "Maroon" },
        { 0x66CDAA, "MediumAquaMarine" },
        { 0x0000CD, "MediumBlue" },
        { 0xBA55D3, "MediumOrchid" },
        { 0x9370DB, "MediumPurple" },
        { 0x3CB371, "MediumSeaGreen" },
        { 0x7B68EE, "MediumSlateBlue" },
        { 0x00FA9A, "MediumSpringGreen" },
        { 0x48D1CC, "MediumTurquoise" },
        { 0xC71585, "MediumVioletRed" },
        { 0x191970, "MidnightBlue" },
        { 0xF5FFFA, "MintCream" },
        { 0xFFE4E1, "MistyRose" },
        { 0xFFE4B5, "Moccasin" },
        { 0xFFDEAD, "NavajoWhite" },
        { 0x000080, "Navy" },
        { 0xFDF5E6, "OldLace" },
        { 0x808000, "Olive" },
        { 0x6B8E23, "OliveDrab" },
        { 0xFFA500, "Orange" },
        { 0xFF4500, "OrangeRed" },
        { 0xDA70D6, "Orchid" },
        { 0xEEE8AA, "PaleGoldenRod" },
        { 0x98FB98, "PaleGreen" },
        { 0xAFEEEE, "PaleTurquoise" },
        { 0xDB7093, "PaleVioletRed" },
        { 0xFFEFD5, "PapayaWhip" },
        { 0xFFDAB9, "PeachPuff" },
        { 0xCD853F, "Peru" },
        { 0xFFC0CB, "Pink" },
        { 0xDDA0DD, "Plum" },
        { 0xB0E0E6, "PowderBlue" },
        { 0x800080, "Purple" },
        { 0xFF0000, "Red" },
        { 0xBC8F8F, "RosyBrown" },
        { 0x4169E1, "RoyalBlue" },
        { 0x8B4513, "SaddleBrown" },
        { 0xFA8072, "Salmon" },
        { 0xF4A460, "SandyBrown" },
        { 0x2E8B57, "SeaGreen" },
        { 0xFFF5EE, "SeaShell" },
        { 0xA0522D, "Sienna" },
        { 0xC0C0C0, "Silver" },
        { 0x87CEEB, "SkyBlue" },
        { 0x6A5ACD, "SlateBlue" },
        { 0x708090, "SlateGray" },
        { 0xFFFAFA, "Snow" },
        { 0x00FF7F, "SpringGreen" },
        { 0x4682B4, "SteelBlue" },
        { 0xD2B48C, "Tan" },
        { 0x008080, "Teal" },
        { 0xD8BFD8, "Thistle" },
        { 0xFF6347, "Tomato" },
        { 0x40E0D0, "Turquoise" },
        { 0xEE82EE, "Violet" },
        { 0xF5DEB3, "Wheat" },
        { 0xFFFFFF, "White" },
        { 0xF5F5F5, "WhiteSmoke" },
        { 0xFFFF00, "Yellow" },
        { 0x9ACD32, "YellowGreen" },
    };
    public static final Map<Integer, String> MAP =
            Arrays.stream(ARRAY).collect(Collectors.toMap(
                    element -> (Integer)element[0],
                    element -> (String)element[1],
                    (v1, v2) -> {
                        throw new IllegalStateException();
                    }, LinkedHashMap::new));
    // 只有值唯一时才能将键值反转
    public static <V, K> Map<V, K> invert(Map<K, V> map) {
        return map.entrySet().stream().collect(Collectors.toMap(
                Map.Entry::getValue,
                Map.Entry::getKey,
                (v1, v2) -> {
                    throw new IllegalStateException();
                }, LinkedHashMap::new));
    }
    public static final Map<String, Integer> INVMAP = invert(MAP);
    // 给定颜色名,查找对应 rgb 值
    public static Integer rgb(String colorName) {
        return INVMAP.get(colorName);
    }
    public static final List<String> LIST =
            Arrays.stream(ARRAY)
                    .map(item -> (String)item[1])
                    .collect(Collectors.toList());
    public static final List<Integer> RGBLIST =
            Arrays.stream(ARRAY)
                    .map(item -> (Integer)item[0])
                    .collect(Collectors.toList());
    public static void show(Map.Entry<Integer, String> e) {
        System.out.format("0x%06X: %s%n", e.getKey(), e.getValue());
    }
    public static void show(Map<Integer, String> map, int count) {
        map.entrySet().stream()
                .limit(count).forEach(v -> show(v));
    }
    public static void show(Map<Integer, String> m) {
        show(m, m.size());
    }
    public static void show(Collection<String> lst, int count) {
        lst.stream().limit(count).forEach(System.out::println);
    }
    public static void show(Collection<String> lst) {
        show(lst, lst.size());
    }
    public static void showrgb(Collection<Integer> lst, int count) {
        lst.stream().limit(count)
                .forEach(n -> System.out.format("0x%06X%n", n));
    }
    public static void showrgb(Collection<Integer> lst) {
        showrgb(lst, lst.size());
    }
    public static void showInv(Map<String, Integer> m, int count) {
        m.entrySet().stream().limit(count)
                .forEach(e -> System.out.format("%s: 0x%06X%n", e.getKey(), e.getValue()));
    }
    public static void showInv(Map<String, Integer> m) {
        showInv(m, m.size());
    }
    public static void border() {
        System.out.println("-------------------------");
    }
}

这里使用流将二维的 ARRAY 转换到 Map 中,这里没有直接使用 Collectors.toMap() 的简单版本,该版本会产生一个 HashMap,它所使用的哈希函数会打乱键的顺序。因此这里使用了它的复杂版本,将键值对直接放入一个 LinkedHashMap 中,保留顺序。它接受的两个函数分别负责从每个被流化的元素中提取键和值,就像简单的 Collectors toMap() 一样。它还需要一个合并函数(merge function),负责在有两个值关联到同一个键的情况下解决冲突,因为我们的数据经过预先检查,所以不会出现这样的情况。如果出现,就抛出异常。最后传入的函数负责生成一个指定类型的空映射,随后由流来填充。

rgb() 方法接受表示颜色名的字符串,返回对应的 RGB 值。为了实现该函数,我们需要 COLORS 的一个逆向版本,它接受一个 String 类型的键,查找 Integer 类型的 RGB 值。逆向是通过 invert() 方法实现的,如果 COLORS 中有任何值是不唯一的,它就会抛出异常。

还创建了包含所有颜色名的 LIST,以及包含所有十六进制 RGB 值的 RGBLIST。

下面是一个简单的测试:

使用 LinkedHashMap,能够保留 ARRAY 的顺序。

List 的行为

List 是除数组之外的最基本的对象存储和检索方式,基本的列表操作有:

  • add() 用于插入元素

  • get() 用于随机访问元素,注意这个操作在特定的 List 实现上成本不同

  • iterator() 用于返回该序列上的 Iterator

  • stream() 用于生成序列中元素的 Stream

List 的构造器总是保留元素插入的顺序。

下列示例中的方法分别涵盖了一组不同的活动:每个 List 都能做的事情(basicTest()), 在 Iterator 上移动(iterMotion())与用 Iterator 修改元素(iterManipulation()),查看 List 操作的效果(testVisual()),以及只能用于 LinkedList 的操作。

在使用每个方法之前,要查看 JDK 文档,充分了解其用法。

Set 的行为

Set 的意义在于测试成员身份,也可用于删除重复元素。如果不关心元素顺序或并发性, HashSet 总是最好的选择,因为它是专门为了快速查找而设计的。

对于其他 Set,元素顺序会不同:

这里需要 Suppresswarnings("unchecked"),因为不确定向 Class.forName(type).newInstance() 传递的 String 到底是什么,编译器不能确保操作能够成功。

Collections.reverse() 通过修改参数来实现逆向,而不是返回一个包含逆向排列元素的新 List。逆向版本 RLIST 可以防止我们误以为 Set 会对结果进行排序。

HashSet 的输出看上去没有明显的顺序(因为它是基于哈希函数的)。Treeset 和 ConcurrentSkipListSet 都会对其元素进行排序,而且都实现了 SortedSet 接口来表明这一点。因为这样的 Set 是有序的,所以它们还提供了更多操作。LinkedHashSet 和 CopyOnWriteArraySet 会保留元素插入的顺序,尽管没有接口表明这一点。

ConcurrentSkipListSet 和 CopyOnWriteArraySet 是线程安全的。

在 Map 上使用函数式操作

和 Collection 接口一样,Map 接口也内置了 forEach()。但是如果想要执行任何其他的基本功能操作,比如 map()flatMap()reduce()filter() 时,查看 Map 接口发现没有这些。

需要通过 entrySet() 连接到这些方法,它会生成一个由 Map.Entry 对象组成的 Set。这个 Set 又包含了 stream()parallelStream() 方法。只需要记住:这里使用的是 Map.Entry 对象。

生成 Stream 之后,所有的基本函数式方法(当然还有更多方法)就都可以使用了。

选择 Map 的部分元素

TreeMap 和 ConcurrentSkipListMap 都实现了 NavigableMap 接口。这个接口的目的是解决需要选择某个 Map 中部分元素的问题。

因为 NavigableMap 的键是有顺序的,所以它支持 firstEntry()lastEntry() 的概念。在 COLORS 上调用 headMap() ,生成的 NavigableMap 包含了从 COLORS 开头到第一个参数所指元素的所有元素,第二个参数(即boolean值)用于指示在结果中是否包含第一个参数所指的元素。在 COLORS 上调用 tailMap() 会执行类似的操作,但范围是从第一个参数所指的元素开始,直到 COLORS 的末尾。subMap() 支持生成该 Map 中间的某段。

ceilingEntry() 从指定的键值向后查找下一个键值对,而 floorEntry() 则按反方向查找。descendingMap() 会反转 NavigableMap 的顺序。

如果要解决的问题可以通过切分 Map 来简化,那么用 NavigableMap 就能解决。其他集合实现也有类似的功能,可以帮助我们解决问题。

填充集合

和 Arrays 一样,集合也有一个叫作 Collections 的伴生类,包含很多静态的工具方法,其中有一个叫作 fill()fill() 会将集合中的所有元素都替换为同一个对象引用。虽然它只能用于 List 对象,但是它生成的列表可以传递给构造器或 addAll() 方法:

这里演示了两种使用某个对象的引用来填充 Collection 的方法,第一种是使用 Collections.nCopies(),它会创建一个 List,该 List 被传递给 ArrayList 的构造器,从而填充这个 ArrayList。第二种是使用 Collections.fill()

Stringaddress 中的 toString() 方法会调用 Object.toString(),产生的信息是类名加该对象的哈希码(由 hashCode() 方法生成)的无符号十六进制表示。

因为 fill() 方法只能替换 List 中已有的元素,而不能添加新元素,所以这个方法不是很有用。

使用 Suppliers 来填充 Collection

onjava.Suppliers 类提供了用于填充 Collection 的通用解决方案,下面用 Suppliers 初始化了几类不同的 Collection:

LinkedHashSet 中的元素会按照插入顺序排列,因为它会维护一个链表来保存顺序信息。

注意,大多数情况下都可以使用 Stream 来创建和填充 Collection,这里使用流填充的部分没有要求注明想创建的元素个数,它会处理所有的 Stream 元素。

使用 Suppliers 填充 Map

要使用 Suppliers 向 Map 中填充数据,需要一个 Pair 类,因为每次调用 Supplier 的 get() 方法,都必须产生一对对象(即一个键,一个值)。

Pair 是只读的信息传输对象(Data Transfer Object)或信使(Messenger)。为了简化 Pair 对象的创建,这里还添加了 static make() 方法。

利用 Stream 生成填充的 Map:

该例子体现出一个模式,可以用它来创建一个自动创建和填充 Map 的工具:

basic() 方法可以生成一个默认的 Map,而 create() 支持指定确切的映射类型,而目会返回这个确切的类型。

使用享元自定义 Collection 和 Map

这里介绍如何创建自定义的 Collection 和 Map 实现。每个 java.util 集合都有自己的抽象类,提供了该集合的部分实现。因此,只需要实现必要的方法就能生成想要的集合。通过继承 java.util.Abstract 类来创建自定义的 Map和 Collection 是很简单的。比如,要创建一个只读的 Set,只需要继承 AbstractSet 并实现 iterator()size() 方法。上面的示例就是生成测试数据的另一种方式。这样生成的集合通常是只读的,而且只需提供最少的方法。

该解决方案还演示了享元(Flyweight)设计模式。当普通解决方案需要太多对象时,或者当生成普通对象占用太多空间时,可以使用享元。享元设计模式将对象的一部分外部化(externalizes)。相比于把对象的所有内容都包含在对象中,这样做使得对象的部分或者全部可以在更有效的外部表中查找,或通过一些节省空间的其他计算生成。

下面定义了一个可以是任何大小的 List,效果上相当于用 Integer 预先初始化了。要从 AbstractList 创建一个只读的 List,必须实现 get()size() 两个方法:

这个类是享元模式的一个简洁的例子。当需要的时候,get() “计算”所需的值,因此没必要存储和初始化实际的底层 List 结构。只有当想要限制 List 的长度时,size 值才是重要的。

在大多数程序中,这里节省的存储空间不会产生什么影响。然而,它允许我们使用一个非常大的 index 来调用 List.get(),不需要一个填充好所有这些值的 List。另外,我们可以在自己的程序中使用大量的 CountingIntegerList,而不用担心存储问题。确实,享元的一大好处就是让我们无须考虑资源问题就能使用更好的抽象。

可以使用享元来实现其他“已初始化”的、数据集大小不限的自定义集合。下面是一个 Map,它会为每个 Integer 键生成一个独一无二的值:

为了创建只读的 Map,我们要继承 AbstractMap 并实现 entrySet() 方法。private value() 方法负责计算任意键的值,并会在 get()Entry.getValue() 方法内使用。CountMap 的大小可以忽略不计。

这里是使用了 LinkedHashSet 而不是创建自定义 Set 类,因此并未完全实现享元。只有在调用 entrySet() 时才会生成此对象。

现在创建一个更复杂的享元。这个示例中的数据集是世界各国及其首都的 Map 。 capitals() 方法生成一个国家和首都的 Map 。 names() 方法生成一个由国家名字组成的 List 。 当给定了表示所需大小的 int 参数时,两种方法都生成对应大小的列表片段:

二维数组 String DATA 是 public 的,因此可以在别处使用。 FlyweightMap 必须实现 entrySet() 方法,该方法需要一个自定义 Set 实现和一个自定义 Map.Entry 类。这是实现享元的另一种方法:每个 Map.Entry 对象存储它自身的索引,而不是实际的键和值。当调用 getKey()getValue() 时,它使用索引返回相应的 DATA 元素。 EntrySet 确保它的 size 不大于 DATA 。

享元的另一部分是在 EntrySet.Iterator 中实现的。这里没有为 DATA 中的每个数据对创建一个 Map.Entry,而是每个迭代器只有一个 Map.Entry。Entry 对象被用作进入 DATA 数据的一个窗口,它只包含一个用于静态 String 数组的 index。每当我们调用迭代器的 next() 方法时,Entry 中的 index 会自增,也就指向了下一个元素对,然后 next() 方法就会返回 Iterator 中的这一个 Entry 对象。(java.util 中的 Map 会使用 getKey()getValue() 方法来实现批量复制,所以这里的做法行得通。如果某个自定义的 Map 只是简单地复制整个 Map.Entry,这么做就会出问题。)

select() 方法会生成一个 FlyweightMap,其中包含了一个指定大小的 EntrySet。该方法被用在了重载的 capitals()names() 方法中。

Collection 的功能

下表列出了可以在 Collection 上做的所有操作(没有包括从 Object 类自动继承的方法),因此这些操作对于 List, Set, Queue 和 Deque 同样可以用(这些接口可能也提供了一些其他的功能)。Map 并非继承自 Collection,所以需要单独处理。

方法名
描述

boolean add(T)

确保将参数(其类型为泛型类型 T)指向的对象添加到集合中。如果没有添加成功,则返回 False(这是一个“可选”的方法)

boolean addAll(Collection<? extends T>)

将参数中的所有元素添加进集合。如果有任何元素被添加进来,则返回 true(“可选”)

void clear()

移除集合中的所有元素(“可选”)

boolean contains(Object o)

如果集合中包含参数 o 所引用的对象,则返回 true

boolean containsAll(Collection<?>)

如果集合中有参数中的所有对象,则返回 true

boolean isEmpty()

如果集合中没有元素,则返回 true

Iterator iterator() Spliterator spliterator()

返回一个可以在集合的元素间移动的迭代器。Spliterator 更为复杂,用于并发场景

boolean remove(Object)

如果参数指向的对象在集合中,则移除该元素的一个实例。如果发生了移除,则返回 true(“可选”)

boolean removeAll(Collection<?>)

移除参数包含的所有元素。如果发生了任何移除,则返回 true(“可选”)

boolean retainAll(Colleaction<?>)

只保留参数包含的元素(即求“交集”)。如果发生了任何变化,则返回 true(“可选”)

boolean removeIf(Predicate<? super E>)

移除这个集合中满足给定谓词条件的每一个元素

Stream stream() Stream parallelStream()

返回一个由这个集合中的元素组成的流

int size()

返回集合中元素的数量

Object[] toArray()

返回一个包含集合中所有元素的数组

T[] toArray(T[] a)

返回一个包含集合中所有元素的数组,结果的运行时类型是参数数组的类型,而不是简单的 Object

这里没有用于随机访问元素的 get() 方法,因为 Collection 也包含 Set,而 Set 维护着自己的内部排序方式(这就使随机访问查找没有了意义)。因此,要检查 Collection 中的元素,必须使用迭代器。

下面演示了所有的 Collection 方法,这里以 ArrayList 为例:

为了演示除了 Collection 接口之外没用到其他东西,这里创建了几个包含不同数据集的 ArrayList,并将其向上转型为 Collection 对象。

可选的操作

Collection 接口中用来执行各种添加和移除操作的方法是可选的操作。这意味着实现类并不一定要提供这些方法的功能定义。

这是一种非常不寻常的定义接口的方式。我们已知接口是一种合约(contract),意思是“无论你如何选择实现这个接口,我保证你可以将这些消息发送到这个对象”。(这里的接口既可以指正式的 interface 关键字,也可以更具一般性地指“任何类或子类支持的方法”)但“可选”操作违反了这一基本原则,它表示调用某些方法不会执行有意义的行为,而是会抛出异常。

如果操作是可选的,编译器仍然能够限制你仅调用该接口中的方法。它不像动态语言那样,可以为任何对象调用任何方法,并在运行时查找特定的调用是否可行。此外,大多数将 Collection 作为参数的方法仅从该 Collection 中读取,并且 Collection 的所有“读取”方法都不是可选的。

“不支持的操作(unsupported operation)”这种方式实现了 Java 集合库的一个重要目标:集合要易于学习和使用。不支持的操作是一种特殊情况,可以推迟到必要的时候。要使用这种方法,要注意:

  1. UnsupportedOperationException 必须是小概率事件。即在大部分情况下,所有的操作都能工作,而某个操作只在特殊情况下不被支持。在 Java 集合类库中确实是这样,因为我们在 99% 的时间中使用的 ArrayList, LinkedList, HashSet 和 HashMap,以及其他的具体实现,都支持所有的操作。这种设计确实提供了一个“后门",可以创建一个新的 Collection,不用为 Collection 接口中的所有方法都提供有意义的定义,而它仍然可以与现有的库融为一体。

  2. 当不支持某个操作时, UnsupportedOperationException 应该出现在实现阶段,而不是在将产品发送给客户之后。毕竟,这个异常表示编程错误:错误地使用了一个具体实现。

值得注意的是,不支持的操作只能在运行时检测到,因此这代表动态类型检查。Java 肯定有静态类型检查,但它也有大量的动态类型,因此很难说它绝对属于某种单一类型的语言。

不支持的操作

不支持的操作常常来源于底层由固定大小的数据结构支撑的集合。当使用 Arrays.asList() 方法将一个数组转变为一个 List 时,就会得到这样的集合。此外,还可以选择使用 Collections 类中的“不可修改(unmodifiable)”方法使任何集合(包括 Map)抛出 UnsupportedOperationException 异常。下面演示了这两种情况:

因为 Arrays.asList() 会产生一个底层由固定大小的数组支撑的 List,所以只支持那些不会改变数组大小的操作是合理的。任何会改变底层数据结构大小的方法都会导致 UnsupportedOperationException,用以说明调用了某个不支持的方法(即编程错误)。

注意,总是可以将 Arrays.asList() 的结果当作一个构造器参数传递给任何 Collection(也可以使用 addAll() 方法或 static Collections.addAll() 方法)来创建一个支持各种操作的普通集合,如 main() 中的第一次调用 test(),这样的调用会生成一个新的且可以调整大小的底层数据结构。

Collections 类中的“不可修改”的方法将该集合包到一个代理中。如果执行了会以任何方式修改该集合的任何操作,这个代理就会抛出 UnsupportedOperationException。使用这些方法的目的是产生一个“常量化”的集合对象。稍后将展示这些“不可修改”的 Collections 方法的完整列表。

Arrays.asList() 返回的是一个固定大小的 List,而 Collections.unmodifiableList() 生成的是一个无法修改的列表。在输出中可以看到,可以修改 Arrays.asList() 所返回 List 中的元素,因为这不会破坏该 List 的“固定大小”本质。但是显然,unmodifiableList() 的结果应该是不能以任何方式修改的。如果使用接口来描述,则需要两个额外的接口,一个具有可用的 set() 方法,而另一个没有。 Collection 的各种不可修改的子类型都将需要额外的接口。

Set 与存储顺序

在之前集合章节已经介绍了基本 Set 操作,但是那些示例使用了如 Integer 和 String 等预定义的 Java 类型,它们被设计为可以在集合中使用。在创建我们自己的类型时,请注意 Set 需要某种方式来维护存储顺序(Map 也是),而 Set 的不同实现之间会有差别。因此,不同的 Set 不仅有不同的行为,还对我们要放到某个特定 Set 中的对象的类型有不同的要求,如下表。

类型
行为

Set(Interface)

向 Set 中添加的每个元素都必须是唯一的,Set 不会添加重复的元素。被添加到 Set 中的元素必须至少定义了 equals() 方法,用以确定对象的唯一性。Set 继承了 Collection,而且没有添加任何东西。Set 接口不保证以任何特定的顺序维护元素

HashSet*

注重快速查找元素的 Set,要添加的元素必须定义 hashCode()equals() 方法。

TreeSet

由树支持的有序 Set。这样就可以从 Set 中获取有序序列,要添加的元素必须实现 Comparable 接口。

LinkedHashSet

具有 HashSet 的查找速度,但在内部使用链表维护元素的插入顺序。因此,当在遍历 Set 时,结果将按元素的插入顺序显示。元素必须定义 hashCode()equals() 方法。

HashSet 上的星号表示,在没有其他约束的情况下,这应该是我们的默认选择,因为它针对速度进行了优化。

关于如何定义 hashCode(),会在附录C中描述。在定义自己的类时,如果其对象会以哈希方式或树方式存储,则必须为其创建 equals() 方法。但是只有在其对象会被放入 HashSet 或 LinkedHashSet 中时(HashSet 的情况更常见,因为它应该是我们用作 Set 实现的第一选择),才必须定义 hashCode() 方法。然而,为了确保良好的编程风格,在重写 equals() 时也要重写 hashCode()

下例演示了要将自己的类型放到某个特定 Set 时,需要实现的必要方法:

为了证明哪些方法对于某个特定的 Set 是必要的,这里创建了三个类。

基类 SetType,保存一个 int 值,并可以用 toString() 输出它。由于存储在 Set 中的所有类都必须具有 equals(),因此该方法也放在基类中。基于 int i 来判断元素是否相等。

然后是继承自 SetType 的 HashType,为了让该类型的对象可以放人 Set 的某个哈希实现中,加人了必要的 hashCode() 方法。

要在某种有序集合,比如在 SortedSet(TreeSet 是其唯一实现)中使用对象,TreeType 所实现的 Comparable 接口是必要的。注意,在 compareTo() 中没有使用 return i-i2 这种简单形式,虽然这是一个常见的编程错误,但只有当 i 和 i2 是“无符号(unsigned)”整型时才能正常工作(如果 Java 有一个“unsigned”关键字的话,不过它没有)。它破坏了 Java 的有符号 int ,它不足以代表两个有符号整数的差异。如果 i 是一个大的正整数而 j 是一个大的负整数, i-j 将溢出并返回一个负值,这不是我们所想要的。

通常希望 compareTo() 方法生成与 equals() 方法一致的自然顺序。如果 equals() 对于特定比较产生 true,则 compareTo() 应该为该比较返回结果 0,并且如果 equals() 为比较产生 false ,则 compareTo() 应该为该比较产生非零结果。

为避免代码重复,fill()test() 都是使用泛型定义的。为了验证 Set 的行为,test() 在用于测试的 set 上调用了 2 次 fill(),尝试引人重复的对象。fill() 可以接受一个任意类型的 Set,而 Function 对象负责产生这个类型。因为这个示例中用到的所有对象都有一个接受单个 int 参数的构造器,所以我们可以将这个构造器当作 Function 来传递,提供填充 Set 的对象。

输出表明,HashSet 是按照升序保存元素的。然而在附录中会看到这是偶然情况,因为哈希会创建自己的存储顺序。只是因为我们的值是一个简单的 int,所以它才是按照升序排列的。LinkedHashSet 会按照元素插入的顺序来保存它们,而 TreeSet 则按照排序结果来维护元素(在这个例子中是降序,这是因为受到了 compareTo() 的实现方式的影响)。

对于 Set 要求的那些操作,如果我们尝试使用的类型并没有正确地提供支持,就会出问题。SetType 或 Treelype 对象没有包含重新定义的 hashCode() 方法,把这样的对象放入任何哈希实现中时,都会出现重复的值,这就破坏了 Set 的主要约定。这让我们相当不安,因为它基至不会抛出运行时错误。然而,默认的 hashCode() 是合法的,所以这虽然并不正确,却是合法的行为。要确保这种程序的正确性,唯一可靠的方法是将单元测试加入我们的构建系统中。

如果尝试在 TreeSet 中使用没有实现 Comparable 接口的类型,则会得到更明确的结果:当 TreeSet 尝试将对象用作一个 Comparable 时,将会抛出异常。

SortedSet

SortedSet 中的元素必然是有序排列的,SortedSet 接口中的以下方法可以产生其他功能:

  • Comparator comparator(): 生成用于此 Set 的 Comparator,若选择自然排序,则返回 null。

  • Object first(): 返回第一个元素。

  • Object last(): 返回最后一个元素。

  • SortedSet subSet(fromElement, toElement): 生成该 Set 的一个视图,包含从 fromElement 到 toElement(不含)的元素。

  • SortedSet headSet(toElement):生成该 Set 的一个视图,包含小于 toElement 的所有元素。

  • SortedSet tailSet(fromElement):生成该 Set 的一个视图,包含大于或等于 fromElement 的所有元素。

注意,SortedSet 表示“根据对象的比较函数进行排序”,而不是“根据插入顺序”。可以使用 LinkedHashSet 保留元素的插入顺序。

Queue

有许多 Queue 实现,其中大多数是为并发应用设计的。许多实现都是通过排序行为而不是性能来区分的。下面是一个涉及大多数 Queue 实现的基本示例,包括基于并发的队列。队列将元素从一端放入并从另一端取出:

Deque 接口也继承自 Queue。除了优先级队列之外,Queue 都是按照元素的放入顺序来输出元素的。在这个示例中,SynchronousQueue 没有输出任何结果,因为它是一个阻塞队列,每一个插人操作都必须等待另一个线程进行的相应移除操作,反之亦然。

优先级队列

考虑一个待办清单(to-do list),其中的每个对象都包含一个 String,以及分别表示主要优先级和次要优先级的值。清单的顺序通过实现 Comparable 来控制。

可以看到优先级队列实现了对列表条目的自动排序。

Deque

Deque(双端队列)像一个队列,但是可以在任意一端添加和移除元素。Java 6 为 Deque 添加了一个显式的接口。下面通过实现了该接口的类来测试最基本的 Deque 方法:

这里只使用了 Deque 方法的 offer 和 poll 版本,因为当 LinkedBlockingDeque 的大小有限时,它们不会抛出异常。注意,当 LinkedBlockingDeque 添加元素时,它会在达到其大小限制时停止,然后忽略后续的 offer。

理解 Map

集合章节已经了解到,Map(也称为关联数组)维护键-值关联(键值对),因此可以使用键来查找值。标准 Java 库包含多个 Map 的基本实现,如 HashMap, TreeMap, LinkedHashMap, WeakHashMap, ConcurrentHashMap 和 IdentityHashMap。 它们有相同的基本 Map 接口,但是在行为上有所区别,包括效率、键值对的保存和演示顺序、保存对象的时间长短、在多线程程序中的表现,以及键的相等性如何确定等。从 Map 接口的实现数量可以着出这个工具的重要性。

为了更深入地了解 Map ,学习如何构造关联数组会很有帮助。下面是一个非常简单的实现:

关联数组中的基本方法是 put()get(),可以从演示中看到其可以工作。

要使用 get() 方法,可以传入要查找的 key,它将生成相关联的值作为结果,如果找不到则返回 null 。get() 方法使用可能是效率最低的方法来定位值:从数组的头部开始并使用 equals() 来比较键。但这里是侧重于简单,而不是效率。

这个版本很有启发性,但它性能不高,而且它只有一个固定的大小,不够灵活。幸运的是, java.util 中的那些 Map 没有这些问题。

性能

性能是映射的一个基本问题。HashMap 不是缓慢地线性查找某个键,而是使用了一个叫哈希码的特殊值,哈希码是一种从相关对象中获取一些信息并将其转换为该对象的“相对唯一” int 的方法。hashCode() 是根类 Object 中的一个方法,因此所有 Java 对象都可以生成哈希码。 HashMap 获取对象的 hashCode() 并使用它来快速搜索键。这就使得性能有了显著的提升。

还可以通过编写自己的 Map 进一步加速表的查找,并根据自己的特定类型来定制,以避免因为与 Object 类型来回转换而造成延迟。

下表是基本的 Map 实现。在没有其他约束的情况下,HashMap 应该是你的默认选择,因为它针对速度进行了优化。其他实现强调其他特性,因此不如 HashMap 快。

集合
描述

HashMap

基于哈希表实现(使用此类来代替 Hashtable),为插入和定位键值对提供了常数时间性能。可以通过构造方法调整性能,构造器支持设置这个哈希表的容量和负载因子

LinkedHashMap

类似于 HashMap。但是在遍历时会以插人的顺序或最近最少使用(least-recently-used,LRU)的顺序获得键值对。除遍历之外,性能比 HashMap 稍低。对于遍历的情况,因为它用了链表来维护内部顺序,所以更快一些

TreeMap

基于红黑树实现。当我们查看键或者键值对时,会发现它们是有序的(顺序通过 Comparable 或 Comparator夹确定)。 TreeMap 的要点是,我们会以有序的方式得到结果。TreeMap 是唯一提供了 subMap() 方法的 Map,它会返回树的一部分

WeakHashMap

由弱键组成的 Map,该映射所引用的对象可以被释放。它是为解决特定类型的问题而设计的。如果在这个映射之外,已经没有指向某个特定键的引用,那么可以对这些键进行垃圾收集

ConcurrentHashMap

线程安全的 Map,没有使用同步锁。将在并发编程章节介绍

IdentityHashMap

哈希映射,使用 == 而不是 equals() 来比较键。仅用于解决特殊问题,不适用于一般用途。

哈希是用来在映射中保存元素的最常用方法。

Map 中使用的键的要求与 Set 中的元素的要求相同。可以在 TypesForSets.java 中看到这些。任何键必须具有 equals() 方法。如果键用于散列映射,则它还必须具有正确的 hashCode() 方法。如果键在 TreeMap 中使用,则必须实现 Comparable 接口。

以下示例使用先前定义的 CountMap 测试数据集显示通过 Map 接口可用的操作:

keySet() 方法会生成一个由 Map 中的键组成的 Set,values() 方法会得到一个包含了 Map 中所有值的 Collection。(注意,键必须是唯一的,但值可以重复)这些 Collection 的底层由 Map 中的结构支撑,所以对 Collection 的任何修改都会在与其关联的 Map 中体现出来。

SortedMap

利用 SortedMap(TreeMap 或 ConcurrentSkiplistMap 实现了该接口),可以确保键是有序的,从而可以使用 SortedMap 接口中的方法提供的功能。

  • Comparator comparator():生成用于该 Map 的比较器对象,如果使用自然排序,则返回 null。

  • T firstKey(): 返回第一个键

  • T lastKey(): 返回最后一个键

  • SortedMap subMap(fromKey, toKey): 生成该 Map 的一个视图,包含从 fromKey 到 toKey(不含) 的键。

  • SortedMap headMap(toKey): 生成该 Map 的一个视图,包含小于 toKey 的所有键。

  • SortedMap tailMap(fromKey): 生成该 Map 的一个视图,包含大于或等于 fromKey 的所有键。

下面示例演示了 TreeMap 的其他行为:

这里,键值对按照键的排序顺序进行排序。因为 TreeMap 是有序的,定位就有了意义,所有可以获得第一个和最后一个元素以及子映射。

LinkedHashMap

为了提高速度,LinkedHashMap 对所有的东西都进行了哈希处理,但是在遍历过程中也会按照插入顺序输出键值对(System.out.println() 可以遍历它,因此可以看到遍历的结果)。此外,可以在 LinkedHashMap 的构造器中进行配置,让它使用基于访问量的最近最少使用(LRU)算法。这样的话,尚未访问的元素就会出现在列表的前面(而且因此成了移除操作的候选)。这样可以轻松创建为节省空间而进行定期清理的程序。下面这个简单的例子演示了这两个特点

键值对确实是按插入顺序遍历的,即使在 LRU 版本中也是一样。然而,在 LRU 版本中,在(仅有的)6 个条目被访问过之后,最后 3 个条目会被放到列表的前面。然后,当 0 再次被访问之后,它会被移到列表的尾部。

工具函数

有一些用于集合的独立工具,它们用 java.util.Collections 中的 static 方法表示。之前已经见过如 addAll(), reverseOrder()binarySearch(),这里介绍另一些(synchronized 和 unmodifiable 工具会在后面介绍)。在此表中,在需要的时候使用了泛型:

方法
描述

checkedCollection(Collection<T>, Class<T> type), checkedList(List<T>, Class<T> type). checkedMap(Map<K, V>, Class<K> keyType, Class<V> valueType), checkedSet(Set<T>, Class<T> type), checkedSortedMap(SortedMap<K, V>, Class<K> keyType, Class<V> valueType), checkedSortedSet(SortedSet<T>, Class<T> type)

从 Collection 或 Collection 的某个具体子类型生成一个动态、类型安全的视图。当无法使用静态检查版本时使用这个版本。(在多态章节演示过)

max(Collection), min(Collection)

使用 Collection 中对象的自然比较方法计算参数集合中的最大或最小元素

max(Collection, Comparator), min(Collection, Comparator)

使用 Comparator 计算 Collection 中最大或最小的元素

indexOfSubList(List source, List target)

返回 target 在 source 内第一次出现的起始索引,如果不存在则返回 -1

replaceAll(List<T>, T oldVal, T newVal)

用 newVal 替换所有的 oldVal

reverse(List)

就地将列表反向

reverseOrder(), reverseOrder(Comparator<T>)

返回一个 Comparator,对于实现了 Comparable<T> 接口的对象组成的集合,它可以逆转其自然排序。第二个版本可以使用所提供的 Comparator 来逆转顺序

rotate(List, int distance)

将所有元素向前移动 distance ,将尾部的元素移到开头(即循环移动)

shuffle(List), shuffle(List, Random)

随机置换指定列表(即打乱顺序)。第一个版本使用了默认的随机化源,也可以使用第二个版本,提供自己的随机化源。

sort(List<T>), sort(List<T>, Comparator<? super T> c)

使用自然顺序对指定 List<T> 排序,第二种形式接受一个 Comparator 来排序

copy(List<? super T> dest, List<? extends T> src)

将 src 中的元素复制到 dest 。

swap(List, int i, int j)

交换 List 中位置 i 和 位置 j 的元素,可能比你手工编写的速度快

fill(List<? super T>, T x)

将 List 中所有元素都替换为 x

nCopies(int n, T x)

返回一个大小为 n 的不可变 List<T>,其中的所有引用都指向 x

disjoint(Collection, Collection)

如果两个集合没有共同元素,则返回 true

frequency(Collection, Object x)

返回 Collection 中,等于 x 的元素个数

emptyList(), emptyMap(), emptySet()

返回不可变的空 List,Map 或 Set 。这些是泛型的,因此生成的 Collection 可以被参数化为所需的类型

singleton(T x), singletonList(T x), singletonMap(K key, V value)

生成一个不可变的 List,Set 或 Map ,其中只包含基于给定参数的单个元素

list(Enumeration<T> e)

生成一个 ArrayList<T>,包含按照顺序从(老式的) Enumeration (Iterator 的前身)返回的元素,用于转按遗留代码

enumeration(Collection<T>)

生成一个老式的 Enumeration<T>

注意 min()max() 是配合 Collection 对象使用的,而非 List 对象,所以我们不用担心是否应该对这个 Collection 排序。(前面也提到过,在执行 binarySearch() 之前必须对 List 或数组执行 sort()。)

下面展示了上表中大多数实用工具程序的基本用法:

输出解释了每种实用方法的行为。注意,当 min() 和 max() 使用 String.CASE_INSENSITIVE_ORDER 时大小写带来的差别。

List 上的排序和查找

在 List 上执行排序和查找的工具函数虽然与用于对象数组的那些拥有同样的名字和签名,但它们是 Collections 而非 Arrays 的 static方法。

同对数组执行查找和排序一样,如果使用一个 Comparator 来排序,也必须使用同样的 Comparator 来执行 binarySearch()

Collections.shuffle() 会将 List 的顺序变成随机的。list.listIterator(10) 是在 list 的位置 10 处创建了一个 ListIterator。后面的 while 循环是删除这个 list 的位置 10 及其后面的元素。

创建不可修改的 Collection 或 Map

通常,创建 Collection 或 Map 的只读版本会很方便。Collections 类通过将原始集合传递给一个方法然后返回一个只读版本的集合。这个方法还有多个变种:用于 Collection 的(在无法把一个 Collection 当作更具体的类型时使用)、用于 List 的、用于 Set 的,以及用于 Map 的。下面这个示例演示了构建每种集合的只读版本的正确方式:

调用某个特定类型的“不可修改”的方法不会触发编译时检查,但是一旦发生转换,只要调用了修改集合内容的方法,就会产生 UnsupportedOperationException。

不管是哪一种情况,在将集合变为只读之前,都必须用有意义的数据填充这个集合。一旦它被加载,最好的方法就是用“不可修改”调用所产生的引用来替换现有的引用。一旦使其成为只读的,就不用担心会不小心修改集合内容了。另一方面,此工具还允许将可修改的集合保留为类中的 private 成员,并从方法调用处返回对该集合的只读引用。所以,你可以在类内修改它,但其他人只能读它。

同步 Collection 或 Map

synchronized 关键字是多线程主题的重要组成部分,后面章节会介绍。这里只是要指出,Collections 包含一种自动同步整个集合的方式,其语法与不可修改的方法类似:

最好是直接通过恰当的“同步”(synchronized)方法来传递新创建的集合,如以上代码所示。这样,就不会意外地暴露出非同步版本。

快速失败

Java 集合类也有一种防止多个任务(task)同时修改集合内容的机制。这里使用了更通用的术语“任务”来表示可以独立运行的事物,而没有使用特定于实现的“线程”,因为 Java 即将带来的变化可能会从线程转向更好的抽象。(Project Loom)

如果在我们遍历集合的过程中有其他任务介入,比如在这个集合中插入、移除或修改了一个对象,那么问题就出现了:或许我们在集合中已经遍历了这个元素,或许它还未被遍历到,或许集合在我们调用 size() 之后变小了……会出错的场景有很多。Java 集合类库使用了一种快速失败(fail-fast)机制,它会寻找任何并非由我们的任务引发的对该集合的修改。如果检测到其他人在修改这个集合,它会立即生成一个 ConcurrentModificationException。这就是所谓的“快速失败”:它不会在以后使用更复杂的算法尝试检测问题。

下面是一个非常基础的对快速失败机制的演示。创建了一个迭代器,并在这个迭代器所指向的集合中添加一个元素:

异常来自于在从集合中获得迭代器之后,又尝试在集合中添加元素。程序的两个部分可能会修改同一个集合,这种可能性的存在会产生不确定状态,因此异常会通知你更改代码。在这种情况下,应先将所有元素添加到集合,然后再获取迭代器。

ConcurrentHashMap, CopyOnWriteArrayList 和 CopyOnWriteArraySet 都使用了可以避免 ConcurrentModificationException 的技术。

持有引用

java.lang.ref 库包含一组类,给垃圾收集提供了更大的灵活性。特别是当拥有可能导致内存耗尽的大对象时,这些类特别有用。这里有三个从抽象类 Reference 继承来的类: SoftReference (软引用), WeakReference (弱引用)和 PhantomReference (虚引用)。如果一个对象只能通过这其中的一个 Reference 对象访问,那么这三种类型都可以为垃圾收集器提供不同级别的间接引用(indirection)。

如果一个对象是可达的(reachable),那么意味着在程序中的某个位置可以找到该对象。这可能意味着在栈上有一个直接引用该对象的普通引用,但也有可能是引用了一个对该对象有引用的对象,这可以有很多中间环节。如果某个对象是可达的,则垃圾收集器无法释放它,因为它仍然被程序所使用。如果某个对象是不可达的,则程序无法使用它,那么垃圾收集器回收该对象就是安全的。

因为我们可以使用 Reference 对象继续持有一个指向那个对象的引用,所以那个对象就是可达的,不过我们也允许垃圾收集器释放该对象。因此,我们有办法使用这个对象,但是如果内存即将耗尽,也可以释放它。

可以通过使用 Reference 对象作为我们和普通引用之间的中介(代理)来实现此目的。另外,不能存在指向该对象的其他普通引用(即没有包在 Reference 对象中)。如果垃圾收集器发现某个对象可以通过一个普通引用访问到,它就不会释放这个对象。

SoftReference, WeakReference 和 PhantomReference 的顺序对应不同级别的可达性,后面的比前面的“更弱”。软引用用于实现对内存敏感的缓存。弱引用用于实现规范化映射(canonicalizing mapping),为节省存储空间,对象的实例可以同时在程序中的多个位置使用,而不会妨碍它们的键(或值)被回收。与 Java 终结机制相比,虚引用用于以更灵活的方式安排事后(post-mortem)清理动作。注意,从 Java 9 开始,Object finalize() 方法已被废弃。

使用 SoftReference 和 WeakReference ,可以选择是否将它们放在 ReferenceQueue (该队列用于 post-mortem 清理操作)中,但 PhantomReference 只能在 ReferenceQueue 上构建。简单演示如下:

当运行此程序(将输出重定向到文本文件以查看页面中的输出)时,将会看到对象是被垃圾收集了的,虽然仍然可以通过 Reference 对象访问它们(使用 get() 来获取实际的对象引用)。 还可以看到 ReferenceQueue 始终生成包含 null 对象的 Reference。

WeakHashMap

Java 集合类库中的 WeakHashMap 持有的是弱引用。这个类使得创建规范映射更容易了。在这种映射中,可以通过仅仅创建一个特定值的实例来节省存储空间。当程序需要这个值时,它就在该映射中查找现有的对象并使用它(而不是从头创建)。该映射可以在其初始化过程中构造这些值,但更有可能按需创建。

由于这是一种节省存储空间的技术,因此 WeakHashMap 允许垃圾收集器自动清理键和值,这是非常方便的。放在 WeakHashMap 中的键和值不需要任何特殊处理,它们会自动被该映射包在 WeakReference 中。当这个键不再使用时,它就可以被垃圾收集器清理了。下面是一个简单的演示:

通过将 String 用作这个映射的键,我们就自动获得了 String 的 hashCode()equals(),所以它作为哈希数据结构中的键是没问题的(参见附录)。

键每隔两个就有一个被放在 savedKeys 数组中,这就告诉垃圾收集器,这个键还处于活跃使用中。所有的键值对都放在名为 map 的 WeakHashMap 中。

在通过调用 System.gc() 强制让垃圾收集器运行之后,你将看到,只有那些键被保存到 savedkeys 中的元素才会继续存在。

Java 1.0/1.1 的集合类

编写新代码时切勿使用旧集合。

Vector 和 Enumeration

在 Java 1.0/1.1 中,唯一可以自动扩展的序列就是 Vector,所以它被大量使用,但它的缺点很多。基本上,可以将它看作方法名又怪又长的 ArrayList。在修订后的 Java 集合类库中,Vector 被调整了,可以作为 Collection 和 List 使用。但实际上它之所以被包含逬来,只是为了支持旧的 Java 代码。

Java 1.0/1.1 版本的选代器选择了一个新名字:enumeration,而不是使用耳熟能详的术语 iterator。Enumeration 接口比 Iterator 小,只有两个方法,而且使用了更长的方法名:boolean hasMoreElements() 在枚举包含更多元素时返回 true,而 Object nextElement() 在有更多元素时返回这个枚举的下一个元素(否则抛出异常)。

Enumeration 只是一个接口,而非实现。如果可能,一定要在自己的代码中使用 Iterator,但是也要做好准备应对某些库交给你的 Enumeration。

通过 Collections.enumeration() 方法,可以为任何 Collection 生成一个 Enumeration,如下例:

要生成 Enumeration,可以调用 elements(),然后使用它来执行前向迭代。

最后一行创建了一个 ArrayList,并使用 enumeration() 方法将其从 ArrayList 的 Iterator 适配为 Enumeration。因此,如果我们有想用 Enumeration 的旧代码,仍然可以使用新的集合类。

Hashtable

正如我们在本章的性能对比中看到的,基本的 Hashtable 与 HashMap 非常相似,甚至包括方法名。在新的代码中,没有理由使用 Hashtable 而不用 HashMap。

Stack

栈很早就和 LinkedList 一起引入了。Java 1.0/1.1 中的 Stack 比较奇怪:它没有以组合方式使用 Vector,而是继承了 Vector。因此,它有 Vector 的所有特性和行为,外加 Stack 的一些行为。很难去知道设计师是否有意识地认为这样做是有用的,或者它是否只是太天真了。

下面是对 Stack 的简单演示,它将 enum 的 String 表示压到栈中。它还演示了我们司以非常轻松地把 LinkedList 当作栈来使用,也可以使用集合章节创建的 Stack 类:

String 表示是从 Month 常量生成的,用 push() 方法插入 Stack 中,之后再用 pop() 方法从栈顶取出。因为存在继承,一个 Stack 就是一个 Vector,因此所有可以在Vector 上执行的操作都可以在 Stack 上执行,比如 elementAt()

如前所述,在需要栈行为时使用 LinkedList ,或者从 LinkedList 类创建的 onjava.Stack 类。

BitSet

BitSet 可以高效地存储一组开关数据。这里的高效是从存储空间大小的角度来看的。如果希望高效访问,它比使用原生的数组稍慢一些。

此外,BitSet 的最小大小是 long :64位。这意味着如果你要存储更小的东西,比如8位, BitSet 就是浪费。如果大小是一个问题,那么创建自己的类或者使用一个数组来保存自己的标志信息是更明智的选择。(只有在你创建许多包含开关信息列表的对象时才会出现这种情况,并且只应根据分析和其他指标来决定。)

当添加更多元素时,普通集合会扩展,BitSet 也会这样做。以下示例显示了 BitSet 的工作原理:

随机数生成器创建了随机的 byte, short 和 int。每一个都被转换为 BitSet 中相应的位模式。因为一个 BitSet 是64位的,所以这样做不会导致其大小增加。然后我们创建了更大的 BitSet。注意,BitSet 会在必要时扩展。

对于数量固定,可以命名的一组标志,EnumSet(参见枚举章节)通常是比 BitSet 更好的选择,因为 EnumSet 允许我们操作名字,而不是数字化的位位置(bit location),从而减少了错误。EnumSet 还可以防止意外添加新的标志位置,误添加可能会导致一些难以发现的严重错误。使用 BitSet 而不是 EnumSet 的理由很少:在运行之前不知道到底需要多少个标志,给标志指派名字不切实际,或者我们需要 BitSet 提供的特殊操作(参见 BitSet 和 EnumSet 的 JDK 文档)。

总结

集合可以说是编程语言中最常用的工具。有些语言(例如 Python)甚至将基本集合组件(列表,映射和集合)作为内置函数包含在其中。

正如我们在集合章节中看到的,借助集合,我们可以轻松地实现一些很有用的功能。某些时候,为了正确地使用集合,我们必须加深对它的了解。特别是,要编写自己的 hashCode() 方法,必须足够了解哈希操作(还必须知道何时需要编写)。而且,我们必须对各种集合实现有足够的了解,以便根据需要选择适合的那个。

设计集合类库是非常困难的(对于大部分库设计问题而营也是如此)。在 C++ 中,集合类用很多不同的类构建了基础。这比之前的 C++ 集合类要好(之前几乎什么都没有)但是它并不能被照搬到 Java 中。这是一种极端,另一种极端是,我见过一个只由一个类(collection)组成的集合类库,它同时担当着线性序列和关联数组的角色。Java 集合类库尝试在能力和复杂性之间取得平衡,结果就是,有些地方看起来有点奇怪。与早期 Java 库中的一些设计决策不同的是,这些奇怪的地方并非偶然,而是仔细权衡了复杂性之后的结果。