自己实现一个ArrayList
几个关键点
关键点一、自动扩缩容
在实际使用动态数组时不仅需要扩容,缩容也是重要的优化手段。比方说一个动态数组开辟了能够存储 1000 个元素的连续内存空间,但是实际只存了 10 个元素,那就有 990 个空间是空闲的。为了避免资源浪费,我们其实可以适当缩小存储空间,这就是缩容。
我们这里就实现一个简单的扩缩容的策略:
当数组元素个数达到底层静态数组的容量上限时,扩容为原来的 2 倍;
当数组元素个数缩减到底层静态数组的容量的 1/4 时,缩容为原来的 1/2。
关键点二、索引越界的检查
下面的代码实现中,有两个检查越界的方法,分别是 checkElementIndex 和 checkPositionIndex,你可以看到它俩的区别仅仅在于 index < size 和 index <= size。
为什么 checkPositionIndex 可以允许 index == size 呢,因为这个 checkPositionIndex 是专门用来处理在数组中插入元素的情况。
比方说有这样一个 nums 数组,对于每个元素来说,合法的索引一定是 index < size:
nums = [5, 6, 7, 8]
index 0 1 2 3
但如果是要在数组中插入新元素,那么新元素可能的插入位置并不是元素的索引,而是索引之间的空隙:
nums = [ | 5 | 6 | 7 | 8 | ]
index 0 1 2 3 4
这些空隙都是合法的插入位置,所以说 index == size 也是合法的。这就是 checkPositionIndex 和 checkElementIndex 的区别。
关键点三、删除元素谨防内存泄漏
单从算法的角度,其实并不需要关心被删掉的元素应该如何处理,但是具体到代码实现,我们需要注意可能出现的内存泄漏。
在我给出的代码实现中,删除元素时,我都会把被删除的元素置为 null,以 Java 为例:
// 删
public E removeLast() {
E deletedVal = data[size - 1];
// 删除最后一个元素
// 必须给最后一个元素置为 null,否则会内存泄漏
data[size - 1] = null;
size–;
return deletedVal;
}
Java 的垃圾回收机制是基于引用计数的,如果一个对象没有被引用,那么这个对象占用的内存才会被释放;否则,垃圾回收器会认为这个对象还在使用中,就不会释放这个对象占用的内存。
如果你不执行 data[size - 1] = null 这行代码,那么 data[size - 1] 这个引用就会一直存在,这个对象的内存就一直不会被释放,进而造成内存泄漏。
其他带垃圾回收功能的语言应该也是类似的,你可以具体了解一下你使用的编程语言的垃圾回收机制,这是写出无 bug 代码的基本要求。
import java.util.Arrays;
import java.util.NoSuchElementException;
public class MyArrayList<E> {
private E[] data;
private int size;
private static final int INIT_CAP = 1;
public MyArrayList(){
this(INIT_CAP);
}
public MyArrayList(int initCapacity){
data = (E[]) new Object[initCapacity];
size = 0;
}
public void addLast(E e){
int cap = data.length;
if(size == cap){
resize(2 * cap);
}
data[size] = e;
size++;
}
public void add(int index, E e){
checkPositionIndex(index);
int cap = data.length;
if(size == cap){
resize(2 * cap);
}
for(int i = size - 1; i >= index; i--){
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
public void addFirst(E e){
add(0,e);
}
public E removeLast(){
if(size == 0){
throw new NoSuchElementException();
}
int cap = data.length;
if( size == cap/4){
resize(cap / 2);
}
E deletedVal = data[size -1];
data[size - 1] = null;
size--;
return deletedVal;
}
public E remove(int index){
checkElementIndex(index);
int cap = data.length;
if(size == cap/4){
resize(cap / 2);
}
E deletedVal = data[index];
for(int i = index + 1; i < size; i++){
data[i - 1] = data[i];
}
data[size -1] = null;
size--;
return deletedVal;
}
public E get(int index){
checkElementIndex(index);
return data[index];
}
public E set(int index, E e){
checkElementIndex(index);
E oldVal = data[index];
data[index] = e;
return oldVal;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
private void resize(int newCap){
if(size > newCap){
return ;
}
E[] temp = (E[]) new Object[newCap];
for(int i = 0 ; i < size; i++){
temp[i] = data[i];
}
data = temp;
}
private boolean isElementIndex(int index){
return index >= 0 && index < size;
}
private boolean isPositionIndex(int index){
return index >= 0 && index <= size;
}
private void checkElementIndex(int index){
if(!isPositionIndex(index))
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
private void checkPositionIndex(int index){
if(!isPositionIndex(index))
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
private void display(){
System.out.println("size = " + size + "\ncap = " + data.length);
System.out.println(Arrays.toString(data));
}
public static void main(String[] args) {
MyArrayList<Integer> list = new MyArrayList<>(3);
for(int i = 1; i <= 5; i++){
list.addLast(i);
}
list.display();
System.out.println("**********");
list.remove(3);
list.add(1,9);
list.addFirst(100);
int val = list.removeLast();
for (int i = 0; i < list.size(); i++){
System.out.println(list.get(i));
}
}
}
- 实际上你还可以对上述代码做一些优化
- 比如数组的拷贝可以用System.arraycopy()