从数组到动态数组的演变

数组

特点

数组是计算机内存里一片连续的区域,可以用来有效的存储元素

优点

根据索引可以直接定位到元素,添加新元素到末尾只需要按顺序放到最后一个位置。因此数组检索元素和插入元素到末尾的效率很高

缺点

在内存里大小是固定的,如果数组满了想继续插入元素怎么办?
如果我们想把新的元素插入到具体某个位置,而不是放到最后一个位置,就需要挪动这个位置之后的所有元素;删除同理。因此数组插入和删除元素的效率不高

数组扩容三种方式

方法一:

int[] arr2=new int[arr1.length*2]   //设置新数组长度
for(int i=0;i<arr1.length;i++){     //数组拷贝
      arr2[i]=arr1[i];
}

方法二:
Array.copyOf() 用于复制指定的数组内容以达到扩容的目的,该方法对不同的基本数据类型都有对应的重载方法

int[] arr2=java.util.Arrays.copyOf(原数组名,新数组长度);

方法三:

int[] arr2=new int[arr1.length*2]
System.arraycopy(原数组名,起始下标,新数组名,起始下标,复制长度);

方法一是我们通过自己拷贝数组来实现数组扩容的;
方法二和方法三是通过调用工具类来实现数组扩容的,区别:
Arrays.copyOf()不只复制数组元素,在拷贝元素时,也创建一个新数组;
System.arrayCopy()只复制已有的数组。
但是如果我们读Arrays.copyOf()的源码也会发现,它也用到了System.arrayCopy()。

public static int[] copyOf(int[] original, int newLength) { 
   int[] copy = new int[newLength]; 
   System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); 
   return copy; 
}

数组扩容实例

//本例以第一种扩容方式为例
//假设一个班里有3名学生,定义一个String类型的数组存放他们的名字
String[] names=new String[3];
//size代表数组中真正有几个元素,也就是真正已经赋值的;length代表总长度,该实例中length为3
int size=0;

//没有赋值之前打印数组
System.out.printLn(Arrays.toString(names));//[null,null,null]

//每次添加之前,考虑是否扩容
//如果不考虑扩容的话,如果添加元素个数超过length,会报ArrayIndexOutOfBoundsException异常
if(size>names.length-1){
   //定义一个临时数组,临时数组的大小设置为初始数组大小的2倍
   String[] temp=new String[names.length*2];
  //实现数组元素的拷贝,将初始数组中的元素循环赋值给临时数组
  for(int i=0;i<names.length;i++){
    temp[i]=names[i]
  }
  //names指向临时数组
  names=temp;
}

动态数组

动态数组由来?

那么我们能不能做出一个无限大小的数组呢?–动态数组
现实中的问题:书架满了,你会怎么做?
image.png
一种办法是直接把书架加长,这样就能放更多的书了;
image.png
但是如果书架旁边的位置有限,比如紧挨着衣柜,就需要把衣柜挪开才能有更多的空间,更糟的是如果旁边是一面墙,为了把书架加长就把墙拆了不现实
image.png
为了避免挪动书架旁边的家具甚至拆墙,我们有了第二种办法,我们可以做一个更大的书架,找一个更大的空闲位置去摆放它,再把原来书架上的书挪到新的书架上,这就是动态数组的原理
image.png

动态数组特点

在动态数组里, 维护了数组的容量和数组内元素的个数,比如我们在内存里创建了一个动态数组,他的容量capacity是10,元素的个数size是4,我们用end_index标识最后一个元素的索引。
image.png
每增加一个元素end_index就+1,当数组元素不断增加end_index达到数组容量的时候,就会重新在内存里分配一个更大的数组,通常是原数组的1.5倍,再把元素按顺序拷贝过去。
image.png
由于动态数组本身也是数组,所以它继承了数组的优缺点,比如能快速根据索引定位到元素,因为内存空间连续所以遍历查询的效率很高,但是插入和删除某个位置元素的效率比较差,因为需要挪动若干元素的位置。除此之外,动态数组本身因为支持了自动扩展,所以不需要我们在创建的时候就指定大小,由于到达上限之后需要重新分配数组和拷贝元素到新的数组,所以自动扩展会影响效率。

创建一个动态数组?–ArrayList

ArrayList概述

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。
每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。
Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

ArrayList特点

(1)ArrayList 是一种变长的集合类,基于定长数组实现。
(2)ArrayList 允许空值和重复元素,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会通过扩容机制重新生成一个更大的数组。
(3)由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。
(4)ArrayList 是非线程安全类,并发环境下,多个线程同时操作 ArrayList,会引发不可预知的异常或错误。
(5)顺序添加很方便
(6)删除和插入需要复制数组,性能差(可以使用LinkindList)
(7)Integer.MAX_VALUE - 8 :主要是考虑到不同的JVM,有的JVM会在加入一些数据头,当扩容后的容量大于MAX_ARRAY_SIZE,我们会去比较最小需要容量和MAX_ARRAY_SIZE做比较,如果比它大, 只能取Integer.MAX_VALUE,否则是Integer.MAX_VALUE -8。 这个是从jdk1.7开始才有的

ArrayList自动扩容机制

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。

数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

ArrayList中维护了一个Object类型的数组elementData,可以简单的理解为数组大小

使用有参构造器

当创建ArrayList对象时,如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

使用无参构造器

当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需再次扩容,则扩容elementData为1.5倍

疑问

数组的扩容和动态数组扩容,都是实现了数组的扩容,过程中都是通过扩大原始数组的倍数,之后将原始数组中的元素拷贝一份到新的数组中,那么两者的区别是什么呢?哪种扩容方式效率更高呢?

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值