第17章 容器深入研究
17.2 填充容器
package com.cy.container; import java.util.*; class StringAddress { private String s; public StringAddress(String s) { this.s = s; } public String toString() { return super.toString() + " " + s; } } public class FillingLists { public static void main(String[] args) { List<StringAddress> list= new ArrayList<StringAddress>(Collections.nCopies(4, new StringAddress("Hello"))); System.out.println(list); Collections.fill(list, new StringAddress("World!")); System.out.println(list); } } /* Output: (Sample) [StringAddress@82ba41 Hello, StringAddress@82ba41 Hello, StringAddress@82ba41 Hello, StringAddress@82ba41 Hello] [StringAddress@923e30 World!, StringAddress@923e30 World!, StringAddress@923e30 World!, StringAddress@923e30 World!] *///:~
这个示例展示了两种用对单个对象的引用来填充Collection的方式,第一种是使用
Collections.nCopies()创建传递给构造器的List,这里填充的是ArrayList.
StringAddress的toString()方法调用Object.toString()并产生该类的名字,后面紧跟该对象的
散列码的无符号十六进制表示.(通过hashCode()生成的)。从输出中你可以看到所有引用都被设
置为指向相同的对象,在第二种方法的Collection.fill()被调用之后也是如此。fill()方法的用处更
有限,因为它只能替换已经在List中存在的元素,而不能添加新的元素。
17.4.1 未获支持的操作
最常见的未获支持的操作,都来源于背后由固定尺寸的数据结构支持的容器。当你用
Arrays.asList()将数组转换为List时,就会得到这样的容器。你还可以通过使用Collections类中
"不可修改"的方法,选择创建任何会抛出UnsupportedOperationException的容器(包括Map ) 。
下面的示例包括这两种情况:
package com.cy.container; import java.util.*; public class Unsupported { static void test(String msg, List<String> list) { System.out.println("--- " + msg + " ---"); Collection<String> c = list; Collection<String> subList = list.subList(1,8); // Copy of the sublist: Collection<String> c2 = new ArrayList<String>(subList); try { c.retainAll(c2); } catch(Exception e) { System.out.println("retainAll(): " + e); } try { c.removeAll(c2); } catch(Exception e) { System.out.println("removeAll(): " + e); } try { c.clear(); } catch(Exception e) { System.out.println("clear(): " + e); } try { c.add("X"); } catch(Exception e) { System.out.println("add(): " + e); } try { c.addAll(c2); } catch(Exception e) { System.out.println("addAll(): " + e); } try { c.remove("C"); } catch(Exception e) { System.out.println("remove(): " + e); } // The List.set() method modifies the value but // doesn't change the size of the data structure: try { list.set(0, "X"); } catch(Exception e) { System.out.println("List.set(): " + e); } } public static void main(String[] args) { List<String> list = Arrays.asList("A B C D E F G H I J K L".split(" ")); test("Modifiable Copy", new ArrayList<String>(list)); test("Arrays.asList()", list); test("unmodifiableList()", Collections.unmodifiableList(new ArrayList<String>(list))); } } /* Output: --- Modifiable Copy --- --- Arrays.asList() --- retainAll(): java.lang.UnsupportedOperationException removeAll(): java.lang.UnsupportedOperationException clear(): java.lang.UnsupportedOperationException add(): java.lang.UnsupportedOperationException addAll(): java.lang.UnsupportedOperationException remove(): java.lang.UnsupportedOperationException --- unmodifiableList() --- retainAll(): java.lang.UnsupportedOperationException removeAll(): java.lang.UnsupportedOperationException clear(): java.lang.UnsupportedOperationException add(): java.lang.UnsupportedOperationException addAll(): java.lang.UnsupportedOperationException remove(): java.lang.UnsupportedOperationException List.set(): java.lang.UnsupportedOperationException *///:~
因为Arrays.asList()会生成一个List,它基于一个固定大小的数组, 仅支持那些不会改变数组
大小的操作,对它而言是有道理的。任何会引起对底层数据结构的尺寸进行修改的方法都会产生
一个UnsupportedOperationException异常,以表示对未获支持操作的调用(一个编程错误).
注意,应该把Arrays.asList()的结果作为构造器的参数传递给任何Collection (或者使用
addAll()方法,或Collections.addAll()静态方法) ,这样可以生成允许使用所有的方法的普通容
器一一这在main()中的第一个对test()的调用中得到了展示, 这样的调用会产生新的尺寸可调的
底层数据结构。Collections类中的"不可修改"的方法将容器包装到了一个代理中,只要你执
行任何试图修改容器的操作,这个代理都会产生UnsupportedOperationException异常。使用这
些方怯的目标就是产生"常量"容器对象。"不可修改"的Collections方法的完整列表将在稍后
介绍。
test()中的最后一个try语句块将检查作为List的一部分的set()方法.这很有趣,因为你可以
看到"未获支持的操作"这一技术的粒度来的是多么方便-一所产生的"接口"可以在
Arrays.asList()返回的对象和Collections.unmodifiableList()返回的对象之间,在一个方法的粒
度上产生变化。Arrays.asList()返回固定尺寸的List,而Collections.unmodifiableList()产生不可
修改的列表.正如从输出中所看到的.修改Arrays.asList()返回的List中的元素是可以的,因为
这没有违反该List“尺寸固定"这一特性.但是很明显, unmodlfiableList()的结果在任何情况
下都应该不是可修改的。如果使用的是接口,那么还需要两个附加的接口,一个具有可以工作
的set()方法,另外一个没有, 因为附加的接口对于Collection的各种不可修改的子类型来说是必
需的。
17.6 Set和存储顺序
在HashSet上打星号表示,如果没有其他的限制,这就应该是你默认的选择,因为它对速度
进行了优化。
定义hashCode()的机制将在本章稍后进行介绍。你必须为散列存储和树型存储都创建一个
equals()方法,但是hashCode()只有在这个类将会被置于HashSet (这是有可能的,因为它通常
是你的Set实现的首选)或者LinkedHashSet中时才是必需的。但是,对于良好的编程风格而言,
你应该在覆盖equals()方法时,总是同时覆盖hashCode()方法。
package com.cy.container; import java.util.*; class SetType { int i; public SetType(int n) { i = n; } public boolean equals(Object o) { return o instanceof SetType && (i == ((SetType)o).i); } public String toString() { return Integer.toString(i); } } class HashType extends SetType { public HashType(int n) { super(n); } public int hashCode() { return this.i; } } class TreeType extends SetType implements Comparable<TreeType> { public TreeType(int n) { super(n); } @Override public int compareTo(TreeType arg) { return (arg.i < i ? -1 : (arg.i == i ? 0 : 1)); } } public class TypesForSets { static <T> Set<T> fill(Set<T> set, Class<T> type) { try { for(int i = 0; i < 10; i++){ T t = type.getConstructor(int.class).newInstance(i); set.add(t); } } catch(Exception e) { throw new RuntimeException(e); } return set; } static <T> void test(Set<T> set, Class<T> type) { fill(set, type); fill(set, type); // Try to add duplicates fill(set, type); System.out.println(set); } public static void main(String[] args) { test(new HashSet<HashType>(), HashType.class); test(new LinkedHashSet<HashType>(), HashType.class); test(new TreeSet<TreeType>(), TreeType.class); // Things that don't work: test(new HashSet<SetType>(), SetType.class); test(new HashSet<TreeType>(), TreeType.class); test(new LinkedHashSet<SetType>(), SetType.class); test(new LinkedHashSet<TreeType>(), TreeType.class); try { test(new TreeSet<SetType>(), SetType.class); } catch(Exception e) { System.out.println(e.getMessage()); } try { test(new TreeSet<HashType>(), HashType.class); } catch(Exception e) { System.out.println(e.getMessage()); } } } /* Output: (Sample) [2, 4, 9, 8, 6, 1, 3, 7, 5, 0] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] [9, 9, 7, 5, 1, 2, 6, 3, 0, 7, 2, 4, 4, 7, 9, 1, 3, 6, 2, 4, 3, 0, 5, 0, 8, 8, 8, 6, 5, 1] [0, 5, 5, 6, 5, 0, 3, 1, 9, 8, 4, 2, 3, 9, 7, 3, 4, 4, 0, 7, 1, 9, 6, 2, 1, 8, 2, 8, 6, 7] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] java.lang.ClassCastException: SetType cannot be cast to java.lang.Comparable java.lang.ClassCastException: HashType cannot be cast to java.lang.Comparable *///:~
HashType继承自SetType , 并且添加丁hashCode()方法,该方法对于放置到Set的散列实现
中的对象来说是必需的。
TreeType实现了Comparable接口,如果一个对象被用于任何种类的排序容器中,例如
SortedSet (TreeSet是其唯一实现) ,那么它必须实现这个接口。
从输出中可以看到, HashSet以某种神秘的顺序保存所有的元素(这将在本章稍后进行解释) ,
LinkedHashSet按照元素插入的顺序保存元素,而TreeSet按照排序顺序维护元素(按照
compareTo()的实现方式,这里维护的是降序)。
如果我们尝试着将没有恰当地支持必需的操作的类型用于需要这些方法的Set ,那么就会有
大麻烦了。对于没有重新定义hashCode()方法的SetType或TreeType,如果将它们放置到任何散
列实现中都会产生重复值,这样就违反了Set的基本契约。这相当烦人,因为这甚至不会有运行
时错误。但是,默认的hashCode()是合法的,因此这是合法的行为,即便它不正确。确保这种程
序的正确性的唯一可靠方法就是将单元测试合并到你的构建系统中.
如果我们尝试着在TreeSet中使用没有实现Comparable的类型,那么你将会得到更确定的结
果: 在TreeSet试图将该对象当作Comparable使用时,将抛出一个异常。
17.6.1 SortedSet
SortedSet中的元素可以保证处于排序状态,这使得它可以通过在SortedSet接口中的下列方
法提供附加的功能:
Comparator comparator()返回当前Set使用的Comparator , 或者返回null ,表示以自然方式排序。
Object flrst() 返回容器中的第一个元素。
Object last() 返回容器中的最末一个元素。
SortedSet subSet(fromElement, toElement) 生成此Set的子集, 范围从fromElement (包含)到toEIement (不包含)。
SortedSet headSet(toElement) 生成此Set的子集,由小于toElement的元素组成。
SortedSet tailSet(fromElement) 生成此Set的子集,由大于或等于fromElement的元素组成。
package com.cy.container; import java.util.*; import static com.java.util.Print.*; public class SortedSetDemo { public static void main(String[] args) { SortedSet<String> sortedSet = new TreeSet<String>(); Collections.addAll(sortedSet,"one two three four five six seven eight".split(" ")); print(sortedSet); String low = sortedSet.first(); String high = sortedSet.last(); print(low); print(high); Iterator<String> it = sortedSet.iterator(); for(int i = 0; i <= 6; i++) { if(i == 3) low = it.next(); if(i == 6) high = it.next(); else it.next(); } print(low); print(high); print(sortedSet.subSet(low, high)); print(sortedSet.headSet(high)); print(sortedSet.tailSet(low)); } } /* Output: [eight, five, four, one, seven, six, three, two] eight two one two [one, seven, six, three] [eight, five, four, one, seven, six, three] [one, seven, six, three, two] *///:~
注意, SortedSet的意思是"按对象的比较函数对元素排序",而不是指"元素插入的次序"。
插入顺序可以用LinkedHashSet来保存。