ArrayList作为最常用的数据结构之一,它为什么会出现?
String[] strArray = new String[10];
以上是一个数组的定义,数组可以集合,但是必须是定长的,这就导致了一个问题,就是动态的变长。于是ArrayList的就解决了这个问题。所以,它的背后的原理就是基于动态数组(redis中的String结构类似于java中的ArrayList)。关于数组复制的文章请看这里。来看一下ArrayList的部分源码(java version “1.8.0_131”):
/**
* 定义一个有特定初始长度的ArrayList
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 无参构造,就会初始化出一个为{}的数组。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 传入一个集合对象转化成ArrayList
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
以上的初始化需要这几个变量:
/**
* 默认的容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组对象
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默认容量的数组对象
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 这就是元素数据的数组,实现ArrayList的基础。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 数组大小
*/
private int size;
看一下add添加元素的方法:
//添加的逻辑主要是两个一个检查容量是否需要扩容;二是添加元素到对象的角标下。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
追踪源码看一下***ensureCapacityInternal***这个私有方法:
可以看到Math.max函数取值最大值,比较的是默认容量10 和添加元素后的size大小(size+1),所以初始就会取值DEFAULT_CAPACITY=10
private void ensureCapacityInternal(int minCapacity) {
//首先判断元素数组是否是空数组,如果是就将minCapacity与DEFAULT_CAPACITY比较取大。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
然后再进入***ensureExplicitCapacity***这个私有方法:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
/**
*如果原来的无法再添加新的了,就需要扩容;
*那就看一下grow方法是怎么进行扩容的?
**/
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
看一下动态扩容的方法:
newCapacity = oldCapacity + (oldCapacity >> 1) ,也就是说首次扩容为10到15.
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//oldCapacity >> 1,扩容原来的一半;
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);
}
这里简要补充一下java位移运算的知识!注意:
- 位移运算发生在int,当double或者float类型的就会报错
- 左移 << ,右移 >>
- 左移 x << n,相当于x*2的n次方
- 右移y >> n,相当于是y/(2的n次方)
- 无符号右移 >>>,对于32、64位的值更有意义
那么看到这里,就看完了完整的一个add添加元素的实现。
再看一下,它是如何往里面插入元素的:
插入操作:首先会判断size+1,是否需要扩容,如果需要就要扩容!注意扩容是需要进行数组复制拷贝的(数组拷贝底层方法),这也是主要的性能开销,当然越往后插入性能开销越小,元素位移的数量也越少!然后,对elementData进行操作,操作过程是System.arraycopy对于index索引位置原数组中此位置后的元素移到index + 1后的位置,空出index索引下的数组位置放置插入的元素! 2018-4-15
public void add(int index, E element) {
//index > size || index < 0 主要是检查是否角标越界。
rangeCheckForAdd(index);
//同样检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
/**
*这是重点。elementData在index位置开始往后的元素往后复制一位,腾出index要插入元素的位置。
*然后把element插入到index位置。
*/
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
删除某个位置的元素方法如下:
/**
* 删除也是如此,依然是利用System.arraycopy进行数组操作,将数组除去index剩余的合并;
* 然后再把最后一个置位null,让GC进行回收。
*
*
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
由此可见,插入(或者删除)元素对于数组末尾是没问题的,但是如果要插入(或者删除)到中间,中间往后的都要顺延一个位置,所以相较于利用索引的随机查询来说,随机插入(或者删除)效率就相对差一些了。
小结一下ArrayList
ArrayList 是基于数组,通过以上的增方法,就可以看出增删改查其实都是基于数组的操作。那么,它的优势也是很明显:那就是基于数组索引的操作,增查方便,但是删除中间的某个元素或者往中间插入某个元素需要操作数组的大部分,所以这就是它的弊端,那么下一篇文章我们再来思考看一下另一个集合LinkedList的实现原理。看看它是怎么解决这个问题的?
自己简单实现一下,加深一下印象:
package screw.learn.collections;
import java.io.Serializable;
import java.util.AbstractList;
import java.util.Arrays;
/**
* @author Kevin
*
* @param <E>
*
* 尝试自己写一下ArrayList,加深理解。 2017-12-29
*
* modCount 这个变量是AbstractList中的,每次操作都要进行更新,例如add要modCount++;
*/
public class ArrList<E> extends AbstractList<E> implements Serializable{
private static final long serialVersionUID = -1800429178525643423L;
/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 数据数组
*
* 注意:transient 它的作用是序列化时,不需要将它包含其中。源码如此,细思确实如此。
*/
transient Object[] tables ;
/**
* 大小
*/
private int size;
/**
* 无参构造,默认数据数组等于默认容量
*/
public ArrList(){
tables = new Object[DEFAULT_CAPACITY];
}
/**
* 带参构造,生成入参大小的数据数组
* @param capacity
*/
public ArrList(int capacity){
if(capacity < 0)
throw new IllegalArgumentException();
else if(capacity == 0)
tables = EMPTY_ELEMENTDATA;
tables = new Object[capacity];
}
/* (non-Javadoc)
* @see java.util.AbstractList#add(java.lang.Object)
*/
public boolean add(E e){
ensureCapacity(size + 1);
tables[size++] = e;
return true;
}
/* (non-Javadoc)
* @see java.util.AbstractCollection#remove(java.lang.Object)
*/
public boolean remove(Object o){
if(o == null){
for(int i = 0;i<size;i++){
if(tables[i] == null){
fastRemove(i);
return true;
}
}
}else{
for(int i = 0;i<size;i++){
if(tables[i].equals(o)){
fastRemove(i);
return true;
}
}
}
return false;
}
/**
* @param index
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(tables, index+1, tables, index,
numMoved);
tables[--size] = null; // clear to let GC do its work
}
/* (non-Javadoc)
* @see java.util.AbstractList#remove(int)
*/
public E remove(int index){
return null;
}
public void ensureCapacity(int minCapacity) {
modCount++;
if(minCapacity - tables.length > 0)
grow(minCapacity);
}
/**
* 扩容方法
*
* oldcapacity >> 1,扩容一半
* @param minCapacity
*/
private void grow(int minCapacity) {
int oldCapacity = tables.length;
int newCapacity = oldCapacity += (oldCapacity>>1);
tables = Arrays.copyOf(tables, newCapacity);
}
public boolean isEmpty(){
return size == 0;
}
@Override
public E get(int index) {
if(index > size && index < 0)
throw new IndexOutOfBoundsException();
Object obj = tables[index];
return (E) obj;
}
@Override
public int size() {
return size;
}
}