HashSet
回顾HashSet之前我们先回顾一下二叉树
-
二叉树:binary tree ,每个节点的下面最多只能有2个子节点。
说明:
1)最多表示一个节点下面可以有两个子节点或者一个子节点或者没有子节点。
在二叉树的根节点左侧的节点称为左子树,在根节点的右侧的节点称为右子树。
2)红黑树也叫做平衡二叉树,查找效率高。
3)二叉树是为了提高查找效率的。
4)TreeSet集合底层就是红黑树。
简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。
二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。
我们这里要说的二叉树也叫做红黑树,红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树中数据仍然是有序的。
补充:
需求:向红黑树中存储数据,如下:
20 31 10 13 23 5 51 5
二叉树的存储流程:
当存储一个元素的时候,如果是树的第一个元素,这个元素就作为根节点。
如果不是第一个元素,那么就拿要存储的元素与根节点进行比较大小:
大于根元素:就将要存储的元素放到根节点的右侧,作为右叶子节点。
等于根元素:丢弃(其他同理)。
小于根元素:就将要存储的元素放到根节点的左侧,作为左叶子节点。
如何遍历二叉树?
遍历二叉树有四种方式:前序遍历、中序遍历、后序遍历、按层遍历。
前序遍历,就是先访问 根节点------>左子树------>右子树
中序遍历,就是先访问 左子树------>根节点------>右子树
后序遍历,就是先访问 左子树------>右子树------>根节点
按层遍历,就是把一棵树从上到下,从左到右依次取出
而在TreeSet的底层使用中序遍历二叉树,即左----->根------>右。
所以TreeSet集合中存储的数据默认都是升序的。从小到大排序的。
然后就是哈希表
什么是哈希表呢?
它是一个数据结构,在JDK1.8之前,哈希表底层采用数组+链表实现。只是不按照数组的下标操作数组中的元素。需要根据数组中存储的元素的哈希值进行元素操作。即使用链表处理冲突,同一hash值的链表都存储在一个链表里。
哈希表的存储过程:
1)哈希表底层含有一个数组,我们必须知道集合中的一个对象元素到底要存储到哈希表中的数组的哪个位置,也就是需要一个下标。
2)哈希表会根据集合中的每个对象元素的内容计算得出一个整数值。由于集合中的对象元素类型是任意的,而现在这里使用的算法必须是任意类型的元素都可以使用的算法。能够让任意类型的对象元素都可以使用的算法肯定在任意类型的对象所属类的父类中,即上帝类Object中,这个算法就是Object类中的hashCode()函数。
结论:要给HashSet集合中保存对象,需要调用对象的hashCode函数。
解释说明:
通过查阅API得知,使用Object的任意子类对象都可以调用Object类中的hashCode()函数并生成任意对象的哈希码值。
3)hashCode算法,得到一个整数,但是这个整数太大了,这个值不能直接作为数组下标的。所以底层还会对这个值结合数组的长度继续计算运行,得到一个在0~数组长度-1之间的整数,这样就可以作为数组的下标了。
问题1:使用hashCode函数生成的一个过大整数是用什么算法将生成哈希码值变成0~数组长度-1之间的数字呢?
其中一种最简单的算法是可以实现的,举例:假设底层哈希表中数组长度是5,那么下标的范围是0~4,
所以我们这里可以使用生成的哈希码值(过大的整数)对数组长度取余数,我们发现任何数在这里对5取余都是在 0 1 2 3 4 之间,所以这样就可以获取到0~数组长度-1之间的下标了。
问题2:如果数据过多,HashSet底层的数组存储不下,怎么办?
hashSet集合底层数组初始容量是16,如果大小不够,那么会继续新创建一个数组,新数组大小等于原来数组的大小*0.75+原来数组的大小。
4)如果上述做法已经计算出底层数组的下标位置,那么就要判断计算出的下标位置是否已经有元素了:
A.如果下标对应的位置没有元素:直接存储数据;
B.如果下标对应的位置有元素:这时就必须调用对象的equals()函数比较两个对象是否相同:
如果结果是true:相同,直接将要添加的数据丢弃;
如果结果是false:不相同,那直接将数据存储到数组当前空间位置;
但是当前位置已经存在元素了,怎么将后来的数据存储到数组中呢?
这里需要使用类似链表的结构了,在当前位置上在划出来一个空间,然后将当前的对象数据保存到新划出来的空间中,在原来的空间中设置一个引用变量记录着新划分空间的地址,如果后面还有数据要存储当前空间,做法和上述相同。
最终结论:哈希表底层通过hashCode和equals算法结合,来保证对象数据在HashSet集合中不重复唯一,并且存储的顺序不固定。
面试题:哈希表如何保证元素唯一?
哈希表保证元素唯一依赖两个方法:hashCode()和equals()。
哈希表底层其实还是一个数组,元素在存储的时候,会先通过hashCode算法结合数组长度得到一个索引。然后判断该索引位置是否有元素:如果没有,不用调用equals函数,直接存储;如果有,再调用元素的equals方法比较是否相同:相同,直接舍弃;如果不同,也存储。
注意:
问题:通过上述描述,当位于一个链表中的元素较多,即hash值相等但是内容不相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过 8 时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只是为了查找效率更高。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示。
看到这张图就有人要问了,这个是怎么存储的呢?
为了方便大家的理解我们结合一个存储流程图来说明一下:(jdk8存储过程)
总结:
JDK1.8引入红黑树大程度优化了哈希表的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
哈希表:数组+链表(JDK1.7)
哈希表:数组+链表+红黑树(JDK1.8之后)
那么下面就回顾一下HashSet
HashSet类的介绍和特点
说明:
1)实现了Set接口,具备了Set集合的特性;
2)不保证集合中的迭代顺序(不保证元素存取一致),允许存储null元素;
3)底层使用哈希表结构;
案例:HashSet集合的应用。
分析和步骤:
1)使用new关键字创建HashSet集合类的对象set,类型是HashSet类型;
2)使用集合对象set调用集合中的add()函数向HashSet集合中添加字符串数据;
3)使用对象set调用iterator()函数获取迭代器对象it;
4)使用for循环遍历,通过迭代器对象调用hasNext()和next()函数,并输出获取的数据;
/*
* hashSet演示
*/
public class HashSetDemo {
public static void main(String[] args) {
//创建HashSet集合对象
HashSet set=new HashSet();
//向集合中添加数据
set.add("JavaSe");
set.add("JavaSE");
set.add("JavaEE");
set.add("AAAA");
set.add("AAAA");
set.add("bbbb");
set.add("bbbb");
//遍历集合
for (Iterator it = set.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
通过以上程序输出结果得出结论:
1)HashSet集合不能存储重复的元素;
2)HashSet集合存储元素的顺序不固定;
接下我们要分析为什么HashSet集合存储的数据顺序不固定和为什么不支持存储重复的元素?
答案肯定和HashSet集合的底层哈希表数据结构有关系。
HashSet保存自定义对象
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一
创建自定义Student类:
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
测试类:
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
结论:
要求以后给HashSet集合即哈希表中保存对象的时候,要求当前这个对象所属的类必须复写Object类中的hashCode和equals方法。建议一定快捷键生成。
HashSet集合总结
HashSet集合存储对象的时候:
1、HashSet集合的底层使用的哈希表结构。那么就要求存放的对象必须具备hashCode功能。由于任何一个类的父类都是Object类,而hashCode函数定义在了Object类中,因此所有的对象都具备hashCode功能。
2、如果我们要把一个对象可以正确的存放在HashSet集合中,这个对象所属的类一般都需要复写hashCode函数。建立本类自己的计算哈希值的方式。
3、如果在HashSet集合中要保证对象唯一,不能仅仅依靠hashCode函数,还要依赖于对象的equals函数,当hashCode函数计算出来的哈希值相同的时候,还要调用equals方法比较2个对象是否相同。
4、要求在向HashSet集合中存储自己定义一个类对象的时候,那么必须在这个自定义类中复写Object类中的hashCode和equals函数。
5、注意当向HashSet集合中存储数据的时候,对象一定会调用hashCode函数计算下标,但是不一定一定会调用equals函数,只有当计算的下标相同时
才会调用equals函数,否则不会调用equals函数来比较两个对象是否相等。
LinkedHashSet介绍
LinkedHashSet集合:它的底层使用的链表+哈希表结构。它和HashSet集合的区别是LinkedHashSet是一个可以保证存取顺序的集合,并且LinkedHashSet集合中的元素也不能重复。
特点:
A:存取有序(底层有一个链接表) 链表记录着存储数据的顺序
B:保证元素的唯一(哈希表) 哈希表是真正存储数据的地方
C:线程不安全,效率高
说明:LinkedHashSet集合没有自己的特有函数,所有的功能全部继承父类。
分析和步骤:
1)定义一个测试类LinkedHashSetDemo;
2)在这个类中使用new关键字创建LinkedHashSet类的对象set,对象set的类型是LinkedHashSet;
3)使用对象set调用add()函数给集合LinkedHashSet添加字符串常量;
4)使用迭代器类Iterator进行迭代并输出集合中的数据;
/*
* LinkedHashSet集合演示
*/
public class LinkedHashSetDemo {
public static void main(String[] args) {
//创建集合对象
LinkedHashSet set=new LinkedHashSet();
//向集合中添加数据
set.add("aaa");
set.add("bbb");
set.add("aaa");
set.add("ccc");
//遍历集合
for (Iterator it = set.iterator(); it.hasNext();) {
//输出集合中的数据
System.out.println(it.next());
}
}
}
结果:
aaa
bbb
ccc