从设计源码入手分析动态数组

数组(Array)

数组是一种顺序存储的线性表,所有元素的内存地址是连续的。

int[] array = new int[]{11, 22, 33};

当创建完这个数组的时候,堆空间只能放3个元素,以后想再放更多的元素是不可能的。

  • 在很多编程语言中,数组都有个致命的缺点,无法动态修改容量。
  • 但在实际开发中,我们希望数组的容量是可以改变的

那么如何实现动态数组呢?

我们可以自己设计动态数组,首先类名为ArrayList(注意,这不是java官方的ArrayList,但是这种源码设计和官方差不多,而且更简单能够理解源码的设计思想),然后是动态数组的接口设计。

		int size();//元素的数量
        boolean isEmpty();//是否为空
        boolean contains(int element);//是否包含某个元素
        void add(int element);//添加元素到最后面
        int get(int index);//返回Index位置对应的元素
        int set(int index,int element);//设置index位置的元素
        void add(int index,int element);//往index位置添加元素
        int remove(int index);//删除Index位置对应的元素
        int indexOf(int element);//查看元素的位置
        void clear();//清楚所有元素

动态数组设计

一个动态数组里面应该有哪些成员变量呢?

	private int size;		// 元素的数量
    private int[] elements; 	// 所有的元素

设计动态数组的容量

private static final int DEFAULT_CAPACITY = 10; // 初始容量
private static final int ELEMENT_NOT_FOUND = -1;
public ArrayList(int capacity) { // 容量小于10一律扩充为10
        capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
        elements = new int[capacity];
    }
public ArrayList(){
        this(DEFAULT_CAPACITY);
    }

简单接口的实现

 	public int size(){
        return size;
    }   
    public boolean isEmpty(){
        return size == 0;
    }
    public boolean contains(int element){
        return indexOf(element) != ELEMENT_NOT_FOUND; // 找的到该元素则返回True
    }
	//获取index位置的元素
	 public int get(int index){
        if(index<0||index>=size){//采用抛出异常更好
            throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
        }
        return elements[index];
    }
    }
    //设置index位置的元素
	 public int set(int index){
        if(index<0||index>=size){//采用抛出异常更好
            throw new IndexOutOfBoundsException("Index:"+index+", size:"+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;//ELEMENT_NOT_FOUND为常量-1
		}
	//清楚所有元素
	public void clear(){
        size = 0;//在使用者看来是清除了,我们设计者看来没有,并且内存没有浪费,因为还要利用
    }
    //添加元素到数组最后
    public void add(int element){
        elements[size++] = element;
    }
    //封装功能函数
     public String toString() {
        // 打印形式为: size=5, [99, 88, 77, 66, 55]
        StringBuilder string = new StringBuilder();
        string.append("size=").append(size).append(", [");
        for (int i = 0; i < size; i++) {
            if(0 != i) string.append(", ");
            string.append(elements[i]);
        }
        string.append("]");
        return string.toString();
    }
    //删除某个元素
    public int remove(int index){
    	if(index<0||index>=size){//采用抛出异常更好
            throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
        }
        int old = elements[index];
        for (int i = index; i < size - 1; i++) {// // 从前往后开始移, 用后面的元素覆盖前面的元素
            elements[i-1] = elements[i];
        }
        size--; // 删除元素后, 将最后一位设置为null
        return old;
    }
    //在index位置插入一个元素
    public void add(int index, int element){
    	if(index<0||index>size){//这里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++;
    }

到此基本封装完毕,可能有人会疑惑动态数组内部new出来的数组内存几时释放?

new是向堆空间申请内存,一旦使用完毕,java的垃圾回收机制会自动回收。

ArrayList list = new ArrayList();

现在我们来进行接口测试

public static void main(String[] args) {
        //new是向堆空间申请内存
        ArrayList list = new ArrayList();
        list.add(99);
        list.add(88);
        list.add(77);
        list.add(66);
        list.add(55);
        list.set(3,80);
        System.out.println(list);
    }

测试结果表明接口功能正常
在这里插入图片描述

扩容动态数组

当我们向数组中添加元素时,如果数组已经满了我们需要对数组进行动态扩容。那么
如何扩容呢?难道在数组后面拼接一段内存?这是不允许的,没有这种操作。

因为我们申请内存的时候要用到new,而new申请内存返回的地址是随机的,这就意味着如果你再申请一段内存,地址很有可能不在数组后面,可以在任意处。

如果想要保持一个连续的空间,那就是申请一块更大的内存而且是连续的,将原来的数组挪到新的内存,原来的内存自动回收。如何实现?

写一个确保容量的私有方法ensureCapacity(int capacity)

//保证要有capacity的容量
private void ensureCapacity(int capacity){
        int oldCapacity = elements.length;//现在的容量就是数组的长度
        if(oldCapacity >= capacity) return;//现在的容量大于等于至少需要的容量,就不需要扩容
        // 扩容操作
        int newCapacity = oldCapacity + (oldCapacity >> 1);// 新容量为旧容量的1.5倍
        int[] newElements = new int[newCapacity];
        for (int i = 0; i < size; i++) {
            newElements[i] = elements[i]; // 拷贝原数组元素到新数组
        }
        elements = newElements;
        System.out.println("size="+oldCapacity+", 扩容到了"+newCapacity);
    }
    // System.arraycopy()是系统自身调用的方法,为了便于理解扩容的本质,我这里就不采用这个方法

测试下能否保证容量,为了便于测试,这里初始容量设为2即DEFAULT_CAPACITY = 2,添加5个数据,运行结果如下
在这里插入图片描述

泛型数组

前面我们所设计的动态数组只针对int类型的数据进行添加,但是如果想要其它类型也能添加进来,我们可以将相应的数据类型设置为泛型E。相应的类型进行强制转换,例如在ArrayList(int capacity)方法里进行修改

 public ArrayList(int capacity) { // 容量小于10一律扩充为10
        capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
        elements = (E[])new Object[capacity];//所有的类都继承Object,强制转换为泛型
    }

假设添加3个对象为Person的数据,测试对象是否添加进来

 public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person(10,"张三"));
        list.add(new Person(20,"李四"));
        list.add(new Person(30,"王五"));
        System.out.println(list);
    }

测试结果如下在这里插入图片描述
注意:使用泛型数组后要注意内存管理。

对象数组

如果new的是一个Object数组,那就是对象数组,对象数组内存里面放的不是对象本身,而是对象的引用(地址),这样可以节省空间。

Object[] objects = new Object[7];
Object[0] = new Person(10,"老李");

如果想销毁数组里的Person对象,该如何实现?

public void clear(){
        // 使用泛型数组后要注意内存管理(将元素置null)
        for (int i = 0; i < size; i++) {
            elements[i] = null;//将数组里指向对象的地址全部清空
        }
        size = 0;//不要设置成elements=null;
    }

对象销毁了,对象的内存自然也就没了,但是数组的内存不能去销毁,将来要放新对象的地址。
正所谓能循环利用的留下,不能循环利用的滚蛋。。。

如何证明Person对象销毁了?在java中有一个方法为finalize(),可以在这个方法里写下对象的遗言,当能调用这个方法说明这个对象要挂了。

@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("Person - finalize");
	}

为了便于看到效果,另外我们需要提醒jvm进行垃圾回收

public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person(10,"张三"));
        list.add(new Person(20,"李四"));
        list.add(new Person(30,"王五"));
        list.clear();
        //提醒JVM进行垃圾回收
        System.gc();
    }

运行结果如下,可以看到添加的3个对象均挂了。
在这里插入图片描述

总结

对比官方的ArrayList源码,会发现和本文实现的动态数组在增删改查功能上基本一致,除了Iterator(迭代器),这是设计模式的内容。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

༄༊心灵骇客༣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值