Set接口的简单概述
java.util.set接口继承自Collection接口,它与Collection接口中的方法基本一致,并没有对 Collection接口进行功能上的扩充,只是比collection接口更加严格了。set接口中元素是无序的,并且都会以某种规则保证存入的元素不出现重复。
简述其特点:
- 不允许存储重复的元素
- 没有索引,也没有带索引的方法,不能使用普通的for循环遍历
Set接口有多个实现类,java.util.HashSet是其常用的子类
HashSet集合的介绍
- java.util.HashSet是set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。
- java.util.HashSet底层的实现其实是一个java.util.HashMap支持
- HashSet是根据对象的哈希值来确定元素在集合中的存储位置的,因此具有良好的存取和查找性能。
- 保证元素唯一性的方式依赖于:hashCode与 equals方法。
代码简单理解
import java.util.HashSet;
import java.util.Iterator;
public class DemoHashSet {
public static void main(String[] args) {
// 创建set集合(HashSet)
HashSet<String> hashSet = new HashSet<>();
// 使用add方法想HashSet集合中添加元素
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
hashSet.add("A");
System.out.println("集合中的元素:" + hashSet);
System.out.println("==============================");
// 使用迭代器遍历集合
Iterator<String> ite = hashSet.iterator();
while (ite.hasNext()) {
System.out.println(ite.next());
}
System.out.println("==============================");
// 使用增强for循环遍历集合(不能使用普通的for循环,对HashSet集合进行遍历)
for (String s: hashSet) {
System.out.println(s);
}
}
}
输出结果:
集合中的元素:[A, B, C, D]
==============================
A
B
C
D
==============================
A
B
C
D
注意:普通for循环不能遍历HashSet集合,HashSet集合中没有重复的元素,元素的存储顺序不一致。
HashSet集合存储数据的结构(哈希表)
什么是哈希表
- 哈希表(又称散列表),它是根据关键码值(Key - Value)而直接进行访问的数据结构。
- 也就是说,哈希表是通过把关键码值(Key)映射到表中一个位置来访问记录(Value),以加快查找的速度。这个映射函数叫做哈希函数(散列函数),存放记录的数组叫做哈希表。
- 在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同hash值的链表都存储在一个链表里。
- 但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
- 而JDK1.8中,哈希表存储采用数组+链表+或数组+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
什么是链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
什么是红黑树
红黑树(又称对称二叉B树),是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
什么是关联数组
是一种具有特殊索引方式的数组。不仅可以通过整数来索引它,还可以使用字符串或者其他类型的值(除了NULL)来索引它。
哈希值
- 哈希值是通过一定的哈希算法,将一段较长的数据映射为较短小的二进制数据,这段小数据就是大数据的哈希值。
- 特点:它是唯一的,一旦大数据发生了变化,哪怕是一个微小的变化,他的哈希值也会发生变化。
- 作用:主要用途是用于文件校验或签名。
在Java程序中的哈希值
/**
* 在Java中哈希值:是一个十进制的整数,由系统随机给出的二进制数经过换算得到的
* 其实它就是对象的地址值,是一个逻辑地址,是模拟出来得到地址,并不是数据实际存储的物理地址
*
* 在Object类有一个方法,可以获取对象的哈希值:
* int hashCode() 返回对象的哈希码值
* hashCode()方法源码:
* public native int hashCode();
* native:代表该方法是调用本地操作系统的方法
*/
// 随便创建一个类
public class Person extends Object{
private String name;
private int age;
public Person() {
}
public Person(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;
}
}
// 用这个类创建对象,查看对象的哈希值
public class DemoHashCode {
public static void main(String[] args) {
// 创建一个p1对象,查看其哈希值
Person p1 = new Person("LeeHua", 21);
// 调用Object的hashCode方法,获取哈希值,p1的哈希值是不变的
int h1 = p1.hashCode();
System.out.println(h1);
// 创建一个p2对象,查看其哈希值
Person p2 = new Person("WanTao", 20);
// 调用Object的hashCode方法,获取哈希值,p2的哈希值也是不变的
int h2 = p2.hashCode();
System.out.println(h2);
// 查看p1、p2的地址值
System.out.println(p1);
System.out.println(p2);
}
}
输出结果:
1639705018
1627674070
view.study.demo18.Person@61bbe9ba
view.study.demo18.Person@610455d6
假如覆盖重写hashCode方法,所创建的对象的哈希值就会被影响
如:
public class Person1 extends Object{
/**
* 重写hashCode方法
* @return 哈希值
*/
@Override
public int hashCode() {
return 666;
}
}
public class DemoHashCode1 {
public static void main(String[] args) {
// 创建一个p1对象,查看其哈希值
Person1 p1 = new Person1();
// 调用Object的hashCode方法,获取哈希值,p1的哈希值是不变的
int h1 = p1.hashCode();
System.out.println(h1);
// 查看p1、p2的地址值
System.out.println(p1);
}
}
输出结果:
666
view.study.demo18.Person1@29a
如:我们常用的String类,它也覆盖重写了hashCode方法
public class DemoStringHashCode {
public static void main(String[] args) {
/*
String类的哈希值
(String类重写了Object类的hashCode方法)
*/
String s1 = new String("LeeHua");
String s2 = new String("WanTao");
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
}
输出结果:
-2022794392
-1711288770
图形理解
-
可以看到,数组的每个位置放到一定的值,每部分值都对应一个哈希值。
-
当值的个数超过8个的时候,采用数组+红黑树;当值的个数不到8个的时候,采用数组+链表
-
如:
值1、2、3、4的哈希值都是11
值13、14、15的哈希值都是15
值a、b、c、d、e、f、g的哈希值都是89
Set集合存储元素不重复原理
set集合在调用add()方法的时候,add()方法会调用元素的hashCode()方法和 equals()方法判断元素是否重复
代码举例
import java.util.HashSet;
public class DemoStringHashCode1 {
public static void main(String[] args) {
HashSet<String> hashSet = new HashSet<>();
String s1 = new String("abc");
String s2 = new String("abc");
hashSet.add(s1);
hashSet.add(s2);
hashSet.add("一号");
hashSet.add("二号");
System.out.println("s1的哈希值:" + s1.hashCode());
System.out.println("s2的哈希值:" + s2.hashCode());
System.out.println("一号的哈希值:" + "一号".hashCode());
System.out.println("二号的哈希值:" + "二号".hashCode());
System.out.println("HashSet集合:" + hashSet);
}
}
输出结果:
s1的哈希值:96354
s2的哈希值:96354
一号的哈希值:640503
二号的哈希值:644843
HashSet集合:[二号, abc, 一号]
代码讲解
最初,hashSet集合是空的
- hashSet.add(s1)的时候
- 第一步:add()方法首先会调用s1的hashCode()方法,计算字符串"abc"的哈希值,其哈希值是96354,
- 第二步:查找集合中哈希值是96354中的元素,没有发现哈希值是96354的key
- 第三步:将s1存储到集合hashSet中(于是集合hashSet中存在哈希值96354,且对应这数据s1)
- hashSet.add(s2)的时候
- 第一步:add()方法首先会调用s2的hashCode()方法,计算字符串"abc"的哈希值,其哈希值是96354,
- 第二步:查找集合hashSet中是否存在哈希值是96354,即哈希值96354冲突,
- 第三步:s2调用equals()方法,和集合中哈希值是96354对应的元素进行比较
- 第四步:s2.equals(s1)返回true,即哈希值是96354对应的元素已经存在,所以就不添加s2进集合了(其中:s1 = "abc",s2 = "abc")
- hashSet.add("一号")的时候
- 第一步:调用 "一号" 的hashCode()方法,计算字符串 "一号" 的哈希值,其哈希值是640503,
- 第二步:查找集合中哈希值是640503中的元素,没有发现哈希值是640503的key,
- 第三步:将 "一号" 存储到集合hashSet中(于是集合hashSet中存在哈希值640503,且对应这数据 "一号")
- hashSet.add("二号")的时候
- 第一步:调用 "二号" 的hashCode()方法,计算字符串 "二号" 的哈希值,其哈希值是644843,
- 第二步:查找集合中哈希值是644843中的元素,没有发现哈希值是644843的key,
- 第三步:将 "二号" 存储到集合hashSet中(于是集合hashSet中存在哈希值644843,且对应这数据 "二号")
- 添加完成,集合hashSet = [abc, 一号, 二号]
HashSet存储自定义类型元素
hashSet存储自定义类型元素,那么自定义的类必须重写hashCode()方法和equals()方法,否则添加的元素可以出现重复,我们平时使用的类型,它们都重写类hashCode()方法和equals()方法。
假如不重写hashCode()方法和equals()方法
例子:
// 随便创建一个类,作为HashSet存入数据的类型
public class Person{
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name=\'" + name + \'\\\'\' +
", age=" + age +
\'}\';
}
public Person() {
}
public Person(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;
}
}
IDEA 编译,阿里巴巴Java Code规范会抛出警告
测试一下会出现什么情况
import java.util.HashSet;
public class Demo01PersonHashSet {
public static void main(String[] args) {
HashSet<Person> hashSet = new HashSet<>();
Person p1 = new Person("小明", 20);
Person p2 = new Person("小明", 20);
Person p3 = new Person("小红", 20);
hashSet.add(p1);
hashSet.add(p2);
hashSet.add(p3);
System.out.println(hashSet);
}
}
输出结果:
[Person{name=\'小明\', age=20}, Person{name=\'小明\', age=20}, Person{name=\'小红\', age=20}]
可以看到,hashSet集合里面可以存在重复的元素。
重写hashCode()方法和equals()方法
还是上面这个例子:
在Person类里面添加要重写hashCode()、equals()方法的代码即可,要添加的代码如下
public class Person{
@Override
public boolean equals(Object o) {
// 参数 == 对象
if (this == o) {
return true;
}
// 传入参数为空,或者对象与参数的hashCode不相等
if (o == null || getClass() != o.getClass()) {
return false;
}
// 向下转型,把Object类型转型为Person类型
Person person = (Person) o;
// 返回 age,name
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
再次用上面的代码测试一下Person类型的数据添加是否会出现重复:
输出结果:
[Person{name=\'小明\', age=20}, Person{name=\'小红\', age=20}]
可以看到,输出结果中,hashSet集合的元素并没有重复,因此,如果我们想要用HashSet集合存储自定义类型的数据,一定要记得覆盖重写hashCode()方法和equals()方法。