Java - 集合
本篇博客主要是记录在个人学习中的一些理解和看法。
一、什么是集合?
集合是Java对数据结构的实现(百度百科),其实集合顾名思义他的概念就是我们学习过的数学中的集合,即“确定的一堆东西”,或者说一个或多个元素所构成的整体。
二、为什么要有集合?
按照我们理解的集合的概念来说,在没学习集合之前,Java中的数组的数组其实和集合的功能非常类似,但是数组的使用对于我们来说局限性太大,第一是数组的长度必须在声明时就指明切之后无法更改;第二是数组的数据结构固定,造成我们在使用时性能很难另人满意,比如增加和删除元素时;所以Java为我们提供了集合。当然由于集合存储必须是Object类型,所以在存储基本数据类型时会进行自动装箱所以可能在这种时候效率不如数组高。
三、集合有什么优点?
集合是Java对数据结构的实现,使用集合免去了我们自己构建数据结构的繁琐步骤,并且为我们提供了封装好的高性能方法,在对集合进行操作时我们不用再更多的考虑操作的性能问题,我们只需要选择适合应用场景的集合就行,另外利用泛型我们还能提高代码的重用性。
四、常用集合的继承关系
实际上这个图并不完整比如AbstractCollection这些抽象类并没有涵盖进去,这里只是为了线条看起来简单清晰;下面的图也是一样不完整的,只是把最常用的列举了出来;我们可以把三个抽象类当作实现了所有通用的方法,可以看到集合的实现主要有两种接口,一种是Collection接口,一种是Map接口。
五、Collection
Collection 接口的具体内容
Collection结构中主要规范了使用集合时一些常用的方法,增删改查,确定大小,判断是否为空等等,可以从上面的图中看出,大部分方法可以从名字知道他的作用。简要说明下这里的Iterator方法是返回迭代当前集合的迭代器,迭代器是帮助我们遍历数组时使用的,而spliterator是JDK1.8为了提高集合的并行处理能力新增的可分割迭代器,而剩下的stream和parallelStream也是1.8新增的Stream类,前者是单管的后者是多管的。
1.List
List 接口的具体内容
在Collection接口的基础上进一步规范了List类型专用的方法。
AbstractList
关于AbstractList我的理解是这里就实现了一些在确定对象为List类型时可以实现的方法,但是在看到AbstractList的源码时发现了访问等级为默认的其他两个类SubList和RandomAccessSubList,我感到很奇怪所以在这里找了一下使用这个类的地方,发现了在AbstractList中有如下代码
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
我们知道subList方法是实现集合的剪切,这里我想看看Java到底是如何通过SubList类来实现集合剪切的,在这里我们看到方法通过判断本类是否实现了RandomAccess接口来决定返回类型,我觉得两者在原理上区别不大,不过是要求分割出来的子集合继承之前的快速随机访问策略,我决定从SubList来看,这里调用了SubList的有参构造代码如下:
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
仔细阅读后发现这里只是设置了偏移量和大小,再SubList下面的增删改查方法明白了其实subList方法并没有创建一个新的List事实上只是把操作框定在一定范围上操作原List而已。
ArrayList
ArrayList顾名思义实现的数据结构是数组,所以实际上存储数据的结构就数组。
- DEFAULT_CAPACITY = 10 默认容量为10
- Objcet[] elementData 存储方式是数组
- MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 最大容量
最大容量为什么是Integer.MAX_VALUE-8
1.是因为有些VM在分配数组时会保留一些头字
2.是因为如果分配过大的数组可能会超过虚拟机限制造成OutOfMemoryError
增删改查
个人认为这里比较简单就简单说明下,查询就不用多说直接通过索引得到即可,增加删除和修改其实都是通过调用System.arraycopy方法对数组进行截取复制实现的。
扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//计算出实际容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
通过分析代码可以得知,ArrayList的扩容方式是建立新数组,扩容机制是如果期望容量大于了实际容量就扩容,而扩容量是原容量的0.5倍,也就是说扩容后容量变为原容量的1.5倍。
LinkedList
LinkedList采用的是链表结构来存储元素,对应的增删改查方式都和链表相同,又由于是链表结构所以不存在扩容问题,这里注意一点LinkedList还实现了Deque双端队列的接口,所以可以执行一些双端队列的操作。
Vector
- Object[] elementData Vector存储数据也是利用数组
- elementCount 是集合内元素的个数
- capacityIncrement 每次扩容时容量的增量
并且通过查看源码可以发现操作Vector集合的方法都被synchronized关键词修饰,所以Vector集合是并发安全的,其他的一些操作和ArrayList类似。
扩容
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
modCount++;
ensureCapacityHelper(minCapacity);
}
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看出和ArrayList的原理其实差不多,只不过扩容的大小变为了capacityIncrement如果不设置capacityIncrement则扩容为原来的两倍。
2.Map
由于Set的实现依赖了Map所以我决定先学习Map
Map接口中只是规范了一些Map常用的方法,并提供了一部分默认方法(JDK1.8支持在接口中提供默认方法),Map是存储的一个个键值对的映射,即,一个键对应一个值,键不能重复,值能重复。
AbstractMap
我们看见AbstractMap中有两个内部类SimpleEntry,SimpleImmutableEntry.
SimpleEntry是一对键值的实现(final修饰key不修饰Value),SimpleImmutableEntry是不可变的一对键值(final修饰key和Value)
HashMap
由于HashMap内容较多所以决定详细了解下。
首先HashMap的实现是由数组+链表的方式实现的,JDK1.8中还加入了树,数组中存储的是每一个链表的头节点,而具体每个链表应该存储的索引是由key的hash值决定的。
- DEFAULT_INITIAL_CAPACITY = 1 << 4;-----默认初始化容量为16
- MAXIMUM_CAPACITY = 1 << 30;-----最大容量为2的30次方
- DEFAULT_LOAD_FACTOR = 0.75f;-----默认加载因子为0.75即size占总容量的0.75时扩容
- TREEIFY_THRESHOLD = 8;-----hashmap自身的优化为减少hash碰撞造成散列表中链表深度过深,在到达一定阈值后会将链表改为树结构来提高性能,而这个阈值就TREEIFY_THRESHOLD默认为8
- UNTREEIFY_THRESHOLD = 6;----- 当hashmap在变为树结构后size重新减小到一定阈值进行缩减操作,重新变为链表,这个阈值就是UNTREEIFY_THRESHOLD
- MIN_TREEIFY_CAPACITY = 64;
- threshold -----扩容阈值(capacity * load factor)
树化
由于JDK1.8中对HashMap做了优化,为了减少链表长度提高查询效率,HashMap在以前数组+链表的结构下增加了红黑树结构,在链表长度达到一定阈值时该链表就会进行树化,而要了解树化的过程,可能需要先了解红黑树红黑树是一个平衡二叉搜索树,但它不是完美平衡二叉树,他可以通过旋转和变色来进行自平衡。具体如何实现这里就先不讲,如果想要了解可以看这里(红黑树)。
/**
* 将给定索引下的所有链接节点替换为二叉树,如果表太小会重新调整大小
*
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {//将该桶树化。
int n, index;
Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//如果表小于最小树容量(默认为6)
resize();//从红黑树变回链表。
else if ((e = tab[index = (n - 1) & hash]) != null) {//判断给定索引下是否为空
TreeNode<K,V> hd = null, tl = null;
do {//遍历找到头节点转化为树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);//树化
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {//遍历桶中每个节点加入红黑树
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) { //如果根节点还没有初始化则初始化
x.parent = null;
x.red = false; //红黑树要求根节点为黑
root = x;
}
else {//如果根节点已经初始化过了
K k = x.key;//当前链表节点的Key
int h = x.hash;//当前链表节点的hash值
Class<?> kc = null;//key的Class
for (TreeNode<K,V> p = root;;) {//遍历红黑树,将链表当前节点添加到红黑树对应位置
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)//判断当前链表节点hash值是否小于当前树节点的hash值
dir = -1;
else if (ph < h)//判断当前链表节点hash值是否大于当前树节点的hash值
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//这里的意思是如果两个key的Hash值相同那么判断key的类型是否实现了Comparable接口,若没有实现,或者实现了但是比较后仍然相同则执行下面代码
dir = tieBreakOrder(k, pk);
/**
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
*/
//这里是用于打破hashcode相同但是没有实现comparable接口或者compartor比较后仍然相等的僵局
//System.identityHashCode(a) <= System.identityHashCode(b) ?-1 : 1
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//判断树对应方向的节点是否为空,并且向对应方向移动,如果为空就将当前链表节点加入到该树节点位置
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);//插入后自平衡
break;
}
}
}
}
moveRootToFront(tab, root);//确保root是桶中的第一个节点
}
扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//原有桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧桶长度
int oldThr = threshold;//旧阈值
int newCap, newThr = 0;
if (oldCap > 0) {//如果原来的容器不是空的则进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {//如果旧容量已经大于最大容量
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//当旧容量扩容后还在范围内,并且大于等于默认容量的话,进行扩容新容量是旧容量的1倍
newThr = oldThr << 1; // 阈值提高一倍
}
else if (oldThr > 0) //将初始容量设置为旧阈值
newCap = oldThr;
else { // 初始阈值为零则使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//阈值 = 加载因子 * 容量
}
if (newThr == 0) {//如果新阈值并没有被设置
float ft = (float)newCap * loadFactor;//计算出阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);//判断计算出的阈值是否符合要求
}
threshold = newThr;//更新阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//按照新容量建立新桶数组
table = newTab;
if (oldTab != null) {//如果以前的桶里有数据
for (int j = 0; j < oldCap; ++j) {//遍历旧桶
Node<K,V> e;
if ((e = oldTab[j]) != null) {//如果桶该位置不为空,继续遍历该位置下的其他节点
oldTab[j] = null;//先清空旧桶方便回收
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//重新计算hash值对应的位置,并将节点存入对应位置
else if (e instanceof TreeNode)//如果该桶是一个树桶
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//因为桶长度变化,树中或者链表中部分节点不应该继续在原位置了,需要分割出来
else { //链表中节点的分割
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
TreeMap
TreeMap的组成数据结构也是红黑树,如果看过HashMap的源码就知道HashMap的一个桶变成树结构时其实就和TreeMap一样
- size----Map中元素个数
由于是树结构所以和链表一样并没有容量规定,所以无需扩容,只用和红黑树一样在插入删除后保持平衡即可这里不做过多讨论了。
LinkedHashMap
LinkedHashMap相对于HashMap保证了插入有序性,他额外维护了一个双向链表来保证插入顺序可知,其他方式和hashMap类似
HashTable
HashTable 相对于HashMap没有加入红黑树的数据结构只是利用了数组和链表的数据结构
- loadFactor----默认加载因子为0.75
- initialCapacity ---- 如果不设置初始容量,默认为11
- threshold---- 如果不设置初始容量,threshold = initialCapacity * loadFactor
- 扩容方式 newCapacity = (oldCapacity << 1) + 1;
扩容方式由于不设计红黑树所以并不复杂只是单纯的新建数组重新计算hash值对应位置(hashtable和hashMap通过hash值寻找桶位置的方式有一点不同这里是(hash & 0x7fffffff) % tab.length)扩容时调用方法为rehash方法。
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
相对于hashMap的扩容方式,rehash方法会遍历所有节点逐个重新计算hash值对应的桶位置,效率更低
在hashtable中所有操作方法都被同步关键字修饰,所以是并发安全的。
ConcurrentHashMap
它的加载因子初始容量,以及扩容方式等等都是和hashMap相同。
他主要的功能是在hashMap的基础上实现了分段锁的基础一定程度上保证了线程安全的问题。这个集合我们主要看看,put,get,remove
get(get方法没有加锁,只是使用了getObjectVolatile保证了可见性)
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);//使用volatile方法保证可见性
}
put(put是加锁的,通过锁住Bin头节点锁住整个bin)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//计算hash值,扩大高位hash值的影响
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//容器是否需要初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//桶为空不需要加锁,直接CAS就行了
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)//如果节点正在resize的转移过程中则先帮助转移
tab = helpTransfer(tab, f);
else {//其他时候正常加锁
V oldVal = null;
synchronized (f) {//加的是Node锁,所以只会锁住当前bin头节点
if (tabAt(tab, i) == f) {
if (fh >= 0) {//正常插入
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {//树桶插入
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {//检测是否需要树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {//树化过程也是加了锁的锁住的是一个bin头节点
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
remove
public boolean remove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
return value != null && replaceNode(key, null, value) != null;
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {//同样锁住的是一个Bin
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
可以大概理解分段锁是如何实现的了,没操作一个bin时,只会锁住当前bin对于其他Bin的操作时没有影响的。且只有在写操作时才会有锁,在读取操作时并没有同步只是使用volatile保证了可见行。
个人认为这里concurrentHashMap如果在实际应用中是会遇到问题的比如需要先读出一个节点通过修改这个节点再写入达到一个修改操作时,并不能保证整个操作的原子性,只是保证了单一写操作的原子性。
3.Set
Set集合实际上是对Map集合的再封装。
Set接口的具体内容
同List一致规范了Set集合中常用的方法
AbstractSet
这里AbstractSet只实现了三种方法,详细看代码就行了不用多说。
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
//用于子类构造函数调用
protected AbstractSet() {
}
//判断两个Set是否相等
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size()) //判断大小
return false;
try {
return containsAll(c); //判断内容
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
//Set的HashCode是包含的元素的HashCode的和
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}
}
HashSet
内部存储实际用的也是HashMap只不过存入的是Key而value是一个静态的类变量Object对象
TreeSet
内部存储同理用的是TreeMap存入的也只是Key,value是一个静态的Object对象