JavaEE - 11集合Collection-Set
(5)Set集合
(5.1)Set集合概述
Set接口: 存储无序、不可重复的数据
- HashSet: Set主要实现类;线程不安全;可以存储null值
- LinkedHashSet: HashSet的子类;遍历内部数据时,可以按照添加的顺序遍历。
- TreeSet: 按照添加对象的指定属性进行排序。
- 无序性: 不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是按照数据的哈希值。
- 不可重复性: 保证添加的元素按照equals()判断时,不能返回true。相同的元素只能添加一个。
- Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
- 向Set中添加的数据,其所在的类一定要重写 hashCode() 和 equals()。 对象中用作equals()方法比较的Field,都应该用来计算hashCode值。
(5.2)HashSet类
- HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。
- HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
- HashSet 特点:
- 不能保证元素的排列顺序。顺序可能与添加顺序不同,顺序也可能发生变化。
- HashSet 不是线程安全的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet 集合时,必须通过代码来保证其线程安全。
- 集合元素可以是 null。
- HashSet 集合判断两个元素相等的标准: 两个对象 通过 equals() 方法比较相等,hashCode() 方法返回值也相等。
- 当把一个对象放入 HashSet 中时,如果需要重写该对象对应类的 equals() 方法,也应该重写 hashCode() 方法。
- 规则是: 如果两个对象通过 equals()方法 比较返回 true,这两个对象的 hashCode 值也应该相同。相等的对象必须具有相等的散列码。
- 如果试图把两个相同的元素加入到同一个Set集合中,add() 方法返回false, 且新元素不会被加入,添加操作会失败。
(5.2.1)HashSet添加元素过程
- 向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值;
- 使用此哈希值通过某种算法计算出在HashSet底层数组中的存放位置(即:索引位置),判断数组此位置上是否已经存在元素。
- 如果此位置没有其他元素,则元素a添加成功。--> 成功情况1
- 如果此位置有其他元素b(或以链表形式存在多个元素),则比较元素a与元素b的hash值:
- 如果hash值不相同,则元素a 添加成功。 --> 成功情况2
- 如果hash值相同,则调用元素a所在类的equals()方法: 返回true,元素a添加失败;返回false,元素b添加成功。 -->成功情况3
对于添加成功的情况2和情况3而言: 元素a 与已经存在于指定索引位置上的数据以链表的形式存储。
- JDK 7: 元素a 放到数组中,指向原来的元素。 元素a 插入到链表的头部。
- JDK 8: 原来的元素在数组中,指向元素a。 元素a 插入链表的尾部。 七上八下。
(5.2.2)HashSet 底层结构
底层也是数组,初始容量为16,当使用率超过0.75(16*0.75=12),就会扩大容量为原来的2倍,(16,32,64,128......)
HashSet底层: 数组 + 链表的结构。
(5.2.3)hashCode()重写,为什么选择31
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的冲突就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits, 相乘造成数据溢出的概率较小。
- 31可以由 i*31 == (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是用一个数字来乘以这个素数,最终的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
@Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; }
(5.2.4)修改Set集合实例变量导致元素相同
如果向 HashSet 中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中其他元素相同,导致HashSet 中包含两个相同的对象。
当程序把可变对象添加到HashSet 中之后,尽量不要 去修改该集合元素中参与计算 hashCode() equals()的实例变量,否则将会导致 HashSet无法正确操作 这些集合元素。
Person 类已重写equals()和 hashcode()
@Test public void test5(){ HashSet set = new HashSet(); Person p1 = new Person("AA",13); Person p2 = new Person("BB",34); set.add(p1); set.add(p2); p1.setName("CC"); set.remove(p1); // false System.out.println(set); //[Person{name='BB', age=34}, Person{name='CC', age=13}] set.add(new Person("CC",13)); System.out.println(set); //[Person{name='BB', age=34}, Person{name='CC', age=13}, Person{name='CC', age=13}] set.add(new Person("BB",34)); System.out.println(set); // [Person{name='BB', age=34}, Person{name='CC', age=13}, Person{name='CC', age=13}] }
(5.2.5)在List内去除重复数字,要求实现简单
public List duplicateList(List list){ HashSet set = new HashSet(); set.addAll(list); return new ArrayList(set); } @Test public void test4(){ List list = new ArrayList(); list.add(new Integer(1)); list.add(new Integer(2)); list.add(new Integer(2)); list.add(new Integer(4)); list.add(new Integer(4)); List list2 = duplicateList(list); for(Object o : list2){ System.out.println(o); } }
(5.3)LinkedHashSet
- LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据的前一个数据和后一个数据。
- 在遍历内部数据时,可以按照添加的顺序遍历。
- 优点: 对于频繁的遍历操作, LinkedHashSet效率高于HashSet。
- 缺点: LinkedHashSet需要维护元素的插入顺序,性能略低于HashSet。
@Test public void test6(){ LinkedHashSet books = new LinkedHashSet(); books.add("疯狂 Java 讲义"); books.add("轻量级 Java EE 企业应用实战"); System.out.println(books); // [疯狂 Java 讲义, 轻量级 Java EE 企业应用实战] books.remove("疯狂 Java 讲义"); books.add("疯狂 Java 讲义"); System.out.println(books); // [轻量级 Java EE 企业应用实战, 疯狂 Java 讲义] }
(5.4)TreeSet
- TreeSet 是 SortedSet 接口的实现类。TreeSet 可以确保集合元素处于排序状态。
- 未实现排序报错: Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable
- TreeSet 底层使用 红黑树 结构存储数据。有序,查询速度比List快。
- TreeSet 并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的。
- 与 HashSet 集合相比, TreeSet 还提供了几个额外的方法
- Comparator comparator(): 如果TreeSet采用了定制程序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序, 则返回null。
- Object first(): 返回集合中的第一个元素。
- Object last(): 返回集合中的最后一个元素。
- Object lower(Object e): 返回集合中位于指定元素之前的元素。即小于指定元素的最大元素,参考元素不需要是 TreeSet 集合里的元素。
- Object higher(Object e): 返回集合中位于指定元素之后的元素。即大于指定元素的最小元素,参考元素不需要是 TreeSet 集合里的元素。
- SortedSet subSet(Object fromElement, Object toElement): 返回此 Set 的子集合, 范围从 fromElement(包含) 到 toElement(不包括)。
- SortedSet headSet(Object toElement): 返回此Set的子集,由小于 toElement 的元素组成。
- SortedSet tailSet(Object fromElement): 返回此Set的子集,由大于或等于 fromElement 的元素组成。
- TreeSet 支持两种排序方法:自然排序(升序,实现Comparable接口) 和定制排序(Comparator)。 在默认情况下, TreeSet 采用自然排序。
- 往TreeSet添加未实现排序的类的对象时,第一次不会报错,添加第二个对象会报错,会引发 ClassCastException 异常。
(5.4.1)添加元素: 要求是相同类型的对象
@Test public void test2(){ TreeSet set = new TreeSet(); set.add(123); set.add(456); // set.add("aa"); //java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String // set.add(new Person("Sun",12)); set.add(332); set.add(132); System.out.println(set.lower(350)); // 332 System.out.println(set.higher(300)); //332 System.out.println(set); // [123, 132, 332, 456] Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); // 123 132 332 456 } }
(5.4.2)自然排序
比较两个对象是否相同的标准: compareTo()返回0,不再是equals()
- Java 提供了一个 Comparable 接口, 该接口 定义了一个 CompareTo(Object obj)方法,
- 该方法返回一个整数值, 实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。
- 当一个对象调用该方法与另一个对象进行比较时, 例如 obj1.compareTo(obj2),
- 如果该方法返回0,表明两个对象相等;返回正整数,表明 obj1 大于 obj2. 返回负整数,表明 obj1 小于 obj2.
- Java 的一些常用类已经实现了 Comparable 接口,并提供了比较大小的标准。
- BigDeciaml、BigInteger以及所有的数值型对应的包装类: 按它们对应的数值大小进行比较。
- Character: 按字符的 Unicode 值进行比较。
- Boolean: true 对应的包装类实例 大于 false 对应的包装类实例。
- String : 按字符串 中字符的Unicode 值进行比较。
- Date、Time: 后面的时间、日期比 前面的日期、时间大。
- 当需要把一个对象放入 TreeSet 中, 重写该对象对应类的equals() 方法时,保证该方法与 CompareTo(Object obj) 方法有一致的结果。
- equals() 返回 true, CompareTo(Object obj) 返回 0。
- CompareTo(Object obj) 返回 0, equals() 返回 false, TreeSet 不会让第二个元素添加进去,与 Set 集合的规则 产生冲突。
- 如果向 TreeSet 中添加 一个可变对象后, 并且后面程序 修改了 该可变对象的实例变量,将导致 它与其他对象的大小顺序发生改变。
- 但 TreeSet 不会再次 调整 它们的顺序, 甚至可能导致 TreeSet 中保存的两个对象通过 CompareTo(Object obj) 方法返回0。
- 一旦改变了 TreeSet 集合里可变元素的实例变量, 当再试图删除该对象时, TreeSet会删除失败,
- 甚至 集合中原有的、实例变量没被修改 但与 修改后元素相等的元素也无法删除。
public class Person implements Comparable { ...... @Override public int compareTo(Object o){ // 按照姓名从小到大排列 if(o instanceof Person){ Person p = (Person) o; return -this.name.compareTo(p.name); }else { throw new RuntimeException("输入的类型不匹配"); } } }
@Test public void test3(){ TreeSet set = new TreeSet(); set.add(new Person("Aaaa",12)); set.add(new Person("Abbb",12)); set.add(new Person("Efrd",13)); set.add(new Person("Greg",32)); set.add(new Person("Dfgr",52)); //[Person{name='Greg', age=32}, Person{name='Efrd', age=13}, Person{name='Dfgr', age=52}, Person{name='Abbb', age=12}, Person{name='Aaaa', age=12}] System.out.println(set); }
(5.4.3)定制排序
比较两个对象是否相同的标准: compare()返回0,不再是equals()。
定制排序依然不可以向 TreeSet 中添加不同类型的对象。否则引发 ClassCastException 异常。
@Test public void test4(){ Comparator com = new Comparator() { // 按照年龄从小到大 @Override public int compare(Object o1, Object o2) { if(o1 instanceof Person && o2 instanceof Person){ Person p1 = (Person) o1; Person p2 = (Person) o2; return Integer.compare(p1.getAge(), p2.getAge()); }else { throw new RuntimeException("输入的类型不匹配"); } } }; TreeSet set = new TreeSet(com); set.add(new Person("Aaaa",12)); set.add(new Person("Abbb",12)); set.add(new Person("Efrd",13)); set.add(new Person("Greg",32)); set.add(new Person("Dfgr",52)); // [Person{name='Aaaa', age=12}, Person{name='Efrd', age=13}, Person{name='Greg', age=32}, Person{name='Dfgr', age=52}] System.out.println(set); }
(5.5)EnumSet
- EnumSet是一个专为枚举类设计的集合类,EnumSet 中所有元素都必须是指定枚举类型的枚举值。该枚举类型在创建 EnumSet 时显式或隐式地指定。
- EnumSet的集合元素也是有序的, EnumSet 以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
- EnumSet在内部以位向量的形式存储, 这种存储形式非常紧凑、高效,EnumSet对象占用内存很小, 而且运行效率很好。
- 尤其是进行批量操作( 调用 containsAll() 和 retainAll() 方法)时,如果参数也是 EnumSet 集合,则批量操作的执行速度也非常快。
- EnumSet集合不允许加入null元素, 如果试图插入 null 元素, EnumSet 将抛出 NullPointerException 异常。
- 如果只是想判断EnumSet是否包含 null 元素或试图 删除 null 元素 都不会抛出异常,只是删除操作将返回 false, 因为没有任何null 元素被删除。
- EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法 来创建 EnumSet 对象。
(5.5.1)方法
- EnumSet allOf(Class elementType): 创建一个包含指定枚举类里所有枚举值的EnumSet集合。
- EnumSet complementOf(EnumSet s): 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
- 新EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值。
- (即 新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值) <差集>。
- EnumSet copyOf(Collection c): 使用一个普通集合来创建 EnumSet 集合。
- EnumSet copyOf(EnumSet s): 创建一个与指定 EnumSet 具有相同元素类型、相同集合元素的 EnumSet 集合。
- EnumSet noneOf(Class elementType): 创建一个元素类型为指定枚举类型的 空 EnumSet.
- EnumSet of(E first, E... rest): 创建一个包含一个或多个枚举值的 EnumSet集合,传入的多个枚举值 必须属于 同一个枚举类。
- EnumSet range(E from, E to): 创建一个包含从 from 枚举值 到 to 枚举值 范围内 所有枚举值的 EnumSet 集合。
EnumSet es1 = EnumSet.allOf(Season.class); System.out.println(es1); // [SPRING, SUMMER, AUTUMN, WINTER] EnumSet es2 = EnumSet.noneOf(Season.class); System.out.println(es2); // [] es2.add(Season.WINTER); es2.add(Season.SPRING); System.out.println(es2); // [SPRING, WINTER] EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER); System.out.println(es3); // [SUMMER, WINTER] EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER); System.out.println(es4); // [SUMMER, AUTUMN, WINTER] EnumSet es5 = EnumSet.complementOf(es4); System.out.println(es5); // [SPRING]
Collection c = new HashSet(); c.clear(); c.add(Season.AUTUMN); c.add(Season.SPRING); // 复制 Collection 集合中的所有元素 来创建 EnumSet 集合 EnumSet enumSet = EnumSet.copyOf(c); System.out.println(enumSet); // [SPRING, AUTUMN] c.add("疯狂 Java 讲义"); c.add("疯狂 IOS 讲义"); System.out.println(c); // [AUTUMN, SPRING, 疯狂 IOS 讲义, 疯狂 Java 讲义] // 下面代码出现异常, 因为 c 集合中的元素 并不是 全部都是 枚举值 enumSet = EnumSet.copyOf(c);
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum at java.util.RegularEnumSet.add(RegularEnumSet.java:36) at java.util.EnumSet.copyOf(EnumSet.java:179) at SetTest.test8(SetTest.java:137)
(5.6)Set实现类比较分析
- HashSet 和 TreeSet 是 Set 的两个典型实现。
- HashSet 的性能总是比 TreeSet 好(特别是 最常用的添加、查询元素等操作),因为 TreeSet 需要额外的红黑树算法来维护集合元素的次序。
- 只有当需要 一个保持顺序的Set时, 才应该使用TreeSet, 否则都应该使用 HashSet。
- HashSet 还有一个子类: LinkedHashSet,对于普通的插入、删除操作, LinkedHashSet 比 HashSet 要略微慢一点。
- 这是由维护 链表所带来的额外开销造成的。由于有了链表, 遍历 LinkedHashSet 会更快。
- EnumSet 是所有 Set 实现类 中性能最好的, 但它只能保存同一个枚举类的枚举值作为集合元素。
- Set 的三个实现类 HashSet、TreeSet、EnumSet都是线程不安全的。
- 如果有多个线程同时访问 一个Set集合, 有超过一个线程修改了Set集合,则必须手动保证该 Set 集合的同步性。
- 通常可以通过 Collections 工具类的 synchronizedSortedSet 方法来“包装”该 Set 集合。
- 此操作最好在创建时进行, 以防止 对Set集合的意外非同步访问。
- SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));