Data Structures with Java | 连载 01 - 动态数组 ArrayList 实现

638 阅读11分钟

一、基本类型动态数组的实现

线性结构

image.png

几个概念

  • e1即索引为0的是首节点,索引为n的是尾节点或尾元素
  • e1是e2的前驱节点
  • e2是e1的后继节点

线性表:数组,链表,队列,哈希表

Java中的数组是一种顺序存储数据的线性表,元素的内存地址是连续的,且数组的容量是在创建时就已经确定的,且无法修改的。那么如何创建一个动态数组?

动态数组实现

首先创建一个maven项目,增加test依赖,用于对接口或者方法进行单元测试

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

动态数组肯定也是一个数组,也是基于数组的,所以成员变量包含一个数组elements以及数组中元素的数量size, 新建动态数组BasicArrayList,包含成员变量的定义,构造方法,toString()等,先设定动态数组只存放int类型的基本数据

public class BasicArrayList {

    private int size;

    private int[] elements;

    private static final int DEFAULT_CAPATICY = 10;

    public ArrayList(int capaticy){
        // 如果capaticy < 10 就使用默认的容量,否则使用自定义的容量
        capaticy = (capaticy < DEFAULT_CAPATICY) ? DEFAULT_CAPATICY : capaticy;
        elements = new int[capaticy];
    }

    public ArrayList(){
        elements = new int[DEFAULT_CAPATICY];
    }
    
    @Override
    public String toString() {
        return "ArrayList{" +
                "size=" + size +
                ", elements=" + Arrays.toString(elements) +
                '}';
    }
}    

需要实现的方法如下:

void clear(); //清除所有元素

int size(); //返回数组中元素的数量

boolean isEmpty(); //判断数组是否为空

boolean contains(T element); //判断数组是否包含

void add(T element); // 在数组尾部添加元素

T get(int index); // 获取指定索引的元素

T set(int index, T element); // 设置指定位置的元素,并返回原来该位置的元素

void add(int index, T element); //在指定位置插入元素

T remove(int index); // 删除指定位置的元素,并返回该元素

int indexOf(T element); //获取指定元素的索引

先实现简单的方法size(),isEmpty(),get(int index)

public int size(){
    return size;
}

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

public int get(int index){
    // 需要对index进行校验
    if (index < 0 || index >= size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }
    return elements[index];
}

新建一个测试类BasicArrayListTest,对size()和isEmpty进行测试

public class BasicArrayListTest {

    @Test
    public void size() {
        BasicArrayList arrayList = new BasicArrayList(10);
        Assert.assertEquals(0,arrayList.size());
    }

    @Test
    public void isEmpty() {
        BasicArrayList arrayList = new BasicArrayList(10);
        Assert.assertTrue(arrayList.isEmpty());
    }
    
}

实现set(),indexOf(),contains()和clear()方法

public int set(int index, int element){
    // 对index进行校验
    if (index < 0 || index >= size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }

    int old = elements[index];
    elements[index] = element;
    return old;
}

// 定义一个常量,当indexOf()方法找不到元素时返回
privare static int final ELEMENT_NOT_FOUND = -1;


public int indexOf(int element){
    // 遍历所有元素
    for (int i = 0; i < size; i++) {
        if (elements[i] == element) return i;
    }
    return ELEMENT_NOT_FOUND;
}

public boolean contains(int element){
    // 调用indexOf方法查看返回值是否为-1
    return indexOf(element) != ELEMENT_NOT_FOUND;
}

public void clear(){
    size = 0;
}

remove()方法实现

image.png

要注意删除成功后size减少一

public int remove(int index){

    // 首先判断index
    if (index < 0 || index >= size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }

    int old = elements[index];
    // 只遍历需要挪动的部分,不需要遍历整个数组
    for (int i = index + 1; i < size; i++) {
        elements[i - 1] = elements[i];
    }
    size --;
    return old;
}

add方法,默认添加到数组末尾,先不考虑容量的问题

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

测试以上实现的方法

public class BasicArrayListTest {

    BasicArrayList arrayList = null;

    @Before
    public void init(){
        arrayList = new BasicArrayList();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(4);
        arrayList.add(3);
        arrayList.add(8);
        arrayList.add(5);
        arrayList.add(7);
        arrayList.add(6);
    }

    @Test
    public void size() {
        Assert.assertEquals(8,arrayList.size());
        System.out.println("动态数组arrayList的size为:" + arrayList.size());
    }

    @Test
    public void isEmpty() {
        Assert.assertFalse(arrayList.isEmpty());
        System.out.println("动态数组arrayList是否为空:" + arrayList.isEmpty());
    }

    @Test
    public void get() {
        int i = arrayList.get(1);
        Assert.assertEquals(2,arrayList.get(1));
        System.out.println("arrayList中索引为1的元素是:" + arrayList.get(1));
    }

    @Test
    public void set() {
        System.out.println("修改前," + arrayList.toString());
        arrayList.set(1,10);
        System.out.println("将索引1位置的元素替换为10," + arrayList.toString());
    }

    @Test
    public void indexOf() {
        Assert.assertEquals(7,arrayList.indexOf(6));
        System.out.println("索引6位置的元素为:" + arrayList.indexOf(6));
    }

    @Test
    public void contains() {
        Assert.assertTrue(arrayList.contains(1));
        System.out.println("是否包含元素1:" + arrayList.contains(1));
    }

    @Test
    public void clear() {
        System.out.println("清空前," + arrayList.toString());
        arrayList.clear();
        System.out.println("清空后,获取索引0的元素" + arrayList.get(1));
    }

    @Test
    public void remove() {
        System.out.println("删除前," + arrayList.toString());
        arrayList.remove(1);
        System.out.println("删除索引1的元素," + arrayList.toString());
    }

    @Test
    public void add() {
        System.out.println(arrayList.toString());
        arrayList.add(9);
        System.out.println(arrayList.toString());
    }
}

add()方法重写,在具体位置增加元素

// 在具体位置增加元素
public void add(int index, int element){
    // 首先判断索引,index可以等于size,相当于在末尾添加元素
    if (index < 0 || index > size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }
    // 要在指定索引增加元素,要现将这个索引位置及之后的元素往后移动一个索引,腾出位置
    // 挪动元素,从最后一个开始挪动,否则后一个元素会被前一个元素覆盖
    for (int i = size - 1; i > index; i--) {
        elements[i + 1] = elements[i];
    }
    elements[index] = element;
    // 增加size
    size ++;
}

测试

@Test
public void add() {
    System.out.println(arrayList.toString());
    arrayList.add(9);
    System.out.println(arrayList.toString());
    arrayList.add(2,11);
    System.out.println(arrayList.toString());
}

0表示数组中空闲的位置

image.png

add(int element)可以优化为

public void add(int element){

    // 数组末尾添加元素
    add(size,element);
}

解决数组的致命弱点-无法动态扩容

如果数组的容量快被占满,则需要向内存申请一块空间用来保存数据,让变量指向新的内存空间,原来的内存空间没有变量指向,将会被回收,回收前需要将原数组中的数据拷贝到新的数组中,并且新数组的容量相应扩展2倍或者一个根据使用情况适合的倍数

增加一个扩容函数

// 扩容方式一,使用指定扩容,方法中自行作判断何时需要扩容,调用时传入扩容的大小
private void expansionCapacity(int newCapacity){ 
    int oldCapacity = elements.length;
    if (oldCapacity >= size + 1) return;

    // 创建一个新的容量的数组
    // newCapacity = oldCapacity + (oldCapacity >> 1);
    int[] newElements = new int[newCapacity];
    // 拷贝元素
    for (int i = 0; i < size; i++) {
        newElements[i] = elements[i];
    }
    // 指向新的内存空间

    elements = newElements;
    System.out.println("扩容成功,由" + oldCapacity + "扩展至" + newCapacity);
}

在add(int index, int element)方法中判断完index之后加入该方法

// 在具体位置增加元素
public void add(int index, int element){
    // 首先判断索引,index可以等于size,相当于在末尾添加元素
    if (index < 0 || index > size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }

    // 如果容量到达临界点会自动扩容,容量扩为原来的1.5倍
    expansionCapacity(elements.length + (elements.length >> 1));

    // 挪动元素,从最后一个开始挪动,否则后一个元素会被前一个元素覆盖
    for (int i = size - 1; i > index; i--) {
        elements[i + 1] = elements[i];
    }
    elements[index] = element;
    // 增加size
    size ++;
}

再次进行优化

将判断索引抽取为工具类中的静态方法,新建utils包,增加工具类CheckUtils

public class CheckUtils {

    private static void outOfBoundary(int index,int size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }
    public static void checkIndex(int index, int size){
        if (index < 0 || index >= size){
            outOfBoundary(index, size);
        }
    }

    public static  void checkIndex4Add(int index, int size){
        if (index < 0 || index > size){
            outOfBoundary(index, size);
        }
    }
}

最终BasicArrayList的代码为

public class BasicArrayList {

    // 元素的个数
    private int size;

    // 数组
    private int[] elements;

    private static final int DEFAULT_CAPATICY = 10;

    private static final int ELEMENT_NOT_FOUND = -1;

    public BasicArrayList(int capaticy){
        // 如果capaticy < 10 就使用默认的容量,否则使用自定义的容量
        capaticy = (capaticy < DEFAULT_CAPATICY) ? DEFAULT_CAPATICY : capaticy;
        elements = new int[capaticy];
    }

    public BasicArrayList(){
        elements = new int[DEFAULT_CAPATICY];
    }

    public int size(){
        return size;
    }

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

    public int get(int index){
        CheckUtils.checkIndex(index,size);
        return elements[index];
    }

    public int set(int index, int element){
        CheckUtils.checkIndex(index,size);

        int old = elements[index];
        elements[index] = element;
        return old;
    }

    public int indexOf(int element){
        for (int i = 0; i < size; i++) {
            if (elements[i] == element) return i;
        }
        return ELEMENT_NOT_FOUND;
    }

    public boolean contains(int element){
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

    // 清空即无法访问任何一个元素
    public void clear(){
        size = 0;
    }

    public int remove(int index){

        // 首先判断index
        CheckUtils.checkIndex(index,size);

        int old = elements[index];
        // 只遍历需要挪动的部分,不需要遍历整个数组
        for (int i = index + 1; i < size; i++) {
            elements[i - 1] = elements[i];
        }
        size --;
        return old;
    }

    public void add(int element){

        // 数组末尾添加元素
        add(size,element);
    }

    // 在具体位置增加元素
    public void add(int index, int element){
        // 首先判断索引,index可以等于size,相当于在末尾添加元素
        CheckUtils.checkIndex4Add(index,size);

        // 增加一个元素,如果超过容量就扩容
        // 如果容量到达临界点会自动扩容,扩容1.5倍
        expansionCapacity(elements.length + (elements.length >> 1));

        // 挪动元素,从最后一个开始挪动,否则后一个元素会被前一个元素覆盖
        for (int i = size - 1; i > index; i--) {
            elements[i + 1] = elements[i];
        }
        elements[index] = element;
        // 增加size
        size ++;
    }

    // 方法中自行作判断,调用时传入扩大的容量
    private void expansionCapacity(int newCapacity){
        int oldCapacity = elements.length;
        if (oldCapacity >= size + 1) return;

        // 创建一个新的容量的数组
        // newCapacity = oldCapacity + (oldCapacity >> 1);
        int[] newElements = new int[newCapacity];
        // 拷贝元素
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        // 指向新的内存空间

        elements = newElements;
        System.out.println("扩容成功,由" + oldCapacity + "扩展至" + newCapacity);
    }

    private void ensureCapacity(int capacity){
        int oldCapacity = elements.length;
        if (oldCapacity >= capacity) return;
        // 创建一个新的容量的数组
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        int[] newElements = new int[newCapacity];
        // 拷贝元素
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        // 指向新的内存空间
        elements = newElements;
        System.out.println("扩容成功,由" + oldCapacity + "扩展至" + newCapacity);
    }

    @Override
    public String toString() {
        return "ArrayList{" +
                "size=" + size +
                ", elements=" + Arrays.toString(elements) +
                '}';
    }
}

测试add

@Test
public void add() {
    System.out.println(arrayList.toString());
    arrayList.add(9);
    System.out.println(arrayList.toString());
    arrayList.add(2,11);
    System.out.println(arrayList.toString());
    arrayList.add(12);
    System.out.println(arrayList.toString());
}

image.png

二、使用泛型

为了让动态数组能够存放多种类型的数据,有必要使用泛型进行改造,使用T表示泛型,创建泛型数组时使用new Object[],然后进行强转,使用T[]来接收,因为Object是所有类的父类,即

T[] elements = (T[]) new Object[];

新建 entity 包,增加一个实体类 Porsche,包含一个 name 属性,getter 方法,setter 方法,toString 方法及有参和无参构造方法

public class Porsche {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Porsche(String name) {
        this.name = name;
    }

    public Porsche() {
    }
}

对象内存管理

复制 BasicArrayList,并重命名为 ArrayList,将 int 修改为泛型 T,动态数组保存对象其实是保存的对象访问地址

image.png

重构 clear 方法,清空意味着将数组中存放的内存地址的指向全部指向 null。

// 清空即无法访问任何一个元素
public void clear(){
    for (int i = 0; i < size; i++) {
        elements[i] = null;
    }
    size = 0;
}

remove 方法,需要将最后一个元素中内存地址的指向指为 null

public T remove(int index){

    // 首先判断index
    if (index < 0 || index >= size){
        throw new IndexOutOfBoundsException("index=" + index + ",size=" + size);
    }

    T old = elements[index];
    // 只遍历需要挪动的部分,不需要遍历整个数组
    for (int i = index + 1; i < size; i++) {
        elements[i - 1] = elements[i];
    }
    size --;
    // 最后一个指向清空
    elements[size] = null;
    return old;
}

重构后 ArrayList 的代码为

public class ArrayList<T> {

    // 元素的个数
    private int size;

    // 数组
    private T[] elements;

    private static final int DEFAULT_CAPATICY = 10;

    private static final int ELEMENT_NOT_FOUND = -1;

    public ArrayList(int capaticy){
        // 如果capaticy < 10 就使用默认的容量,否则使用自定义的容量
        capaticy = (capaticy < DEFAULT_CAPATICY) ? DEFAULT_CAPATICY : capaticy;
        elements = (T[]) new Object[capaticy];
    }

    public ArrayList(){
        elements = (T[]) new Object[DEFAULT_CAPATICY];
    }

    public int size(){
        return size;
    }

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

    public T get(int index){
        CheckUtils.checkIndex(index,size);
        return elements[index];
    }

    public T set(int index, T element){
        CheckUtils.checkIndex(index,size);

        T old = elements[index];
        elements[index] = element;
        return old;
    }

    public int indexOf(T element){
        if (element == null){
            for (int i = 0; i < size; i++) {
                if (elements[i] == null);
                return i;
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (element.equals(elements[i])) return i;
            }
        }

        return ELEMENT_NOT_FOUND;
    }

    public boolean contains(T element){
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

    // 清空即无法访问任何一个元素
    public void clear(){
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
        size = 0;
    }

    public T remove(int index){

        // 首先判断index
        CheckUtils.checkIndex(index,size);

        T old = elements[index];
        // 只遍历需要挪动的部分,不需要遍历整个数组
        for (int i = index + 1; i < size; i++) {
            elements[i - 1] = elements[i];
        }
        size --;
        // 最后一个指向清空
        elements[size] = null;
        return old;
    }

    public void add(T element){

        // 数组末尾添加元素
        add(size,element);
    }

    // 在具体位置增加元素
    public void add(int index, T element){
        // 首先判断索引,index可以等于size,相当于在末尾添加元素
        CheckUtils.checkIndex4Add(index,size);

        // 如果容量到达临界点会自动扩容
        expansionCapacity(elements.length + (elements.length >> 1));

        // 挪动元素,从最后一个开始挪动,否则后一个元素会被前一个元素覆盖
        for (int i = size; i > index; i--) {
            elements[i] = elements[i-1];
        }
        elements[index] = element;
        // 增加size
        size ++;
    }

    private void expansionCapacity(int newCapacity){
        int oldCapacity = elements.length;
       if (oldCapacity >= size + 1) return;

        // 创建一个新的容量的数组
        // newCapacity = oldCapacity + (oldCapacity >> 1);
        T[] newElements = (T[]) new Object[newCapacity];
        // 拷贝元素
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i];
        }
        // 指向新的内存空间
        elements = newElements;
        System.out.println("扩容成功,由" + oldCapacity + "扩展至" + newCapacity);
    }
    
    @Override
    public String toString() {
        return "ArrayList{" +
                "size=" + size +
                ", elements=" + Arrays.toString(elements) +
                '}';
    }
}

对于 indexOf 方法,需要判断对象是否相等,因此需要重写对象的 equals 方法,否则没有哪两个对象的内存地址是相同的,Porsche 实体类重写 equals 方法。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Porsche porsche = (Porsche) o;

    return name.equals(porsche.name);
}

@Override
public int hashCode() {
    return name.hashCode();
}

对于存放 null 的处理,indexOf 方法需要进行判空操作

public int indexOf(T element){
    if (element == null){
        for (int i = 0; i < size; i++) {
            if (elements[i] == null);
            return i;
        }
    } else {
        for (int i = 0; i < size; i++) {
            if (element.equals(elements[i])) return i;
        }
    }

    return ELEMENT_NOT_FOUND;
}

测试 ArrayList

public class ArrayListTest {

    ArrayList<Porsche> arrayList = null;

    @Before
    public void init(){
        arrayList = new ArrayList<>();
        arrayList.add(new Porsche("Porsche 718"));
        arrayList.add(new Porsche("550Spyder"));
        arrayList.add(new Porsche("Macan"));
        arrayList.add(new Porsche("Taycan"));
        arrayList.add(new Porsche("Cayenne"));
        arrayList.add(new Porsche("Panamera"));
        arrayList.add(new Porsche("Porsche 911"));
        arrayList.add(new Porsche("Cayman"));
    }


    @Test
    public void size() {
        Assert.assertEquals(8,arrayList.size());
        System.out.println("动态数组arrayList的size为:" + arrayList.size());
    }

    @Test
    public void isEmpty() {
        Assert.assertFalse(arrayList.isEmpty());
        System.out.println("动态数组arrayList是否为空:" + arrayList.isEmpty());
    }

    @Test
    public void get() {
        Porsche porsche = arrayList.get(1);
        Assert.assertEquals(new Porsche("550Spyder"),arrayList.get(1));
        System.out.println("arrayList中索引为1的元素是:" + arrayList.get(1));
    }

    @Test
    public void set() {
        System.out.println("修改前," + arrayList.toString());
        arrayList.set(1,new Porsche("Taycan 2021"));
        System.out.println("将索引1位置的元素替换为" + new Porsche("Taycan 2021") + "," + arrayList.toString());
    }

    @Test
    public void indexOf() {
        Assert.assertEquals(3,arrayList.indexOf(new Porsche("Taycan")));
        System.out.println("索引6位置的元素为:" + arrayList.indexOf(new Porsche("Taycan")));
    }

    @Test
    public void contains() {
        Assert.assertTrue(arrayList.contains(new Porsche("Taycan")));
        System.out.println("是否包含" + new Porsche("Taycan") +  ":" + arrayList.contains(new Porsche("Taycan")));
    }

    @Test
    public void clear() {
        System.out.println("清空前," + arrayList.toString());
        arrayList.clear();
        System.out.println("清空后,获取索引0的元素" + arrayList.get(1));
    }

    @Test
    public void remove() {
        System.out.println("删除前," + arrayList.toString());
        arrayList.remove(1);
        System.out.println("删除索引1的元素," + arrayList.toString());
    }

    @Test
    public void add() {
        System.out.println(arrayList.toString());
        arrayList.add(new Porsche("Boxster"));
        System.out.println(arrayList.toString());
        arrayList.add(2,new Porsche("Taycan 2020"));
        System.out.println(arrayList.toString());
        arrayList.add(new Porsche("Taycan 2022"));
        System.out.println(arrayList.toString());
    }
}

执行 add 方法

image.png

至此,自定义动态数组 ArrayList 完结 🎉