Java核心卷Ⅰ(原书第10版)笔记(中)
第8章 泛型程序设计
8.2 定义简单泛型类
public class Pair<T>
类型变量使用大写形式,且比较短, 这是很常见的。在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母 U 和 S ) 表示“ 任意类型”。
8.3 泛型方法
class ArrayAlg {
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}
// 调用方式
String middle = ArrayAlg.<String>getMiddle("]ohn", "Q.", "Public");
// 绝大多数情况下,方法调用中可以省略 <String> 类型参数。编译器有足够的信息能够推断出所调用的方法。
String middle = ArrayAlg.getHiddle("]ohn", "Q.", "Public");
// PS:如果需要限制泛型类型的实现接口或继承类可以改写成这样
public static <T extends Comparable & Serializable> T getMiddle(T... a){. . }
8.5 泛型代码和虚拟机
虚拟机没有泛型类型对象—所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在 1.0 虚拟机上运行的类文件! 这个向后兼容性在 Java 泛型开发的后期被放弃了。
- 泛型擦除:无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased) 类型变量 , 并替换为限定类型 ,无限定的变量用 Object(
Pair<T>
变为Pair
,T
变为Object
)。- 如果是像这样的切换限定:
class Interval<T extends Serializable & Comparable>
,则原始类型用Serializable
替换T
,编译器在必要时向Comparable
插入强制类型转换(需要用到Comparable
类型则将Serializable
强转)。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。
- 如果是像这样的切换限定:
- 翻译泛型:当程序调用泛型方法或泛型域时,如果擦除返回类型,编译器插入强制类型转换。
Employee buddy = buddies.getFirst();
擦除getFirst
的返回类型后将返回Object
类型。编译器自动插入Employee
的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法
Pair.getFirst
的调用。 - 将返回的
Object
类型强制转换为Employee
类型。
- 泛型转换的事实
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。(书中的例子感觉很模糊,大致意思就是:父子类都有一个同名方法,父类方法接收参数是Object类型,子类是LocalDate类型。实例化一个子类类型,将其赋值给父类类型,调用方法时只会走父类的方法,如果需要“多态”就需要子类重写父类方法,用这个重写的方法调用子类特有的方法。PS:子类都没有重写父类方法,装到父类类型里肯定少了自己特有的方法,肯定调用父类的方法。感觉跟泛型没什么关系。)
- 为保持类型安全性,必要时插入强制类型转换。
8.6 约束与局限性
- 不能用基本类型实例化类型参数。 如:
Pair<double>
应该用Pair<Double>
。 - 运行时类型查询只适用于原始类型(编译时泛型会被擦除)。 则以下判断是否类型相同都是错误的:
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair
stringPair.getClass() == employeePair.getClass() // they are equal, both return Pair.class
- PS:如果一定要获取怎么办?用对象
Class
的getGenericSuperclass()
方法。
- 不能创建参数化类型的数组。
- 如果java允许,那么
Pair<String>[] table = new Pair<String>[10]; // Error
被擦除后,table 将是Pair[]
,可以将它转为Object[]
而数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个 ArrayStoreException 异常,这违背了初衷。 - 可以声明通配类型的数组, 然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
,但这样还是不安全的。 - 需要说明的是,只是不允许创建这些数组,而声明类型为
Pair<String>[]
的变量仍是合法的。不过不能用new Pair<String>[10]
初始化这个变量。总之别用泛型数组就对了。ArrayList 不香吗?
- 如果java允许,那么
- 在可变数组中可以使用泛型。 如:
public static <T> void addAll(Collections coll, T... ts)
。- 实际上参数 ts 是一个数组,包含提供的所有实参,为了调用这个方法,Java 虚拟机必须建立一个 Pair 数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。
- 抑制警告的方法(抑制前请确保操作安全,没有被其他数组对象引用,乱存其他类型参数):
- 增加注解
@SuppressWamings("unchecked")
。 - 在 Java SE 7中,还可以用
@SafeVarargs
标注。
- 不能实例化类型变量。 如
new T(...)
,new T[...]
或T.class
这样的表达式中的类型变量,如果有实例化类型变量的需要,请参考下列代码:
// 在Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p = Pair.makePair(String::new);
/**
* makePair 方法接收一个 Supplier<T>,这是一个函数式接口, 表示一个无参数而且返回类型为 T 的函数
**/
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
// 或者使用以下方法构建:
Pair<String> p = Pair.makePair(String.class);
/**
* 注意,Class类本身是泛型。 例如,String.class 是一个 Class<String> 的实例(事实上,它是唯一的实例)。
* 因此,makePair 方法能够推断出 pair 的类型。
**/
public static <T> Pair<T> makePair(Class<T> cl){
try { return new Pair<>(cl.newInstance(), cl.newInstance()) }
catch (Exception ex) { return null; }
}
- 不能构造泛型数组。 考虑下面的例子,类型擦除会让这个方法永远构造
Comparable[2]
数组:public static <T extends Comparable〉T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // Error
- 如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为
Object[]
,并且在获取元素时进行类型转换。ArrayList
底层数组就是这样的。 - 如果有构造泛型数组的需要,请参考下列代码:
// 让用户提供一个数组构造器表达式
String[] ss = ArrayAlg.minmax(String[]::new,"Tom", "Dick", "Harry");
/**
* minmax 方法使用这个参数生成一个有正确类型的数组:
*/
public static <T extends Comparable〉T[] minmax(IntFunction<T[]> constr, T... a) {
T[] mm = constr.apply(2);
. . .
}
// 比较老式的方法是利用反射, 调用 Array.newlnstance:
public static <T extends Comparable〉T[] minmaxfT... a) {
T[] mm = (T[]) Array.newlnstance(a.getClass().getComponentType() , 2);
}
- 泛型类的静态上下文中类型变量无效。 不能在静态域或方法中引用类型变量,例如:
private static T singlelnstance; // Error
- 不能抛出或捕获泛型类的实例。 既不能抛出也不能捕获泛型类对象。甚至泛型类扩展
Throwable
都是不合法的。public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable
catch
子句中不能使用类型变量,但在异常规范中使用类型变量是允许的:
catch (T e){. . } // Error can't catch type variable
catch (Throwable realCause){t.initCause(realCause); throw t; } // ok
- 可以消除对受查异常的检查。
- 通过使用泛型类、 擦除和
@SuppressWamings
注解,就能消除 Java 类型系统的部分基本限制, 正常情况下,你必须捕获线程 run 方法中的所有受查异常,把它们“包装”到非受查异常中,因为 run 方法声明为不抛出任何受查异常。
- 通过使用泛型类、 擦除和
/**
* 下面把这个代码包装在一个抽象类中。用户可以覆盖 body 方法来提供一个具体的动作。
* 调用 toThread 时, 会得到 Thread 类的一个对象, 它的 run 方法不会介意受查异常。
*/
public abstract class Block{
public abstract void body() throws Exception;
public Thread toThread() {
return new Thread() {
public void run(){
try {
body();
} catch (Throwable t){
Block.<RuntimeException>throwAs(t);
}
}
};
}
@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw (T) e;
}
}
- 注意擦除后的冲突。 泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类, 而这两个接口是同一接口的不同参数化。”
例如,下述代码是非法的:
class Employee implements Coinparable<Employee> { . . . }
class Manager extends Employee implements Comparable<Manager>{ . . .} // Error
Manager
会实现Comparable<Employee>
和Comparable<Manager>
,这是同一接口的不同参数化。这一限制与类型擦除的关系并不十分明确。毕竟,非泛型版本是合法的(去除<...>
)。其原因非常微妙,有可能与合成的桥方法产生冲突。实现了Compamble<X>
的类可以获得一个桥方法:public int compareTo(Object other) { return compareTo((X) other); }
对于不同类型的 X 不能有两个这样的方法。
8.7 泛型类型的继承规则
泛型直接没有继承关系,
Pair<Manager>
不是Pair<Employee>
的子类。
8.8 通配符类型
- 通配符类型
Pair<? extends Employee〉
:表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类。Pair<? super Manager〉
:表示任何泛型 Pair 类型,它的类型参数是 Manager 的超类。Pair<?>
:无限制通配符。Pair<?>
和Pair
本质的不同在于:可以用任意Object
对象调用原始Pair
类的某个方法。
此时会有疑问,无限制通配符
<?>
和没有通配符有什么区别呢?以下代码为例,此部分摘至泛型通配符详解:
// 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null).
public static void addTest(List<?> list) {
Object o = new Object();
// list.add(o); // 编译报错
// list.add(1); // 编译报错
// list.add("ABC"); // 编译报错
list.add(null);
// String s = list.get(0); // 编译报错
// Integer i = list.get(1); // 编译报错
Object o = list.get(2);
}
由于我们根本不知道
list
会接受到具有什么样的泛型List
, 所以除了null
之外什么也不能add
。
还有,List<?>
也不能使用get
方法,只有Object
类型是个例外。原因也很简单,因为我们不知道传入的List
是什么泛型的, 所以无法接受得到的get
,但是Object
是所有数据类型的父类, 所以只有接受他可以。
PS:如果不使用通配符,而是直接List list
,方法内的list
就无法接收到泛型参数了。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。例如,
ArrayList<Pair<T>>
中的T
永远不能捕获ArrayList<Pair<?>>
中的通配符。数组列表可以保存两个Pair<?>
, 分别针对?
的不同类型。
8.9 反射和泛型
public static <T extends Comparable<? super T>> T min(T[] a)
-
为了表达泛型类型声明, 使用java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:
Class
类,描述具体类型。TypeVariable
接口,描述类型变量(如T extends Comparable<? super T>
)。WildcardType
接口,描述通配符(如? super T
)。ParameterizedType
接口,描述泛型类或接口类型(如Comparable<? super T>
)。GenericArrayType
接口,描述泛型数组(如T[]
。)
-
Class<T>
类实现Type
接口,后面四个接口是Type
接口的子类。注意,最后4个子类型是接口,虚拟机将实例化实现这些接口的适当的类。
第9章 集合
9.2 具体的集合
PS:9.2 部分有清晰的类图(P354)。
PS:9.1.5 有清晰的接口图(P352)。
- Java库中的具体集合
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列 |
LinkedList | 一种可以在任何位置进行高效地插入和删除操作的有序序列 |
ArrayDeque | 一种用循环数组实现的双端队列 |
HashSet | 一种没有重复元素的无序集合 |
TreeSet | —种有序集 |
EnumSet | 一种包含枚举类型值的集 |
LinkedHashSet | 一种可以记住元素插入次序的集 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键/值关联的数据结构 |
TreeMap | —种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的映射表 |
LinkedHashMap | 一种可以记住键/值项添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种用 == 而不是用 equals 比较键值的映射表 |
链表
数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动。而链表(linked list) 解决了这个问题。链表是一个有序集合(ordered collection), 每个对象的位置十分重要。
- 在 Java 程序设计语言中, 所有链表实际上都是双向链接的(doubly linked) —— 即每个结点还存放着指向前驱结点的引用。
Iterator
接口中就没有add
方法,而其子接口Listlterator
包含add
方法,其与Collection.add
不同,这个方法不返回boolean
类型的值,它假定添加操作总会改变链表。另外,Listlterator
接口还提供了两个用于反向遍历链表的方法:previous()
和hasPrevious()
。- 在用“光标” 类比时要格外小心。
remove
操作与 BACKSPACE 键的工作方式不太一样。在调用next
之后remove
方法确实与 BACKSPACE 键一样删除了迭代器左侧的元素。但是,如果调用previous
就会将右侧的元素删除掉,并且不能连续调用两次remove()
。
如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了, 就会抛出一个
ConcurrentModificationException
异常。
对于并发修改列表的检测肴一个奇怪的例外。链表只负责跟踪对列表的结构性修改,例如,添加元素、删除元素。set
方法不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用set
方法对现有结点的内容进行修改。在本章后面所介绍的Collections
类的许多算法都需要使用这个功能。
add
方法只依赖于迭代器的位置,而remove
方法依赖于迭代器的状态。set
方法用一个新元素取代调用next
或previous
方法返回的上一个元素。get
方法做了微小的优化:如果索引大于size()/2
就从列表尾端开始搜索元素。list.listIterator(n)
将返回一个迭代器,这个迭代器指向索引为n
的元素前面的位置。也就是说, 调用next
与调用list.get(n)
会产生同一个元素, 但获得这个迭代器的效率比较低。
数组列表
- 需要随机地访问每个元素时,数组更有优势。
- 建议在不需要同步时使用
ArrayList
, 而不要使用Vector
(Vector 类的所有方法都是同步的)。
散列集
有一种众所周知的数据结构,可以快速地査找所需要的对象, 这就是散列表(hash table)。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。在 Java 中,散列表用链表数组实现。每个列表被称为桶 (bucket)。
- 査找表中对象的位置:先计算它的散列码,并与桶的总数取余,所得到的结果就是保存此元素的桶的索引。
- 在 JavaSE 8 中,桶满时会从链表变为平衡二叉树。如果选择的散列函数不当,会产生很多冲突,或者如果有恶意代码试图在散列表中填充多个有相同散列码的值,这样就能提高性能。
如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的 75% ~ 150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是 2 的幂, 默认值为 16 (为表大小提供的任何值都将被自动地转换为 2 的下一个幂)。
- 装填因子。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中, 然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。 例如,如果装填因子为 0.75 (默认值) 而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说, 装填因子为0.75 是比较合理的。
- HashSet。 Java 集合类库提供了一个
HashSet
类,它实现了基于散列表的集。可以用add
方法添加元素。contains
方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。在更改集中的元素时要格外小心。如果元素的散列码发生了改变, 元素在数据结构中的位置也会发生变化。
树集
要使用树集,必须能够比较元素。这些元素必须实现
Comparable
接口(参见 6.1.1节),或者构造集时必须提供一个Comparator
(参见 6.2.2 节和 6.3.8 节)。
TreeSet
类与散列集
十分类似, 不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。正如TreeSet
类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree))。每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
- 从 Java SE 6 起,
TreeSet
类实现了NavigableSet
接口。这个接口增加了几个便于定位元素以及反向遍历的方法。详细信息请参看 API 注释。
队列与双端队列
有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在 Java SE 6 中引入了
Deque
接口,并由ArrayDeque
和LinkedList
类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
优先级队列
与
TreeSet
—样,一个优先级队列既可以保存实现了Comparable
接口的类对象,也可以保存在构造器中提供的Comparator
对象。
优先级队列(priority queue)中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove
方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加 (add) 和删除(remore) 操作, 可以让最小的元素移动到根,而不必花费时间对元素进行排序。
示例: 使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将 1 设为“最高”优先级,所以会将最小的元素删除 )
9.3 映射
集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有要查找元素的精确副本。这不是一种非常通用的査找方式。通常, 我们知道某些键的信息,并想要查找与之对应的元素。映射(map) 数据结构就是为此设计的。映射用来存放键/值对。
9.3.1 基本映射操作
- Java 类库为映射提供了两个通用的实现:
HashMap
和TreeMap
。这两个类都实现了Map
接口。 - 散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。如果无需按照排列顺序访问键,选择散列映射(HashMap),否则选择树映射(TreeMap)。
9.3.3 映射视图
- 如果在键集视图(keySet) 上调用迭代器或
Set
的remove
方法,实际上会从映射中删除这个键和与它关联的值。不过,不能向键集视图增加元素,会抛出UnsupportedOperationException
异常。 - WeakHashMap。 使用弱引用(weak references) 保存键。
9.3.4 弱散列映射
WeakReference
对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的对象, 垃圾回收器用一种特有的方式进行处理。通常, 如果垃圾回收器发现某个特定的对象已经没有他人引用了, 就将其回收。然而,如果某个对象只能由WeakReference
引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap
将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用, 并且已经被收集起来。于是,WeakHashMap
将删除对应的条目。
9.3.5 链接散列集与映射
LinkedHashSet
和LinkedHashMap
类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。LinkedHashMapcK, V>(initialCapacity, loadFactor, true)
,使用以上构造器可以构造一个具有下面特性的散列映射:- 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。每次调用
get
或put
,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。
- 链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。每次调用
访问顺序对于实现高速缓存的“最近最少使用”原则十分重要。例如,可能希望将访问频率高的元素放在内存中, 而访问频率低的元素则从数据库中读取。当在表中找不到元素项且表又已经满时,可以将迭代器加入到表中, 并将枚举的前几个元素删除掉。这些是近期最少使用的几个元素。
甚至可以让这一过程自动化。即构造一个LinkedHashMap
的子类,然后覆盖下面这个方法:
protected boolean removeEldestEntry(Map.Entry<K, V> eldest)
每当方法返回true
时,就添加一个新条目,从而导致删除eldest
条目。另外,还可以对eldest
条目进行评估,以此决定是否应该将它删除。例如,可以检査与这个条目一起存在的时间戳。
9.3.6 枚举集与映射
/**
* EmimSet 是一个枚举类型元素集的高效实现。 由于枚举类型只有有限个实例, 所以 EnumSet 内部用位序列实现。
* 如果对应的值在集中,则相应的位被置为 1。 EnumSet 类没有公共的构造器。可以使用静态工厂方法构造这个集:
*/
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
/**
* EnumMap 是一个键类型为枚举类型的映射。它可以直接且高效地用一个值数组实现。在使用时,需要在构造器中指定键类型
*/
EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
在
EnumSet
的 API 文档中, 将会看到E extends Enum<E>
这样奇怪的类型参数。简单地说, 它的意思是 “ E 是一个枚举类型。” 所有的枚举类型都扩展于泛型Enum
类。例如,Weekday
扩展Enum<Weekday>
。
9.3.7 标识散列映射
类
IdentityHashMap
有特殊的作用。在这个类中,键的散列值不是用hashCode
函数计算的, 而是用System.identityHashCode
方法计算的。 这是Object.hashCode
方法根据对象的内存地址来计算散列码时所使用的方式。 而且, 在对两个对象进行比较时,IdentityHashMap
类使用==
, 而不使用equals
。
也就是说,不同的键对象,即使内容相同,也被视为是不同的对象。 在实现对象遍历算法 (如对象串行化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。
9.4 视图与包装器
通过使用视图(views)可以获得其他的实现了
Collection
接口和Map
接口的对象。映射类的keySet
方法就是一个这样的示例。初看起来,好像这个方法创建了一个新集,并将映射中的所有键都填进去,然后返回这个集。但是,情况并非如此。取而代之的是:keySet
方法返回一个实现Set
接口的类对象,这个类的方法对原映射进行操作。这种集合称为视图。
9.4.1 轻量级集合包装器
Arrays
类的静态方法asList
将返回一个包装了普通 Java 数组的List
包装器。返回的对象不是ArrayList
。它是一个视图对象, 带有访问底层数组的get
和set
方法。改变数组大小的所有方法(例如,与迭代器相关的add
和remove
方法)都会抛出一个Unsupported OperationException
异常。
// 这个方法调用,将返回一个实现了 List 接口的不可修改的对象, 并给人一种包含 n 个元素, 每个元素都像是一个 anObject 的错觉。
Col1ections.nCopies(n, anObject);
// 例如,下面的调用将创建一个包含100个字符串的List,每个串都被设置为“DEFAULT”,存储代价很小。这是视图技术的一种巧妙应用:
List<String> settings = Collections.nCopies(100, "DEFAULT");
/** Collections 类包含很多实用方法, 这些方法的参数和返回值都是集合。不要将它与 Collection 接口混淆起来。**/
// 则将返回一个视图对象。这个对象实现了 Set 接口(与产生 List 的 ncopies 方法不同)。返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。singletonList 方法与 singletonMap 方法类似。
Collections.singleton(anObject);
// 还有一些方法可以生成空集、 列表、 映射, 等等。特别是, 集的类型可以推导得出:
Set<String> deepThoughts = Col1ecti ons.emptySet();
9.4.2 子范围
// 对于有序集和映射, 可以使用排序顺序而不是元素位置建立子范围。SortedSet 接口声明了 3 个方法:
SortedSet<E> subSet(E from, E to)
SortedSet<E> headSet(E to)
SortedSet<E> tail Set(E from)
// 这些方法将返回大于等于 from 且小于 to 的所有元素子集。有序映射也有类似的方法:
SortedMap<K, V> subMap(K from, K to)
SortedMap<K, V> headMap(K to)
SortedMap<K, V> tailMap(K from)
// 返回映射视图, 该映射包含键落在指定范围内的所有元素。
// Java SE 6 引入的 NavigableSet 接口赋予子范围操作更多的控制能力。可以指定是否包括边界:
NavigableSet<E> subSet ( E from, boolean fromlnclusive, E to, boolean toInclusive)
NavigableSet<E> headSet(E to, boolean toInclusive)
Navigab1eSet<E> tail Set(E from, boolean fromInclusive)
9.4.3 不可修改的视图
Collections
还有几个方法,用于产生集合的不可修改视图(unmodifiable views)。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改, 就抛出一个异常,同时这个集合将保持未修改的状态。
// 可以使用下面 8 种方法获得不可修改视图:
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap
不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
由于视图只是包装了接口而不是实际的集合对象, 所以只能访问接口中定义的方法。例如, LinkedList
类有一些非常方便的方法,addFirst
和 addLast
,它们都不是 List
接口的方法,不能通过不可修改视图进行访问。
unmodifiableCollection
方法(与本节稍后讨论的synchronizedCollection
和checked Collection
方法一样)将返回一个集合, 它的equals
方法不调用底层集合的equals
方法。相反, 它继承了Object
类的equals
方法, 这个方法只是检测两个对象是否是同一个对象。 如果将集或列表转换成集合, 就再也无法检测其内容是否相同了。 视图就是以这种方式运行的, 因为内容是否相等的检测在分层结构的这一层上没有定义妥当。视图将以同样的方式处理hashCode
方法。
然而,unmodifiableSet
类和unmodifiableList
类却使用底层集合的equals
方法和hashCode
方法。
9.4.4 同步视图
如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。
类库的设计者使用视图机制来确保常规集合的线程安全, 而不是实现线程安全的集合类。例如,Collections
类的静态 synchronizedMap
方法可以将任何一个映射表转换成具有同步访问方法的 Map
:
Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());
9.4.5 受查视图
“受査” 视图用来对泛型类型发生问题时提供调试支持。 如同第 8 章中所述,实际上将错误类型的元素混入泛型集合中的问题极有可能发生。例如:
ArrayList<String> strings = new ArrayList<>();
ArrayList rawList = strings; // 为了兼容遗留代码,这种写法只有警告,没有异常错误。
rawList.add(new Date()); // 这个集合中加入了不同类型的对象。
这个错误的 add 命令在运行时检测不到。相反,只有在稍后的另一部分代码中调用 get 方法, 并将结果转化为 String 时,这个类才会抛出异常。
// 受査视图可以探测到这类问题。下面定义了一个安全列表,视图的 add 方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个 ClassCastException。这样做的好处是错误可以在正确的位置得以报告:
List<String> safestrings = Collections.checkedList(strings, String.class);
ArrayList rawList = safestrings;
rawList.add(new Date()); // checked list throws a ClassCastException
// 受查视图受限于虚拟机可以运行的运行时检查。 例如,对于 ArrayList <Pair <String>>, 由于虚拟机有一个单独的“ 原始” Pair 类, 所以,无法阻止插入 Pair <Date>。
9.4.6 关于可选操作的说明
通常,视图有一些局限性,即可能只可以读、无法改变大小、只支持删除而不支持插入,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个
UnsupportedOperationException
。
在集合和迭代器接口的 API 文档中,许多方法描述为“可选操作”。这看起来与接口的概念有所抵触。毕竟,接口的设计目的难道不是负责给出一个类必须实现的方法吗?确实,从理论的角度看,在这里给出的方法很难令人满意。一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。不过, 这将会使接口的数量成倍增长,这让类库设计者无法接受。
是否应该将“可选” 方法这一技术扩展到用户的设计中呢?我们认为不应该。尽管集合被频繁地使用, 其实现代码的风格也未必适用于其他问题领域。集合类库的设计者必须解决一组特别严格且又相互冲突的需求。用户希望类库应该易于学习、使用方便,彻底泛型化,面向通用性,同时又与手写算法一样高效。要同时达到所有目标的要求, 或者尽量兼顾所有目标完全是不可能的。但是,在自己的编程问题中, 很少遇到这样极端的局限性。应该能够找到一种不必依靠极端衡量“ 可选的” 接口操作来解决这类问题的方案。
9.5 算法
-
下面是有关的术语定义:
- 如果列表支持
set
方法,则是可修改的。 - 如果列表支持
add
和remove
方法,则是可改变大小的。
- 如果列表支持
-
排序 >
Collections.sort
:- 这个算法的时间复杂度是 O(n logn), 其中 n 为列表的长度。
集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是, 归并排序有一个主要的优点:稳定, 即不需要交换相同的元素。 为什么要关注相同元素的顺序呢? 下面是一种常见的情况。 假设有一个已经按照姓名排列的员工列表。现在,要按照工资再进行排序。 如果两个雇员的工资相等发生什么情况呢?如果采用稳定的排序算法,将会保留按名字排列的顺序。换句话说,排序的结果将会产生这样一个列表,首先按照工资排序,工资相同者再按照姓名排序。
- 这个算法的时间复杂度是 O(n logn), 其中 n 为列表的长度。
-
混排 >
Collections.shuffle
- 这个算法的时间复杂度是 O(n a(n)), n 是列表的长度,a(n)是访问元素的平均时间。
其功能与排序刚好相反,随机地混排列表中元素的顺序。如果提供的列表没有实现
RandomAccess
接口,shuffle
方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
- 这个算法的时间复杂度是 O(n a(n)), n 是列表的长度,a(n)是访问元素的平均时间。
-
二分查找 >
Collections.binarySearch
- 只有采用随机访问,二分査找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分査找就完全失去了优势。因此,如果为
binarySearch
算法提供一个链表,它将自动地变为线性查找。如果
binarySearch
方法返回的数值大于等于 0, 则表示匹配对象的索引。也就是说,c.get(i)
等于在这个比较顺序下的 element。 如果返回负值, 则表示没有匹配的元素。但是,可以利用返回值计算应该将 element 插入到集合的哪个位置,以保持集合的有序性。插入的位置是:
- 只有采用随机访问,二分査找才有意义。如果必须利用迭代方式一次次地遍历链表的一半元素来找到中间位置的元素,二分査找就完全失去了优势。因此,如果为
insertionPoint = -i - 1;
// 这并不是简单的-i, 因为 0 值是不确定的。也就是说,下面这个操作,将把元素插入到正确的位置上:
if (i < 0) {
c.add(-i - 1, element);
}
-
批操作 >
coll1.removeAll(coll2);
coll1.retainAll(coll2);
coll1.removeAll(coll2);
将从coll1中删除coll2中出现的所有元素。coll1.retainAll(coll2);
将从coll1中删除所有未在coll2中出现的元素。
-
集合与数组的转换 >
toArray
// toArray方法返回的数组是一个 Object[] 数组, 不能改变它的类型。实际上, 必须使用 toArray 方法的一个变体形式,提供一个所需类型而且长度为 0 的数组。这样一来,返回的数组就会创建为相同的数组类型:
list.toArray(new String[0]);
// 如果愿意,可以构造一个指定大小的数组,在这种情况下,不会创建新数组:
list.toArray(new String[list.size()]);
你可能奇怪为什么不能直接将一个 Class 对象(如 String.class) 传递到 toArray 方法。 原因是这个方法有“ 双重职责”, 不仅要填充一个已有的数组(如果它足够长), 还要创建一个新数组
9.6 遗留的集合
-
Hashtable
Hashtable
类与HashMap
类的作用一样,实际上,它们拥有相同的接口。与Vector
类的方法一样。Hashtable
的方法也是同步的。如果对同步性或与遗留代码的兼容性没有任何要求,就应该使用HashMap
。如果需要并发访问, 则要使用ConcurrentHashMap
。
-
枚举
- 遗留集合使用
Enumeration
接口对元素序列进行遍历。Enumeration 接口有两个方法,即hasMoreElements
和nextElement
。跟Iterator
接口的hasNext
、next
方法十分相识
- 遗留集合使用
-
属性映射
- 属性映射(property map) 是一个类型非常特殊的映射结构。它有下面 3 个特性:
- 键与值都是字符串。
- 表可以保存到一个文件中,也可以从文件中加载。
- 使用一个默认的辅助表。
- 实现属性映射的 Java 平台类称为 Properties。属性映射通常用于程序的特殊配置选项。
-
栈
- 从 1.0 版开始,标准类库中就包含了
Stack
类,其中有大家熟悉的push
方法和pop
方法。但是,Stack
类扩展于Vector
类, 从理论角度看,Vector
类并不太令人满意,它可以让栈使用不属于栈操作的insert
和remove
方法, 即可以在任何地方进行插入或删除操作,而不仅仅是在栈顶。
- 从 1.0 版开始,标准类库中就包含了
-
位集
- Java 平台的
BitSet
类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。如果需要高效地存储位序列(例如,标志)就可以使用位集。由于位集将位包装在字节里, 所以,使用位集要比使用Boolean
对象的ArrayList
更加高效。
- Java 平台的
BitSet 类提供了一个便于读取、设置或清除各个位的接口。使用这个接口可以避免屏蔽和其他麻烦的位操作。如果将这些位存储在 int 或丨ong 变量中就必须进行这些繁琐的操作。例如,对于一个名为 bucketOfBits 的 BitSet:
// 如果第 i 位处于“ 开” 状态,就返回 true; 否则返回 false。
bucketOfBits.get(i);
// 将第 i 位置为“ 开” 状态。
bucketOfBits.set(i);
// 将第 i 位置为“关” 状态。
bucketOfBits.clear(i);