目录
一. Set 集合的特点
二. Set 集合的实现类
三. Set 接口中的方法
四. Set 的三种遍历方法
五. HashSet 底层原理
六. 关于HashSet的几个问题
七. LinkedHashSet底层原理
八. TreeSet详解
九. 单列集合的使用场景
上帖 Java单列集合——List 我们讲述了 Java 中的其中一种单列集合 List,这期我们来说说Java中的另一种单列集合Set。
一. Set 集合的特点
- 无序:存取顺序不一致;
- 不重复:不允许有相同元素的出现,可以用来去重;
- 无索引:没有关于索引的方法,所以List中关于索引的方法Set都不能用,也不能使用for循环遍历,不能通过索引存取元素。
二. Set 集合的实现类
Set的实现类主要有以下三个:
(1)HashSet:无序,不重复,无索引;
(2)LinkedHashSet:因为加入了链表,有序,不重复,无索引;
(3)TreeSet:可排序,不重复,无索引。
三. Set 接口中的方法
因为Set也是单列集合Collection中的一种,所以方法基本与Collection中的方法一致。这里还是简单罗列一下吧
注意!!!
因为Set没有索引,这里边的删除方法只能根据对象删除,与List稍有区别。
四. Set 的三种遍历方法
- 迭代器遍历;
- 增强for遍历;
- Lambda表达式遍历;
遍历方法在单列集合篇已详细展开,这里就不重复展示了。
五. HashSet 底层原理
(1)HashSet底层采用哈希表存储数据;
(2)哈希表是一种增删改查数据性能都很好的数据结构;
哈希表的组成在不同的JDK版本中也略显不同
JDK1.8之前:数组+链表
JDK1.8之后:数组+链表+红黑树
哈希表中,有一个哈希值,非常重要
如下图所示是一个哈希表,暂时所有元素都为空,
以下性质需要掌握
(1)创建一个长度为16,默认加载因子为0.75的数组,数组名为table;
(2)当我们向哈希表中去添加元素时,它不是向数组那样从数组第一个位置挨个添加元素的,而是通过哈希值与数组的长度计算出它应该存放的位置;
(3)存放时,会判断当前位置是否为null,如果是则直接存入;
(4)如果位置不为null,表示有元素,则会调用equal()方法比较属性值;
(5)判断得出结果,如果一样则不存,不一样则存入数组形成链表;
这里是对上述的补充:
1. 这里JDK8以前,新元素存入时,会采用头插法,插在老元素的头部,老元素下移,形成链表;
2. 在JDK8以后,新元素存入时,采用尾插法,插在老元素的尾部,老元素位置不动,形成链表;
3. 这里我们再来说加载因子,当我们的数组中存入16 * 0.75 = 12 个元素的时候,那么此时数组就会扩容成原来的2倍;
4. 当链表的长度大于8且数组的长度大于64时,链表会转换成红黑树,从而提高查找效率。
关于什么是红黑树以及红黑树的性质,可以浏览我之前的文章 “数据结构——红黑树”,点击下方链接浏览。
数据结构——红黑树_m0_70325779的博客-CSDN博客
5. 如果集合中存贮的是自定义对象,必须要重写equals()和hashcode()方法,原因可以参照我之前的文章结合理解,链接如下:
为什么重写equals方法之后必须要重写hashCode_m0_70325779的博客-CSDN博客
六. 关于HashSet的几个问题
6.1 HashSet是怎样添加元素的呢?
答: 当想要存储一个元素时,先计算它的哈希值,再根据哈希值判断它应该存放的位置。
6.2 HashSet为什么存和取的顺序不一样?
答: 我们可以设想当我们存入了一个字符串 a = “abcdefg”,根据计算哈希值,它放在哈希表的最后,后来存入了一个字符串 b = “hijklmn” ,根据计算哈希值,它放在哈希表的最前面,当我们去取字符串a时,他是要从头遍历哈希表的,在第二次会取到字符串a,而存入时是率先存入的。这样看就很明显的说明了为什么存入的顺序和取出的顺序不一样。
6.3 HashSet为什么没有索引?
答: 因为哈希表它不像数组那样,里面还包含着链表以及红黑树,加入哈希表中第二个位置是一个链表,存储了三个元素,那么这三个元素的索引都是2吗?而且我们还没有考虑红黑树的情况,这样很明显是不可以的,所以在哈希表中就干脆取消了索引这个性质。
6.4 HashSet是利用什么机制保证数据的不可重复性的?
答: equals()方法和hashCode()方法,当想要存储一个元素时,先计算它的哈希值,再根据哈希值判断它应该存放的位置,有元素则进行equals()比较,如果是链表或红黑树,要对其中的每一个元素进行equals()比较,不同则插入。从而达到了元素的不可重复性。
七. LinkedHashSet底层原理
LinkedHashSet的特点:有序,不重复,无索引。
相比于HashSet,从无序变成了有序,这里的有序指的是存入和取出的顺序是有序的。
为什么呢?想必大家通过名字就能猜出来个大概了。LinkedHashSet的前缀Linked,说明在HashSet的基础上添加了一个链表,而且这个链表是双向链表。
这里我来距离简单说明是如何有序的,如下图,四个元素和一个哈希表
添加第一个元素,找到自己所在的位置,因为为null,直接加入,如下
添加第二个元素,找到自己的位置,直接加入,然后,通过双向链表的形式与第一次插入的元素形成关联,如下所示
同理,插入第三个元素,找到自己的位置,再与第二个插入的元素形成链表指向,如下
插入第四个元素时,也是如此,如下所示
插入完成后,当我们遍历LinkedHashSet时,它就不会像HashSet那样从第一个位置从头便利哈希表,而是那个元素最先插入,从哪个元素开始遍历,以此遍历直到得到自己想要的元素,从而达到了存取的有序性。
八. TreeSet详解
TreeSet 的特点:不重复,无索引,可排序。
可排序:按照元素的默认规则,(从小到大)排列。
TreeSet 集合的底层结构
TreeSet的底层是基于红黑树这种数据结构实现排序的,增删改查性能都很好。
TreeSet 集合的默认排序规则
(1)对于数值类型:Integet,Double,默认按照从小到大的顺序排列;
(2)对于字符,字符串类型:按照字符在ASCII码表中的数字升序排序。这里字符的比较,是一位一位的比较,先比较第一位谁大谁小,小的排前面,第一位一样再比较第二位,谁小排前面,后面同理。
TreeSet 的两种比较方式
8.4.1 方式一:默认排序/自然排序(javaBean类实现Comparable接口指定比较规则)
如下图,现要对student类进行排序,实现Comparable接口,
然后我们就可以在Comparable方法中指定我们自己想要的比较规则,假设我这里只看年龄,修改后代码如下所示:
这里有一点需要注意,TreeSet底层是红黑树,所以不需要强制重写HashCode()和equals()方法。这里需要了解红黑树的性质与插入规则,不懂的可以看我之前的文章,数据结构——红黑树_m0_70325779的博客-CSDN博客
上面我们定义的是按照年龄排序,方法返回值为int,这里分为三种情况
当int > 0(即返回值为正数时,该元素存到当前元素的右边);
当int < 0(即返回值为负数时,该元素存到当前元素的左边);
当int = 0 (表明当前元素已经存在,会舍弃不存)
这里要知道,当前节点首先指红黑树的根节点,然后从根一步步进行判断,找到自己应该插入的位置。
8.4.2 方式二:比较器排序(创建TreeSet的时候,传递比较器指定Comparator规则)
这两种比较器的使用原则:默认使用第一种比较器,如果第一种无法满足我们的需求,就会使用第二种。
来看线面两个练习题,我们就能更好地理解它了。
练习1:
分析题目后很明显可以发现,原有的比较规则一已经无法达到我们想要的目的了,我们接着看,其实String类中已经帮我们重写定义好了排序规则,如下,
String类实现了Comparable<>接口,
上面就是我们的排序规则一
但我们现在要添加另一种排序规则,改Java的源码似乎不太合适吧。
这里我们可以采用TreeSet的另一个构造方法,它需要传一个比较器,但这个比较器Comparator是一个接口,所以我们要对它进行实现。实现后如下所示:
o1是要插入的元素,o2是红黑树中已经存在的元素;
这里我们先把o1与o2的长度作比较,然后下面做判断,如果等于零,再调用String类中已经定义好的排序规则,如果不等于0,直接返回 i 值,插入到红黑树中。
练习2:
这里分析得知,也是需要重新定义比较规则的,我们再Student类中实现Comparable<>接口,如下:
九. 单列集合的使用场景
(1)如果想要集合中的元素可重复
用ArrayList集合,基于数组的(也是实际开发中用的最多的)。
(2)如果想要集合中的元素可重复,而且增删操作远多于查询
用LinkedList集合,基于链表的。
(3)如果想对集合中的元素去重
用HashSet集合,基于哈希表的(实际开发中也用得最多)。
(4)如果想对集合中的元素去重,而且保证存取有序
用LinkedHashSet集合,基于哈希表和链表,但效率要比HashSet低。
(5)想要对集合中的元素进行排序
用TreeSet集合,基于红黑树。后续也可以用List集合实现排序。