一、ArrayList源码分析
- 继承结构与接口实现
-
JDK 8.0中ArrayList的变化:
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组 list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
源码解析:
//无参构造方法
public ArrayList() {
//创建一个 空的 ArrayList,此时其内数组缓冲区 elementData = {}, 长度为 0
//当元素第一次被加入时,扩容至默认容量 10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//创建一个初试容量的、空的ArrayList
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//当初始容量值(小于0)时抛出IllegalArgumentException
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public boolean add(E e) {
确保对象数组elementData有足够的容量,可以将新加入的元素e加进去
ensureCapacityInternal(size + 1); // Increments modCount!!
//在数据中正确的位置上放上元素e,并且size++
elementData[size++] = e;
return true;
}
//确定内部容量的方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//判断初始化的elementData是不是空的数组,也就是没有长度
//minCapacity = size+1,DEFAULT_CAPACITY = 10,所以将minCapacity变成10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断elementData是否够用
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);newCapacity就是1.5倍的oldCapacity
if (newCapacity - minCapacity < 0)//如果扩大1.5倍还不行,则进行大容量分配
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 两个常用构造方法分析:
- add()方法
- JDK 7.0情况下
- ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
- 不再判断初始化的elementData是不是空的数组了(相比JDK 8.0)
后续的添加和扩容操作与JDK 8.0 无异
小结:
jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
二、LinkedList源码分析
-
继承结构与接口实现
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。 LinkedList 实现 List 接口,能对它进行队列操作。 LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。 LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。 LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。 LinkedList 是非同步的
- LinkedList的属性
//实际元素个数
transient int size = 0;
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
- 静态内存类
private static class Node<E> {
E item; //存储当前节点的值
Node<E> next; //后继
Node<E> prev; //前驱
//构造函数,赋值前驱后继
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
- 两个构造方法
//无参构造方法
public LinkedList() {
}
//有参构造方法
public LinkedList(Collection<? extends E> c) {
this();
//添加集合中的所有元素
addAll(c);
}
- 添加操作分析
1)add(E e)方法
add(E e)用于将元素添加到链表尾部
public boolean add(E e) {
//添加到末尾
linkLast(e);
return true;
}
//Links e as last element.
void linkLast(E e) {
final Node<E> l = last;//临时节点l保存last,也就是l指向了最后一个节点
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)//如果链表为空,则newNode就成为了第一个节点,first和last都要指向它
first = newNode;
else //否则,在最后一个节点进行追加
l.next = newNode;
size++;//添加一个节点,size自增
modCount++;
}
2)add(int index,E e)方法
add(int index,E e)用于在指定位置添加元素
public void add(int index, E element) {
//检查索引是否处于[0-size]之间
checkPositionIndex(index);
if (index == size)//添加到链表尾部
linkLast(element);
else//添加到链表中间
linkBefore(element, node(index));
}
//返回idx指定位置的节点
Node<E> node(int index) {
// assert isElementIndex(index);
//如果索引位置靠近链表前半部分,从头开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//否则,从尾部开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
//新节点的前驱指向pred,后继为succ
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)//如果该节点插入在头结点之前,重置first头结点
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
总结:
add(int index,E e)方法:
1. 检查index的范围,否则抛出异常
2. 如果插入位置是链表尾部,那么调用linkLast方法
3. 如果插入位置是链表中间,那么调用linkBefore方法
linkBefore()方法
1. 创建newNode节点,将newNode的后继指针指向succ,前驱指针指向pred
2. 将succ的前驱指针指向newNode
3. 根据pred是否为null,进行不同操作。
- 如果pred为null,说明该节点插入在头节点之前,要重置first头节点
- 如果pred不为null,那么直接将pred的后继指针指向newNode即可
- arrayList和LinkedList区别
arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好
LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好。
分析不够全面(addAll()待补)
三、Vector源码分析
- 继承结构与接口实现
Vector是一个古老的集合,JDK 1.0就有了。大多数操作与ArrayList相同,区别在于Vector是线程安全的
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免选择使用。
JDK 7.0和JDK 8.0中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
在扩容方面,默认扩容为原来的数组长度的2倍。
- 构造方法
public Vector() {
this(10);
}
//设置初始化容量,默认当由于增加数据导致容量增加时,每次容量会增加一倍
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//capacity是Vector的默认容量大小,capacityIncrement是每次Vector容量增加时的增量值。
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//新建一个数组,数组容量是initialCapacity
this.elementData = new Object[initialCapacity];
//设置增长容量增长系数,如果该值为0,那数组就变为两倍的原长度
this.capacityIncrement = capacityIncrement;
}
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
- add()方法
public synchronized boolean add(E e) {
modCount++;
//增加元素前,检查容量是否够用
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
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;
//如果capacityIncrement不为0,那么增长的长度就是capacityIncrement,如果为0,那么扩增为2倍的原容量
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);
}
四、HashMap源码分析
JDK1.8之前,HashMap的底层数据结构是数组+链表,数组中的每个元素称为一个Entry,包含(hash,key,value,next)这四个元素,其中链表是用来解决碰撞(冲突)的,如果hash值相同即对应于数组中同一一个下标,此时会利用链表将元素插入链表的尾部,(JDK1.8是头插法)。在JDK1.8及之后,底层的数据结构是:数组+(链表,红黑树),引入红黑树是为了避免链表过长,影响元素值的查找,因此当整体的数组大小大于64时,并且链表的长度大于或等于8时,会把链表转化成红黑树。
JDK1.7源码分析
- 底层存储结构:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
- HashMap属性和构造方法
//初始默认数组的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子,表示当map集合中存储的数据达到当前数组大小的75%则需要进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//构造函数 1
public HashMap(int initialCapacity, float loadFactor) {
//如果初始容量 小于0 则抛异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//超过了最大值,则取最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//初始因子为小于等于0,或者不存在则抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//构造函数 2
public HashMap(int initialCapacity) {
//调用构造函数 1
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//构造函数 3
public HashMap() {
//调用构造函数 1
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
分析:
对于HashMap对的构造函数有三种,我们通常使用的构造函数 3 ,载荷因子表示当存储容量达到75%时需要对数组进行扩容。当选择 构造函数 2 和 构造函数 3 时,最终都会走构造函数1。
构造函数 1 :能够设置初始化数组长度,及载荷因子。
构造函数 2 :能够设置初始化数组长度,对于载荷因子默认为0.75。 会去调构造函数 1
构造函数 3 :默认初始化数组长度为16,载荷因子为0.75。 会去调构造函数 1
- put方法
1)put(K key,V value),注意一个问题,map是允许存储key=null且value=null的,而hashTable则不允许
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key==null,也放进去
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key);
int i = indexFor(hash, table.length);
//遍历table[i]位置上的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果元素相同,则将value更新并返回oldValue(元素相同:hash值相同并且equals相同)
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果table[i]上没有对应的key,则进行新添一个entry对象
addEntry(hash, key, value, i);
return null;
}
2)void addEntry(int hash, K key, V value, int bucketIndex)
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果size>=临界值并且要存放的桶的位置非空时
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容操作,默认情况下扩容为原来的2倍,扩容的同时重现计算
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = 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++;
}
JDK1.8源码分析
HashMap原理总结(重要)
HashMap是基于拉链法实现的一个散列表,内部由数组和链表实现。
数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算。
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能,这里是一个平衡点。
对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。
-
底层存储结构:
-
HashMap属性和构造方法:
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
public HashMap(int initialCapacity, float loadFactor) {
//校验 小于0报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//capacity大于最大值取最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能小于等于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor方法
this.threshold = tableSizeFor(initialCapacity);
}
---------------------------------------------------------
//传入一个初始容量,默认负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
---------------------------------------------------------
//无参数,负载因子默认0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
---------------------------------------------------------
//传入一个map的对象
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
TreeNode是Node是子类,继承关系如下:Node是单向链表节点,Entry是双向链表节点,TreeNode是红黑树节点
2. put方法
put()执行过程图解:
1)put(K key, V value)方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2)putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果当前table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//进行扩容,如果是首次添加,底层构建一个容量为16的数组
//计算数组索引,获取该索引位置的首节点,如果为null,添加一个新的节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//桶中已经存在元素
Node<K,V> e; K k;
// 如果首节点的key和要存入的key相同,那么直接覆盖value的值。
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 {
// 此时首节点为链表,如果链表中存在该键值对,直接覆盖value。
// 如果不存在,则在末端插入键值对。然后判断链表是否大于等于7,尝试转换成红黑树。
// 注意此处使用“尝试”,因为在treeifyBin方法中还会判断当前数组容量是否到达64,
// 否则会放弃次此转换,优先扩充数组容量。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//当p的next==null时,将新元素放到尾部元素的下一个
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //当链表上的元素个数大于等于7时,尝试转换成红黑树
treeifyBin(tab, hash);
break;
}
//如果在链表中找到相同的元素,跳出循环;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//p指向下一个元素
}
}
if (e != null) {
//对value进行更新操作,同时返回oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;// fail-fast机制
// 如果元素个数大于阈值,扩充数组。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
总结:
检查数组是否为空,执行resize()扩充;
通过hash值计算数组索引,获取该索引位的首节点。
如果首节点为null,直接添加节点到该索引位。
如果首节点不为null,那么有3种情况
① key和首节点的key相同,覆盖value;否则执行②或③
② 如果首节点是红黑树节点(TreeNode),将键值对添加到红黑树。
③ 如果首节点是链表,将键值对添加到链表。添加之后会判断链表长度是否到达TREEIFY_THRESHOLD - 1这个阈值,“尝试”将链表转换成红黑树。
最后判断当前元素个数是否大于threshold,扩充数组。
3)resize() 方法
final HashMap.Node<K,V>[] resize() {
HashMap.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;
}
// 否则数组容量扩充一倍。(2的N次方)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果数组还没创建,但是已经指定了threshold(这种情况是带参构造创建的对象),threshold的值为数组长度
// 在 "构造函数" 那块内容进行过说明。
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 这种情况是通过无参构造创建的对象
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 可能是上面newThr = oldThr << 1时,最高位被移除了,变为0。
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"})
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
table = newTab;
// 下面代码是将原来数组的元素转移到新数组中。问题在于,数组长度发生变化。
// 那么通过hash%数组长度计算的索引也将和原来的不同。
// jdk 1.7中是通过重新计算每个元素的索引,重新存入新的数组,称为rehash操作。
// 这也是hashMap无序性的原因之一。而现在jdk 1.8对此做了优化,非常的巧妙。
if (oldTab != null) {
// 遍历原数组
for (int j = 0; j < oldCap; ++j) {
// 取出首节点
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果链表只有一个节点,那么直接重新计算索引存入新数组。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果该节点是红黑树,执行split方法,和链表类似的处理。
else if (e instanceof HashMap.TreeNode)
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 此时节点是链表
else {
// preserve order
// loHead,loTail为原链表的节点,索引不变。
HashMap.Node<K,V> loHead = null, loTail = null;
// hiHeadm, hiTail为新链表节点,原索引 + 原数组长度。
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.Node<K,V> next;
// 遍历链表
do {
next = e.next;
// 新增bit为0的节点,存入原链表。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 新增bit为1的节点,存入新链表。
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;
}
对于扩充数组:
因为元素的索引是通过hash&(n - 1)得到的,那么数组的长度由n变为2n,重新计算的索引就可能和原来的不一样了。
在jdk1.7中,是通过遍历每一个元素,每一个节点,重新计算他们的索引值,存入新的数组中,称为rehash操作
而java1.8对此进行了一些优化l。