ArrayList
是平時相當常用的List
實現, 其中boolean add(E e)
的實現比較直接:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
有時候也使用 void add(int index, E element)
把元素插入到指定的index
上. 在JDK中的實現是:
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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++;
}
略有差別, 需要保證當前elementData
陣列容量夠用, 然後把從index
處一直到尾部的陣列元素都向後挪一位. 最後把要插入的元素賦給陣列的index
處.
一直以來, 我都認為 System.arraycopy
這個native方法, 它的c++實現是呼叫底層的memcpy
, 直接方便, 效率也沒問題.
但今天看了openJDK的原始碼發現並非如此.
以openJDK8u60 為例, 在 objArrayKlass.cpp 中:
void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
int dst_pos, int length, TRAPS) {
assert(s->is_objArray(), "must be obj array");
if (!d->is_objArray()) {
THROW(vmSymbols::java_lang_ArrayStoreException());
}
// Check is all offsets and lengths are non negative
if (src_pos < 0 || dst_pos < 0 || length < 0) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
// Check if the ranges are valid
if ( (((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length())
|| (((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length()) ) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
// Special case. Boundary cases must be checked first
// This allows the following call: copy_array(s, s.length(), d.length(), 0).
// This is correct, since the position is supposed to be an `in between point`, i.e., s.length(),
// points to the right of the last element.
if (length==0) {
return;
}
if (UseCompressedOops) {
narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
do_copy<narrowOop>(s, src, d, dst, length, CHECK);
} else {
oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
do_copy<oop> (s, src, d, dst, length, CHECK);
}
}
可以看到copy_array
在做了各種檢查之後, 最終copy的部分在do_copy
方法中, 而這個方法實現如下:
// Either oop or narrowOop depending on UseCompressedOops.
template <class T> void ObjArrayKlass::do_copy(arrayOop s, T* src,
arrayOop d, T* dst, int length, TRAPS) {
BarrierSet* bs = Universe::heap()->barrier_set();
// For performance reasons, we assume we are that the write barrier we
// are using has optimized modes for arrays of references. At least one
// of the asserts below will fail if this is not the case.
assert(bs->has_write_ref_array_opt(), "Barrier set must have ref array opt");
assert(bs->has_write_ref_array_pre_opt(), "For pre-barrier as well.");
if (s == d) {
// since source and destination are equal we do not need conversion checks.
assert(length > 0, "sanity check");
bs->write_ref_array_pre(dst, length);
Copy::conjoint_oops_atomic(src, dst, length);
} else {
// We have to make sure all elements conform to the destination array
Klass* bound = ObjArrayKlass::cast(d->klass())->element_klass();
Klass* stype = ObjArrayKlass::cast(s->klass())->element_klass();
if (stype == bound || stype->is_subtype_of(bound)) {
// elements are guaranteed to be subtypes, so no check necessary
bs->write_ref_array_pre(dst, length);
Copy::conjoint_oops_atomic(src, dst, length);
} else {
// slow case: need individual subtype checks
// note: don`t use obj_at_put below because it includes a redundant store check
T* from = src;
T* end = from + length;
for (T* p = dst; from < end; from++, p++) {
// XXX this is going to be slow.
T element = *from;
// even slower now
bool element_is_null = oopDesc::is_null(element);
oop new_val = element_is_null ? oop(NULL)
: oopDesc::decode_heap_oop_not_null(element);
if (element_is_null ||
(new_val->klass())->is_subtype_of(bound)) {
bs->write_ref_field_pre(p, new_val);
*p = element;
} else {
// We must do a barrier to cover the partial copy.
const size_t pd = pointer_delta(p, dst, (size_t)heapOopSize);
// pointer delta is scaled to number of elements (length field in
// objArrayOop) which we assume is 32 bit.
assert(pd == (size_t)(int)pd, "length field overflow");
bs->write_ref_array((HeapWord*)dst, pd);
THROW(vmSymbols::java_lang_ArrayStoreException());
return;
}
}
}
}
bs->write_ref_array((HeapWord*)dst, length);
}
可以看到, 在設定了heap barrier之後, 元素是在for迴圈中被一個個挪動的. 做的工作比我想象的要多.
如果有m個元素, 按照給定位置, 使用ArrayList.add(int,E)
逐個插入到一個長度為n的ArrayList中, 複雜度應當是O(m*n)
, 或者O(m*(m+n))
, 所以, 如果m和n都不小的話, 效率確實是不高的.
效率高一些的方法是, 建立m+n長度的陣列或ArrayList, 在給定位置賦值該m個要插入的元素, 其他位置依次賦值原n長度List的元素. 這樣時間複雜度應當是O(m+n)
.
還有, 在前面的實現中, 我們可以看到有對ensureCapacityInternal(int)
的呼叫. 這個保證陣列容量的實現主要在:
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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);
}
大家知道由於效率原因, ArrayList容量增長不是正好按照要求的容量minCapacity
來設計的, 新容量計算的主要邏輯是: 如果要求容量比當前容量的1.5倍大, 就按照要求容量重新分配空間; 否則按當前容量1.5倍增加. 當然不能超出Integer.MAX_VALUE
了. oldCapacity + (oldCapacity >> 1)
實際就是當前容量1.5倍, 等同於(int) (oldCapacity * 1.5)
, 但因這段不涉及浮點運算只是移位, 顯然效率高不少.
所以如果ArrayList一個一個add元素的話, 容量是在不夠的時候1.5倍增長的. 關於1.5這個數字, 或許是覺得2倍增長太快了吧. 也或許有實驗資料的驗證支撐.
關於這段程式碼中出現的Arrays.copyOf
這個方法, 實現的是重新分配一段陣列, 把elementData
賦值給新分配的空間, 如果新分配的空間大, 則後面賦值null, 如果分配空間比當前陣列小則截斷. 底層還是呼叫的System.arraycopy
.