首先说说我们的ArrList的产生背景吧:
在Java最开始的时候,储存数据的方式和很多的语言一样,用变量和数组来进行存储。在数组中可以存储一定量的数据,但是数组的有一个缺点,那就是在使用的时候数组的长度是不可变的,有时候我们需要存储一些数据就不怎么友好了;因为大多数情况下,我们存储的数据的个数是不确定的,这个时候用数组就显得有点吃力了。因为需要我们去掌握它的长度,一旦长度不够长,那我需要存储的数据也就没有办法存储完,一旦数组的长度过于长了,那么多出来的空间又浪费了。所以在这样的一个背景下,让我们的重点都在开发业务上,而不需要去对数组的长度去做太多的纠结,JDK给我们提供了一个ArrayList的api供我们去使用。在一系列的研究后,自己去总结的核心的内容为下:
1、在这个集合中,我们依然使用数组的方式来存储数据。
2、在这个集合中,我们需要自动改变数组的长度,至于是在原有的基础上去改变多少,根据我们的需要去扩展,在JDK里面做的是扩展1.5倍,原因是在里面有这么一段代码。
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
其中最重要的就是oldCapacity>>1这一步操作,这个操作叫做补码右移1位,那么为什么是1.5倍呢?原因是我们在第一点交代了,我们需要使用数组来存储数据,那么就需要进行指定它的初始长度,而在JDK里面给我们的是10,
那么我们现在来进行计算一下吧,以便真正的去知道它为什么是扩展了1.5倍:(在计算机中八位为一个字节,我们就用八位来进行计算,右移的意思就是将补码进行右移,这里是整数,也就不需要考虑符号位了)。
a、首先10的八位二进制补码是:00001010
b、将这个二进制数字右移一位后可以得到:00000101,那么就是5.
c、现在再去看代码中的 int newCapacity = oldCapacity + (oldCapacity >> 1);就可以换成10+5=15,所以与原来的相比较他就扩展了1.5倍了。
3、在这个集合中,我们需要能够真实的添加进数据进去,也就是add()方法。
4、在这个集合中,我们需要能够像获取数组一样的简单,也就是根据下标去获取;那么在ArrayList中商用的是get(int index)这个方法。
5、在这个集合中我们还可以删除指定的某个指定下标的元素,也就是像在数组中我们将某个数组中的某个元素指定为默认值,或者说将这个元素用后面的元素来覆盖要删除的这个元素,将最后的一个数组的元素置为默认值。这在ArrayList中使用的是remove(int index)这个方法。
6、在这个数组中我们还可以修改某个元素的值,也就是像我们去操作数组时,我想要数组的某个元素变成我想要的结果,这在ArrayList中使用的是 set(int index, E element)
7、还可以去删除指定区域的一些数据
8、还可以去根据某个下标我们去删除个数,
9、还可以在指定的下标下后添加元素
10、还可以在指定下标后将另外一个数组进行添加进去
11、还可以根据指定区域去用某个我想要的数组数据来替换成最终我想要的。
所以根据这些功能,我自己写了一个类似的类,记录一下其中的思想。
package com.yxw.list;
/**
* @author : TQY 杨显伍
* @date : 2022年09月30日 13:46
* effect:
*/
public class MyArray<E> {
//真正数组的真正初始长度
private static final int DEFAULT_CAPACITY=10;
//真正的元素初始长度
private static final int DEFAULT_SIZE=0;
private E[] elementData;//真正的存储数据的数组
//在添加数据时需要记录实际的长度和剩余的空间,永远比实际的数组下标大1
private int size=DEFAULT_SIZE;
public MyArray(){
this.elementData= (E[]) new Object[DEFAULT_CAPACITY];
}
//用户可以指定初始的长度,当数据量比较大的时候从十个进行扩容也比较麻烦。
public MyArray(int length){
this.elementData=(E[]) new Object[length];
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 13:58
* effect: 添加元素
* 1、用户肯定是循环的调用我的方法------故应该给我需要存储的数据,也就是该方法需要参数。
* 2、还是给用户一个提示,提示调用该方法是否成功了-----------那么就应用有返回值.
* a.在存储之前我需要一个去判断是否长度还够,但是这个方法的职责只是一个添加的功能,为了以后的维护方便,需要调用一个方法。
*/
public boolean add(E enterData){
this.ensureCapacityInterInternal(size+1);//判断容量是否够用。
elementData[size++]=enterData;//正在的存储
return true;
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 14:11
* @param minCapacity 最终的剩余容量
* effect: 这个方法一般用户拿去没用,所示就写成了私有的方法
* 结论:1、我这个方法只是用来判断容量是否还足够的,那么这个方法就不需要返回值了
* 2、这个方法靠什么去做判断容量是否需要呢?--------那就需要调用这个方法时给一个剩余的容量。
* A、这个方法也只有一个功能---判断剩余的容量是否还足够。那么依然还是扩容的具体事宜交给其他的方法。
* B、不过在此之前,我们需要去考虑怎样的去扩充容量,那么我就需要进行一个计算。
*/
private void ensureCapacityInterInternal(int minCapacity) {
if (minCapacity-elementData.length>0){//容量不够了,需要扩容
this.grow(minCapacity);//计算容量。
}
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 14:22
* @param minCapacity 最终的剩余容量
* effect: 这个方法依然只是做一个长度的扩充。具体的扩充依然交给另外一个方法来做。
*/
private void grow(int minCapacity) {
int odlCapacity = elementData.length;
int newCapacity=odlCapacity+(odlCapacity>>1);//扩容1.5倍
if (newCapacity-minCapacity<0){
newCapacity=minCapacity;
}
//真正的扩容
elementData=copyNewArray(newCapacity,elementData);
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 14:35
* @param newCapacity
* @param oldArray
* @return E[]
* effect: 首先这个方法需要两个参数,一个是扩容前的数据,一个是经过计算后得到的扩容的容量大小
* 肯定要再次的赋值给真正的数据。所以必须要返回给调用者一个新的数组。
*/
private E[] copyNewArray(int newCapacity,E[] oldArray){
E[] newArray= (E[]) new Object[newCapacity];
for (int copyCount=0;copyCount<oldArray.length;copyCount++){
newArray[copyCount]=oldArray[copyCount];
}
return newArray;
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 14:43
* @return int
* effect: 用户肯定需要获取有效元素个数,所以此方法就一个真实数据的大小。
*/
public int getSize(){
return size;
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 14:55
* @return E
* effect: 根据下标获取元素 首先用户想要数据,那么我们的方法就应该是有返回值,也是根据某个下标来获取的元素,那么就应该有对应的下标。
* 在用户不知道具体的容量大小的情况下为了防止数据下标越界,给用户提示这个异常。
*/
public E get(int index){
this.checkIndex(index);//先捕获异常
return elementData[index];
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 16:05
* effect: 下标越界的异常处理
*/
private void checkIndex(int index) {
if (index<0 || index>size-1){
throw new RuntimeException("Data Index Is:"+(size-1)+",Size Is:"+size+",Your Index is:"+index);
}
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 16:20
* effect: 提前做个下标检测 从下标开始,到size-1结束往前覆盖 有效数字的最后一个元素置为空
*/
public E remove(int index){
this.checkIndex(index);
E removeDate=elementData[index];
for (;index<size-1;index++){
elementData[index]=elementData[index+1];
}
elementData[--size]=null;
return removeDate;
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 16:44
* @return boolean
* effect: 在指定的位置插入数据。 从前往后以一位,然后将留出来的位置进行赋值。
*/
public boolean add(int index,E addDate){
this.checkIndex(index); //判断下标是否合法
this.ensureCapacityInterInternal(this.size+1);//判断位置是否够用
for (int star=size++;star>index;star--){
elementData[star]=elementData[star-1];
}
elementData[index]=addDate;
return true;
}
/**
* @author TQY 杨显伍
* @date 2022/9/30 17:19
* effect: 根据下标进行替元素 给用户返回旧元素
* 1、先检测下标是否合法,
* 2、接受一下被替换的数据(oldData)给用户返回
* 3、根据传过来的下标进行替换。
*/
public E replace(int index,E replaceData){
this.checkIndex(index);
E oldData=elementData[index];
this.elementData[index]=replaceData;
return oldData;
}
private int enterIndex=0;//添加数组时的数组下标
/**
* @author TQY 杨显伍
* @date 2022/10/8 9:59
* effect: 在原有的集合里面加数组
* 1、需要一个可变的参数 E enterData
* 2、需要一个返回值给用户提示是否添加成功
*/
public boolean add(E ... enterData){
this.ensureCapacityInterInternal(this.size+enterData.length);//判断位置是否够用
for (enterIndex = 0; enterIndex < enterData.length ; enterIndex++) {
elementData[size++]=enterData[enterIndex];
}
return true;
}
/**
* @author TQY 杨显伍
* @date 2022/10/8 10:31
* effect: 在已有集合中的指定位置添加数组
* 需要的参数有1、指定的下标 2、添加的数组
*/
private int indexMove=0;//接受移动数组的下标
private int copyIndex=0;//赋值时移动数组的下标
public E[] add(int index,E ... addData){
int moveLength=size-index;//移动数组的长度
this.checkIndex(index); //判断下标是否合法
this.ensureCapacityInterInternal(this.size+addData.length+1);//判断容器位置是否可用
E[] move=(E[]) new Object[moveLength];//移动的数组
for (int moveIndex=index;moveIndex<this.size;moveIndex++){
move[indexMove++] = this.elementData[moveIndex];
}
for (int i = index+ addData.length ; i < addData.length+size; i++) {
elementData[i]=move[copyIndex++];
}
for (int addStart = index; addStart < index+addData.length; addStart++) {
elementData[addStart]=addData[enterIndex++];
}
this.size+=addData.length;
return addData;
}
/**
* @author TQY 杨显伍
* @date 2022/10/8 13:15
* effect: 删除指定下标区间的元素 其中范围为[a,b]
*/
public E[] remove(int startIndex,int endIndex){
//判断下标是否合法
this.checkRemoveIndex(startIndex,endIndex);
E[] removeData=this.getRemove(startIndex,endIndex);
this.removeData(startIndex,endIndex);
this.size-=(endIndex-startIndex+1);
return removeData;
}
//真正做移除数据的方法,
private void removeData(int startIndex, int endIndex) {
int cacheLength=0;//缓存数据的数组长度
E[] cache =(E[]) new Object[elementData.length-endIndex];//临时存储
for (int i = endIndex+1; i < elementData.length ; i++) {
cache[cacheLength++]=elementData[i];
}
cacheLength=0;
for (int i = startIndex; i <cache.length+startIndex ; i++) {
elementData[i]=cache[cacheLength++];
}
for (int i = size-1; i < elementData.length; i++) {//这里别忘了将剩余的空位置为空。
elementData[i]=null;
}
}
private E[] getRemove(int startIndex, int endIndex) {
E[] removeData=(E[]) new Object[endIndex-startIndex+1];
int j=0;
for (int i=startIndex; i <endIndex+1 ; i++) {
removeData[j++]=elementData[i];
}
return removeData;
}
/**
* @author TQY 杨显伍
* @date 2022/10/8 15:11
* effect: 判断下标是否合法
* 这里除了要验证下标是否合法以外,还需要判断起始下标是否大于终止下标
*/
private void checkRemoveIndex(int startIndex, int endIndex) {
this.checkIndex(startIndex);
this.checkIndex(endIndex);
if (startIndex-endIndex>0){
throw new RuntimeException("StartIndex bigger for EndIndex");
}
}
/**
* @author TQY 杨显伍
* @date 2022/10/8 15:17
* @param index
* @param removeNumber
* @param isRemove
* @return E[]
* effect: 根据下标删除从下标开始的几个元素即下标可取区间为:[index,index+removeNumber-1];
* 思路,在之前我们写了一个删除指定下标之间的方法,具体的我们就可以去调用那个方法,只是需要注意的是,结束下标(endIndex)=起始下标(index)+移除的个数(removeNumber)-1
*/
public E[] remove(int index,int removeNumber,boolean isRemove){
if (isRemove==false){//考虑到在区间去删除的时候,我们的方法的参数已经存在了,为了不报错和给用户选择的权利,我们进行一下判断,以便于方法的重载
return null;
}
this.checkRemoveIndex(index,index+removeNumber-1);//判断下标是否合法,也就是起始下标,和我们要删除的数据够不够我们操作
E[] remove = this.getRemove(index, index + removeNumber - 1);//给用户返回要删除的数据
this.removeData(index,index+removeNumber-1);//真正的删除数据
this.size-=removeNumber;//这里别忘了需要改变元素的有效个数。
return remove;
}
}
注意:既然我们是要通用,那么就需要什么数据都能够进行添加,那么就需要用到泛型。
最后测试的结果。
这里做的是功能 8的测试结果。
哎,这个工具还是有点不好啊,如果就是因为在它的实质还是基于数组来实现的,除去容量不够的时候实际的扩容需要进行对已有的数组进行拷贝到新的数组以外,还有就是在指定位置插入和删除数据的时候需要进行移动数组,那么有没有什么办法在我进行这两个操作的时候提升一下性能呢?
下一节来写写一个能够解决刚刚说的问题一种数据集合。
基于数组实现的一个集合还有一个类,就是Vector,它的底层呢默认的长度还是十,但是扩容的时候是进行的两倍扩容,原因是:在这个源码里面的int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
还有一点与ArrayList的区别就是,Vector是线程安全的,因为它基本上在每个公开的方法上面都加了synchronized这个关键字
对于线程后面会说到