建议Debug模式调试
一、Set接口基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有:EnumSet、HashSet、TreeSet、LinkedHashSet、CopyOrWriteArraySet等
二、Set的遍历方式
同Collection的遍历方式一样,因为Set接口是Collection接口的子接口
- 可以使用迭代器
- 增强for
- 不能使用索引的方式来获取
三、Set的演示
public static void main(String[] args) {
//1、以Set接口的实现类HashSet来讲解Set接口的方法
//2、Set接口的实现类,不能存放重复元素,可以添加一个null
//3、Set接口对象存放数据是无序(即添的顺序和取出的顺序不一致)
//4、注意:取出的顺序虽然不是添加的顺序,但是他是固定的。
Set<Object> set = new HashSet<>();
set.add("john");
set.add("jack");
set.add("john");//重复
set.add("mark");
set.add(null);
set.add(null);//重复
System.out.println("set=" + set);//set=[null, john, jack, mark]
//遍历
//方式1、使用迭代器
System.out.println("========使用迭代器遍历Set集合============");
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj:" + obj);
}
//方式2、使用增强for循环
System.out.println("========使用增强for循环============");
for (Object obj : set) {
System.out.println("obj:" + obj);
}
//Set接口对象,不能通过索引来获取,因为没有get方法
}
四、HashSet底层机制
模拟HaseSet(或HashMap)的底层
创建Node类,来储存数据,可以指向下一个结点,从而形成链表
class Node {
Object item;//存放数据
Node next;//指向下一个结点
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
", next=" + next +
'}';
}
}
模拟HaseSet(或HashMap)的底层
public static void main(String[] args) {
//1、创建一个数组,数组的类型是Node[]
//2、有些人,直接把node[]数组称为一个表
Node[] table = new Node[16];
System.out.println("table:" + table);
//3、创建结点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;//将jack结点挂载到john
Node rose = new Node("Rose", null);
jack.next = rose;//将rose结点挂载到jack
Node luck = new Node("luck", null);
table[3] = luck;//把luck放到table表索引为3的位置
System.out.println("table:" + table);
}
五、HashSet 源码分析(重点)
HashSet的介绍
- 可以通过构造器看出HashSet底层还是采用HashMap实现的,HashMap底层是(数组+链表+红黑树)
public HashSet() {
map = new HashMap<>();
}
- 可以存放null值,但是只能有一个null 3、HashSet不保证元素是有序的,取决于hash后,再确定索引的结果,即不保证存放元素的顺序和取出顺序的一致 4、不能有重复的元素/对象
HashSet#add方法源码分析
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值–>会转成–>索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用equals(具体对象调用具体对象的equals方法,比较的不一定就是内容)比较,如果相同,就放弃添加,如果不相同,则添加到最后
- 在Java1.8中,如果一个链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树)
HashMap是如何存放元素的呢

元素存放过程分析
当我们向HashMap中存放数据时,首先会根据key的hashCode方法计算出值,然后结合数组长度和算法(如取余法、位运算等)来计算出向数组存放数据的位置索引值。如果索引位置不存在数据的话则将数据放到索引位中;如果索引位置已经存在数据,这时就发生了hash碰撞(两个不同的原始值在经过哈希运算后得到相同的结果),为了解决hash碰撞,JDK1.8前用的是数组+链表的形式,而JDK1.8后用的是数组+链表+红黑树,这里以链表为例。由于该位置的hash值和新元素的hash值相同,这时候要比较两个元素的内容如果内容相同则进行覆盖操作,如果内容不同则继续找链表中的下一个元素进行比较,以此类推如果都没重复的则在链表中新开辟一个空间存放元素。以上图为例,假设张三、王五、赵六、王五对应的数组数组下标是1,李四对应的数组下标是2。刚开始索引位置1为空,(张三,15)直接放入1位置,索引位置 2为空,(李四,22)直接放入2位置,(王五,18)发现索引位
置1已经有数据,这时候调用equals方法和张三进行内容比较,发现内容不同,链表开辟新空间存放数据;(赵六,25)发现1位置已经有元素,调用equals和张三比较,内容不同,继续向下和王五比较,内容不同,开辟新空间存放数据;(王五,28)发现1位置已经有元素,调用equals和张三进行内容比较,不同,继续向下和(王五,18)比较,发现内容也相同,这时候则进行覆盖操作将原来的(王五,18)改成(王五,28)
HashMap在JDK1.8和JDK1.7的区别
HashMap在1.7和1.8中最大的区别就是底层数据结构的变化,在1.7中HashMap采用的底层数据结构是数组+链表的形式,而在1.8中HashMap采用的是数组+链表+红黑树的数据结构(当链表长度大于8且数组长度大于等于64时链表会转成红黑树,当长度低于6时红黑树又会转成链表),红黑树是一种平衡二叉搜索树,它能通过左旋、右旋、变色保持树的平衡。之所以用红黑树是因为他能够大大提高查找效率,链表的时间复杂度是O(n)而红黑树的时间复杂度是O(logn),那么为啥要链表长度大于8且数组长度大于64才转成红黑树呢,简单来说就是节点太少的时候没必要转换数据结构,因为不仅转换数据结构需要浪费时间同时也要浪费空间。
分析HashSet的扩容和转成红黑树机制
- HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)0.75 = 12
- 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推。
- 在Java8中,如果一条链表得元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组的扩容机制。
注意:其加载因子的作用是放置大量线程进来填充数据的时候,防止没有容量做的提前扩容
那么为啥要链表长度大于8且数组长度大于64才转成红黑树呢?
简单来说就是节点太少的时候没必要转换数据结构,因为不仅转换数据结构需要浪费时间同时也要浪费空间。
不直接一直用红黑树呢?这是因为树的结构太浪费空间,只有节点足够多的情况下用树才能体现出它的优势,而如果在节点数量不够多的情况下用链表的效率更高,占用的空间更小
为什么不直接一直用红黑树呢?
这是因为树的结构太浪费空间,只有节点足够多的情况下用树才能体现出它的优势,而如果在节点数量不够多的情况下用链表的效率更高,占用的空间更小
附代码
public static void main(String[] args) {
//说明
//1、在执行add方法后,会返回一个Boolean的
//2、如果添加成功,返回true,否则返回false
//3、可以通过remove指定删除那个对象
HashSet hashSet = new HashSet();
System.out.println(hashSet.add("jack"));//T
System.out.println(hashSet.add("lucy"));//T
System.out.println(hashSet.add("jack"));//F
//TODO HashSet#add方法 源码分析(重点)
/**
* 1.TODO 执行
* HashSet() public HashSet() {
* map = new HashMap<>();
* }
* 2.TODO 执行第一次 hashSet.add("jack")
* public boolean add(E e) {//e = "jack"
* return map.put(e, PRESENT)==null;// (static)static final Object PRESENT = new Object()仅仅用于占位;
* }
* 3、TODO 执行put方法
* public V put(K key, V value) { key = "jack" value = PRESENT(共享)
* return putVal(hash(key), key, value, false, true);
* }
* 4、TODO 调用hash(),计算key对应的hash值:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),由此可看,key的hash值不等于key的hashCode
*
* 5、TODO 执行putVal方法逻辑分析
* final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
* boolean evict) {
* //TODO 定义了辅助变量
* Node<K,V>[] tab; Node<K,V> p; int n, i;
* //TODO table是HashMap的一个属性,是一个Node[]数组
* //TODO if语句表示当前table是null,或者大小-0,就是第一次扩容,到16个空间
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
* //TODO (1) 根据key,得到hash ,去计算该key应该存放到table表的那个索引位置,并把这个位置的对象,赋值给p
* //TODO (2) 再判断p是否为null,
* //TODO (2.1) 如果p为null,表示没有存放过元素,就创建一个Node(key ="jack",value =PRESENT )
* //TODO (2.2) 就放在该位置 tab[i] = newNode(hash,key value,null)
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* else {
* //如果当前索引位置上有结点数据了,就会进行以下复杂的判断
* Node<K,V> e; K k;//开发技巧提示:在需要的地方创建辅助(局部)变量
* //p.hash == hash:如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
* //并且满足下面两个条件之一,
* //1、准备加入的key和p指向的Node结点的key是同一个对象
* //2、或者p指向的Node结点的key的equals()和准备加入的key比较后相同,(针对对象)
* //就不能往里面添加/
* if (p.hash == hash &&
* ((k = p.key) == key || (key != null && key.equals(k))))
* e = p;
* //再判断p是不是红黑树,如果是红黑树,就要用putTreeVal方法添加元素了
* else if (p instanceof TreeNode)
* e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* else {//处理table当前索引下是链表数据,就使用for循环比较
* //(1)、依次和该链表的每一个元素比较后,都比相同,则加入到该链表的最后
* for (int binCount = 0; ; ++binCount) {//死循环
* //判断链表下一个元素是不是null,如果为null,说明后面没有元素了
* if ((e = p.next) == null) {
* p.next = newNode(hash, key, value, null);
* //TODO 把元素添加到链表后,立刻判断该链表是否已经达到8个结点,如果达到,就调用treeifyBin对当前这个链表进行树化(红黑树)
* //TODO 注意:在转成红黑树时,要进行判断,其条件是:if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
* //TODO 如果上面条件成立,会对table扩容,调用resize()
* //TODO 只有当上面条件成立时,才进行转成红黑树
* if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
* treeifyBin(tab, hash);
* break;
* }
* //如果链表下一个元素不是null,则比较以下两个条件(跟上面的条件一样):如果满足条件,就break
* //1、准备加入的key和p指向的Node结点的key是同一个对象
* //2、或者p指向的Node结点的key的equals()和准备加入的key比较后相同,(针对对象)
* if (e.hash == hash &&
* ((k = e.key) == key || (key != null && key.equals(k))))
* //break这一刻,相同的就是e这个元素
* break;
* //如果上面条件都不满足,将e指向p。相当于指向下一个结点,继续循环判断
* p = e;
* }
* }
* if (e != null) { // existing mapping for key
* V oldValue = e.value;
* if (!onlyIfAbsent || oldValue == null)
* e.value = value;
* afterNodeAccess(e);
* return oldValue;
* }
* }
* ++modCount;
* //TODO 判断是否需要扩容
* if (++size > threshold)
* resize();
* //TODO afterNodeInsertion方法是HashMap留给他的子类(比如:LinkedHashMap)使用的,在此处是空实现
* afterNodeInsertion(evict);
* //TODO 返回null,说明插入成功,于add方法:return map.put(e, PRESENT)==null对应
* return null;
* }
* 6、TODO 执行第二次 hashSet.add("lucy")
* 同上2
* 7、TODO 执行put方法
* 同上3
* 8、TODO 调用hash()
* 同上4
* 9、TODO 执行putVal方法逻辑分析
* final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
* boolean evict) {
* Node<K,V>[] tab; Node<K,V> p; int n, i;
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* else {
* Node<K,V> e; K k;
* if (p.hash == hash &&
* ((k = p.key) == key || (key != null && key.equals(k))))
* e = p;
* else if (p instanceof TreeNode)
* e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* else {
* for (int binCount = 0; ; ++binCount) {
* if ((e = p.next) == null) {
* p.next = newNode(hash, key, value, null);
* if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
* treeifyBin(tab, hash);
* break;
* }
* if (e.hash == hash &&
* ((k = e.key) == key || (key != null && key.equals(k))))
* break;
* p = e;
* }
* }
* if (e != null) { // existing mapping for key
* V oldValue = e.value;
* if (!onlyIfAbsent || oldValue == null)
* e.value = value;
* afterNodeAccess(e);
* return oldValue;
* }
* }
* ++modCount;
* //TODO size就是每加入一个结点Node(k,v,next),size++;
* if (++size > threshold)
* resize();
* afterNodeInsertion(evict);
* return null;
* }
*
*
*
*/
}
HashSet添加元素,调用对象的hashCode方法,计算对应的hash值,来决定
HashSet hashSet = new HashSet();
hashSet.add("lucy");//添加成功
hashSet.add("lucy");//添加不成功
hashSet.add(new Dog("tom"));//OK
hashSet.add(new Dog("tom"));//OK
System.out.println("hashSet=" + hashSet);//打印出来是三个内容
Dog对象
class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
上面两次hashSet.add(new Dog("tom"))之所以都成功,是因为每次new都是一个新的对象,其hash值都不一样。加入对Dog对象重写hash值,
让Dog对象的hash值返回固定值,这样就能促使其进行equals方法判断
经典面试题,有关字符串对象往HashSet中存储
//再加深一下,非常经典的面试题‘
//看源码
HashSet hashSet = new HashSet();
hashSet.add(new String("hsp"));//OK
hashSet.add(new String("hsp"));//加入不了,涉及到字符串常量池问题
System.out.println("hashSet=" + hashSet);
HashSet的扩容机制1
HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)0.75 = 12 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,依次类推。
private static void testLoadFactor() {
HashSet<Object> hashSet = new HashSet<>();
for (int i = 0; i < 1000; i++) {
//观察当添加到第13个、第24个元素的时候发生的变化即可发现扩容机制的奥秘
hashSet.add(i);
}
}
HashSet的扩容机制2
创建内部类A、B,并重写hashCode方法
class A {
private int n;
public A(int n) {
this.n = n;
}
//重写A对象的hashCode方法,固定返回100.这样就会导致追加到HashSet上的数据都会追加到一个链表上
@Override
public int hashCode() {
return 100;
}
}
class B{
private int n;
public B(int n) {
this.n = n;
}
/**
* 重写B对象的hashCode方法,固定返回200.这样就会导致追加到HashSet上的数据都会追加到一个链表上
* @return
*/
@Override
public int hashCode() {
return 200;
}
}
触发扩容机制的,是元素的总个数。跟在不在一个链表无关
private static void testHashSetIncrement() {
HashSet<Object> hashSet = new HashSet<>();
for (int i = 0; i <= 7; i++) {//在hashSet的某一条链表上添加了7个A对象
hashSet.add(new A(i));
}
for (int i = 0; i <= 7; i++) {//在hashSet的另外一条链表上添加了7个B对象
hashSet.add(new B(i));//当hashSet增加到第13个元素的时候,就会触发HashSet数组的扩容机制,调用resize()方法
}
}
HashSet中链表如何转换成树型结构(红黑树)
在Java1.8中,如果一个链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树)
private static void testTreeifyThreshold() {
HashSet<Object> hashSet = new HashSet<>();
for (int i = 0; i < 12; i++) {
hashSet.add(new A(i));
}
System.out.println("hashSet=" + hashSet);
}
六、LinkedHashSet 源码分析(重点)
LinkedHashSet的全面说明
- LinkedHashSet是HashSet的子类
- LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
- LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存得
- LinkedHashSet不允许添加重复元素
LinkedHashSet底层机制
1、LinkedHashSet加入顺序和取出元素的顺序是一致的
2、LinkedHashSet底层维护的是LinkedHashMap(是HashMap的子类)
3、LinkedHashSet底层结构维护的是(数组+双向链表)
4、添加第一次时,直接将数组table扩容到16,存放的结点类型是LinkedHashMapEntry(跟HashSet底层不一样,HashSet底层采用的Node),由此可猜出Entry实现或继承了Node(多态)5、table数组是HashMapEntry(跟HashSet底层不一样,HashSet底层采用的Node),由此可猜出Entry实现或继承了Node(多态)
5、table数组是HashMapEntry(跟HashSet底层不一样,HashSet底层采用的Node),由此可猜出Entry实现或继承了Node(多态)5、table数组是HashMapNode[],其存放的数据/元素是LinkedHashMap$Entry类型
6、LinkedHashMap中维护了一个静态内部类Entry,继承了HashMap.Node类(继承关系是在内部类中发生的),Node在HashMap属于静态内部类,因为它是通过类调用的
static class Entry<K,V> extends HashMap.Node<K,V> {
//TODO 此处就是实现双向链表的关键
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
附代码
内部类Customer创建
class Customer {
private String name;
private Integer number;
public Customer(String name, Integer number) {
this.name = name;
this.number = number;
}
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", number=" + number +
'}';
}
}
代码测试LinkedHashSet添加(建议Debug模式调试)
public static void underlyingMechanism() {
Set<Object> set = new LinkedHashSet<>();
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(new Customer("刘", 1001));
set.add(123);
set.add("HSP");
System.out.println("set=" + set);
}
到此为止,Set相关实现类源码分析到此为止

本文主要围绕Java的Set接口展开,介绍了Set接口无序、无索引、不允许重复元素的特点及遍历方式。重点分析了HashSet和LinkedHashSet的底层机制与源码,包括HashSet基于HashMap的实现、扩容和树化机制,以及LinkedHashSet基于LinkedHashMap的实现和元素插入顺序的维护。
740

被折叠的 条评论
为什么被折叠?



