Set接口底层实现类
一、Set接口继承关系
存储的数据特点:无序的、不可重复的元素
|—-Set接口:存储无序的、不可重复的数据 –>高中讲的“集合”
- |—-HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
- |—-LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。 对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
- |—-TreeSet:可以照添加对象的指定属性,进行排序。
二、HashSet
1.继承情况
可以看到是直接实现了Set接口,和间接继承了Collection接口
2.创建HashSet
debug跟进源代码
可以看到空参构造器里new了一个HashMap出来
而HashMap初始化了一个加载因子的常量,这里由于是map内容不多做细讲
实际上HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变,此类允许使用null元素。
在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object();
,(定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。) 在添加数据时就能观察到。
3.添加数据
跟进 首先是一个自动装箱
而PRESENT就是刚才提及到的固定的value值 实现了双列变单列
private static final Object PRESENT = new Object();
接下来的put就是map的操作了
4.不可重复性和无序性
HashSet的俩大特点
1. 无序性:不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
2. 不可重复性:保证添加的元素照equals()判断时,不能返回true.即:相同的元素只能添加一个。
在进行测试过程中 我发现避免不了去到map的源码,由于太过复杂 尽量用大白话说明白,map会单独出一篇
在jdk7和之前 底层使用的是数组加链表,在jdk8后使用的是数组加链表加红黑树。
因为我们知道map的key是不允许的重复的,如果有重复的key值就会把map的key和value进行更新操作,这一来就说的通了,HashSet就是限制了map的功能,让map的value值固定为常量,只使用key去操作,所以说HashSet不允许有重复的值,其实也是这个新的值把原来的值给替换掉了
而无序性,要重存放的方式说起。(注意,存进HashSet的对象一定要重写HashCode和equals方法)
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置,判断
数组此位置上是否已经元素):
此时有俩种情况:
1.如果此位置上没其他元素,则元素a添加成功。 —>插入成功情况1
2.如果此位置上其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
此时又有俩种情况:
1.如果hash值不相同,则元素a添加成功。—>成功情况2
2.如果hash值相同,进而需要调用元素a所在类的equals()方法:
此时还是有俩种情况:
1.equals()返回true,元素a添加失败
2.equals()返回false,则元素a添加成功。—>成功情况3
对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
(链表指向区别↓)
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
此时同学们肯定会有疑问,如果HashCode相同难道会出现equals不同的情况嘛?(还真会待会贴出优秀代码)
由于在数组 和 链表 和 红黑树 的数据结构有点抽象 下面几副图可以形象看到HashSet添加时全过程
情况2:
情况3:
这里没有用到红黑树,是因为加入红黑树是让遍历等操作速度变快,具体的方式为,在数组某一个位置上的链表的层数到8层或总数量多于64个时,链表重新打散,改为使用树结构,当然树结构的头还是数组,变成一颗颗倒着的树。
5.添加代码
class Person{
Integer num;
String name;
public Person(Integer num, String name) {
this.num = num;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"num=" + num +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return Objects.equals(num, person.num) &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(num, name);
}
}
@Test
public void test1(){
HashSet set = new HashSet();
Person p1 = new Person(1002,"AA");
Person p2 = new Person(1002,"BB");
set.add(p1);
set.add(p2);
System.out.println(set);
p1.name = "CC";
set.remove(p1);
System.out.println(set);
set.add(new Person(1002,"CC"));
System.out.println(set);
set.add(new Person(1002,"AA"));
System.out.println(set);
}
运行test1中代码结果为:
[Person{num=1002, name='AA'}, Person{num=1002, name='BB'}]
[Person{num=1002, name='CC'}, Person{num=1002, name='BB'}]
[Person{num=1002, name='CC'}, Person{num=1002, name='BB'}, Person{num=1002, name='CC'}]
[Person{num=1002, name='CC'}, Person{num=1002, name='BB'}, Person{num=1002, name='CC'}, Person{num=1002, name='AA'}]
是不是很诡异?其实归根结底还是那三种情况
三、LinkedHashSet
1.继承情况
2.优劣
LinkedHashSet 与 HashSet 区别在于 LinkedHashSet 遍历其内部数据时,可以按照添加的顺序遍历,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
特性就剩不可重复性了
对于频繁的遍历操作,LinkedHashSet效率高于HashSet
四、TreeSet
1.继承情况
TreeSet不太一样,它是可以将数据按照一定的排列顺序进行输出。有俩种方式(定制排序,自然排序)
2.自然排序
自然排序就是对象实现了Comparable接口
我们知道,Set是无序的,但是用TreeSet的数据就是有序的,那基本数据类型为什么存进去就会变有序呢?
其实这里有一个自动装箱的过程在Integer中就实现了这个接口
public final class Integer extends Number implements Comparable<Integer> {
在里面重写了compareTo
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
以后要想放在TreeSet实现排序的对象 先要自己重写compareTo方法,用自己的排列方式才能生效
3.定制排序
往往在很多时候,自然排序根本不能用,因为不能去更改源代码,所以有另一种方法,帮助我们定制排序的顺序
这种方法就是给TreeSet构造器中给一个Comparator对象,在Comparator对象中重写compare方法,定制自己需要的排列规则
@Test
public void test2(){
Comparator com = new Comparator() {
//照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
一般来说如果某个对象不需要经常使用排序,则使用定制排序就行,因为可以使用匿名内部类的方式,一次性的排序,而使用自然排序则是一直绑定着排序规则,不灵活。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!