本身动手系列——实现一个简单的ArrayList

ArrayList是Java集合框架中一个经典的实现类。他比起经常使用的数组而言,明显的优势在于,能够随意的添加和删除元素而不需考虑数组的大小。处于练手的目的,实现一个简单的ArrayList,而且把实现的过程在此记录。
实现的ArrayList主要的功能以下:java

  • 默认构造器和一个参数的有参构造器
  • add方法
  • get方法
  • indexOf方法
  • contains方法
  • size方法
  • isEmpty方法
  • remove方法

这个简单的ArrayList类 取名为SimpleArrayList,所有的代码查看SimpleArrayList代码git

构造器

源码ArrayList一共有三个构造器,一个无参构造器,一个参数为int型有参构造器,一个参数为Collection型的有参构造器。参数为Collection型的构造器用来实现将其余继承Collection类的容器类转换成ArrayList。SimpleArrayList类由于尚未手动实现其余的容器类,因此实现的构造方法只有2个。代码以下:github

public SimpleArrayList(){
        this(DEFAULT_CAPACITY);
    }


    public SimpleArrayList(int size){
        if (size < 0){
            throw new IllegalArgumentException("默认的大小" + size);
        }else{
            elementData = new Object[size];
        }
    }

无参构造器中的 DEFAULT_CAPACITY是定义的私有变量,默认值是10,用来建立一个大小为10的数组。有参构造器中,int参数是用来生成一个指定大小的Object数组。将建立好的数组传给elementDataelementData是真正的用来存储元素的数组。数组

add方法

add 方法用来往容器中添加元素,add方法有两个重载方法,一个是add(E e),另外一个是add(int index, E e)。add自己很简单,可是要处理动态数组,即数组大小不知足的时候,扩大数组的内存。具体的代码以下:框架

public void add(E e){
        isCapacityEnough(size + 1);
        elementData[size++] = e;
    }

方法isCapacityEnough就是来判断是否须要扩容,传入的参数就是最小的扩容空间。由于add一个元素,因此最小的扩容空间,即新的长度是全部元素+ 1。这里的size就是真正的元素个数。this

private void isCapacityEnough(int size){
        if (size > DEFAULT_CAPACITY){
            explicitCapacity(size);
        }
       if (size < 0){
            throw new OutOfMemoryError();
        }
    }

判断扩容的方法也很简单,判断须要扩容的空间是否是比默认的空间大。若是须要的空间比默认的空间大,就调用explicitCapacity进行扩容。这里有个size小于0的判断,出现size小于0主要是由于当size超过Integer.MAX_VALUE就会变成负数。code

private final static int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

    private void explicitCapacity(int capacity){
        int newLength = elementData.length * 2;
        if (newLength - capacity < 0){
            newLength = capacity;
        }
        if (newLength > (MAX_ARRAY_LENGTH)){
            newLength = (capacity > MAX_ARRAY_LENGTH ? Integer.MAX_VALUE : MAX_ARRAY_LENGTH);
        }
        elementData = Arrays.copyOf(elementData, newLength);
    }

上面的代码是扩容的代码,首先,定义一个数组最大的容量的常量为最大值,这个值按照官方的源码中的解释是要有些VM保留了数组的头部信息在数组中,所以实际存放数据的大小就是整数的最大值 - 8
而后设定一个要扩容的数组的大小,虽然上面说了有一个扩容空间的值 size + 1 ,这个是实际咱们最小须要扩容的大小。但为了继续增长元素,而不频繁的扩容,所以一次性的申请多一些的扩容空间。这里newLength 打算申请为 数组长度的2倍,而后去判断这个长度是否知足须要的扩容空间的值。 即有了后续的两段代码继承

if (newLength - capacity < 0){
            newLength = capacity;
      }
      if (newLength > (MAX_ARRAY_LENGTH)){
            newLength = (capacity > MAX_ARRAY_LENGTH ? Integer.MAX_VALUE : MAX_ARRAY_LENGTH);
      }

若是2倍的长度仍然不知足,则申请到须要的扩容长度。在咱们只增长一个元素的状况下,这个判断是永远不会生效的,可是若是有addAll方法,则增长的元素不少,就要致使一次申请2倍的长度是不够的。第二个判断是判断newLength的长度若是超过上面定义的数组最大长度则判断要须要的扩容空间是否大于数组最大长度,若是大于则newLength为 MAX_VALUE ,不然为 MAX_ARRAY_LENGTH。
最后,真正实现数组扩容到设定长度的方法就没意思了,调用Arrays.copyOf(elementData, newLength)获得一个扩容后的数组。
add的另外一个重载方法也很简单。内存

public void add(int index, E e) {
       //判断是否是越界
        checkRangeForAdd(index);
        //判断需不须要扩容
        isCapacityEnough(size + 1);
        //将index的元素及之后的元素向后移一位
        System.arraycopy(elementData,index,elementData,index + 1,size - index);
        //将index下标的值设为e
        elementData[index] = e;
        size++;
    }
private void checkRangeForAdd(int index){
        //这里index = size是被容许的,即支持头,中间,尾部插入
        if (index < 0 || index > size){
            throw new IndexOutOfBoundsException("指定的index超过界限");
        }
    }

至此,一个简单的add方法就实现完了。ci

get方法

get方法用来获得容器中指定下标的元素。方法实现比较简单,直接返回数组中指定下标的元素便可。

private void checkRange(int index) {
        if (index >= size || index < 0){
            throw new IndexOutOfBoundsException("指定的index超过界限");
        }
    }
    public E get(int index){
        checkRange(index);
        return (E)elementData[index];
    }

indexOf方法

indexOf方法用来获得指定元素的下标。实现起来比较简单,须要判断传入的元素,代码以下:

public int indexOf(Object o){
        if (o != null) {
            for (int i = 0 ; i < size ; i++){
                if (elementData[i].equals(o)){
                    return i;
                }
            }
        }else {
            for (int i = 0 ; i < size ; i++){
                if (elementData[i] == null) {
                    return i;
                }
            }
        }

        return -1;
    }

判断传入的元素是否为null,若是为null,则依次与null。若是不为空,则用equals依次比较。匹配成功就返回下标,匹配失败就返回-1。

contains方法

contains用来判断该容器中是否包含指定的元素。在有了indexOf方法的基础上,contains的实现就很简单了。

public boolean contains(Object o){
        return indexOf(o) >= 0;
     }

size方法

size方法用来获得容器类的元素个数,实现很简单,直接返回size的大小便可。

public int size(){
        return size;
    }

isEmpty方法

isEmpty方法用来判断容器是否为空,判断size方法的返回值是否为0便可。

public boolean isEmpty(){
        return size() == 0;
    }

remove方法

remove方法是用来对容器类的元素进行删除,与add同样,remove方法也有两个重载方法,分别是
remove(Object o)和remove(int index)

public E remove(int index) {
        E value = get(index);
        int moveSize = size - index - 1;
        if (moveSize > 0){
            System.arraycopy(elementData,index + 1, elementData,index,size - index - 1);
        }
        elementData[--size] = null;
        return value;
    }
    
    public boolean remove(Object o){
        if (contains(o)){
            remove(indexOf(o));
            return true;
        }else {
            return false;
        }
    }

第一个remove方法是核心方法,首先获得要删除的下标元素的值,而后判断index后面的要前移的元素的个数,若是个数大于零,则调用库方法,将index后面的元素向前移一位。最后elementData[--size] = null;缩减size大小,并将原最后一位置空。
第二个remove方法不须要向第一个方法同样,须要告诉使用者要删除的下标对应的元素,只须要判断是否删除成功便可。若是要删除的元素在列表中,则删除成功,若是不在则失败。所以调用contains方法就能够判断是否要删除的元素在列表中。在则调用remove(int index),不在则返回失败。

总结

自此,一个简单的ArrayList就实现完了,实现的目的是为了弄清ArrayList动态数组的原理以及add与remove方法的内容实现。同时,也清楚了ArrayList最大的扩容空间就是Integer的最大值。该类的全部代码在SimpleArrayList代码

相关文章
相关标签/搜索