当Set使用自己创建的类型时,存储的顺序如何维护,在不同的Set实现中会有不同,而且它们对于在特定的Set中放置的元素类型也有不同的要求:
Set(interface) | 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set和Collection具有完全一样的接口,但Set不保证元素的顺序。 |
HashSet* | 为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()方法。 |
TreeSet | 一种可维护元素顺序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口。 |
LinkedHashSet | 具有HashSet的查询速度,而且内部使用链表维护元素的顺序(插入的顺序),于是在使用迭代器遍历Set时,结果会按元素插入的顺序显示。元素也必须定义hashCode()方法。 |
在HashSet打*号,表示如果没有其他的限制,这就应该是默认的选择,因为它的速度很快。
你必须为散列存储和树形存储都定义一个equals()方法,但是hashCode()只有在这个类将会被放入HashSet或者LinkedHashSet中才是必须的。但是对于良好的变成风格而言,你应该在覆盖equals()方法的同时覆盖hashCode()方法。下面的示例演示了为了成功的使用特定的Set实现类而必须定义的方法:
import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.TreeSet; class SetType { int i; public SetType (int n) { i = n; } public boolean equals(Object obj) { return obj instanceof SetType && (i == ((SetType)obj).i); } public String toString() { return Integer.toString(i); } } //能正常被HashSet,LinkedHashSet使用的类型 class HashSetType extends SetType { public HashSetType(int n) { super(n); } public int hashCode() { return i; } } //能正常被TreeSet使用的类型 class TreeSetType extends SetType implements Comparable<TreeSetType>{ public TreeSetType(int n) { super(n); } public int compareTo(TreeSetType o) { return (o.i < i ? -1 : (o.i > i ? 1 : 0));//降序排列 } } public class TypesForSets { static <T> Set<T> fill(Set<T> set, Class<T> clazz) { try { for (int i = 0; i < 10; i++) { set.add(clazz.getConstructor(int.class).newInstance(i)); } } catch (Exception e) { throw new RuntimeException(e); } return set; } static <T> void test(Set<T> set, Class<T> clazz) { fill(set, clazz); fill(set, clazz);//尝试重复向Set中添加 fill(set, clazz); System.out.println(set); } public static void main(String[] args) { //正确的装法 System.out.println("---------Correct----------"); test(new HashSet<HashSetType>(), HashSetType.class); test(new LinkedHashSet<HashSetType>(), HashSetType.class); test(new TreeSet<TreeSetType>(), TreeSetType.class); //错误的装法 System.out.println("---------Wrong----------"); test(new HashSet<SetType>(), SetType.class); test(new HashSet<TreeSetType>(), TreeSetType.class); test(new LinkedHashSet<SetType>(), SetType.class); test(new LinkedHashSet<TreeSetType>(), TreeSetType.class); try { test(new TreeSet<SetType>(), SetType.class); } catch (Exception e) { System.out.println(e.getMessage()); } try { test(new TreeSet<SetType>(), SetType.class); } catch (Exception e) { System.out.println(e.getMessage()); } } }
执行结果(样例):
---------Correct---------- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] ---------Wrong---------- [1, 1, 5, 0, 7, 3, 4, 6, 5, 9, 8, 0, 7, 5, 9, 6, 8, 2, 4, 1, 7, 4, 3, 6, 8, 2, 2, 0, 9, 3] [3, 5, 1, 8, 5, 4, 1, 0, 9, 3, 0, 8, 5, 7, 6, 9, 7, 3, 4, 0, 7, 6, 2, 1, 2, 8, 6, 9, 4, 2] [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: SetType cannot be cast to java.lang.Comparable
为了证明哪些方法是对于某种特殊的Set是必须的,同时也为了避免代码重复,我们创建了三个类型。基类SetType只存储了一个int,并且通过toString()方法产生它的值。因为所有在Set中存储的类型都必须具有equals()方法,因此在基类中也有该方法。
HashSetType继承自SetType,并添加了hashCode()方法,该方法对于放入Set的散列实现中的对象来说是必需的。
TreeType实现了Comparable接口,如果一个对象被用于任何种类的排序容器中,例如TreeSet,那么它必须实现这个接口。注意:在compareTo()方法中,我没有使用简洁明了的形式return o.i-i,因为这是一个常见的编程错误,它只有在i和i2都是无符号的int(如果Java确实有unsigned关键字的话)才能正常工作。对于有符号的int,它就会出错,因为int不够大,不足以表现两个有符号的int的差。例如o.i是很大的正数而且i是很小的负数时,i-j就会溢出并返回负值,这就不对了。
你通常希望compareTo()产生与equals()一致的自然顺序。如果equals()对于某个特定的比较返回true,那么compareTo()对于该比较就应该返回0,反之亦然。
在TypesForSets中,fill()和test()方法都是用泛型定义的,这是为了避免代码重复。为了验证某个Set的行为,test()会在被测Set上调用三次,尝试着在其中添加重复对象。fill()方法接受任意类型的Set,以及对应类型的Class对象,它使用Class对象来发现构造器并构造对象后添加到Set中。
从输出可以看到,HashSet以某种顺序保存所有的元素(这结果是我用 jdk1.7.0_79 跑出来的,而书中描述是用的jdk1.5,因此不知道是不是这里存在的差异。我这里使用HashSet的元素的结果是有序的,但书中顺序是乱的),LinkedHashSet按照元素插入的顺序保存元素,而TreeSet则按照排序(按照compareTo()定义的顺序,这里是降序)保存元素。
如果我们尝试着将没有恰当地支持必须操作的类型用于这些方法的Set,那么就会有麻烦了。对于没有重新定义hashCode()方法的SetType或TreeType,如果将它们放置到任何散列表中都会产生重复值,这样就违反了Set的基本约定。这相当烦人,因为这种情况甚至不会有运行时错误。