如果一个程序只包含固定数量的且其生命期都是已知的对象 那么这是一个非常简单的程序
泛型和类型安全的容器
使用Java SE5之前的容器的一个主要问题就是编译器允许你向容器中插入不正确的类型
在本例中 Apple和Orange都放置在了容器中 然后将它们取出 正常情况下 Java编译器会报告警告信息 因为这个示例没有使用泛型 在这里 我们使用Java SE5所特有的注解来抑制了警告信息 注解以 @ 符号开头 可以接受参数 这里的@SuppressWarnings注解及其参数表示只有有关 不受检查的异常 的警告信息应该被抑制
要想定义用来保存Apple对象的ArrayList 你可以声明ArrayList 而不仅仅只是ArrayList 其中尖括号括起来的是类型参数(可以有多个) 它指定了这个容器实例可以保存的类型 通过使用泛型 就可以在编译期防止将错误类型的对象放置到容器中
现在 编译器可以阻止你将Orange放置到apples中 因此它变成了一个编译期错误 而不再是运行时错误
当你指定了某个类型作为泛型参数时 你并不仅限于只能将该确切类型的对象放置到容器中 向上转型也可以像作用于其他类型一样作用于泛型
程序的输出是从Object默认的toString()方法产生的 该方法将打印类名 后面跟随该对象的散列码的无符号十六进制表示(这个散列码是通过hashCode()方法产生的)
基本概念
Java容器类类库的用途是 保存对象 并将其划分为两个不同的概念
添加一组元素
Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数) 并将其转换为一个List对象 Collection.addAll()方法接受一个Collection对象 以及一个数组或是一个用逗号分隔的列表 将元素添加到Collection中
Collection的构造器可以接受另一个Collection 用它来将自身初始化 因此你可以使用ArrayList.List()来为这个构造器产生输入 但是 Collection.addAll()方法运行起来要快得多 而且构建一个不包含元素的Collection 然后调用Collection.addAll()这种方式很方便 因此它是首选方式
Collection.addAll()成员方式只能接受另一个Collection对象作为参数 因此它不如ArrayList.asList()或Collection.addAll()灵活 这两个方法使用的都是可变参数列表
你也可以直接使用ArrayList.asList()的输出 将其当作List 但是在这种情况下 其低层表示的是数组 因此不能调整尺寸 如果你试图用add()或delete()方法在这种列表中添加或删除元素 就有可能会引发去改变数组尺寸的尝试 因此你将在运行时获得 Unsupported Operation(不支持的操作)错误
Arrays.asList()方法的限制是它对所产生的List的类型做出了最理想的假设 而并没有注意你对它会赋予什么样的类型 有时这就会引发问题
当试图创建snow2时 Arrays.asList()中只有Powder类型 因此它会创建List而不是List 尽管Collection.addAll()工作的很好 因为它从第一个参数中了解到了目标类型是什么
正如你从创建snow4的操作中所看到的 可以在Arrays.asList()中间插入一条 线索 以告诉编译器对于由Arrays.asList()产生的List类型 实际的目标类型应该是什么 这称为显式类型参数说明
容器的打印
你必须使用Arrays.toString()来产生数组的可打印表示 但是打印容器无需任何帮助
这里展示了Java容器类库中的两种主要类型 它们的区别在于容器中每个 槽 保存的元素个数 Collection在每个槽中只能保存一个元素 此类容器包括 List 它以特定的顺序保存一组元素 Set 元素不能重复 Queue 只允许在容器的一 端 插入对象 并从另外一 端 移除对象 Map在每个槽内保存了两个对象 即键和与之相关联的值
HashSet与HashMap提供了最快的查找技术 没有按照任何明显的顺序来保存元素 TreeSet与TreeMap按照比较结果的升序保存对象 LinkedHashSet与LinkedHashMap按照被添加的顺序保存对象
Map(也被称为关联数组)使得你可以用键来查找对象 就像一个简单的数据库 键所关联的对象称为值 对于每一个键 Map只接受存储一次
List
List承诺可以将元素维护在特定的序列中 List接口在Collection的基础上添加了大量的方法 使得可以在List的中间插入和移除元素
有两种类型的List
与数组不同 List允许在它被创建之后添加元素 移除元素 或者自我调整尺寸 这正是它的重要价值所在 一种可修改的序列
迭代器
迭代器(也是一种设计模式)是一个对象 它的工作是遍历并选择序列中的对象 而客户端程序员不必知道或关心该序列底层的结构 此外 迭代器通常被称为轻量级对象 创建它的代价小 因此 经常可以见到对迭代器有些奇怪的限制
例如 Java的Iterator只能单向移动 这个Iterator只能用来
Iterator还可以移除由next()产生的最后一个元素 这意味着在调用remove()之前必须先调用next()
现在考虑创建一个display()方法 它不必知晓容器的确切类型
display()方法不包含任何有关它所遍历的序列的类型信息 而这也展示了Iterator的真正威力 能够将遍历序列的操作与序列底层的结构分离 正由于此 我们有时会说 迭代器统一了对容器的访问方式
ListIterator
ListIterator是一个更加强大的Iterator的子类型 它只能用于各种List类的访问 尽管Iterator只能向前移动 但是ListIterator可以双向移动 它还可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引 并且可以使用set()方法替换它访问过的最后一个元素 你可以通过调用listIterator()方法产生一个指向List开始处的ListIterator 并且还可以通过调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator
Pet.randomPet()方法用来替换在列表中从位置3开始向前的所有Pet对象
LinkedList
LinkedList也像ArrayList一样实现了基本的List接口 但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效 但在随机访问操作方面却要逊色一些
LinkedList还添加了可以使其用作栈 队列或双端队列的方法
Stack
栈 通常是指 后进先出(LIFO) 的容器 有时栈也被称为叠加栈 因为最后 压入 栈的元素 第一个 弹出 栈
LinkedList具有能够直接实现栈的所有功能的方法 因此可以直接将LinkedList作为栈使用
类名之后的告诉编译器这将是一个参数化类型 而其中的类型参数 即在类被使用时将会被实际类型替换的参数 就是T 大体上 这个类是在声明 我们在定义一个可以持有T类型对象的Stack Stack是用LinkedList实现的 而LinkedList也被告知它将持有T类型对象
如果你只需要栈的行为 这里使用继承就不合适了 因为这样会产生具有LinkedList的其他所有方法的类
如果你想在自己的代码中使用这个Stack类 当你在创建其实例时 就需要完整指定包名 或者更改这个类的名称 否则 就有可能与java.util包中的Stack发生冲突
尽管已经有了java.util.Stack 但是LinkedList可以产生更好的Stack 因此net.mindview.util.Stack所采用的方式更是可取的
你还可以通过显示的导入来控制对 首选 Stack实现的选择
import net.mindview.util.Stack;
现在 任何对Stack的引用都将选择net.mindview.util版本 而在选择java.util.Stack时 必须使用全限定名称
Set
Set不保存重复的元素 如果你试图将相同对象的多个实例添加到Set中 那么它就会阻止这种重复现象 Set中最常被使用的是测试归属性 你可以很容易地询问某个对象是否在某个Set中 正因如此 查找就成为了Set中最重要的操作 因此你通常都会选择一个HashSet的实现 它专门对快速查找进行了优化
Set是基于对象的值来确定归属性的
TreeSet将元素存储在红-黑树数据结构中 而HashSet使用的是散列函数 LinkedHashSet因为查询速度的原因也使用了散列 但是看起来它使用了链表来维护元素的插入顺序
如果你想对结果排序 一种方式是使用TreeSet来代替HashSet
TextFile继承自List 其构造器将打开文件 并根据正则表达式 \W+ 将其断开为单词 这个正则表达式表示 一个或多个字母 所产生的结果传递给了TreeSet的构造器 它将把List中的内容添加到自身中 由于它是TreeSet 因此其结果是排序的 在本例中 排序是按字典序进行的 因此大写和小写字母被划分到了不同的组中 如果你想要按照字母序排序 那么可以向TreeSet的构造器传入String.CASE_INSENTIVE_ORDER比较器(比较器就是建立排序顺序的对象)
Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏 例如 考虑一个程序 它将用来检查Java的Random类的随机性 理想状态下 Random可以将产生理想的数字分布 但要想测试它 则需要生成大量的随机数 并对落入各种不同范围的数字进行计数 Map可以很容易地解决该问题 在本列中 键是由Random产生的数字 而值是该数字出现的次数
下面的示例允许你使用一个String描述来查找Pet 它还展示了你可以使用怎样的方法通过使用containsKey()和containsValue()来测试一个Map 以便查看它是否包含某个键或某个值
Map与数组和其他的Collection一样 可以很容易地扩展到多维 而我们只需将其值设值为Map(这些Map的值可以是其他容器 甚至是其他Map) 因此 我们能够很容易地将容器组合起来从而快速地生成强大的数据结构 例如 假设你正在跟踪拥有多个宠物的人 你所需只是一个Map
Map可以返回它的键的Set 它的值的Collection 或者它的键值对的Set keySet()方法产生了由在petPeople中的所有键组成的Set 它在foreach语句中被用来迭代遍历该Map
Queue
队列是一个典型的先进先出(FIFO)的容器 即从容器的一端放入事物 从另一端取出 并且事物放入容器的顺序与取出的顺序是相同的 队列常被当作一种可靠的将对象从程序的某个区域传输到另一个区域的途径
LinkedList提供了方法以支持队列的行为 并且它实现了Queue接口 因此LinkedList可以用作Queue的一种实现 通过将LInkedList向上转型为Queue
Queue接口窄化了对LinkedList的方法的访问权限 以使得只有恰当的方法才可以使用 因此 你能够访问的LinkedList的方法会变少(这里实际上可以将queue转型回LinkedList 不鼓励这么做)
PriorityQueue
先进先出描述了最典型的队列规则 队列规则是指在给定一组队列中的元素的情况下 确定下一个弹出队列的元素的规则 先进先出声明的是下一个元素应该是等待时间最长的元素
优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)
当你在PriorityQueue上调用offer()方法来插入一个对象时 这个对象会在队列中被排序 默认的排序将使用对象在队列中的自然顺序 但是你可以通过提供自己的Comparator来修改这个顺序 PriorityQueue可以确保当你调用peek() poll()和remove()方法时 获取的元素将是队列中优先级最高的元素
如果你想在PriorityQueue中使用自己的类 就必须包括额外的功能以产生自然顺序 或者必须提供自己的Comparator
Collection和Iterator
Collection是描述所有序列容器的共性的根接口 它可能会被认为是一个 附属接口 即因为要表示其他若干个接口的共性而出现的接口 另外 java.util.AbstractCollection类提供了Collection的默认实现 使得你可以创建AbstractCollection的子类型 而其中没有不必要的代码重复
实现Collection就意味着需要提供iterator()方法
当你要实现一个不是Collection的外部类时 由于让它去实现Collection接口可能非常困难或麻烦 因此使用Iterator就会变得非常吸引人
如果你实现Collection 就必须实现iterator() 并且只拿实现iterator()与继承AbstractCollection相比 花费的代价只有略微减少 但是 如果你的类已经继承了其他的类 那么你就不能再继承AbstractCollection了 在这种情况下 要实现Collection 就必须实现该接口中的所有方法 此时 继承并提供创建迭代器的能力就会显得容易得多了
生成Iterator是将队列与消费队列的方法连接在一起耦合度最小的方式 并且与实现Collection相比 它在序列类上所施加的约束也少得多
Foreach与迭代器
foreach语法主要用于数组 但是它也可以应用于任何Collection对象
之所以能够工作 是因为Java SE5引入了新的被称为Iterable的接口 该接口包含一个能够产生Iterator的iterator()方法 并且Iterable接口被foreach用来在序列中移动 因此如果你创建了任何实现Iterable的类 都可以将它用于foreach语句中
在Java SE5中 大量的类都是Iterable类型 主要包括所有的Collection类(但是不包括各种Map) 下面的代码可以显示所有的操作系统环境变量
foreach语句可以用于数组或其他任何Iterable 但是这并不意味着数组肯定也是一个Iterable 而任何自动包装也不会自动发生
尝试把数组当作一个Iterable参数传递会导致失败 这说明不存在任何从数组到Iterable的自动转换 你必须手工执行这种转换
适配器方法惯用法
如果现有一个Iterable类 你想要添加一种或多种在foreach语句中使用这个类的方法 应该怎么做呢 例如 假设你希望可以选择以向前的方向或是向后的方向迭代一个单词列表 如果直接继承这个类 并覆盖iterator()方法 你只能替换现有的方法 而不能实现选择
一种解决方案是所谓适配器方法的惯用法
通过使用这种方式 可以在IterableClass.java示例中添加两种适配器方法
从输出中可以看到 Collection.shuffle()方法没有影响到原来的数组 而只是打乱了shuffled中的引用 之所以这样 只是因为randomized()方法用一个ArrayList将Arrays.asList()方法的结果包装了起来 如果这个由Arrays.asList()方法产生的List被直接打乱 那么它就会修改底层的数组 就像下面这样
在第一种情况中 Arrays.asList()的输出被传递给了ArrayList()的构造器 这将创建一个引用ia的元素的ArrayList 因此打乱这些引用不会修改该数组 但是 如果直接使用Arrays.asList(ia)的结果 这种打乱就会修改ia的顺序 意识到Arrays.asList()产生的List对象会使用底层数组作为其物理实现是很重要的 只要你执行的操作会修改这个List 并且你不想原来的数组被修改 那么你就应该在另一个容器中创建一个副本
总结
Java提供了大量持有对象的方式
简单的容器分类
点线框表示接口 实线框表示普通的(具体的)类 带有空心箭头的点线表示一个特定的类实现了一个接口 空心箭头表示某个类可以生成箭头所指向类的对象 例如 任意的Collection可以生成Iterator 而List可以生成ListIterator(也能生成普通的Iterator 因为List继承自Collection)
下面的示例展示了各种不同的类在方法上的差异
可以看到 除了TreeSet之外的所有Set都拥有与Collection完全一样的接口 List和Collection存在着明显的不同 尽管List所要求的方法都在Collection中 另一方面 在Queue接口中的方法都是独立的 在创建具有Queue功能的实现时 不需要使用Collection方法 最后 Map和Collection之间的唯一重叠就是Map可以使用entrySet()和values()方法来产生Collection