1、ArrayList
(1)实现List接口,底层数组实现。初始容量为10,每一次扩容是上一次容量的1.5倍。
需要注意的是,size是按照调用add,remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1。
(2)源码分析
添加:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断是否需要扩容,Increments modCount!!
elementData[size++] = e;
return true;
}
//向指定位置插入元素
public void add(int index, E element) {
rangeCheckForAdd(index);//判断索引是否越界
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, //将指定位置之后的所有元素向后移动一个位置
size - index);
elementData[index] = element;//将元素插入指定位置
size++;//ArrayList大小加1
}
//移除指定位置的元素
public E remove(int index) {
rangeCheck(index);//判断是否越界
modCount++;//修改次数加1
E oldValue = elementData(index);
int numMoved = size - index - 1;//因为index是从0开始
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
//删除指定位置的元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
扩容
我们看一下,构造ArrayList的时候,默认的底层数组大小是10:
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this(10);
}
modCount++;//ArrayList被修改的次数
// overflow-conscious code
if (minCapacity - elementData.length > 0) //minCapacity为现在ArrayList真实大小,elementData为现在ArrayList // 最 大 容 量
grow(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);//将旧数组拷贝到扩容后的新数组
}
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
//修改指定位置的元素
public E set(int index, E element) {
rangeCheck(index);E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
(3)ArrayList的优缺点
1、ArrayList底层以数组实现,是一种 随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
不过ArrayList的缺点也十分明显:
1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能
因此, ArrayList比较适合顺序添加、随机访问的场景。
(4) ArrayList和Vector的区别
Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码是这样的:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
们看一下ArrayList中的数组,是这么定义的:
private transient Object[] elementData;
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out array length
s.writeInt(elementData.length);
// Write out all elements in the proper order.
for (int i=0; i<size; i++)
s.writeObject(elementData[i]);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
1、加快了序列化的速度
2、减小了序列化之后的文件大小
2、HashMap
(1)底层为数组加链表实现,初始容量为16,扩容因子是0.75,每一次扩容都是上一次容量的2倍。
(2)源码分析
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
源码如下:
- /**
- * The table, resized as necessary. Length MUST Always be a power of two.
- */
- transient Entry[] table;
- static class Entry<K,V> implements Map.Entry<K,V> {
- final K key;
- V value;
- Entry<K,V> next;
- final int hash;
- ……
- }
可以看出,Entry就是数组中的元素,每个 Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
存储:
- public V put(K key, V value) {
- // HashMap允许存放null键和null值。
- // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
- if (key == null)
- return putForNullKey(value);
- // 根据key的keyCode重新计算hash值。
- int hash = hash(key.hashCode());
- // 搜索指定hash值在对应table中的索引。
- int i = indexFor(hash, table.length);
- // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
- for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果key已经存在,则用新值代替老值,
- //并返回老值
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- // 如果i索引处的Entry为null,表明此处还没有Entry。
- modCount++;
- // 将key、value添加到i索引处。
- addEntry(hash, key, value, i);
- return null;
- }
- void addEntry(int hash, K key, V value, int bucketIndex) {
- // 获取指定 bucketIndex 索引处的 Entry
- Entry<K,V> e = table[bucketIndex];
- // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
- table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
- // 如果 Map 中的 key-value 对的数量超过了极限
- if (size++ >= threshold)
- // 把 table 对象的长度扩充到原来的2倍。
- resize(2 * table.length);
- }
从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。如果要添加的key已经存在,则用新的value代替旧的value,并返回旧的value。
我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
例:15%4=3,代替为 15 & 3=1111 & 0011=0011=3
例:
两个key,调用Object的hash方法后值分别为:
32,64,然后entry数组大小为:16,即在调用indexFor时参数分别为[32,15],[64,15],
这时分别对它们调用indexFor方法:
32计算过程:
100000 & 1111 => 000000 =>0
64计算过程如下:
1000000 & 1111 => 000000 =>0
可以看到indexFor在Entry数组大小不是很大时只会对低位进行与运算操作,高位值不参与运算(如果Entry大小为32,则只会与低5位进行与操作),很容易发生hash冲突。
这里,32与64这两个hash值,都被存储在Entry数组0的位置上。
为了解决这个问题,HashMap在做indexFor操作前,需要调用hash方法,使hash值的位值在高低位上尽量分布均匀,hash方法:static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
还是按前面的key,经过Object的hash方法后,分别为32,64来进行运算:
32调用hash运算过程如下:
原始h为32的二进制:
100000
h>>>20:
000000
h>>>12:
000000
接着运算 h^(h>>>20)^(h>>>12):
结果: 100000
然后运算: h^(h>>>7)^(h>>>4),
过程如下:
h>>>7: 000000
h>>>4: 000010
最后运算: h^(h>>>7)^(h>>>4),
结果: 100010,即十进制34
调用indexFor方法:
100010 & 1111 => 2,即存放在Entry数组下标2的位置上
------------------------------------
64的运算结果为:1000100,十进制值为68
调用indexfor方法:
1000100 & 1111 => 4,即存放在Entry数组下标4的位置上
可以看到经过hash方法后,再调用indexFor方法,这样可以减少冲突。
读取:
- public V get(Object key) {
- if (key == null)
- return getForNullKey();
- int hash = hash(key.hashCode());
- for (Entry<K,V> e = table[indexFor(hash, table.length)];
- e != null;
- e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
- return e.value;
- }
- return null;
- }
从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
HashMap的resize(rehash):
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,此时抛出ConcurrentModificationException()。
注意到modCount声明为volatile,保证线程之间修改的可见性。