如果一个程序只包含固定数量的且生命周期都是已知的对象,那么这是一个非常简单的程序。
通常,程序总是根据运行时才知道的某些条件去创建对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型。为解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。所以,就不能依靠创建命名的引用来持有每一个对象:
Mytype aReference;
一、泛型和类型安全的容器
当你制定了某个类型作为泛型参数时,你并不仅限于只能将该确切类型的对象放置到容器中。向上转型也可以像作用于其他类型一样作用于泛型:
//: holding/GenericsAndUpcasting.java
import java.util.*;
class GrannySmith extends Apple {}
class Gala extends Apple {}
class Fuji extends Apple {}
class Braeburn extends Apple {}
public class GenericsAndUpcasting {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<Apple>();
apples.add(new GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn());
for(Apple c : apples)
System.out.println(c);
}
}
因此,你可将Apple的子类型添加到被指定为保存Apple对象的容器中。
程序的输出时从Object默认的toString( )方法产生的,该方法打印类名,后面跟随该对象的散列码的无符号十六进制表示(这个散列码是通过hashCode( )方法产生的)。
二、基本概念
1:Collection。一个独立元素的序列,这些元素都服从一条或多条的规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
2:Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找某个对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在了一起;或者被称为“字典”,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。
三、添加一组元素
在java.util包中的Arrays和Collection类中都有很多实用方法,可以在一个Collection中添加一组元素。Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数并将其转换为一个List对象)。Collections.addAll()方法接受一个Collection对象,以及一个数组或是用一个用逗号分隔的列表,将元素添加到Collection中。
//: holding/AddingGroups.java
// Adding groups of elements to Collection objects.
import java.util.*;
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5));
Integer[] moreInts = {6, 7, 8, 9, 10};
collection.addAll(Arrays.asList(moreInts));
// Runs significantly faster, but you can't
// construct a Collection this way:
Collections.addAll(collection, 11, 12, 13, 14, 15);
Collections.addAll(collection, moreInts);
// Produces a list "backed by" an array:
List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
list.set(1, 99); // OK -- modify an element
// list.add(21) // Runtime error because the
} // underlying array cannot be resized
}
Collection的构造器可以接受另一个Collection,用它来将自身初始化,因此你可以使用Arrays.List()来为这个构造器产生输入。但是Collection.addAll()方法运行起来要快得多,而且构建一个不包含元素的Collection,然后调用Collections.addAll()这种方式很方便,因此它是首选方式。
Collection.addAll()成员方法只能接受另一个Collection对象作为参数,因此它不如Arrays.asList()或Collections.addAll()灵活。这两个方法使用的都是可变参数列表。
你也可以直接使用Arrays.asList()输出,将其当作List,但是在这种情况下,其底层表示的是数组,因此不能调整尺寸。如果你试图用add() 或 delete()方法在这种列表中添加或删除元素,就有可能引发去改变数组尺寸的尝试,因此你将在运行时获得“Unsupported Operation”(不支持的操作)错误。
Arrays.asList()方法的限制是它对所产生的List的类型做出了最理想的假设,而并没有注意你对它会赋予什么样的类型。
//: holding/AsListInference.java
// Arrays.asList() makes its best guess about type.
import java.util.*;
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInference {
public static void main(String[] args){
List<Snow> snow1 = Arrays.asList(new Crusty(), new Slush(), new Powder());
// Won't compile:
// List<Snow> snow2 = Arrays.asList(new Light(), new Heavy());
// Compiler says:
// found : java.util.List<Power>
// required: java.util.List<Snow>
// Collections.addAll() doden't get confused:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy());
// Give a hint using an
// explicit type argument specification:
List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy());
}
}
当试图创建snow2时,Arrays.asList()中只有Powder类型,因此它会创建List<Powder>而不是List<Snow>,尽管Collections.addAll()工作的很好,因为它从第一个参数中了解到了目标的类型是什么。
正如你从创建snow4的操作中所看到的,可以在Arrays.asList()中间插入一条“线索”,以告诉编译器对于由Arrays.asList()产生的List类型,实际的目标类型应该是什么。这称为显式类型参数说明。
四、容器的打印
你必须使用Arrays.toString()来产生数组的可打印表示,但是打印容器无需任何帮助。
//: holding/PrintingContainers.java
// Containers print themselves automatically
import java.util.*;
public class PrintingContainers {
static Collection fill(Collection<String> collection) {
collection.add("rat");
collection.add("cat");
collection.add("dog");
collection.add("dog");
return collection;
}
static Map fill(Map<String, String> map) {
map.put("rat", "Fuzzy");
map.put("cat", "Rags");
map.put("dog", "Bosco");
map.put("dog", "Spot");
return map;
}
public static void main(String[] args) {
System.out.println(fill(new ArrayList<String>()));
System.out.println(fill(new LinkedHashSet<String>()));
System.out.println(fill(new HashSet<String>()));
System.out.println(fill(new TreeSet<String>()));
System.out.println(fill(new LinkedHashSet<String>()));
System.out.println(fill(new HashMap<String, String>()));
System.out.println(fill(new TreeMap<String, String>()));
System.out.println(fill(new LinkedHashMap<String, String>()));
}
}
这里展示了Java容器类库中的两种主要类型,它们的主要区别在于容器中每个“槽”保存的元素个数。Collection在每个槽中只能保存一个元素。此类容器包括:List,它以特定的顺序保存一组元素;Set,元素不能重复;Queue,只允许在容器的一“端”插入对象,并从另外一“端”移除对象。Map在每个槽内保存了两个对象,即键和与之关联的值。
默认的打印行为(使用容器提供的toString()方法)即可生成可读性很好的结果。Collection打印出来的内容用方括号括住,每个元素由逗号隔开。Map则用大括号括住,键与值由等号联系(键在等号左边,值在右边)。
五、List
List承诺可以将元素维护在特定的序列中。List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。
ArrayList,它常用于随机访问元素,但是在List的中间插入和移除元素是较慢。
LinkedList,它通过代价较低的在List中间进行的插入和删除操作,提供了优化的顺序访问。LinkedList在随机访问方面相对比较慢,但是它的特性集较ArrayList更大。
contains( ): 确定某个对象是否在列表中
remove( ): 移除一个对象,传递对象的引用
indexOf( ): 发现对象在List中所处位置的索引编号
subList( ): 从较大的列表中创建出一个片断
containsAll( ): 确定多个对象是否在列表中
Collections.sort( ): 排序 向上转型
Collections.shuffle( ): 随机排序 向上转型
retainAll( ): 交集 ∩
removeAll( ): 移除所有元素
set( ): 在指定索引处,替换整个位置的元素
addAll( ):插入新的列表
isEmpty( ): 判空
clear( ): 清空
六、迭代器
迭代器(也是一种设计模式)的概念可以用于达成此目的。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列的底层的结构。此外,迭代器通常被称为轻量级对象:创建它的代价小。
iterator( ): 要求容器返回一个Iterator,Iterator将准备好返回序列的第一个元素
next( ): 获得序列中的下一个元素
hasNext( ): 检查序列中是否还有元素
remove( ): 将迭代器新近返回的元素删除
iterator的真正威力:能够将遍历序列的操作与序列底层的结构分离。
迭代器统一了对容器的访问方式。
ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。
previousIndex( ): 当前位置的前一个元素的索引
hasprevious( ): 检查序列中是否还有前一个元素
previous( ): 获得序列的前一个元素
set( ): 替换它访问过的最后一个元素
listIterator(): 产生一个指向List开始处的ListIterator
listIterator(n): 产生一个指向列表索引为n的元素处的ListIterator
七、LinkedList
getFirst( ): 返回列表的头(第一个元素),而不移除它,如果List为空,则抛出NoSuchElementException
element( ): 返回列表的头(第一个元素),而不移除它,如果List为空,则抛出NoSuchElementException
peek( ): 返回列表的头(第一个元素),而不移除它,如果List为空,返回null
remove( ): 移除并返回列表的头,如果List为空,则抛出NoSuchElementException
removeFirst( ): 移除并返回列表的头,如果List为空,则抛出NoSuchElementException
poll( ): 移除并返回列表的头,如果List为空,返回null
addFirst( ): 将某个元素插入到列表的头部
offer( ): 将某个元素插入到列表的尾部
add( ): 将某个元素插入到列表的尾部
addLast( ): 将某个元素插入到列表的尾部
removeLast( ): 移除并返回列表的最后一个元素。
八、Stack
“栈”通常是指“后进先出”(LIFO)的容器。有时栈也被称为叠加栈,因为最后压入栈的元素,第一个弹出栈。
push( ): 将元素压入栈中
pop( ): 移除并返回栈顶元素
peek( ): 返回栈顶元素,但是并不将其从栈顶移除
九、Set
Set不保存重复的元素。
Set具有和Collection完全一样的接口,因此没有任何额外的功能,不像前面有两个不同的List。实际上Set就是Collection,只是行为不同。(继承与多态)
TreeSet: 将元素存储在红-黑树数据结构中,有序
HashSet: 使用散列函数,无序
LinkedHashSet:因为查询速度的原因也使用了散列,但是看起来它使用了链表来维护元素的插入顺序
//: holding/SetOperations.java
import java.util.*;
public class SetOperations {
public static void main(String[] args) {
Set<String> set1 = new HashSet<String>();
Collections.addAll(set1, "A B C D E F G H I J K L".split(" "));
set1.add("M");
System.out.println("H: " + set1.contains("H"));
System.out.println("N: " + set1.contains("N"));
Set<String> set2 = new HashSet<String>();
Collections.addAll(set2, "H I J K".split(" "));
System.out.println("set2 in set1" + set1.contains(set2));
set1.remove("H");
System.out.println("set1: " + set1);
System.out.println("set2 in set1" + set1.contains(set2));
set1.removeAll(set2);
System.out.println("set2 removed from set1:" + set1);
Collections.addAll(set1, "X Y Z".split(" "));
System.out.println("'X Y Z' added to set1: " + set1);
}
}
十、Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。
get( ): 如果键不在容器中,get( )方法将返回null, 否则返回与该键相关联值
containsKey( ): 查看是否包含某个键
containsValue( ): 查看是否包含某个值
keySet( ): 产生所有键组成的Set
values( ): 值的Collection
Map可以返回它的键的Set,它的值的Colleation,或者它的键值对的Set。
十一、Queue
队列是一个典型的先进先出(FIFO)的容器,即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。
LinkedList提供了方法以支持队列的行为,并且它实现了Queue接口,因此LinkedList可以用作Queue的一种实现。通过将LInkedList向上转型为Queue。
//: holding/QueueDemo.java
// Upcasting ot a Queue from a LinkedList.
import java.util.*;
public class QueueDemo {
public static void printQ(Queue queue) {
while(queue.peek() != null) {
System.out.println(queue.remove() + " ");
}
}
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
Random rand = new Random(47);
for(int i = 0; i < 10; i++)
queue.offer(rand.nextInt(i + 10));
printQ(queue);
Queue<Character> qc = new LinkedList<Character>();
for(char c : "Brontosaurus".toCharArray())
qc.offer(c);
printQ(qc);
}
}
offer( ): 它在允许的情况下,将一个元素插入到队尾,或者返回false
peek( ): 不移除的情况下,返回队头,队列为空时,返回null
element( ): 不移除的情况下,返回对头,队列为空时,抛出NoSuchElementException异常
poll( ): 移除并返回队头,队列为空时,返回null
remove( ):移除并返回队头,队列为空时,抛出NoSuchElementException异常
PriorityQueue
优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。
当你在PriorityQueue上调用offer() 方法来插入一个对象时,这个对象会在队列中被排序。默认的排序将使用对象在队列中的自然顺序,但是你可以通过提供自己的Comparator来修改这个顺序。PriorityQueue可以确保当你调用peek( ), poll( ) 和 remove( )方法时,获取的元素将是队列中优先级最高的元素。
//: holding/PriortyQueueDemo.java
import java.util.*;
public class PriorityQueueDemo {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();
Random rand = new Random(47);
for(int i = 0; i < 10; i++)
priorityQueue.offer(rand.nextInt(i+10));
QueueDemo.printQ(priorityQueue);
List<Integer> ints = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1, 2, 3);
priorityQueue = new PriorityQueue<Integer>(ints);
QueueDemo.printQ(priorityQueue);
priorityQueue = new PriorityQueue<Integer>(ints.size(), Collections.reverseOrder());
priorityQueue.addAll(ints);
QueueDemo.printQ(priorityQueue);
String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION";
List<String> strings = Arrays.asList(fact.split(" "));
PriorityQueue<String> stringPQ = new PriorityQueue<String>(strings);
QueueDemo.printQ(priorityQueue);
stringPQ = new PriorityQueue<String>(strings.size(), Collections.reverseOrder());
stringPQ.addAll(strings);
QueueDemo.printQ(stringPQ);
Set<Character> charSet = new HashSet<Character>();
for(char c : fact.toCharArray())
charSet.add(c); // Autoboxing
PriorityQueue<Character> characterPQ = new PriorityQueue<Character>(charSet);
QueueDemo.printQ(characterPQ);
}
}
重复是允许的,最小的值拥有最高的优先级(如果是String, 空格也可以算作值,并且比字母的优先级高,自然顺序)。可以通过提供自己的Comparator对象来改变排序,Collections.reverseOrder( )产生的反序的Comparator。
十二、Collection和Iterator
Collection是描述所有序列容器的共性的根接口,它可能会被认为是一个“附属接口”,即因为要表示其他若干个接口的共性而出现的接口。另外,java.util.AbstractCollection类提供了Collection的默认实现,使得你创建AbstractCollection的子类型,而其中没有不必要的代码重复。
使用接口描述的一个理由是它可以使我们能够创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多的对象类型。因此,如果我编写的方法将接受一个Collection,那么该方法就可以应用于任何实现了Collection的类——这也是一个新类可以选择去实现Collection接口,以便我的方法可以使用它。
当你要实现一个不是Collection的外部类时,由于让它去实现Collection接口可能非常困难或麻烦,因此使用Iterator就会变得非常吸引人。例如,如果我们通过继承一个持有某个对象的类来创建一个Collection的实现,那么我们必须实现所有的Collection方法。尽管这可以通过继承AbstractCollection而很容易地实现,但是你无论如何还是要被强制去实现iterator()和size(),以便提供AbstractCollection没有实现,但是AbstractCollection中的其他方法会使用到的方法。
十三、Foreach与迭代器
foreach语法主要用于数组,但是它也可以应用于任何Collection对象。
Java SE5引入了新的被称为Iterable的接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。因此如果你创建了任何实现Iterable的类,都可以将它用于foreach语句中。
//: holding/IterableClass.java
// Anything Iterable works with foreach.
import java.util.*;
public class IterableClass implements Iterable<String> {
protected String[] words = ("And that is how" + "we know the Earth to be banana-shaped.").split(" ");
public Iterator<String> iterator() {
return new Iterator<String> () {
private int index = 0;
public boolean hasNext() {
return index < words.length;
}
public String next() {
return words[index++];
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
for(String s : new IterableClass())
System.out.println(s);
}
}
iterator()方法返回的是实现了Iterator<String>的匿名内部类的实例,该匿名内部类可以遍历数组中的所有单词。
在Java SE5中,大量的类都是Iterable类型,主要包括所有的Collection类(但是不包括各种Map)。
//: holding/EnvironmentVariables.java
import java.util.*;
public class EnvironmentVariables {
public static void main(String[] args) {
for(Map.Entry entry : System.getenv().entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
上面的代码可以显示所有的操作系统环境变量。System.getenv()返回一个Map,entrySet产生一个由Map.Entry的元素构成的Set,并且这个Set是一个Iterable,因此它可以用于foreach循环。
foreach语句可以用于数组或其他任何Iterable,但是这并不意味着数组肯定也是一个Iterable,而任何自动包装也不会自动发生。
不存在任何从数组到Iterable的自动转换,你必须手工执行这种转换。
//: holding/ArrayIsNotIterable.java
import java.util.*;
public class ArrayIsNotIterable {
static <T> void test(Iterable<T> ib) {
for(T t : ib)
System.out.println(t);
}
public static void main(String[] args) {
test(Arrays.asList(1, 2, 3));
String[] strings = {"A", "B", "C"};
// An array works in foreach, but it's not Iterable:
//! test(strings);
// You must explicitly convert it to an Iterable:
test(Arrays.asList(strings));
}
}
适配器方法惯用法
假如,假设你希望可以选择以向前的方向或是向后的方向迭代一个单词列表。如果直接继承这个类,并覆盖iterator()方法,你只能替换现有的方法,而不能实现选择。
一种解决方案是所谓适配器方法的惯用法。适配器部分来自于设计模式,因为你必须提供特定接口以满足foreach语句。当你有一个接口并需要另一个接口时,编写适配器就可以解决问题。这里,我希望在默认的前向迭代器的基础上,添加产生反向迭代器的能力,因此我不能使用覆盖,而是添加了一个能够产生Iterable对象的方法,该对象可以用于foreach语句。
//: holding/AdapterMethodIdiom.java
// The "Adapter Method" idiom allows you to use foreach
// with additional kinds of Iterables.
import java.util.*;
class ReversibleArrayList<T> extends ArrayList<T> {
public ReversibleArrayList(Collection<T> c) {
super(c);
}
public Iterable<T> reversed() {
return new Iterable<T> () {
public Iterator<T> iterator() {
return new Iterator<T>() {
int current = size() - 1;
public boolean hasNext() {
return current > -1;
}
public T next() {
return get(current--);
}
public void remove() {
// Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
}
public class AdapterMethodIdiom {
public static void main(String[] args) {
ReversibleArrayList<String> ral = new ReversibleArrayList<String>(Arrays.asList("to be or not to be".split(" ")));
// Grabs the ordinary iterator via iterator:
for(String s : ral)
System.out.println(s);
// Hand it the Iterable of your choice
for(String s : ral.reversed())
System.out.println(s);
}
}
如果直接将ral对象置于foreach语句中,将得到(默认的)前向迭代器。但是如果在该对象上调用reversed()方法,就会产生不同的行为。
//: holding/MultiIterableClass.java
// Adding several Adapter Methods.
import java.util.*;
public class MultiIterableClass extends IterableClass{
public Iterable<String> reversed() {
return new Iterable<String>() {
public Iterator<String> iterator() {
return new Iterator<String>() {
int current = words.length - 1;
public boolean hasNext() {
return current > -1;
}
public String next() {
return words[current--];
}
public void move() {
// Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
public Iterable<String> randomzed() {
return new Iterable<String>() {
public Iterator<String> iterator() {
List<String> shuffled = new ArrayList<String>(Arrays.asList(words));
Collections.shuffle(shuffled, new Random(47));
return shuffled.iterator();
}
};
}
public static void main(String[] args) {
MultiIterableClass mic = new MultiIterableClass();
for(String s : mic.reversed())
System.out.println(s);
for(String s : mic.randomzed())
System.out.println(s);
for(String s : mic)
System.out.println(s);
}
}
第二个方法的random()没有创建它自己的Iterator,而是直接返回被打乱的的List中的Iterator。Collection.shuffle()方法没有影响到原来的数组,而只是打乱了shuffled中的引用。之所以这样,只是因为randomized()方法用一个ArrayList将Arrays.asList()方法的结果包装了起来。
如果这个有Arrays.asList()方法产生的List被直接打乱,那么它就会修改底层的数组。
//: holding/ModifyingArrayAsList.java
import java.util.*;
public class ModifyingArraysAsList {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] ia = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
List<Integer> list1 = new ArrayList<Integer>(Arrays.asList(ia));
System.out.println("Before shuffling: " + list1);
Collections.shuffle(list1, rand);
System.out.println("After shuffling: " + list1);
System.out.println("array: " + Arrays.toString(ia));
List<Integer> list2 = Arrays.asList(ia);
System.out.println("Before shuffling: " + list2);
Collections.shuffle(list2, rand);
System.out.println("After shuffling: " + list2);
System.out.println("array: " + Arrays.toString(ia));
}
}
在第一种情况中,Arrays.asList()的输出被传递给了ArrayList()的构造器,这将创建一个应用ia的元素的ArrayList,因此打乱这些引用不会修改该数组。但是,如果直接使用Arrays.asList(ia)的结果,这种打乱就会修改ia的顺序。意识到Arrays.asList()产生的List对象会使用底层数组作为其物理实现是很重要的。只要你执行的操作会修改这个List,并且你不想原来的数组被修改,那么你就应该在另一个容器中创建一个副本。
十四、总结
Java提供了大量持有对象的方式:
1:数组将数字与对象联系起来。它保存类型明确的对象,查询对象时,不需要对结果做类向转换。它可以是多维的,可以保存基本类型的数据。但是,数组一旦生成,其容量就不能改变。
2:Collection保存单一的元素,而Map保存相关联的键值对。有了Java的泛型,你就可以指定容器中存放的对象类型。因此你就不会将错误类型的对象放置到容器中,并且在从容器中获取元素时,不必进行类型转换。各种Collection和各种Map都可以在你向其中添加更多元素时,自动调增尺寸。容器不能持有基本类型,但是自动包装机制会仔细地执行基本类型到容器中所持有的包装器类型之间双向转换。
3:像数组一样,List也建立数字索引与对象的关联,因此,数组和List都是排好序的容器。List能够自动扩充容量。
4:如果要进行大量的随机访问,就使用ArrayList;如果要经常从表中间插入或删除元素,则应该使用LinkedList。
5:各种Queue以及栈的行为,由LinkedList提供支持。
6:Map是一种将对象(而非数字)与对象相关联的设计。HashMap设计用来快速访问;而TreeMap保持键始终处于排序状态,所以没有HashMap快。LinkedHashMap保持元素插入的顺序,但是也通过散列提供了快速访问能力。
7:Set不接受重复元素。HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态。LinkedHashSet以插入顺序保存元素。
8:新程序中不应该使用过时的Vector、HashTable和Stack。