话不多说,直接上操作!!
ArrayList源码分析
- Iterable接口:提供迭代器访问能力,实现此接口允许对象通过foreach遍历。
- Collection接口:集合的根接口
- AbstractCollection抽象类:此类提供了Collection接口的基本实现,以最大程度减少实现此接口所需工作。
- List接口:Collection接口的子接口
- AbstractList抽象类:此类提供List接口的基本实现以最大程度减少由“随机访问”数据存储实现此接口所需要的工作。
- RandomAccess接口:该标记接口提供支持随机访问的能力。
- Cloneable接口:该标记接口提供实例克隆的能力。
- Serializable接口:该标记接口提供类序列化或反序列化的能力。
属性
//默认容量为10
private static final int DEFAULT_CAPACITY = 10;
//空数组,传入容量为0时使用,通过new ArrayList(0)创建
private static final Object[] EMPTY_ELEMENTDATA = {};
//空数组,与EMPTY_ELEMENTDATA区分开,通过new ArrayList()创建
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储元素的数组,transient修饰表示不序列化该属性,因为ArrayList具有动态扩容的特性,数组中的元素会有剩余,通过writeObject和readObject实现序列化和反序列化
transient Object[] elementData; // non-private to simplify nested class access
//数组的实际长度
private int size;
构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//初始容量大于0,则按照指定容量构造
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//初始容量为0,初始化为空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//初始容量小于0则异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 无参构造器,懒初始化,在添加第一个元素时elementData扩容为DEFAULT_CAPACITY,减少内存开销
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
//将集合转为Object[]类型数组
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
// 利用Arrays的copyOf函数复制成Object[]类型的elementData
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//如果传入集合长度为0,则初始化为空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
add方法
1、add(E e)方法
添加指定元素到集合末尾
public boolean add(E e) {
//检查插入一个元素是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素插入到最后一位
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//确保传入的最小容量minCapacity
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//计算所需最小容量,如果当前list是空数组,最小容量就是默认容量和传入的minCapacity的最大值,否则最小容量就是minCapacity
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//修改次数+1,用于fail-fast机制
modCount++;
// 当所需的最小容量大于现有数组的长度时,需要进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//新容量=旧容量+旧容量*0.5,也就是扩容为之前的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量小于所需的旧容量(还不够),那就按照旧容量的扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新容量超过了所能分配的最大容量,则新容量的大小根据所需的最小容量重新计算
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容完毕,将新容量复制给elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
//所需容量大于MAX_ARRAY_SIZE时返回 Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE,其中MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
2、add(int index, E element)方法
将指定的元素插入到集合指定位置,将当前在该位置的元素及之后的元素向后移动。
public void add(int index, E element) {
//越界检查
rangeCheckForAdd(index);
//是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将elementData中index位置开始的元素复制到index+1开始的位置,复制的长度为size-index
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将该元素添加到指定下标位置
elementData[index] = element;
//数组长度+1
size++;
}
3、addAll(Collection c)方法
将指定集合中所有元素追加到集合末尾
public boolean addAll(Collection<? extends E> c) {
//将集合c转成object[]数组
Object[] a = c.toArray();
//插入的集合长度
int numNew = a.length;
//检查新容量size + numNew时是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//将集合a中的所有元素拷贝到elementData的最后
System.arraycopy(a, 0, elementData, size, numNew);
//数组长度扩大为size+numNew
size += numNew;
//如果插入的集合不为空就返回true,否则返回false
return numNew != 0;
}
4、addAll(int index, Collection c)方法
从指定位置开始,将指定集合的所有元素插入到此集合中,将当前位于该位置的元素和所有的元素后移
public boolean addAll(int index, Collection<? extends E> c) {
//越界检查
rangeCheckForAdd(index);
//将要插入的集合c转成Object[]数组
Object[] a = c.toArray();
//要插入的数组长度
int numNew = a.length;
//插入之后的新长度size + numNew是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//指定位置后数组要移动的长度,如果index恰好等于size就不需要移动
int numMoved = size - index;
if (numMoved > 0)
//将elementData中index开始的元素复制到elementData中index+numNew的位置,复制长度为numMoved
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//将添加的数组a复制到elementData中index开始的位置,复制长度为numNew
System.arraycopy(a, 0, elementData, index, numNew);
//增加后的数组长度为size+numNew
size += numNew;
return numNew != 0;
}
remove方法
1、remove(int index)
删除集合中指定位置的元素,将所有后续元素向左移动
public E remove(int index) {
//越界检查
rangeCheck(index);
//修改次数+1,用于fail-fast机制
modCount++;
//取出要删除的元素
E oldValue = elementData(index);
//删除该下标的元素,后面元素要移动的元素,最好的情况是index=size-1即最后一个元素,那就不需要移动元素
int numMoved = size - index - 1;
if (numMoved > 0)
//将elementData中index+1位置开始的元素复制到index开始的位置,复制长度为numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将最后一个元素置为null,方便GC回收,删除的时候并没有缩小容量
elementData[--size] = null; // clear to let GC do its work
//返回删除的元素
return oldValue;
}
2、remove(Object o)
从集合中删除第一次出现的指定元素,如果存在,则将其删除。如果集合中不包含该元素,则保持不变
public boolean remove(Object o) {
if (o == null) {
//如果删除的元素为null,遍历elementData数组,用==比较,找到第一个值为null的,快速删除对应的下标
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//遍历所有的elementData,用equal比较两个对象,找到第一次相等位置的下标快速删除
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
//没有找到对应的元素返回false
return false;
}
private void fastRemove(int index) {
//修改次数+1,用于fail-fast机制
modCount++;
//需要移动的长度,如果index恰好为size-1,即删除最后一个元素时不需要移动
int numMoved = size - index - 1;
if (numMoved > 0)
//将elementData中index+1开始的元素复制到index开始的位置,复制长度为numMoved
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将最后一个元素置为null,方便GC回收,删除的时候并没有缩小容量
elementData[--size] = null; // clear to let GC do its work
}
3、removeAll(Colleciton c)
从集合中删除指定的集合中包含的所有元素
public boolean removeAll(Collection<?> c) {
//非空检查
Objects.requireNonNull(c);
//批量删除包含集合c的元素
return batchRemove(c, false);
}
//complement为true表示删除不包含在c中的元素,求两个集合的交集,complement为false表示删除包含在集合c中的元素,求两个集合的差集
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
//r为写指针,w为读指针
int r = 0, w = 0;
//是否修改的标志
boolean modified = false;
try {
for (; r < size; r++)
//当c中不包含elementData[r]且complement=false时, elementData[w++]存放的保留元素就是elementData中除去集合c有的元素
//当c中包含elementData[r]且complement=true时,elementData[w++]存放的保留元素就是elementData和集合c的交集
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// 正常循环结束,r==size,否则c.contain()抛出了异常
if (r != size) {
//将elementData中r位置开始的元素复制到elementData中w开始的位置,复制长度为size-r,也就是将出错位置r开始的所有元素移动到已经保留的元素w位置之后
System.arraycopy(elementData, r,
elementData, w,
size - r);
//更新保留的元素个数
w += size - r;
}
//如果w==size,表示全部元素保留,没有修改,返回false
if (w != size) {
// 将未保留的元素置为null,方便GC回收
for (int i = w; i < size; i++)
elementData[i] = null;
// 修改的次数+未保留的元素个数
modCount += size - w;
size = w;
//标记修改成功
modified = true;
}
}
//修改失败返回false
return modified;
}
HashSet源码分析
类定义
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
- 是一个泛型类
- 继承自AbstractSet,实现了Set接口
- 实现了Cloneable接口,表示HashSet支持克隆
- 实现了Serializable,表示支持序列化和反序列化
字段属性
//使用HashMap存放数据,从泛型E可以看出HashSet的值就是hashMap中的key
private transient HashMap<E,Object> map;
// 默认对象,存入map中的value
private static final Object PRESENT = new Object();
从字段属性可以看出:hashSet底层使用HashMap存储数据,hashSet的数据实际就是hashMap的key,value默认为创建的PRESENT对象。
构造方法
//默认的构造方法,初始化map
public HashSet() {
map = new HashMap<>();
}
//传入一个集合对象的构造方法
public HashSet(Collection<? extends E> c) {
//初始化hashMap,(c.size()/.75f) + 1作为默认长度,如果小于HashMap的默认长度16,则使用HashMap的默认长度
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//传入初始化容量和负载因子
public HashSet(int initialCapacity, float loadFactor) {
//调用hashMap的构造方法
map = new HashMap<>(initialCapacity, loadFactor);
}
//传入初始化容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//这个构造方法的意义是让LinkedHashSet使用,因为初始化的map是LinkedHashMap,dummy没什么用
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
其他方法
1、add方法
public boolean add(E e) {
//调用map添加,从这可以看出hashSet的元素实际上就是hashMap的key
return map.put(e, PRESENT)==null;
}
2、remove方法
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
可以看出,都是调用map方法进行的操作。
TreeSet的应用
1、首先定义一个Student类,需要实现hashCode和equals方法
public class Student {
private String name;
private int age;
public Student() {}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {
return age*31+name.hashCode()*31;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student s = (Student) obj;
if (this.age == s.getAge() && this.name.equals(s.getName())) {
return true;
}
}
return false;
}
}
2、测试类
public class TreeSetTest {
public static void main(String [] args) {
Set<Student> set = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//按照年龄倒叙排序
return o2.getAge()-o1.getAge();
}
});
Student s1 = new Student("小明", 12);
Student s2 = new Student("小华", 13);
Student s3 = new Student("小坤", 14);
set.add(s1);
set.add(s2);
set.add(s3);
for (Student student : set) {
System.out.println(student.getName()+"-"+student.getAge());
}
}
}
输出结果: /* 输出结果 小坤-14 小华-13 小明-12 */
Comparable和Comparator
Comparable
Comparable接口定义很简单,源码如下:
public interface Comparable<T> {
int compareTo(T t);
}
如果一个类实现了Comparable接口,只需要重写compareTo方法,就可以按照自己制定的规则将由它创建的对象进行比较。
举例
1、定义一个Stundet类,实现Comparable接口,重写compareTo方法
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int hashCode() {
return age*31+name.hashCode()*31;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student s = (Student) obj;
if (this.age == s.getAge() && this.name.equals(s.getName())) {
return true;
}
}
return false;
}
@Override
public int compareTo(Student o) {
//按照年龄顺序排序
return this.age-o.getAge();
}
}
2、测试类
public class ComparableTest {
public static void main(String [] args) {
List<Student> list = new ArrayList<>();
Student s1 = new Student("小敏", 12);
Student s2 = new Student("小建", 11);
Student s3 = new Student("小可", 10);
list.add(s1);
list.add(s2);
list.add(s3);
Collections.sort(list);
for (Student student : list) {
System.out.println("name="+student.getName()+",age="+student.getAge());
}
}
}
3、输出结果: name=小可,age=10 name=小建,age=11 name=小敏,age=12
CompareTo方法返回值与排序效果
- 返回负数:代表当前对象小于传入的对象。
- 返回0:代表当前对象等于传入的对象。
- 返回正数:代表当前对象大于传入的对象。
Comparator
相较于Comparable复杂,核心方法只有两个:
public interface Comparator<T> {
//返回值可能是负数、零或者正数,代表第一个对象小于、等于或大于第二个对象
int compare(T o1, T o2);
//需要传入一个Object作为参数,并判断该Object是否和Comparator保持一致
boolean equals(Object obj);
}
自定义比较器
public class ComparatorStudent implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
//先按照年龄顺序排序,年龄一样,再按照名字的字母表顺序排序
if (o1.getAge() < o2.getAge()) {
return -1;
} else if (o1.getAge() == o2.getAge()) {
if (o1.getName().compareTo(o2.getName()) <0) {
return -1;
} else {
return 1;
}
} else {
return 1;
}
}
}
测试类:
public class ComparatorTest {
public static void main(String [] args) {
List<Student> list = new ArrayList<>();
Student s1 = new Student("xiaoming", 12);
Student s2 = new Student("haha", 10);
Student s3 = new Student("nihao", 10);
list.add(s1);
list.add(s2);
list.add(s3);
list.sort(new ComparatorStudent());
for (Student student : list) {
System.out.println("name="+student.getName()+",age="+student.getAge());
}
}
}
/*
输出结果:(先按年龄顺序排序,再按照名字的字母表顺序排序)
name=haha,age=10
name=nihao,age=10
name=xiaoming,age=12
*/
总结
- Comparable位于java.lang包下,属于内部比较器,一个类如果想要使用Collections.sort(list)进行排序,则需要实现该接口
- Comparator位于java.util包下,属于外部比较器,对于那些没有实现Comparable接口或者对Comparable排序规则不满意的可以自定义类比较器。
Map
HashMap的java7实现
HashMap里面是一个数组,数组中每个元素是一个单向链表。上图中每个绿色的实体是嵌套类Entry的实例,Entry包含四个属性:key,value,hash值和指向下一元素的next指针。
HashMap的java8实现
Java8对HashMap做了一些修改,最大不同是利用了红黑树,结构变成了数组+链表+红黑树。根据Java7的数组+链表实现,查找时根据hash值可以快速定位到数组的具体下标。之后,就要顺着链表查找那个唯一的元素,时间复杂度取决于链表的长度O(n)。为了降低这部分开销,当链表长度超过一定值(默认为8),会将链表转换为红黑树,在链表上查找的时间复杂度变为O(logN)
初始容量为什么是16
容量就是一个hashMap中“桶”的个数,当我们要往一个HashMap中国put一个元素时,需要通过一定的算法(hash算法)计算出应该把它放到哪个桶里,这个过程就叫做哈希。对应的就是hash()方法。哈希的功能就是根据key来定位这个K-V在链表数组中的位置,也就是hash方法的输入应该是个Object类型的key,输出是个int类型的数组下标。
hash的实现是由两个方法 int hash(Object k)和int indexFor(int h, int length)实现的。我们来看下indexFor()方法,java8虽然没有这样一个单独的方法,但是查询下标的算法和java7是一样的:
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor主要是将hashCode转换成链表数组中的下标,h表示元素的hashCode值,length表示hashMap的容量。h&(length-1)就是取模运算,之所以用位运算是因为位运算更快,原理是:X % 2^n = X & (2^n - 1) 既然length需要是2的幂,那么为什么一定是16呢?可以是4,8,32吗?其实16是一个经验值,在效率和空间上做的一个权衡,这个值不能太大,太大了浪费空间,也不能太小,太小了可能频繁触发扩容。
负载因子
什么是负载因子
第一次创建HashMap时,就会指定容量(不指定默认为16),随着我们不断put元素,就有可能超过容量,那么就需要扩容。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length); }
createEntry(hash, key, value, bucketIndex);
}
从代码可以看出,在添加元素时,如果元素数量超过临界值threshold时,就会自动扩容,并且扩容之后,还需要对HashMap中原有元素进行rehash,将原来桶中元素重新分配到新的桶中,在hashMap中,临界值threshold = 负载因子(loadFactor)* 容量(capacity)。负载因子表示HashMap满的程度,默认值为0.75,也就是说默认情况下,HashMap中元素个数达到了容量的3/4就会自动扩容。
为什么要扩容
HashMap再扩容不仅要对容量扩充,还要rehash,这个过程很耗时,且随着元素越多越好时。为什么还要扩容?这跟哈希碰撞有关。hashMap底层是基于哈希函数实现的,哈希函数有个特点:根据同一哈希函数计算出的散列值如果不同,输入值肯定不同,但是计算出的散列值相同的输入值也不一定相同。两个不同的输入值,根据同一哈希函数计算出的散列值相同的现象就叫做碰撞。解决碰撞的方法很多,其中链地址法比较常见,也是hashMap采用的。
- 当put元素时,先定位到是数组中的哪个链表,然后把元素挂到这个链表的后面。
- 当get元素时,先定位到是数组的哪个链表,然后逐一遍历链表的元素,直到找到需要的元素为止。 解决碰撞最有效的方法是扩大数组容量,再通过一个合适的hash算法计算元素分配到哪个桶里,就可以大大减少冲突的概率。
为什么默认是0.75
JDK官方文档描述:默认的负载因子0.75在时间和空间成本之间提供了很好的权衡,更高的减少了空间开销,但增加了查找成本。 如果负载因子为1,就表示hashMap满了才扩容,那么在hashMap中最好的情况就是16个元素平均分配到16个桶里,否则必然会碰撞。随着元素越多碰撞概率越大。
为什么是线程不安全的
HashMap的线程不安全体现在会造成死循环、数据丢失以及数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7出现的问题,在JDK1.8已经得到解决。但是1.8还是有数据覆盖的问题。
扩容引发的数据不安全
HashMap的线程不安全主要发生在扩容函数里,根源在transfer函数中,JDK1.7中HashMap的transfer函数如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算每个桶的下标
int i = indexFor(e.hash, newCapacity);
//采用头插法将元素迁移到新数组中
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
这段代码是JDK1.7中HashMap的扩容操作,重新定位每个桶的下标,采用头插法将元素迁移到新数组中。头插法会将链表的顺序反转,这是死循环的原因。
扩容引发的数据不一致问题
在JDK1.7出现的问题在JDK1.8中得到很好的解决,在1.8中没有了transfer方法,直接在resize()完成了数据迁移,而且元素插入采用尾插法。
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) // 如果没有hash碰撞则直接插入元素
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 第六行代码是判断是否出现hash碰撞,假设两个线程A和B都在进行put操作,并且hash函数计算出结果相同,当线程A执行完第六行代码时让出了CPU,线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所以此时不会再判断,而是直接插入,就会覆盖线程B的数据。
- if ((e = p.next) == null)也会出现线程安全问题,当两个线程A和B都执行到这里,线程A和B都判断if ((e = p.next) == null)为真,此时线程A和B持有的e和p都是同一链表的节点,此时线程A挂起,线程B完成了插入,即p.next=new Node(hash,key,value,null),然后A获得时间片也执行了插入,就发生了覆盖。
源码分析
字段和构造函数
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于64时,链表才会转化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//链表的节点,一个哈希桶中的Node,hash和key可能都不相同,因为槽点是通过hash%(n-1)得到的。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//当前node的hash值,作用是决定位于哪个哈希桶,是通过key的hashCode计算的
final K key;//可能是包装类实例也可能是自定义对象,不同key可能有相同的hash
V value;
Node<K,V> next;//指向下一个node节点,构成单向链表。
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
//存放数据的数组
transient Node<K,V>[] table;
//遍历的entrySet
transient Set<Map.Entry<K,V>> entrySet;
//HashMap的实际大小。注意两点:1、具体节点的数量并没有成员变量做记录,只是在每次遍历一个桶时用binCount作为局部变量计数。2、size可能不准,因为当你拿到这个值时,可能又发生变化。
transient int size;
//记录迭代过程的HashMap结构是否发生变化,如果有变化,迭代时会fail-fast
transient int modCount;
//阈值,两个作用:初始容量和扩容阈值
int threshold;
//负载因子,当元素容量达到了数组总容量的loadFactor,就自动扩容
final float loadFactor;
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始化容量不能大于最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//使用传入的初始化容量和0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用16和0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//使用已有的HashMap构建一个新的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
capacity必须是2的幂
- 目的:保证计算出的桶位置tab[i=(n-1) & hash]的结果在[0,Cap-1]之间。
- 实现:空参构造:n=16,指定初始容量:通过threshold=tableForSize计算初始容量,得到>=cap的最接近2的幂,比如给定初始大小为19,则实际上初始化大小为32。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 后续扩容resize>>1,即两倍扩容。
threshold的阈值
threshold有两个作用:
- 初始容量:用于指定容量时的初始化,=tableDizeFor(initialCap),在resize()中使用。
- 扩容阈值:=数组容量*0.75,在putValue()中使用,用于记录数组扩容阈值。
hash算法
计算每个Node的hash值,这个hash值是确定hash桶的依据。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 如果某个类型要作为hashMap的key,必须要有hashCode方法。
- 将hashCode值再右移16位是为了让计算出的hash更分散,以减少哈希碰撞。
- 得到hash值后,具体的槽点=hash % (length)=hash & (length-1)
put方法
public V put(K key, V value) {
//传入hash值的目的是定位目标桶
return putVal(hash(key), key, value, false, true);
}
//onlyIfAbsent:为true表示只在key不存在时添加,默认为false。返回的是oldValue,若不存在相同key返回null。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//n为数组长度,i为数组下标索引,p为i下标位置的Node
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//如果数组为空,使用resize()方法初始化
n = (tab = resize()).length;
//根据hash值和数组长度n计算数组下标(n - 1) & hash其实就是hash%(n-1),如果当前索引位置为空,直接生成节点在当前索引位置上
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))))
//如果key的hash和值都相等,直接把当前下标的Node值赋值给临时变量
e = p;
else if (p instanceof TreeNode)
//如果是红黑树,使用红黑树的方式新增
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//走到这里,说明还是个链表,自旋,从链表头开始遍历,binCount还有记录链表长度的作用
for (int binCount = 0; ; ++binCount) {
//p.next==null表示已经遍历到链表尾
if ((e = p.next) == null) {
//尾插法
p.next = newNode(hash, key, value, null);
//插入之后当链表长度大于8要转成红黑树
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 = p.next
p = e;
}
}
//说明链表中存在与key相同的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
//当onlyIfAbsent为false时,才会覆盖值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//修改次数+1,用于fail-fast
++modCount;
//如果hashMap的实际大小大于扩容的阈值,开始扩容,注意这里是先增加再扩容
if (++size > threshold)
resize();
//对于LinkedListMap判断是否执行LRU条件之一(默认为true)
afterNodeInsertion(evict);
return null;
}
链表树化&&向红黑树中添加元素
treeifyBin():链表转化为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前hash表长度小于64,不会树化而是进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
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);
}
}
树化的条件:
- 链表长度大于等于8
- 整个数组的大小大于64(当数组大小小于64时,只会进行扩容,不会树化) 链表长度阈值8是怎么得来的呢?
- 在链表数据不多的时候,使用链表遍历较快,且红黑树占用空间是链表的两倍。
- 参考泊松分布概率函数,由泊松分布得出结论,链表各个长度命中概率:当链表的长度为8时,出现的概率是0.00000006,不到千分之一,正常情况下,链表的长度不可能达到8,一旦达到8肯定是hash算法出现了问题。 putTreeVal()方法:
//h是key的hash值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//找到根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//自旋,从根节点遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//p的hash值大于h,说明p在h的右边
if ((ph = p.hash) > h)
dir = -1;
//p的hash值小于h,说明p在h的左边
else if (ph < h)
dir = 1;
//要放进去的key在树中已经存在了,直接返回。
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//自己实现的Comparable的话,不能用hashCode比较了,需要用compareTo
else if ((kc == null &&
//得到key的Class类型,如果key没有实现Comparable就是null
(kc = comparableClassFor(k)) == null) ||
//当前节点pk和入参k不等
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//找到和当前hashCode值接近的节点(当前节点的左右子节点其中一个为空即可)
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//生成新的节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//把新节点挡在当前子节点为空的位置上
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//当前节点和新节点建立父子前后关系
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
/*
balanceInsertion:对红黑树进行着色或旋转
着色:新节点总是为红色,如果父节点是黑色则不需要重新着色,如果父节点是红色则需要重新着色或旋转再次达到红黑树的5个约束条件。
旋转:父亲是红色,叔叔是黑色时,进行旋转。如果当前节点是父亲的右节点,则进行左旋。如果当前节点是父亲的右节点,则进行右旋。
moveRootToFront是把算出来的root放到根节点上
*/
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
红黑树的5个原则
- 节点是红色或黑色
- 根节点是黑色
- 所有叶子节点都是黑色
- 从任意节点到每个叶子的所有简单路径都包含相同数目的黑色节点
- 从每个叶子到根的所有路径上不能有连续的红色节点
扩容resize()函数
1、计算新数组大小
- 初始化:指定容量,newCap = threshold,未指定容量,newCap=16
- 扩容:newCap = oldCap * 2
2、指定扩容:逐个遍历所有槽点 - 当前槽点只有一个node,重新分配到新数组即可:newTable[e.hash & (newCap-1)]
- 当前槽点下是红黑树
- 当前槽点下是链表:因为链表中所有node的hash和key相同,而现在数组扩容了两倍,怎么把当前链等分成两部分呢?分成high链和low链,两链的关系近似理解为单双数节点。Head用来标识链首,Tail用来尾连接。low链置于newTab[j],high链置于newTab[j+oldCap](j表示原来数组位置)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//情况1,oldCa>0表示要扩容
if (oldCap > 0) {
//当旧数组容量大于最大值,无法再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//newCap=oldCap*2,newThr=oldThr*2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//情况2,初始化,且已指定初始化容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//情况3,初始化,未指定初始化容量,则cap=16,threshold=16*0.75
else { // zero initial threshold signifies using defaults
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
table = newTab;
//oldTab != null表示扩容,初始化不走这里
if (oldTab != null) {
//遍历原数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//当原数组的当前槽点有值时,将其复制给e
if ((e = oldTab[j]) != null) {
//释放原数组的当前节点内存,帮助GC
oldTab[j] = null;
//若当前槽点只有一个值,即链表只有一个节点,直接找到新数组响应位置并赋值
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//若当前节点下是红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//若当前节点下是链表
else { // preserve order
//将链表分成两条链,low低位链和high高位链。Head标识链头,Tail用来连接
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);
//将低位链放置于老链同一下标,例如原来当前节点位于oldTab[2],则现在低位链还处于newTab[2]
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链放置于老链下标+oldCap,例如原来当前节点为与oldTab[2],则现在高位链置于newTab[2+8=10]
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
ConcurrentHashMap
ConcurrentHashMap的出现主要为了解决HashMap在多线程环境下的不安全,JDK1.8设计巧妙,大量利用了volatile和CAS等乐观锁机制减少锁竞争对于性能的影响,ConcurrentHashMap保证线程安全的方案是:
- Java8:synchronized+CAS+HashEntry+红黑树
- Java7:ReentrantLock+Segment+HashEntry
Java7的ConcurrentHashMap
在jdk1.7中是由分段锁Segment数据结构+HashEntry数组组成,且主要通过Segment分段锁技术实现线程安全。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment包含一个HashEntry数组,每个HashEntry又是一个链表结构,因此ConcurrentHashMap查询一个元素需要两次Hash过程。
- 第一次hash定位到Segment
- 第二次hash定位到元素所在的链表的头部
通过Segment分段锁技术,将数据分成一段段存储,然后给每段分配一把锁,当一个线程访问其中一段时,其他段的数据也能被其他线程访问,实现段之间的并发访问。这样的结构使得hash过程变长,影响读性能,但写操作可以并发。
Java8的ConcurrentHashMap
JDk8的ConcureentHashMap的内部结构是:数组+链表+红黑树,Java8在链表长度超过一定值(默认是8)会转换成红黑树,跟hashMap一样。
源码分析
Node类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; //使用了volatile属性
volatile Node<K,V> next; //使用了volatile属性
...
}
ConcurrentHashMap采用Node作为基本的存储单元,每个键值对都存储在一个Node中,使用volatile修饰next和value,保证可见性。 ConcurrentHashMap中查找元素、替换元素和赋值元素都是基于sun.misc.Unsafe中原子操作实现多并发的无锁化操作。
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);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
put方法
put方法添加一个键值对封装成Node对象到ConcurrentHashMap中,通过key的hashCode计算出一个数组下标索引用来存储。
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//空值校验
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
//遍历table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//table没有初始化则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//计算要存储的索引位置i = (n - 1) & hash,跟HashMap实现一样。索引位置元素为空,则尝试CAS设置Node
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当前正在扩容,帮助扩容迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//存储的位置hash碰撞了
else {
V oldVal = null;
//锁定table[i]桶中的头结点
synchronized (f) {
//二次校验头结点是否变化
if (tabAt(tab, i) == f) {
//当前桶中的冲突时链表结构
if (fh >= 0) {
binCount = 1;//计算链表元素个数,用于判断是否要转成红黑树
//遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//找到key相同的节点,如果onlyIfAbsent=false就覆盖原值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//没有相同的key就生成一个新的节点插入链表尾端
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果头结点是红黑树结构,putTreeVal将节点加入到红黑树
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;
}
}
}
}
//根据table[i]桶node个数是否超过TREEIFY_THRESHOLD阈值进行红黑树转换
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//size计算以及判断是否扩容
addCount(1L, binCount);
return null;
}
put方法需要注意的几点:
- 如何解决的hash碰撞:当出现hash冲突时,也就是计算出的table[i]已经有node了,通过链表和红黑树两种方式处理。
- 如何解决多线程并发安全问题: (1)在第一次put数据的时候,调用initTable()方法,使用sizeCtl参数作为控制标志,当插入元素时才会初始化Hash表,在开始初始化时,先判断sizeCtl,如果小于0表示有线程正在初始化,当前线程放弃初始化操作。否则将sizeCtl设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。
//Hash表的初始化和调整大小的控制标志。为负数表示Hash表正在初始化或正在扩容。(-1表示正在初始化,-N表示N-1个线程在进行扩容),否则,当表为null时,保存创建时使用的初始化大小或者默认0。初始化以后保存下一个调整大小的尺寸。
private transient volatile int sizeCtl;
//第一次put,初始化数组
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果已经有别的线程在初始化,这里等待一下
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//-1表示正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
(2)判断在table[i]位置没有node时会尝试CAS原子性插入 (3)table[i]位置已经有node了,会锁定头结点,synchronized(table[i])来操作链表或红黑树,通过锁定头结点的方式来确保线程安全,所有节点都是尾插,头结点不变。
- 多线程扩容实现:(fh==f.hash)==MOVED时会辅助扩容,而且当插入完成之后addCount(1L,binCount)还会判断是否需要扩容。
- 计算hash值,通过hash算法可以将元素分散到哈希桶中。
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
今天就到这里了,不知不觉已经一万字了..