通过使用视图(views)可以获得其他的实现了 Collection
接口和 Map
接口的对象。Map
中的 KeySet()
就是这样的一个示例。初看起来,好像这个方法创建了一个 Set
,并将 Map
中的所有键都填进去,然后返回这个 Set
。但是,情况并非如此。取而代之的是:keySet()
返回一个实现 Set
接口的类对象,这个类的方法对原 Map
进行操作。这种集合称为视图。
轻量级集合包装器
Arrays
类的静态方法 asList()
将返回一个包装了普通 Java 数组的 List
包装器。该方法可以将数组传递给一个期望得到列表或集合参数的方法:
Employee[] staffs = new Employee[52];
List<Employee> staffList = Arrays.asList(staffs);
List<String> names = Arrays.asList("Amy", "Bob", "Carl");
返回的对象不是 ArrayList
。返回一个视图对象,带有访问底层数组的 get()
和 set()
。需要注意的是,改变数组大小的所有方法,例如与迭代器相关的 add()
和 remove()
都会抛出一个 UnsupportedOperationException
异常。简单来说,asList()
生成了一个实现 List
接口的不可修改的对象。Collections
也有一些静态方法来生成包装器:
Collections.nCopies(n, anObject);
该方法将返回一个实现了 List
接口的不可修改的对象,并给人一种包含 n 个元素,每个元素都像是一个 anObject 的错觉。
例如:创建一个 100 个字符串都为「DEFAULT」的 List
:
List<String> settings = Collection.nCopies(100, "DEFAULT");
还有一些方法会返回一个不可修改的对象:
Collections.singleton(anObject); // Set
Collections.singletonMap(Key, Value); // List
Collections.singletonList(anObject); // Map
上述返回的对象都是不可修改的单元集合,而不需要付出建立数据结构的开销。
当然,还可以生成一些空的 Set
, List
, Map
:
Collections.emptySet();
Collections.emptyList();
Collections.emptyMap();
子范围
可以为集合建立值范围(subrange)视图。假设有一个列表 staff,想从中取出第 10 ~ 19 个元素。可以使用 subList()
来获得一个列表的子范围视图。
List group = staff.subList(10. 20); // 第一个参数包含在内,第二个参数不包含在内。可以理解成 走闭右开
可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。例如,可以删除整个子范围:
group.clear(); // staff reduction // staff 为空
对于 SortedSet
和 SortedMap
还可以根据排序顺序建立子范围:
// SortedSet
SortedSet<E> subSet(E from, E to);
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);
// SottedMap
SortedMap<E> subMap(E from, E to);
SortedMap<E> headMap(E to);
SortedMap<E> tailMap(E from);
不可修改视图
Collections
还有几个方法,用于产生集合的不可修改视图(unmodifiableviews)。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态。
Collections.unmodifiableCollection(Collection);
Collections.unmodifiableList(List);
Collections.unmodifiableSet(Set);
Collections.unmodifiableSortedSet(SortedSet);
Collections.unmodifiableNavigableSet(NavigableSet);
Collections.unmodifiableMap(Map);
Collections.unmodifiableSortedMap(SortedMap);
Collections.unmodifiableNavigableMap(NavigableMap);
比如,你先查看某个集合的代码,但是不想改变他,使用不可修改视图就可以帮你完成这个操作:
List<Stirng> staff = new LinkedList();
...
LookAt(Collections.ummodifiableList(staff));
不可修改视图并不是集合本身不可修改。仍然可以通过集合的原始引用(在这里是 staff )对集合进行修改。并且仍然可以让集合的元素调用更改器方法。
由于视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的方法。
同步视图
如果由多个线程访问集合,就必须确保集合不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。
例如,Collections
类的静态 synchronizedMap()
可以将任何一个映射表转换成具有同步访问方法的 Map
:
Map<String, Employee> map = Collections.synchronizeMap(new HashMap<String, Employee>());
现在,就可以由多线程访问 map 对象了。像 get()
和 put()
这类方法都是同步操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成。
检查视图
检查视图用来对泛型类型发生问题时提供调试支持。
例如,将错误类型的元素混入泛型集合中的问题极有可能发生:
List<String> strings = new ArrayList<>();
List rawList = strings; // warning only, not an error, for compatibility with legacy code
rawList.add(new Date()); // now strings contains a Date object!
上述问题在 add()
并不会检测到。但是根据泛型的擦拭法,但在下面的代码中调用 strings.get(0)
时,我们知道它会将其结果转换成 String
,这个时候就会抛出 ClassCastException
异常。
检查视图可以解决上述问题:
List<String> strings = new ArrayList<>();
List<String> safeString = Collections.checkedList(strings, String.class);
List rawList = safeString;
rawList.add(new Date()); // checked list throws a CalssCastException
检查视图依然不会再编译阶段被检查出来,虚拟机运行时,他会检查 add()
插入的对象是否属于给定的类。如果属于,就立即抛出 ClassCastExcpetion
。这样做的好处是能过快速的找到正确的错误位置。
受查视图受限于虚拟机可以运行的运行时检查。例如,对于
ArrayList<Pair<String>>
,由于虚拟机有一个单独的「原始」Pair
类,所以,无法阻止插入Pair<Date>
。说到底还是擦拭法在搞怪。
关于可操作的说明
通常,视图有一些局限性,即:只能读、无法改变大小、只支持删除而不支持插入,这些与映射的键视图情况相同。如果试图进行不恰当的操作,受限制的视图就会抛出一个 UnsupportedOperationException
。
在集合和迭代器接口的 API 文档中,许多方法描述为「可选操作」。
这看起来与接口的概念有所抵触。毕竟,接口的设计目的难道不是负责给出一个类必须实现的方法吗?确实,从理论的角度看,在这里给出的方法很难令人满意。一个更好的解决方案是为每个只读视图和不能改变集合大小的视图建立各自独立的两个接口。不过,这将会使接口的数量成倍增长,这让类库设计者无法接受。
是否应该将「可选」方法这一技术扩展到用户的设计中呢?我们认为不应该。尽管集合被频繁地使用,其实现代码的风格也未必适用于其他问题领域。集合类库的设计者必须解决一组特别严格且又相互冲突的需求。用户希望类库应该易于学习、使用方便,彻底泛型化,面向通用性,同时又与手写算法一样高效。要同时达到所有目标的要求,或者尽量兼顾所有目标完全是不可能的。但是,在自己的编程问题中,很少遇到这样极端的局限性。应该能够找到一种不必依靠极端衡量「可选的」接口操作来解决这类问题的方案。