目录
上帖我们讲述了Java中的其中一种单列集合List,这期我们来说说Java中的另一种单列集合Set。
1. Set集合的特点
(1)无序:存取顺序不一致;
(2)不重复:不允许有相同元素的出现,可以用来去重;
(3)无索引:没有关于索引的方法,所以List中关于索引的方法Set都不能用,也不能使用for循环遍历,不能通过索引存取元素。
2. Set集合的实现类
Set的实现类主要有以下三个:
(1)HashSet:无序,不重复,无索引;
(2)LinkedHashSet:因为加入了链表,有序,不重复,无索引;
(3)TreeSet:可排序,不重复,无索引。
3. Set接口中的方法
因为Set也是单列集合Collection中的一种,所以方法基本与Collection中的方法一致。这里还是简单罗列一下吧
注意!!!
因为Set没有索引,这里边的删除方法只能根据对象删除,与List稍有区别。
4. Set的遍历方法
Set有三种遍历方式
(1)迭代器遍历;
(2)增强for遍历;
(3)Lambda表达式遍历。
因为在上期帖子List集合我们已经说过了怎样实现上述的三种遍历方法,所以这里不再做说明,有不了解的可以点下方链接去看看。
5. 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时,链表会转换成红黑树,从而提高查找效率。
关于什么是红黑树以及红黑树的性质,可以浏览我之前的文章 “数据结构——红黑树”,点击下方链接浏览。
(3条消息) 数据结构——红黑树_m0_70325779的博客-CSDN博客https://blog.csdn.net/m0_70325779/article/details/131393598?spm=1001.2014.3001.55025. 如果集合中存贮的是自定义对象,必须要重写equals()和hashcode()方法,原因可以参照我之前的文章结合理解,链接如下:
6. 关于HashSet的几个问题
6.1 HashSet是怎样添加元素的呢?
答: 当想要存储一个元素时,先计算它的哈希值,再根据哈希值判断它应该存放的位置。
6.2 HashSet为什么存和取的顺序不一样?
答: 我们可以设想当我们存入了一个字符串 a = “abcdefg”,根据计算哈希值,它放在哈希表的最后,后来存入了一个字符串 b = “hijklmn” ,根据计算哈希值,它放在哈希表的最前面,当我们去取字符串a时,他是要从头遍历哈希表的,在第二次会取到字符串a,而存入时是率先存入的。这样看就很明显的说明了为什么存入的顺序和取出的顺序不一样。
6.3 HashSet为什么没有索引?
答: 因为哈希表它不像数组那样,里面还包含着链表以及红黑树,加入哈希表中第二个位置是一个链表,存储了三个元素,那么这三个元素的索引都是2吗?而且我们还没有考虑红黑树的情况,这样很明显是不可以的,所以在哈希表中就干脆取消了索引这个性质。
6.4 HashSet是利用什么机制保证数据的不可重复性的?
答: equals()方法和hashCode()方法,当想要存储一个元素时,先计算它的哈希值,再根据哈希值判断它应该存放的位置,有元素则进行equals()比较,如果是链表或红黑树,要对其中的每一个元素进行equals()比较,不同则插入。从而达到了元素的不可重复性。
7. LinkedHashSet底层原理
LinkedHashSet的特点:有序,不重复,无索引。
相比于HashSet,从无序变成了有序,这里的有序指的是存入和取出的顺序是有序的。
为什么呢?想必大家通过名字就能猜出来个大概了。LinkedHashSet的前缀Linked,说明在HashSet的基础上添加了一个链表,而且这个链表是双向链表。
这里我来距离简单说明是如何有序的,如下图,四个元素和一个哈希表
添加第一个元素,找到自己所在的位置,因为为null,直接加入,如下
添加第二个元素,找到自己的位置,直接加入,然后,通过双向链表的形式与第一次插入的元素形成关联,如下所示
同理,插入第三个元素,找到自己的位置,再与第二个插入的元素形成链表指向,如下
插入第四个元素时,也是如此,如下所示
插入完成后,当我们遍历LinkedHashSet时,它就不会像HashSet那样从第一个位置从头便利哈希表,而是那个元素最先插入,从哪个元素开始遍历,以此遍历直到得到自己想要的元素,从而达到了存取的有序性。
8. TreeSet详解
8.1 TreeSet的特点:不重复,无索引,可排序。
可排序:按照元素的默认规则,(从小到大)排列。
8.2 TreeSet集合的底层结构
TreeSet的底层是基于红黑树这种数据结构实现排序的,增删改查性能都很好。
8.3 TreeSet集合的默认排序规则
(1)对于数值类型:Integet,Double,默认按照从小到大的顺序排列;
(2)对于字符,字符串类型:按照字符在ASCII码表中的数字升序排序。这里字符的比较,是一位一位的比较,先比较第一位谁大谁小,小的排前面,第一位一样再比较第二位,谁小排前面,后面同理。
8.4 TreeSet的两种比较方式
8.4.1 方式一:默认排序/自然排序(javaBean类实现Comparable接口指定比较规则)
如下图,现要对student类进行排序,实现Comparable接口,
然后我们就可以在Comparable方法中指定我们自己想要的比较规则,假设我这里只看年龄,修改后代码如下所示:
这里有一点需要注意,TreeSet底层是红黑树,所以不需要强制重写HashCode()和equals()方法。这里需要了解红黑树的性质与插入规则,不懂的可以看我之前的文章,点击下方链接即可(3条消息) 数据结构——红黑树_m0_70325779的博客-CSDN博客https://blog.csdn.net/m0_70325779/article/details/131393598?spm=1001.2014.3001.5501
上面我们定义的是按照年龄排序,方法返回值为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<>接口,如下:
9. 单列集合的使用场景
(1)如果想要集合中的元素可重复
用ArrayList集合,基于数组的(也是实际开发中用的最多的)。
(2)如果想要集合中的元素可重复,而且增删操作远多于查询
用LinkedList集合,基于链表的。
(3)如果想对集合中的元素去重
用HashSet集合,基于哈希表的(实际开发中也用得最多)。
(4)如果想对集合中的元素去重,而且保证存取有序
用LinkedHashSet集合,基于哈希表和链表,但效率要比HashSet低。
(5)想要对集合中的元素进行排序
用TreeSet集合,基于红黑树。后续也可以用List集合实现排序。