利用Java来手写ArrayList

Posted weixin_45747080

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用Java来手写ArrayList相关的知识,希望对你有一定的参考价值。

利用Java来手写ArrayList

几乎所有的语言都会有数组,Java也不例外。数组有个特点就是在初始化的时候必须确定长度,即使容量到达了也无法自动扩容,无法满足需求,所以我们可以利用动态数组(ArrayList)来实现可以自动扩容的数组。参考Java官方的ArrayList实现:java.util.ArrayList。ArrayList的底层还是数组,相当于数组的强化版,能够进行自动扩容并且进行数据的增删改查。

注意:

以下利用动态数组来表示ArrayList,利用数组来表示Object[]

私有属性

最起码需要表示动态数组的大小size,存放数据的基本数组elements[],所以应该有以下属性:

public class ArrayList<E> {

    /**
     * 动态数组大小
     */
    private int size;

    /**
     * 动态数组
     */
    private E[] elements;
    
}

注意这里使用到了泛型,是为了方便顺序存储相似的数据(类型相同的数据)

构造方法

无参构造方法:创建一个默认容量的数组;

有参构造方法:创建一个指定容量的数组。

既然涉及到默认,那我们可以指定一个常量为默认容量,可以按照自己的需求来,随便多少都可以。

    /**
     * 动态数组默认容量
     */
    private static final int DEFAULT_CAPACITY = 10;


    /**
     * 无参构造,创建默认大小的数组
     */
    public ArrayList(){

    }

    /**
     * 有参构造,根据容量创建数组
     * @param capacity      自定义容量
     */
    public ArrayList(int capacity){

    }

注意:

容量(capacity)是指该动态数组能够容纳多少个元素,而不代表它里面已经存了多少个元素,通常情况数组的长度elements.length和他相等。

大小(size)是指该动态数组已经有多少个元素。

无参构造

创建默认容量的Object数组,再强制类型转换。

    public ArrayList(){
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

有参构造

创建指定容量的数组,需要控制一下传入的容量大小,不允许为负数。

    public ArrayList(int capacity){
        if (capacity<=0) capacity = DEFAULT_CAPACITY; //如果传入容量不为自然数,则强制为默认容量
        elements = (E[]) new Object[capacity];
    }

基本方法

仿造java.util.ArrayList,得出以下基本方法包含基本的添加删除查询元素等。

public class ArrayList<E> {


    /**
     * 动态数组大小
     * @return      动态数组大小
     */
    public int size(){}

    /**
     * 动态数组是否为空
     * @return      动态数组是否为空
     */
    public Boolean isEmpty(){}

    /**
     * 添加元素
     * @param element   元素
     */
    public void add(E element){}

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  1 2 3   4 5 6 7 8
     * 向指定位置添加元素
     * @param index     位置
     * @param element   元素
     */
    public void add(int index,E element){}

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  a b c 1 d e f g h
     * 移除指定位置的元素
     * @param index     位置
     */
    public void remove(int index){}

    /**
     * 删除指定元素
     * @param element   元素
     */
    public void remove(E element){}

    /**
     * 清空动态数组中的元素
     */
    public void clear(){}

    /**
     * 修改指定位置的元素
     * @param index      位置
     * @param element    元素
     */
    public void set(int index,E element){}

    /**
     * 获得指定位置的元素
     * @param index     位置
     * @return          元素
     */
    public E get(int index){}

    /**
     * 判断数组是否包含该元素
     * @param element    元素
     * @return           true包含,false不包含
     */
    public Boolean contains(E element){}

    /**
     * 该元素第一次出现的下标
     * @param element    元素
     * @return           下标
     */
    public int indexOf(E element){}

    @Override
    public String toString() {}
}

size()

返回动态数组的大小

    public int size(){
        return size;
    }

isEmpty()

返回动态数组是否为空,判断size是否为0

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

toString()

打印动态数组,这个按照自己的需求来就可以了

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("ArrayList{");
        string.append("size=" + size + ", elements=[");
        for (int i = 0; i < size; i++) {
            string.append(elements[i]);
            if (i != size -1) { //最后一个元素末尾不添加逗号
                string.append(", ");
            }
        }
        string.append("]");
        string.append("}");
        return string.toString();
    }

indexOf()和contains()

indexOf()返回的元素第一次出现的下标(循环数组,如果有该元素则返回下标,下标为-1则表示没有该元素)

    public int indexOf(E element){
        for (int i = 0; i < size; i++) {
            if (elements[i].equals(element)) return i; //如果element存在返回下标
        }
        return -1;
    }

contains()判断是否存在该元素,利用indexOf()来判断,如果存在下标则说明存在该元素,不存在下标则不存在该元素

    public Boolean contains(E element){
        return indexOf(element) >= 0;
    }

get()

返回指定下标的数组元素

    public E get(int index){
        return elements[index];
    }

set()

向指定下标塞入元素

    public void set(int index,E element){
        elements[index] = element;
    }

add()

添加元素,向动态数组末尾添加元素。

    public void add(E element){
        elements[size] = element;
        size++;
    }

重载方法add(int index, E element)向指定位置添加元素,原来的元素会依次向后挪动。元素挪动的目的是为了空出index下标的元素,所以index下标及其后面的元素都需要依次往后挪动。挪动的顺序应是

注意第一是先挪动最后一个,如果你从index才是挪动后面的会被覆盖。依次挪动然后index下标处元素就会被空出来,然后塞入要插入的元素即可。

    public void add(int index,E element){
        for (int i = size; i > index ; i--) { //元素依次往后挪动
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }

同理,其实add(E element)就是往动态数组末尾添加元素,所以可以直接调用重载方法,指定下标为末尾(size)就可以了。

    public void add(E element){
        add(size,element);
    }

remove()

移除指定位置的元素,移除指定位置的元素之后,指定位置的元素会被删除,然后指定位置后面的元素会依次往前挪动

注意,实际上不用硬性删除指定的元素,在Java的特性中,只要某一内存地址没有再被指向那么它就会被垃圾回收掉(视为删除),所以直接用后面的元素直接覆盖掉指定下标的元素,原来的元素的地址就没有被指向了,所以就会被删除,最后再将末尾的一个元素置为null即可。但是需要注意的是删除是先挪动前面再挪动后面,如果从后往前挪会被覆盖掉。

    public void remove(int index){
        for (int i = index; i < size-1 ; i++) { //元素依次往前挪
            elements[i] = elements[i+1];
        }
        elements[size-1] = null; //将数组的最后一个元素置为null
        size--;
    }

clear()

清空动态数组,直接将所有位置置为null即可,那么之前的元素的地址就不没有被指向了,就会被删除。

    public void clear(){
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
        size = 0;
    }

下标越界

在上述方法传入index的时候,可能会出现index越界的情况,比如为负数或者超出上限,所以需要控制index的有效范围,并且在每次传入进行检查就可以了,错误就抛出异常。

注意

add()和remove()、get()、set()的index范围有所区别。添加元素允许往动态数组的最后一个位置添加元素,所以index是可以访问到size的,但是删除、查询和修改都是在元素已经在动态数组中存在的基础上,所以index是不可以访问到size的,切记!!!

checkIndex()

检查非添加时的下标index,不能小于0或者不能大于等于size(0<index<size)

private void checkIndex(int index){
    if (index<0||index>=size) indexOutOfBoundsException(index);
}

checkIndexAdd()

检查添加时下标index,不能小于0或者不能大于size(0<index ⩽ \\leqslant ​size)

private void checkIndexAdd(int index){
    if (index<0||index>size) indexOutOfBoundsException(index);
}

indexOutOfBoundsException()

下标越界时抛出的自定义异常

private void indexOutOfBoundsException(int index){
    throw new IndexOutOfBoundsException("index="+index+", size="+size);
}

动态扩容

动态数组相较于传统数组的优势就是能够动态扩容,但其实底层仍然是利用数组扩容,实质就是利用新的容量的数组来装填原来的数据,这当中会涉及到很多底层的内存消耗以及资源最优化的问题,这里不讲解,主要讲解如何实现。

扩容也只可能会在添加元素的时候发生,并且是添加元素时的动态数组的size也就是元素个数刚好达到容量这个时候就该扩容了:

  1. 判断动态数组元素个数已经达到数组容量,说明已经满了需要扩容了
  2. 新的数组容量为当前数组容量的1.5倍
  3. 以新的数组容量大小创建数组
  4. 旧数组依次向新的数组赋值
  5. 旧数组的指针指向新数组
public void add(int index,E element){
    checkIndexAdd(index);
    if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
        int nowCapacity = elements.length;                  //当前动态数组容量就是数组长度
        int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
            newElements[i] = elements[i];
        }
        elements = newElements;     //elements对象指向newElements
        System.out.println(nowCapacity + "扩容至" + newCapacity);
    }
    for (int i = size; i > index ; i--) { //元素依次往后挪动
        elements[i] = elements[i - 1];
    }
    elements[index] = element;
    size++;
}

我们将数组扩容单独提出来(提高可维护性和可读性)

    private void ensureCapacity(){
        if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
            int nowCapacity = elements.length;                  //当前动态数组容量就是数组长度
            int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
                newElements[i] = elements[i];
            }
            elements = newElements;     //elements对象指向newElements
            System.out.println(nowCapacity + "扩容至" + newCapacity);
        }
    }

综合:

    private void ensureCapacity(){
        if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
            int nowCapacity = elements.length;                  //当前动态数组容量就是数组长度
            int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
                newElements[i] = elements[i];
            }
            elements = newElements;     //elements对象指向newElements
            System.out.println(nowCapacity + "扩容至" + newCapacity);
        }
    }

    public void add(int index,E element){
        checkIndexAdd(index);
        ensureCapacity();
        for (int i = size; i > index ; i--) { //元素依次往后挪动
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }

总结

在解决了动态数组的大小、元素存放的数组等属性和一些基本的添加、删除、修改和查询方法后,以及对下标越界的处理和最核心的动态扩容部分解决后,一个最基本的动态数组ArrayList就大致完成了:


public class ArrayList<E> {

    /**
     * 动态数组大小
     */
    private int size;

    /**
     * 动态数组
     */
    private E[] elements;

    /**
     * 动态数组默认容量
     */
    private static final int DEFAULT_CAPACITY = 10;


    /**
     * 无参构造,创建默认大小的数组
     */
    public ArrayList(){
        elements = (E[]) new Object[DEFAULT_CAPACITY];
    }

    /**
     * 有参构造,根据容量创建数组
     * @param capacity      自定义容量
     */
    public ArrayList(int capacity){
        if (capacity<=0) capacity = DEFAULT_CAPACITY; //如果传入容量不为自然数,则强制为默认容量
        elements = (E[]) new Object[capacity];
    }

    /**
     * 动态数组大小
     * @return      动态数组大小
     */
    public int size(){
        return size;
    }

    /**
     * 动态数组是否为空
     * @return      动态数组是否为空
     */
    public Boolean isEmpty(){
        return size == 0;
    }

    /**
     * 下班超出范围的报错信息
     * @param index
     */
    private void indexOutOfBoundsException(int index){
        throw new IndexOutOfBoundsException("index="+index+", size="+size);
    }

    /**
     * 检查添加元素时候的index
     * @param index
     */
    private void checkIndexForAdd(int index){
        if (index<0||index>size) indexOutOfBoundsException(index);
    }

    /**
     * 检查删改查时的index
     * @param index
     */
    private void checkIndex(int index){
        if (index<0||index>=size) indexOutOfBoundsException(index);
    }

    /**
     * 确保容量(如果动态数组元素大小达到当前动态数组的容量满了则扩容)
     */
    private void ensureCapacity(){
        if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
            int nowCapacity = elements.length;                  //当前动态数组容量就是数组长度
            int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
                newElements[i] = elements[i];
            }
            elements = newElements;     //elements对象指向newElements
            System.out.println(nowCapacity + "扩容至" + newCapacity);
        }
    }

    /**
     * 添加元素
     * @param element   元素
     */
    public void add(E element){
        add(size,element);

    }

    /**
     *
     *  0 1 2 3 4 5 6 7 8 9
     *  1 2 3   4 5 6 7 8
     * 向指定位置添加元素
     * @param index     位置
     * @param element   元素
     */
    public void add(int index,E element){
        if (element == null) throw new NullPointerException("不允许添加null");
        checkIndexForAdd(index);
        ensureCapacity();                     //确保数组容量(如果容量满了则扩容)
        for (int i = size; i > index ; i--) { //元素依次往后挪动
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }

    /**
     *
     *  0 1 2 

以上是关于利用Java来手写ArrayList的主要内容,如果未能解决你的问题,请参考以下文章

利用Java手写LinkedList

利用Java手写LinkedList

1 手写ArrayList核心源码

恋上数据结构手写ArrayList + Java动态扩容分析

前端面试题之手写promise

Java面试题之手写ArrayList