数组
特点
数组是计算机内存里一片连续的区域,可以用来有效的存储元素
优点
根据索引可以直接定位到元素,添加新元素到末尾只需要按顺序放到最后一个位置。因此数组检索元素和插入元素到末尾的效率很高
缺点
在内存里大小是固定的,如果数组满了想继续插入元素怎么办? 如果我们想把新的元素插入到具体某个位置,而不是放到最后一个位置,就需要挪动这个位置之后的所有元素;删除同理。因此数组插入和删除元素的效率不高
数组扩容三种方式
方法一:
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;
}
动态数组
动态数组由来?
那么我们能不能做出一个无限大小的数组呢?–动态数组 现实中的问题:书架满了,你会怎么做? 一种办法是直接把书架加长,这样就能放更多的书了; 但是如果书架旁边的位置有限,比如紧挨着衣柜,就需要把衣柜挪开才能有更多的空间,更糟的是如果旁边是一面墙,为了把书架加长就把墙拆了不现实 为了避免挪动书架旁边的家具甚至拆墙,我们有了第二种办法,我们可以做一个更大的书架,找一个更大的空闲位置去摆放它,再把原来书架上的书挪到新的书架上,这就是动态数组的原理
动态数组特点
在动态数组里, 维护了数组的容量和数组内元素的个数,比如我们在内存里创建了一个动态数组,他的容量capacity是10,元素的个数size是4,我们用end_index标识最后一个元素的索引。 每增加一个元素end_index就+1,当数组元素不断增加end_index达到数组容量的时候,就会重新在内存里分配一个更大的数组,通常是原数组的1.5倍,再把元素按顺序拷贝过去。 由于动态数组本身也是数组,所以它继承了数组的优缺点,比如能快速根据索引定位到元素,因为内存空间连续所以遍历查询的效率很高,但是插入和删除某个位置元素的效率比较差,因为需要挪动若干元素的位置。除此之外,动态数组本身因为支持了自动扩展,所以不需要我们在创建的时候就指定大小,由于到达上限之后需要重新分配数组和拷贝元素到新的数组,所以自动扩展会影响效率。
创建一个动态数组?–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倍
疑问
数组的扩容和动态数组扩容,都是实现了数组的扩容,过程中都是通过扩大原始数组的倍数,之后将原始数组中的元素拷贝一份到新的数组中,那么两者的区别是什么呢?哪种扩容方式效率更高呢?
|