Set接口是 Collection接口的子接口。
1、无序,即添加元素和去除元素的顺序不一致。
但是每次取出的顺序是一致的。
2、不允许重复元素,可以有null,但只能有一个。
3、实现类很多,主要介绍HashSet、LinkedHashSet 和 TreeSet。
常用方法
因为其为Collection的子接口,因此常用方法和Collection一致。
遍历方式
因为其是Collection的子接口,所以其遍历方法与Collection一致。
即:
-
使用迭代器Iterator
-
使用增强for
一、HashSet
-
HashSet实现了Set接口
-
HashSet底层上是HashMap,其底层为:数组+链表+红黑树
-
可以存放null,但是只能存放一个。
-
HashSet不保证数据是有序的(即不保证存取顺序一致),取决于hash后,再确定索引的结果。
-
不能有重复的对象
源码
-
HashSet底层是HashMap
-
添加一个元素时,先得到hash值,将其转变为索引值
-
找到存储数据表table,判断该索引位置是否已经有存放的元素
-
没有,则直接加入
-
有,则调用 equals 比较,相同则放弃添加;不同,则添加到链表后面
-
-
在Java8中,如果一条链表个数到达了TREEIFY_THRESHOLD(默认值为8),
并且table>=MIN_TREEIFY_CAPACITY(默认64),就会转化成红黑树。
1、新建对象
调用自身构造器,在构造器内调用HashMap()构造器。
2、add()添加对象
执行add()方法,调用put()方法
然后执行put()方法,调用hash()获取 key值 ,之后调用putVal()方法
putVal()则是真的存放数据的方法。
①add()方法:
可见,add()方法直接调用
put()方法,传入两个参数,分别为key和value;
key为我们add的数据;
value为PRESENT,下面第二张图,可见其为一个静态的常量,为所用对象共有的。
因为其底层是HashMap,是键值对,其中key为HashSet要存放的数据,Value如果填Null,虽然节省空间,
但是意味着无论是否添加成功,都返回Null,那么则无法判断是否添加成功,因此放了
PRESENT。
可见,
返回null才是添加成功。
②HashMap.put()方法:
put()方法直接先调用hash()获取hash值,然后调用putVal()方法。
hash()将我们 即将添加的值hashcode值 与 其hashcode无符号向右位移16位的值 按位异或。
目的就是为了减少碰撞,
具体原因:
h = key.hashCode()) ^ (h >>> 16)
hashcode()方法获取hash值:
不同类不一样,一般都会重写。
Object类:
Object的hashcode 方法是本地方法,也就是用 c 或 c++ 实现的,该方法直接返回对象的内存地址。
注意其为native修饰。详情:
Object顶级类(包含==和equals区别)
③HashMap.putval()方法:
源码前,先看:
table为HashMap存放数据的数组,为Node类型的数组;
Node为每个数据所存放的对象:
hash:存放对象Key所对应的hash值
key:存放数据对象
value:每个对象都一样,就是①中所说的PRESENT,一个Object类型的常量对象。
next:是下一个节点的引用。
resize()数组扩容源码
final Node<K,V>[] resize() {
//将table赋值给oldtab
Node<K,V>[] oldTab = table;
//oldcap存放旧table数组的大小
// 如果旧数组为null,则oldcap为0
// 如果旧数组不为null,则oldcap为其原来的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldthr存放扩容阈值,HashMap并不是数据满了才存放,而是达到阈值就进行扩容
int oldThr = threshold;
//newcap存放新的数组大小,newthr存放新的扩容阈值
int newCap, newThr = 0;
//oldcap大于0,即table扩容前不为null的情况
if (oldCap > 0) {
...
}
//初始容量设置为阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//table为null的情况
//newcap赋值为DEFAULT_INITIAL_CAPACITY,
//newthr赋值为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
//DEFAULT_INITIAL_CAPACITY为默认初始容量16,定义:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//DEFAULT_LOAD_FACTOR为默认缓存大小0.75,定义:static final float DEFAULT_LOAD_FACTOR = 0.75f;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//修改newThr
if (newThr == 0) {
...
}
//将newthr赋值给threshold,即缓存大小设置完毕
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//因为table为数组,因此扩容需要新建立数组
//然后将新数组赋给table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧数组table不为null,则需要将旧数组中的数据转移到新数组,具体先不研究
if (oldTab != null) {
. . .
}
//最后返回新的数组table
return newTab;
}
treeifyBin():判断对应链表是否需要树化
先进行判断,如果数组长度小于MIN_TREEIFY_CAPACITY(默认64),则调用resize()进行扩容
否则进行树化。
putVal()源码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab为table
//n用来存放table长度;
//p用来存放table中将要存放的位置的对象
//i用来存放table中将要存放位置的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//将table赋给tab,判断是否为null或者判断length是否为0
//若table不为空,则跳过该段代码,否则进入resize()方法
//将长度赋给n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算出待存放位置,即i位置的table[i]为null
//1.将 n-1 和 传进来的hash 进行按位与,并将其赋值给 i
//2.将tab[i],即按位与得到下标的位置在数组中的对象赋值给p
//3.如果p为null,说明该位置还没有存放,则新建一个结点存放进去,结点信息在上面
//4.然后该结点赋值给tab[i]
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//计算出待存放位置不为空,即 待存储数据的hash值 与 之前存储过的数据hash值 一致,
//是否存入新数据如下:
else {
//创建辅助变量
Node<K,V> e; K k;
//如果 p.hash即数组当前位置的链表的第一个元素的hash值 与 hash即待插入的新元素的hash值相同
//且满足下面条件之一:(将p.key赋给k)
// k == key 即 第一个元素的key 和 带加入元素的key 是同一个对象
// key != null && key.equals(k) 即 key非空 且 key 和 kequals相同,即内容相同
//则将 p 赋值给 e,p就是当前数组当前位置的链表的第一个元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是一颗红黑树,就以红黑树添加元素的方式进行比较并添加新的对象
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不满足上述两种情况,即值不同,且p还不是红黑树
//则当前p为一个链表
//就进行循环
else {
for (int binCount = 0; ; ++binCount) {
//遍历到了尾部,追加新节点到尾部
//先判断p.next是否为null
//如果为null则直接将新结点存入,并退出循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果新增加元素后 > TREEIFY_THRESHOLD(默认为8)-1=7,即有了8个结点,
//则调用treeifyBin()进行判断是否树化,方法在上面
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;
}
}
//如果e不等于null,则说明原来存在与带加入节点hash值相同,但是value值不同节点
//然后用新的value代替旧的value
//则返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空方法,没实现,LinkedHashMap的时候有用到
afterNodeAccess(e);
return oldValue;
}
}
//modCount即操作数+1
++modCount;
//size即数组大小+1
//+1之后的size如果大于了threshold,则需要扩容
//注意,此处是size>threshold,而size每次增加是在加入一个结点之后;
//意味着并不是数组table用到了threshold个,而是结点达到threshold个。
if (++size > threshold)
resize();
//该函数在HashMap中是一个空函数
//该函数由HashMap的子类实现,用来实现排序存放等。
afterNodeInsertion(evict);
//最后返回null为添加成功
return null;
}
二、LinkedHashSet
-
LinkedHashSet是HashSet的子类。
-
LinkedHashSet底层是 LinKedHashMap,维护了一个 数组 + 双向链表
-
LinkedHashSet根据元素的hashcode决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的。
-
LinkedHashSet不允许添加重复元素。
源码
-
LinkedHashSet维护一个hash表和双向链表
-
该类有两个属性head 和 tail ,用来表示头节点和尾节点
-
-
每一个节点还有 pre 和 next 属性,用来形成双向链表
-
添加一个元素时,先求hash值,再求索引,确定其在table中的位置
-
如果没有重复,跟HashSet一样加入,之后再加入双向链表
-
有重复则不加入
-
-
因此,遍历时使用链表,就会呈现和添加顺序一样的遍历顺序了。
-
存放的节点不是Node了,而是
1、构造器:
可见构造器是直接调用父类的有参构造器,默认初始容量为16,阈值为0.75。
注意,
HashSet()中创建的对象是LinkedHashMap(),而不是HashMap()。
2、add()添加元素:
与HashSet类似,也是直接调用LinkedHashMap的add()方法
然后调用 HashMap 的put()方法,因为LinkedHashMap并未重写该方法
再调用 HashMap 的putVal()方法
putVal()方法:
与HashSet一样,但是在 newNode()时,使用的是LinkedHaspMap的方法:
先创建一个Entry对象。
然后调用父类构造方法构造Node。
注意:Entry继承了HashMap的内部类Node
再调用linkNodeLast()方法将该节点链接在链表当中。
与HashSet不同的还有在最后 afterNodeInsertion(evict)是实现了的