导读:
本篇是JAVA基础系列的第12篇,昨天我们梳理了集合中的List相关的知识点,今天我们接着梳理Set集合的相关知识点。
1.Set接口
java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
2.List和Set的区别
List,Set都是继承自Collection接口。都是用来存储一组相同类型的元素的。
List特点:
-
元素有放入顺序,元素可重复 。
-
有顺序,即先放入的元素排在前面。
Set特点:
-
元素无放入顺序,元素不可重复。
-
无顺序,即先放入的元素不一定排在前面。不可重复,即相同元素在set中只会保留一份。所以,有些场景下,set可以用来去重。
3.Set如何保证元素不重复
在Java的Set体系中,根据实现方式不同主要分为两大类。HashSet
和TreeSet
。
-
TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值
-
HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束
在HashSet中,基本的操作都是由HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后通过扰动计算和按位与的方式计算出这个元素的存储位置,如果这个位置位空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
TreeSet的底层是TreeMap的keySet(),而TreeMap是基于红黑树实现的,红黑树是一种平衡二叉查找树,它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。
TreeMap是按key排序的,元素在插入TreeSet时compareTo()方法要被调用,所以TreeSet中的元素要实现Comparable接口。TreeSet作为一种Set,它不允许出现重复元素。TreeSet是用compareTo()来判断重复元素的。
4.HashSet类
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 是按照 Hash 算法来存储集合中的元素。因此具有很好的存取和查找性能。
HashSet 具有以下特点:
-
不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
-
HashSet 不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步。
-
集合元素值可以是 null。
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回的结果为 true,但它们的 hashCode 不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。
也就是说,两个对象的 hashCode 值相等且通过 equals() 方法比较返回结果为 true,则 HashSet 集合认为两个元素相等
代码示例:
public class SetDemo { public static void main(String[] args) { HashSet<String> nameSet = new HashSet<String>(); // 创建一个空的 Set 集合 String name1 = "张三"; String name2 = "李四"; String name3 = "王五"; String name4 = "钱六"; nameSet.add(name1); // 将 name1 存储到 Set 集合中 nameSet.add(name2); // 将 name2 存储到 Set 集合中 nameSet.add(name3); // 将 name3 存储到 Set 集合中 nameSet.add(name4); // 将 name4 存储到 Set 集合中 nameSet.add(name4); // 重复添加name4,但是在集合中只能看到一个,因为Set集合不允许重复元素 Iterator<String> it = nameSet.iterator(); while (it.hasNext()) { System.out.println( it.next() ); // 输出 Set 集合中的元素 } System.out.println("集合长度是:" + nameSet.size()); } }
5.TreeSet类
TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序,因此使用 TreeSet 类实现的 Set 接口默认情况下是自然排序的,这里的自然排序指的是升序排序。
TreeSet 只能对实现了 Comparable 接口的类对象进行排序,因为 Comparable 接口中有一个 compareTo(Object o) 方法用于比较两个对象的大小。例如 a.compareTo(b),如果 a 和 b 相等,则该方法返回 0;如果 a 大于 b,则该方法返回大于 0 的值;如果 a 小于 b,则该方法返回小于 0 的值。
TreeSet类的常用方法
方法名称 | 说明 |
---|---|
E first() | 返回此集合中的第一个元素。其中,E 表示集合中元素的数据类型 |
E last() | 返回此集合中的最后一个元素 |
E poolFirst() | 获取并移除此集合中的第一个元素 |
E poolLast() | 获取并移除此集合中的最后一个元素 |
SortedSet<E> subSet(E fromElement,E toElement) | 返回一个新的集合,新集合包含原集合中 fromElement 对象与 toElement 对象之间的所有对象。包含 fromElement 对象,不包含 toElement 对象 |
SortedSet<E> headSet<E toElement〉 | 返回一个新的集合,新集合包含原集合中 toElement 对象之前的所有对象。不包含 toElement 对象 |
SortedSet<E> tailSet(E fromElement) | 返回一个新的集合,新集合包含原集合中 fromElement 对象之后的所有对 象。包含 fromElement 对象 |
注意:表面上看起来这些方法很多,其实很简单。因为 TreeSet 中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了 3 个从 TreeSet 中截取子 TreeSet 的方法。
示例代码:
public class TreeSetDemo { public static void main(String[] args) { TreeSet<Double> scores = new TreeSet<Double>(); // 创建 TreeSet 集合 Scanner input = new Scanner(System.in); System.out.println("------------学生成绩管理系统-------------"); for (int i = 0; i < 5; i++) { System.out.println("第" + (i + 1) + "个学生成绩:"); double score = input.nextDouble(); // 将学生成绩转换为Double类型,添加到TreeSet集合中 scores.add(Double.valueOf(score)); } Iterator<Double> it = scores.iterator(); // 创建 Iterator 对象 System.out.println("学生成绩从低到高的排序为:"); while (it.hasNext()) { System.out.print(it.next() + "\t"); } System.out.println("\n请输入要查询的成绩:"); double searchScore = input.nextDouble(); if (scores.contains(searchScore)) { System.out.println("成绩为:" + searchScore + " 的学生存在!"); } else { System.out.println("成绩为:" + searchScore + " 的学生不存在!"); } // 查询不及格的学生成绩 SortedSet<Double> score1 = scores.headSet(60.0); System.out.println("\n不及格的成绩有:"); for (int i = 0; i < score1.toArray().length; i++) { System.out.print(score1.toArray()[i] + "\t"); } // 查询90分以上的学生成绩 SortedSet<Double> score2 = scores.tailSet(90.0); System.out.println("\n90 分以上的成绩有:"); for (int i = 0; i < score2.toArray().length; i++) { System.out.print(score2.toArray()[i] + "\t"); } } }
运行结果
------------学生成绩管理系统------------- 第1个学生成绩: 13 第2个学生成绩: 50 第3个学生成绩: 80 第4个学生成绩: 90 第5个学生成绩: 99 学生成绩从低到高的排序为: 13.050.080.090.099.0 请输入要查询的成绩: 50 成绩为:50.0 的学生存在! 不及格的成绩有: 13.050.0 90 分以上的成绩有: 90.099.0
6.HashSet实现原理
HashSet实现原理(基于源码总结)
-
基于HashMap实现的,默认构造函数是构建一个初始容量为
16
,负载因子为0.75
的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。 -
当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的
equals(Object obj)
方法和hashCode()
方法,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。 -
HashSet的其他操作都是基于HashMap的。
HashSet源码
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); } //下方代码省略 }
博观而约取,厚积而薄发!