自定义Java集合类之ArrayList

前言

本博客主要讲解自己对ArrayList的底层实现的一些理解,以及实现一个简略的MyArrayList,具体实现方式可能与Java原生ArrayList有所不同。文章最后有代码地址。

目录

一、实现思路以及类的基础结构搭建

二、size()、isEmpty()、toString()、clear()、toArray()方法的实现

三、集合的扩容以及缩小方法的实现

四、iterator()的实现

五、四个add方法的实现

六、四个remove方法的实现

七、get和set方法实现

八、indexOf和lastIndexOf方法的实现

九、sort方法的实现

十、总结

一、实现思路以及类的基础结构搭建

对于ArrayList可以动态调整大小的特点,可以这样来实现:在底层封装一个指定大小的数组,当添加元素的数量到达某个阈值时,创建一个长度更长的数组,将原数组中的数据复制过去,这样就相当于实现了集合的自动扩容。废话不多说,直接上代码。

package com.llg.collection;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class MyArrayList<E> extends AbstractList<E> {

    //默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    //默认扩容阈值
    private static final float EXPANSION_THRESHOLD = 0.75f;
    //默认增长因子
    private static final float EXPANSION_FACTOR = 0.5f;
    //默认缩小阈值
    private static final float NARROW_THRESHOLD = 0.2f;
    //默认缩小因子
    private static final float NARROW_FACTOR = 0.5f;

    //存储元素的数组
    private Object[] elements;
    //list元素个数
    private int size;
    //该集合的操作次数
    private int modifyCount;

    public MyArrayList() {
        //以默认初始容量创建list
        this(DEFAULT_CAPACITY);
    }

    /**
     * 创建具有指定初始容量的集合
     * @param capacity
     */
    public MyArrayList(int capacity) {
        if (capacity < 0) throw new IllegalArgumentException("容量不能为负数!");
        elements = new Object[capacity];
    }
}

我创建了一个名为MyArrayList的类,继承AbstractList类(主要是为了可以知道List有那些方法,然后快捷键生成方法体),然后定义了几个静态的常量,分别解释一下:


    DEFAULT_CAPACITY :默认初始容量,如果创建MyArrayList对象时不指定大小,将会用次值初始化底层数组。
    EXPANSION_THRESHOLD :扩容阈值,当  集合元素个数  >=  扩容阈值 * 集合容量 时将会自动扩容
    EXPANSION_FACTOR :增长因子,当集合自动扩容时,扩容后的容量=扩容前的容量 * (1+增长因子)
    NARROW_THRESHOLD :缩小阈值,当移除集合元素时,当 元素个数 <=  缩小阈值 * 集合容量 时将会自动缩小容量
    NARROW_FACTOR :缩小因子,当缩小容量时,扩容后的容量=扩容前的容量 * 缩小因子

 

紧接着申明了集合的底层数组elements、集合大小size、modifyCount暂时不做讲解、一个使用默认初始化容量初始化底层数组的无参构造器,一个用指定容量初始化底层数组的带参构造器。

二、size()、isEmpty()、toString()、clear()、toArray()方法的实现

接着来实现最简单的五个方法:

    /**
     * 返回集合元素个数
     * @return
     */
    @Override
    public int size() {
        return size;
    }

    /**
     * 返回集合是否为空
     * @return
     */
    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 清空集合
     */
    @Override
    public void clear() {
        elements = new Object[DEFAULT_CAPACITY];
        size = 0;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");
        for (int i = 0; i < size; i++) {
            if (i > 0) builder.append(", ");
            builder.append(elements[i].toString());
        }
        builder.append("]");
        return builder.toString();
    }

    /**
     * 返回次集合对象的数组表示形式
     * @return
     */
    @Override
    public Object[] toArray() {
        //创建一个长度为size的数组
        Object[] objects = new Object[size];
        //将elements中的元素复制到数组中
        for (int i = 0; i < objects.length; i++) {
            objects[i] = elements[i];
        }
        return objects;
    }

对于size()和isEmpty()方法不做讲解,因该能看懂吧。

clear()方法直接重新创建一个空数组指向elements,并将size置为0即可。

toString()方法遍历数组,拼接字符串并返回。

toArray()方法由于elements数组的末尾元素很有可能为空,所以必须将已有元素转化成一个新数组返回

三、集合的扩容以及缩小方法的实现

    /**
     * 判断list是否需要扩容
     * @return
     */
    private boolean isExpansion() {
        return size >= elements.length * EXPANSION_THRESHOLD;
    }

    /**
     * 判断是否需要减少容量
     * @return
     */
    private boolean isNarrow() {
        return elements.length > DEFAULT_CAPACITY && size <= elements.length * NARROW_THRESHOLD;
    }

    /**
     * 对集合进行扩容操作
     */
    private void expansion() {
        //创建一个新数组,长度为扩容后的长度
        Object[] newElement = new Object[(int) (elements.length * (1 + EXPANSION_FACTOR))];
        //遍历element,将元素复制到新数组中
        for (int i = 0; i < size; i++) {
            newElement[i] = elements[i];
        }
        //用新数组覆盖旧数组完成扩容
        elements = newElement;
    }

    /**
     * 对集合进行缩小容量的操作
     */
    private void narrow() {
        //定义一个新数组,长度为减少容量后的长度
        int len = (int) (elements.length * NARROW_FACTOR) < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : (int) (elements.length * NARROW_FACTOR);
        Object[] newElements = new Object[len];
        //遍历list,将list中的元素复制到新数组中
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        //用新数组覆盖原数组完成减少容量
        elements = newElements;
    }

isExpansion():将当前集合的大小size与 扩容阈值 * 集合容量 比较,判断是否需要扩容

isNarrow():将当前集合的大小size与 缩小阈值 * 集合容量 比较,判断是否需要缩小容量

expansion():执行扩容操作,扩容后的容量=扩容前的容量 * (1+增长因子)

narrow():执行缩小容量操作,扩容后的容量=扩容前的容量 * 缩小因子

四、iterator()的实现

此方法返回一个用于遍历该集合的迭代器对象,所以必须先自定义一个迭代器类,我定义了一个内部类MyIterator如下

private class MyIterator<T> implements Iterator<T> {

        //当前指针指向位置
        int cursor;
        //使用迭代器修改集合的次数
        int iterModifyCount;

        public MyIterator() {
            iterModifyCount = modifyCount;
        }

        @Override
        public boolean hasNext() {
            return cursor < size;
        }

        /**
         * 使指针指向下一个元素并返回当前指向的元素
         *
         * @return
         */
        @Override
        public T next() {
            //如果在使用迭代器过程中使用了非迭代器的remove方法修改了集合结构则抛出此异常
            if (iterModifyCount != modifyCount) throw new ConcurrentModificationException();
            return (T) elements[cursor++];
        }

        /**
         * 使用移除当前指针指向的前一个元素
         */
        @Override
        public void remove() {
            if (cursor == 0) throw new IllegalStateException("在调用remove()方法前必须调用next()方法");
            //调用集合的remove方法删除指定索引的元素
            MyArrayList.this.remove(--cursor);
            iterModifyCount++;
        }

        @Override
        public void forEachRemaining(Consumer<? super T> action) {
            throw new NullPointerException();
        }
    }

在这里讲解一下之前在MyArrayList类中声明的属性modifyCount和此处的iterModifyCount,在java的ArrayList中是不允许在使用迭代器时使用集合的add、remove等方法修改集合的,我同样想实现这个效果,所以定义了这两个属性,在获得迭代器对象时,将modifyCount的值赋值给iterModifyCount,然后在调用next方法时判断这两个值是否相等,如果不想等则表示使用了使用集合的add、remove等方法修改了集合,便抛出一个并发修改异常,如果想要删除元素只能调用迭代器的remove方法,此方法会在添加成功后将iterModifyCount的值加一,然后调用MyArrayList.this.remove(--cursor)是会将modifyCount的值加一,所以他们最后还是相等,forEachRemaining()方法没有予以具体实现。

五、四个add方法的实现

void add(int index, E element):将指定元素插入到指定索引位置。

boolean add(E element):将指定元素追加到集合末尾,返回是否插入成功。

boolean addAll(Collection<? extends E> c):将指定集合中的所有元素追加到此集合尾部,如果插入了元素返回true。

boolean addAll(int index, Collection<? extends E> c):将指定集合中的所有元素插入到此集合指定索引位置,如果插入了元素返回true。

 

    /**
     * 向list指定位置添加一个元素
     *
     * @param index
     * @param element
     */
    @Override
    public void add(int index, E element) {
        //判断索引是否越界
        if (index < 0 || index > size) throw new IndexOutOfBoundsException();
        //判断是否需要扩容
        if (isExpansion()) expansion();
        //反向遍历数组,将数组每个元素向后移动一个位置,直到找到index对应的位置,将element插入
        for (int i = size; i >= 0; i--) {
            //判断当前索引和index是否匹配,如果匹配则直接插入element,否则将前一个元素移至当前位置
            if (index == i) {
                elements[i] = element;
                break;
            } else elements[i] = elements[i - 1];
        }
        //list大小加一
        size++;
        //对该集合的操作次数加一
        modifyCount++;
    }

来看第一个add方法,在指定索引位置插入指定元素,首先判断索引是否越界,然后调用之前定义的判断是否扩容的isExpansion()方法判断是否需要扩容,需要则直接调用expansion()方法,然后将元素插入数组,后面的元素一次往后移动,最后集合大小加一,对集合的操作次数加一。

    /**
     * 向list中添加一个元素,添加到末尾
     *
     * @param element
     * @return
     */
    @Override
    public boolean add(E element) {
        add(size, element);
        return true;
    }

第二个add方法,在尾部追加指定元素,由于size始终等于elements的最大有效索引+1,所以调用add(size, element);相当于在集合尾部追加。

    /**
     * 将指定集合中的元素添加到list末尾
     *
     * @param c
     * @return
     */
    @Override
    public boolean addAll(Collection<? extends E> c) {
        boolean isModify = false;
        //遍历集合,将集合中的元素放入list
        for (E t : c) {
            add(size, t);
            isModify = true;
        }
        return isModify;

    }

第三个add方法与第二个类似,只不过遍历集合c,将所有元素追加到此集合尾部,然后定义了一个变量isModify表示是否修改了次集合,如果集合c为空,那么isModify = true;永远不会被执行,则返回false,否则返回true;

    /**
     * 将指定集合中的元素添加到list指定位置
     *
     * @param index
     * @param c
     * @return
     */
    @Override
    public boolean addAll(int index, Collection<? extends E> c) {
        //判断索引是否越界
        if (index < 0 || index > size) throw new IndexOutOfBoundsException();
        //遍历集合,将集合中的元素添加到list指定位置
        //获得集合迭代器
        Iterator iterator = c.iterator();
        boolean isModify = false;
        for (int i = 0; i < c.size(); i++) {
            add(index + i, (E) iterator.next());
            isModify = true;
        }
        return isModify;
    }

第四个add方法,将指定集合中的所有元素添加到次集合的指定索引位置。首先判断索引是否越界,然后遍历集合c将c中的所有元素复制到此集合,由于是插入整个集合,那么c中的第一个元素将插入到index的位置,第二个元素插入到index+1的位置,一次类推,因此这里(add(index + i, (E) iterator.next());)使用了index + i。

六、四个remove方法的实现

E remove(int index):移除并返回指定索引位置的元素

boolean remove(Object o):移除指定的元素,没找到此元素返回false

boolean removeAll(Collection<?> c):移除次集合中包含在集合c中的所有元素

removeIf(Predicate<? super E> filter):移除此集合中符合条件的元素

    /**
     * 删除并返回指定索引的元素
     * @param index
     * @return
     */
    @Override
    public E remove(int index) {
        //判断索引是否越界
        if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
        E result = (E) elements[index];
        //遍历数组,将元素前移
        for (int i = index; i < size; i++) {
            elements[i] = elements[i + 1];
        }
        //list大小减一
        size--;
        //集合操作次数加一
        modifyCount++;
        //判断是否需要减少容量
        if (isNarrow()) narrow();
        return result;
    }

第一个remove方法,移除并返回指定索引位置的元素。首先判断索引是否越界,然后记录index索引的值,将index后边的元素一次前移,然后集合大小减一,对集合的操作加一,最后判断是否需要减少容量。

 

    /**
     * 移除list中的指定元素
     * @param o
     * @return
     */
    @Override
    public boolean remove(Object o) {
        //遍历list,找到指定元素索引并移除
        for (int i = 0; i < size; i++) {
            if (elements[i] == o) {
                remove(i);
                return true;
            }
        }
        return false;
    }

第二个remove方法,移除指定的元素,没找到此元素返回false,直接遍历此集合,找到该元素的索引,然后通过索引删除元素。

 

    /**
     * 删除指定集合中的元素,如果成功删除了一个元素则返回true,否则返回false
     * @param c
     * @return
     */
    @Override
    public boolean removeAll(Collection<?> c) {
        //定义标记,表示是否删除了元素
        boolean mark = false;
        //遍历集合,删除相同的元素
        for (Object o : c) {
            if (remove(o)) mark = true;
        }
        return mark;
    }

第三个remove方法,删除此集合中包含指定集合c中的所有元素,直接遍历集合c,然后调用第二个remove方法删除

    /**
     * 删除满足条件的元素
     * @param filter
     * @return
     */
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
        //定义标记,表示是否删除了元素
        boolean mark = false;
        //遍历list,删除满足条件的元素
        Iterator<E> iterator = iterator();
        while (iterator.hasNext()) {
            E item = iterator.next();
            if (filter.test(item)) {
                iterator.remove();
                mark = true;
            }
        }
        return mark;
    }

最后一个remove方法,删除此集合中符合条件的元素。先获得此集合的迭代器遍历,将每个元素依次作为参数调用filter.test(item)方法,如果返回true,就删除该元素。

七、get和set方法实现

E get(int index):获取指定索引位置的元素。

E set(int index, E element):修改指定索引元素的值,并返回修改前的值

    /**
     * 通过索引获取元素
     * @param index
     * @return
     */
    @Override
    public E get(int index) {
        if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
        return (E) elements[index];
    }

先来看get方法,首先判断索引是否越界,然后直接返回elements[index],由于elements是Object类型的数组,所以需要向下转型。

 

    /**
     * 设置指定索引的元素,并返回之前的元素
     * @param index
     * @param element
     * @return
     */
    @Override
    public E set(int index, E element) {
        if (index < 0 || index > size - 1) throw new ArrayIndexOutOfBoundsException("索引越界异常:index=" + index);
        E old = (E) elements[index];
        elements[index] = element;
        return old;
    }

再来看set方法,同样是先判断索引是否越界,然后记录旧值,修改值,最后返回旧值。

八、indexOf和lastIndexOf方法的实现

int indexOf(Object o):返回指定元素在此集合中第一次出现的索引,如果集合中没有此元素返回-1

lastIndexOf(Object o):返回指定元素在此集合中最后一次出现的索引,如果集合中没有此元素返回-1

    /**
     * 返回指定元素在列表中第一次出现的索引,如果不存在则返回-1
     * @param o
     * @return
     */
    @Override
    public int indexOf(Object o) {
        int index = -1;
        //遍历element找到元素对应索引
        for (int i = 0; i < size; i++) {
            if (elements[i] == o) {
                index = i;
                break;
            }
        }
        return index;
    }

直接遍历elements,如果找到和此元素相等的元素,就记录索引值,然后返回索引,没找到则返回-1

    /**
     * 返回指定元素在列表中最后一次出现的索引,如果不存在返回-1
     * @param o
     * @return
     */
    @Override
    public int lastIndexOf(Object o) {
        int index = -1;
        //反向遍历element找到元素对应索引
        for (int i = size - 1; i >= 0; i--) {
            if (elements[i] == o) {
                index = i;
                break;
            }
        }
        return index;
    }

在来看lastIndexOf,实现思路和indexOf基本一致,只不过遍历elements时反向遍历。

九、sort方法的实现

void sort(Comparator<? super E> c):通过指定的比较器,对此集合进行排序。

    /**
     * 通过传入的比较器将集合排序
     *
     * @param c
     */
    @Override
    public void sort(Comparator<? super E> c) {
        //使用冒泡排序
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - 1 - i; j++) {
                if (c.compare((E) elements[j], (E) elements[j + 1]) > 0) {
                    Object temp = elements[j];
                    elements[j] = elements[j + 1];
                    elements[j + 1] = temp;
                }
            }
        }
    }

我在这里使用了冒泡排序,也可以使用其他的排序方法。通过传入的比较器依次对相邻的两个元素进行比较,如果比较器返回值大于0就交换两个元素的位置。

十、总结

ArrayList的实现原理要理解他是怎么自动扩容的,理解之后这些方法还是比较简单的。首先在底层创建一个数组,添加元素时,将集合的元素个数与数组的长度进行比较,如果达到阈值,就扩容,扩容其实就是创建一个长度更长的数组,然后将原数组中的元素复制到新数组中,相反,删除元素时判断是否需要减少容量。还有一些比较简单的方法没有提到,可以到我的github查看。

 

附上代码地址:https://github.com/18280230896/MyCollection/blob/master/MyCollection/src/com/llg/collection/MyArrayList.java

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值