接口继承图以及相关方法分析
先了解一下ArrayList整个接口继承图有哪些?
- 看接口的时候,特别简单的我们不必赘述,有一些小的细节我们会在方法上标记出来。
- 另外父类接口和lambda方法会被省略掉。
Iterable
这里封装着迭代器通用的方法
public interface Iterable<T> {
//返回一个T的迭代器
Iterator<T> iterator();
//其他lambda的方法这里省略掉
}
Collection
这里封装着一些集合通用的一些方法
public interface Collection<E> extends Iterable<E> {
//----查询操作----
//返回集合长度
int size();
//集合为空返回true
boolean isEmpty();
//集合包含指定元素返回true
boolean contains(Object o);
/**
* 返回包含所有集合元素的数组,这个方法用于array和collection转换,注意:
* - 深复制出一个全新数组,不和之前的共享一个引用。
* - 保证顺序和之前的一致。
*/
Object[] toArray();
/**
* 返回包含所有集合元素的数组,注意:
* - 如果数组a大于集合长度,多余的部分填充null
*/
<T> T[] toArray(T[] a);
//----修改操作----
/**
* 添加一个元素,注意:
* - 这个方法有可能出现空指针,是和比较容易踩坑的方法,具体实现类如果不允许添加空元素的话。
*/
boolean add(E e);
boolean remove(Object o);
//----块操作----
//当包含c集合所有元素返回true反之false。
boolean containsAll(Collection<?> c);
//添加集合所有元素,注意:这个方法很容易出现空指针,c要判空
boolean addAll(Collection<? extends E> c);
//差集操作,移除c集合存在的元素。注意:这个方法很容易出现空指针,c要判空
boolean removeAll(Collection<?> c);
//交集操作,保留集合和c集合都存在的元素。注意:这个方法很容易出现空指针,c要判空
boolean retainAll(Collection<?> c);
//清楚集合所有元素
void clear();
//----比较和哈希----
//判断两个集合是否相等
boolean equals(Object o);
//获得hashCode
int hashCode();
}
T[] toArray(T[] a);这个方法,我们做一个小测试:
代码如下:
测试结果:
List
这里封装着一些list通用的一些操作
public interface List<E> extends Collection<E> {
//在指定位置插入c集合所有元素
boolean addAll(int index, Collection<? extends E> c);
//获取指定位置的元素
E get(int index);
//在指定位置替换元素。
E set(int index, E element);
//在指定位置添加元素。
void add(int index, E element);
//移除指定位置的元素。
E remove(int index);
//获取元素的第一次找到的位置下标,没有返回-1。
int indexOf(Object o);
//获取元素的最后一次找到的位置下标,没有返回-1。
int lastIndexOf(Object o);
//返回list迭代器
ListIterator<E> listIterator();
//返回index及以后元素的list迭代器
ListIterator<E> listIterator(int index);
//返回从fromIndex到toIndex的视图
List<E> subList(int fromIndex, int toIndex);
}
Cloneable
这是一个标记接口,实现它代表你是否支持深复制。深复制和浅复制这里不赘述,感兴趣的同学可以搜索相关资料。我们可以通过调用其clone方法获取一个深复制的实例。
public interface Cloneable {
}
RandomAccess
这也是一个标记接口,实现它标记你是否支持快速随机访问List中的元素。
public interface RandomAccess {
}
AbstractCollection
这里面主要把Collection接口中里面一些通用的方法给实现了。
这里有一个隐含约定俗称的东西,我们自己在做设计的时候AbstractXXX就代表对XXX做了一些通用逻辑的实现,不需要交给一堆的子类重复实现一遍。
这里有一个小细节,最大数组长度:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 之所以要-8是考虑到了一些虚拟机实现中在数组保留一下头部信息。
AbstractList
这里面主要把List接口中里面一些通用的方法给实现了。
在Ideal里面观察AbstractList可以发现增加了一个fieidmodCount,它代表着“结构性修改”的次数。但你使用迭代器(inerator或者listIterator)时,迭代器会保存modCount,遍历每个元素时都会校验一次迭代器中modCount和List中modCount中是否一致,一旦不一致,就抛出ConcurrentModificationException异常。这就是fast-fail的策略。
ArrayList源码分析
了解完接口了,下面我们来开始了解ArrayList的代码。
ArrayList数据结构:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始化大小。具体怎么初始化待会讲
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空的数组实例,当需要返回空的数组实例时,都会返回这个实例。
* 平时写代码也是鼓励能复用的实例提前声明,然后共享。减少无用new的次数。
* 构造方法和trimSize都使用到了这个实例。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 也是一个空的数组,用于默认大小的空实例。
* 但是它和EMPTY_ELEMENTDATA用途是不一样的,它是用于确定什么时候添加了第一个元素。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存放具体元素的地方。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 用于保存集合的长度,这样在进行size计算,就不需要重复计算一遍,直接读取这个值。
*/
private int size;
ArrayList是一个典型的线性表结构,这里我们简单的回顾一下线性表结构和常用操作的复杂度。
初始化方法分析
//jdk1.8的构造方法不再默认构造一个10长度的示例,而是构造一个空的。那么在什么时候扩容呢?答案是每一次add的时候
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//add方法实现如下
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//然后我们追溯到这个方法
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//注意这行判断,DEFAULTCAPACITY_EMPTY_ELEMENTDATA的作用体现出来了,用于判断什么时候第一次添加。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//指定大小的构造我们就不必说了。
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);
}
}
扩容方法分析
//每次add(addAll)的时候会有一次内部容量确认的环节,代码如下:
ensureCapacityInternal(size + 1);
//我们来看下实现,当容量不够可能导致溢出时,会调用grow方法进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//grow方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//新容量扩大到原容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//对新容量的边界判断
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//进行具体的扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
边界方法分析
ArrayList大多数读写方法都会调用边界检测逻辑,实现比较简单,看一下就行了。
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
ArrayList的源码分析暂时就到这里了,主要分析了一下面试中经常会问到的地方。
做一些关于ArrayList的题
arrayList是并发安全的么?
显然不是,比如数据结构中关键性的变量连可见性的保证都没有,比如size等。更谈不上并发安全了。
arrayList扩容策略是什么?
1.8实现中,每次add类方法会做一次容量检测,每次扩容1.5倍容量,同时会做边界检测。