利用Java来手写ArrayList

Posted F3nGaoXS

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++;
    

缩容

在很多时候可能动态数组中没有使用的位置占比较多时,就可以使用缩容来节省空间。比如动态数组容量由于不断add然后扩容到了1000个,但是这个时候只用了100个来装元素,剩下的900个都没用装元素,这个时候其实是浪费的。所以可以考虑采用缩容的方式来节省空间。

remove

如果当前数组大小(元素个数)比当前容量的一半还要小并且当前容量大于默认容量时开始缩容:创建一个新的数组(大小为原容量的一半),然后旧数组依次赋值。

	public void remove(int index)
        checkIndex(index);
        //缩容开始
        int nowCapacity = elements.length;  //当前容量
        if ( size <= nowCapacity>>1 && nowCapacity>DEFAULT_CAPACITY)
            int newCapacity = nowCapacity>>1; //即nowCapacity/2
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) 
                newElements[i] = elements[i];
            
            elements = newElements;
            System.out.println(nowCapacity + "缩容至" + newCapacity);
        
        //缩容结束
        for (int i = index; i < size-1 ; i++)  //元素依次往前挪
            elements[i] = elements[i+1];
        
        elements[size-1] = null; //将数组的最后一个元素置为null
        size--;
    

即trim():

	/**
     * 缩容(在动态数组中未使用的部分过大时缩容)
     */
    private void trim()
        int nowCapacity = elements.length;  //当前容量
        //如果当前数组元素个数比当前容量的一半还要小,并且容量是大于默认容量的则开始缩容
        if ( size <= nowCapacity>>1 && nowCapacity>DEFAULT_CAPACITY)
            int newCapacity = nowCapacity>>1; //即nowCapacity/2
            E[] newElements = (E[]) new Object[newCapacity];
            for (int i = 0; i < size; i++) 
                newElements[i] = elements[i];
            
            elements = newElements;
            System.out.println(nowCapacity + "缩容至" + newCapacity);
        
    

remove():

	public void remove(int index)
        checkIndex(index);
        trim();         //缩容
        for (int i = index; i < size-1 ; i++)  //元素依次往前挪
            elements[i] = elements[i+1];
        
        elements[size-1] = null; //将数组的最后一个元素置为null
        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以上是关于利用Java来手写ArrayList的主要内容,如果未能解决你的问题,请参考以下文章

利用Java手写LinkedList

利用Java手写LinkedList

1 手写ArrayList核心源码

Java面试题之手写ArrayList

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

如何在Java中的ArrayList末尾附加元素?