ArrayList与LinkedList源码解析
相信使用java的同仁对这个都不陌生, 在java中我们最长使用的数据结构就是List与Map了, 而今天我们就来看看List中最常用的两种实现。
1. 首先我们来看ArrayList集合, 他的类层级图如下:
1). 首先我们需要了解ArrayList的数据结构模型为:
struct ArrayList {
static final int DEFAULT_CAPACITY = 10;static final Object [] EMPTY_ELEMENTDATA = {} ;
static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};Object [] elementData ;
int size;
}
从这个结构中我们可以知道ArrayList的结构是以数组的形式进行操作的。
2). 然后我们一般就需要关心数据结构的增删查改操作了, 数据结构主要的操作就是增删查改。
(1). 首先看一下ArrayList的add操作:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这段代码的增加操作和普通的数据操作一样, 关键在于ensureCapacityinternal()这个方法, 该方法会在ArrayList长度不够时进行对应的扩容,具体源代码如下:
private void grow(int 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);
}
在我们进入ensureCapacityInternal()方法去时,我们最后发现grow()才是真正进行操作的方法从这上面来看, 我们到一般情况下扩容的时候计算方法为增加原有数组的1/2的长度。 而且我们从第二个If中我们可以知道ArrayList的限制为Integer的最大值2147483647,当然我们的对象不可能这么多, 不然服务器不爆都难。然后使用Arrays.copyOf来进行扩容。
(2).其他的删改查都很简单, 这里就不进行介绍了。
2. LinkedList的类层次结构图:
从这个图中我们知道,ArrayList和LinkedList都继承了最左边这个分支类层级, 他们中有些通用的方法都来自这个实现结构。
1). 首先来了解LinkedList的数据结构:
struct Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
struct LinkedList {
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
}
从这个结构中我们可以知道, LinkedList使用的是双向链表来进行数据存储的。
2). 现在我们来看看它的相关操作:
(1). add操作
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
这段代码就是LinkedList的操作, 增加一个新节点, 就是追加该节点到链表的最后。 从代码来看, LinkedList的长度是没有限制的。对比ArrayList与LinkedList的add方法发现: 当使用ArrayList新增数据时, 我们要进行对应的数据移动工作, 它会先创建一个扩容后的数组, 将原数组数据拷贝到新数据中; 而我们在使用LinkedList的时候,则是直接在链表尾部追加新元素。 很明显, LinkedList的add操作比ArrayList的操作要便利很多, 性能更加。所以当我们是List时, 如果该List有频繁的增删改时, 推荐使用LinkedList操作。
(2). get操作
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
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;
}
}
这就是它的get操作, 从该操作来看, 它先看当前索引是靠近头部还是尾部, 然后进行链表的遍历进行查找。 从LinkedList和ArrayList的get中我们可以知道,由于ArrayList采用数据方式存储数据, 该种存储是连续的,所以查找数据时不需要遍历数组就可以根据下标获得对应数据, 而LinkedList则是使用链表, 该种方式存储数据不是连续的, 当我们需要查找数据时, 需要对整个链表进行遍历; 可以知道, 当数据靠近链表中部时, 我们最快查找也需要size/2次才能查到。 所以当我们在使用List进行数据存储,更多的操作是查找的时候, 推荐使用ArrayList。
(3). remove操作:
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
上面的代码时根据对象值进行删除的, 因为根据索引删除的基本就是unlink在操作, 所以没有贴出代码, 从上面可以看到当使用对象值进行删除时, 所有的值相同的对象都会被删除; 而我们使用索引删除的时候, 只是删除对应索引位置的对象。在这点上, ArrayList和LinkedList的做法也是一样的。
(3). set操作: 我们想替换一个对象的时候使用的是set方法进行操作。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
set操作通过node方式找到对应的节点, 然后替换调值即可。
(4). contains操作:
public boolean contains(Object o) {
return indexOf(o) != -1;
}
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
为什么在这里要讲contains方法, 当我们在使用List并且想去重的时候, 我们就可以使用这个方法, 但是我们观察一下当前这个方法, 它会找到第一个值相等的元素并且返回对应的索引下标, 而在它进行对比是,它调用了当前对象的equals方法, 这个方法是关键。 所以在我们需要使用List的contains的时候, 我们需要重写当前对象对应类的equals方法, 让对应的比较按我们的需求进行。
总结: 在介绍对应源码时, 主要介绍了LinkedList, 这里并不是说它更重要, 而是因为ArrayList的是采用数组进行保存的, 对于数据的操作相比开发人员都很清楚, 所以就集中精力分析了下LinkedList。 从这些分析中知道:
1. ArrayList采用的是数组方式进行数据保存, 而LinkedList采用的是双向链表进行保存。
2. 从它们的操作实现中, 我们可以知道在查询比较多的情况下使用ArrayList, 增删改频繁的时候使用LinkedList是比较不错的选择。
3. 在我们使用pojo时, 最好别忘记了重写equals方法, 防止以后比较会使用到。