Set集合与Collection基本相同,具有一些不同的行为。Set集合中的元素是无序的,不可以重复添加元素。
1 HashSet类
集合值可以为null,不是同步的,元素是无序的,通过哈希算法计算元素存储的位置,因而可以自动增加其容量。HashSet中元素使用的槽位是“桶‘,如果两个对象的equals()方法返回值是true,会放在一个桶里,通过链式方法存储。因此最好避免出现不同对象equals方法返回true的情况,防止性能下降。
如果把一个对象放入HashSet集合中,重写equals()方法时,也应该重写hashcode()方法。规则是:如果两个对象的equals()返回值是true,则hashcode值应该相同。重写hashcode()方法的步骤是:1.根据规则计算每个实例变量的hashcode值 2.各个实例变量乘以一个质数加权相加。
如果是可变对象,后面的Java程序改变该对象,使该对象中实例变量与另一个对象相同,就可能导致两个对象的hashcode值相同。这会导致HashSettings发生混乱,这个被修改了的对象无法访问了(无法计算hashcode找到它的真实位置了)。当我们把一个元素装入HashSet集合中后,尽量不要修改参与计算hashcode值的实例变量。
LinkedHashSet类是HashSet类的子类,在物理上是根据hash算法存储元素,但是还通过链表方式来维护其元素顺序。
2 TreeSet类
TreeSet类是SorttedSet接口的实现类。其元素是排序(按大小)的。
public class TreeSetTest { public static void main(String[] args) { TreeSet nums = new TreeSet(); nums.add(5); nums.add(2); nums.add(10); nums.add(-9); // [-9, 2, 5, 10] System.out.println(nums); System.out.println(nums.first()); // 返回小于4的子集 [-9, 2] System.out.println(nums.headSet(4)); // [5, 10] System.out.println(nums.tailSet(5)); //[2] System.out.println(nums.subSet(-3, 4)); } }
TreeSet是通过调用集合元素的commpareTo(Object obj)方法比较元素的大小关系。如果试图把一个对象添加到TreeSet时,则该对象必须实现Comparable接口,否则会抛出异常。
class Err { } public class TreeSetErrorTest { public static void main(String[] args) { TreeSet ts = new TreeSet(); ts.add(new Err()); //添加第二个元素时自动调用compareTo()反复,引发ClassCastException ts.add(new Err()); } }
TreeSet中元素的比较完全是基于compareTo()方法来进行的。
class Z implements Comparable { int age; public Z(int age) { this.age = age; } public int compareTo(Object o) { return 1; } public boolean equals(Object o) { return true; } @Override public String toString() { return "Z [age=" + age + "]"; } } public class TreeSetTest2 { public static void main(String[] args) { TreeSet ts = new TreeSet(); Z z1 = new Z(6); ts.add(z1); // 打印true,即使是同一个对象,但由于compareTo()方法的返回值是1,仍然可以添加 System.out.println(ts.add(z1)); // [Z [age=6], Z [age=6]] System.out.println(ts); ((Z) ts.first()).age = 9; //[Z [age=9], Z [age=9]] System.out.println(ts); } }
TreeSet中的对象存储可参考下图
但是如果一个可变的对象放入TreeSet中后,该对象的实例变量如果在后续代码中改变了,TreeSet不会再调整它们的顺序。该元素也无法被删除了(甚至与该元素值相同的未被修改的元素也无法被删除)。
可以通过Coparator自定义TreeSet的比较规则。
class M { int age; public M(int age) { this.age = age; } @Override public String toString() { return "M [age=" + age + "]"; } } public class TreeSetTest3 { public static void main(String[] args) { // 此处Lambda表达式的目标类型是comparator TreeSet ts = new TreeSet((o1, o2) -> { M m1 = (M) o1; M m2 = (M) o2; return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0; }); ts.add(new M(5)); ts.add(new M(-3)); ts.add(new M(4)); //[M [age=5], M [age=4], M [age=-3]] System.out.println(ts); } }
3 EnumSet类
EnumSet类专门为枚举类型数据设计,在内部以位向量形式存储数据,这种高效的存储方式使EnumSet对象占用内存小,效率高(尤其是在进行批处理时)。EnumSet中不允许加入null值,否则会报出空指针异常。
enum Season { SPRING, SUMMER, FALL, WINTER } public class EnumSetTest { public static void main(String[] args) { EnumSet es1 = EnumSet.allOf(Season.class); System.out.println(es1); EnumSet es2 = EnumSet.noneOf(Season.class); // [] System.out.println(es2); es2.add(Season.WINTER); System.out.println(es2); } }
4 各Set类的性能分析
HashSet在查找、增加数据时性能都要比TreeSet更好,这是因为TreeSet底层要通过红黑树来维护,只有需要保持排序的存储元素时才选用TreeSet。LinkedHashSet在增加、删除元素时,比HashSet略慢,这是链表造成的开销,但是由于链表结构,LinkedHashSet会在遍历上更快。EnumSet是所有Set中性能最好的,但是只能存储枚举类型数据。Set集合都是线程非安全的,可以使用collections工具类中sychronizedSortedSet方法包装该集合来实现同步。
SortedSet s=Collections.synchronizedSortedSet(new TreeSet());