modCount
记录当前集合数据被修改的次数。[添加,删除] >>> 这两个操作影响元素的个数。 Itr内部有expectedModCount预期的修改次数变量。 当我们使用迭代器或foreach遍历时,如果你在foreach遍历时,会自动调用迭代器的迭代方法,此时在遍历过程中调用了集合的add,remove方法时,modCount就会改变。而迭代器记录的modCount是开始迭代之前的,如果两个不一致,就会报异常,说明有两个线程同时操作集合,这种操作有风险,为了保证结果的正确性,避免这样的情况发生,一旦发现modCount与expectedCount不一致,立即报错。
Set的底层实现
1、HashSet : HashMap
2、TreeSet : TreeMap
3、LinkedHashSet: LinkedHashMap
Set添加元素时用add(元素),而Map添加元素put(key,value)。
发现添加到Set中的元素,是作为底层的Map的key,那么value它们选用了一个Object类型的常量对象PRESENT。
所有的HashSet共用同一个PRESENT对象。
所有的TreeSet共用同一个PRESENT对象。
所有的LinkedHashSet共用同一个PRESENT对象。
Map底层实现
1、哈希表系列:
数组 + 链表
数组 + 链表/红黑树
2、TreeMap:红黑树
HashMap的底层实现:
JDK1.7以及之前:数组 + 链表
JDK1.8以及之后:数组 + 链表/红黑树
数组的优点:访问速度快,因为可以根据下标直接定位到某个元素
链表的优点:不需要元素是挨着存储,不需要连续空间,在添加和删除元素时不需要移动元素,只需要修改前后元素的引用关系就可以。
HashMap:会根据key的hashCode-->公式/算法-->[index]
因为不同的hashCode值,可能得到的[index]是相同的,那么此时就冲突了,那么只能把[index]的多个映射关系用链表连接起来
二叉树的优点:查找的速度比链表快
旧版的HashMap,如果key的hashCode算出了[index]相同的话(我们称为冲突)都在一个table[index]下面,如果严重的话,会导致[index]下面的链表很长,就会导致查询速度减慢。当链表长到一定程度时,就需要把链表变为二叉树,以提高我们查询速度。
JDK1.7的HashMap源码追踪
public HashMap(){
this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);// 默认容量16,默认加载因子0.75
}
public HashMap(int initialCapacity, float loadFactor) {
//排除非法值
this.loadFactor=loadFactor;
threshold=initialCapcaity;
init();
}
transient Entry<K,V>[] table=(Entry<K,V>[]) EMPTY_TABLE; // table数组初始化一个长度为0的空数组。
LoadFactor加载因子用来计算threshold=capacity*loadFactor。 数组需要考虑扩容的阈值=threshold。
为什么是0.75。 太小就频繁扩容,太大就可能导致某个table[index]下面的链表会很长,查询速度就低。
put的过程:
1.public void put(K key , V value){
if(table==EMPTY_TABLE){ inflateTable(threshold)}; //判断数组是否是空数组。如果是空数组就充气Table
int hash=hash(key);//根据key的hashCode用异或,无符号右移等各种运算,得到一个int类型的hash值。下面再用hash值来算[index]。它的设计者用户重写的hashCode可能不够均匀。
int i=indexFor(hash,table.length); // 哈希与 (上数组的长度-1) // table数组的长度意思2^n.
// table.lengt-1.的二进制,前面都是0,后面都是1, hash&table.lenght-1的结果一定是在0~table.length-1范围内。 //i决定了要存的数组的下标、
for(Entry<K,V> e=table[i];e!=null;e=e.next){ // 查找table[i]下面的链表中是否有映射关系的key是和我重复的,如果是重复的,就用新的value代替就得value。
if(e.hash==hash && ((k=e.key)==key || key.equals(k) )){// 哈希值的相等,或者key的地址相等,或者相等,就是key重复了,
V oldValue=e.value;
e.value=value;
e.recoredAccess(this);
return oldValue;
}
}
//key没有重复
modCount++;
addEntry(hash,key,value,i);
return null;
}
2.private void inflateTable(int toSize){
int capacity=runUpToPowerOf2(toSize); // 一开始是add时,传进来的toSize=16
threshold=(int)Math.min(capacity*loadFactory,MAXIMUM_CAPACITY+1); // 修改阈值为12.
table=new Entry[capacity]; // 创建一个大小为16的数组。
initHashSeedAsNeeded(capacity);
}
3. private static int runUpToPowerOf2(int number){
return number>=MAXIMUM_CAPACITY? MAXIMUM_CAPACITY:(number>1)? Integer.higestOneBit((number-1)<<1):1; //离number最近的2^n的数
}
//一开始的threshold的值为16.
(2) put()
发现数组table为空数组后,会把数组初始化长度为16的Entry类型的数组,并且把threshold计算为12.
如果不是空数组。检查Key==NULL?单独处理。如果Key!=null,算Key的哈希值。
final int hash(Object k){ //按位异或运算
int h=haseSedd;
if(0!=h&& k instanceOf String){ return sum,misc.Hashing.stringHash32((String) k)};
h^=k.hashCode;
h^=(h>>>20)^ (h>>>12); // 无符号右移,
return h^(h>>>7)^(h>>>4); // 为了扰乱,哈希值
}
public int indexFor(int hash,int lnegth){
//按位与运算。
}
void addEntry(int hash,K key,V value,int bucketIndex){
// 当元素的总个数达到阈值 && 新的映射关系要添加的table[index]下面不是空的。
if((size>=thres hold)&&(null!=table[bucketIndex])){
resize(2*table.length);// 数组扩容为原来的2倍。然后重新算哈希+bucketIndex
hash=(null!=key) ?hash(key):0;
buckenIndex=indexFor(hash,table.length);
}
createEntry(hash,key,value,bucketIndex);
}
void createEntry(int hash,K key,V value,int bucketIndex){
Entry<K,V> e=table[bucketIndex]; // 原来的节点都添加到我的下面。
table[bucketIndex]=new Entry<>(hash,key,value,e);
size++;
}
static class Entry{
final K key;
V value;
Entry<K,V> next;
int has;
Entry(int h,K k,V v,Entry<K,V> n){
value=v;
next=n;
key=k;
hash=h;
}
}
JDK1.8的HashMap源码追踪