手写集合框架ArrayList篇

ArrayList

Hello,头发多的我又来了,刚利用业余时间,实现了一个简易版的ArrayList,下面我来分享一下我的实践内容。由于水平有限,咱们尽量都用白fa(话)文

一、思路

首先,我的思路是这样的,因为ArrayListList接口下的,所以我在接口中定义了我要实现的抽象方法,以便接下来将这些方法给ArrayList重写,而且,因为到时候LinkedList也会要用到这些公用的方法名,所以写了List接口以供实现。

二、定义主要方法

    /**
 * 提供的方法 让arraylist继承 , 并重写这些方法 
 * @author Administrator
 *
 */
public interface newList{   
	public void add();  //集合的添加方法 
	public int size();  //获取集合的长度  
	public Object get(Integer i );  //获取下标的为i的元素  
	public void remove(Object object);  //移除object对象  
}

几个主要的方法,add,remove,size,以及get。这里就没有去实现指定位置添加的方法,相信这几个弄懂了,其他的不在话下,稍加看看源码即可
集合接口有了,我们照着思路往下走,让ArrayList去实现它,并重写它所有的方法就OK了。
基本的结构就这样了,废话不多说,直接开始我们ArrayList的实现

三、定义全局属性

        //一个object类型的数组  
	private Object[] array;  
	private int size ; //当前集合的长度 
	private static final int DEFAULT_SIZE = 10 ; //数组的默认长度  

通过源码,我们了解到,ArrayList是通过数组实现的,而且是一个Object类型的,为什么是Object,也是为了符合不同数据类型的数组存储,毕竟Object是类型老大
定义完数组,是不是发现它并没有默认长度,所以我们在下面定义了一个默认的常量DEFAULT_SIZE=10,这个就是它默认的长度,这个时候可能会有一个疑惑,就是这个size是干莫子的,你是否联想到我们的size()方法【获取数组ArrayList的长度】,所以这里的size全局属性就是用来控制整个ArrayList集合的内容长度的,并非是我们的数组长度,也就是size!=数组长度,它等于的是ArrayList这个集合的长度。

四、定义构造函数

方法有了,接下来就是也是比较重要的一步骤,就是ArrayList的构造函数

        //定义一个构造器  用来实例该类  
	public newArrayList() {
		array =  new Object[DEFAULT_SIZE];  //初始化一个数组  
		size = 0 ; 
	}
	
	public newArrayList(int resetsize){
		if(resetsize>0) {  //指定长度的初始数组
			array = new Object[resetsize];
		}else if(resetsize == 0) { //如果等于0 使用默认的长度DEFAULT_SIZE
			array =  new Object[DEFAULT_SIZE]; 
		}else {  //不合法的长度 比如-1
			throw new IllegalArgumentException("指定了错误的长度:"+resetsize);
		}
	}

为什么要有多种的不同的构造函数呢?很-单嘛,根据你平常的使用的习惯,你可能不想给集合长度,或者给它一个长度,这不就涉及到了两个不同的构造函数嘛,所以我们也定义两个,无参构造为了满足不给长度的情况下,不给指定长度我们就使用我们之前定义的DEFAULT_SIZE这个常量,如果指令了默认长度我们就使用第二个带int参数的构造函数,并且在里面判断,判断这个整型数字是否满足我们的条件。

五、实现主要的方法

1. add()方法 集合添加
    //  进行 判断 如果 数组溢出都 扩容 
	@Override
	public void add(Object object) { 
		if(size == array.length) { //达到扩容的条件  
			this.ensureCapacity();
		}
		array[size] = object;    //赋值 并累计size 
		size ++ ;
	}
	//扩容的方法  
		private void ensureCapacity() {
			//创建一个新的数组 是原来的2倍  
			Object[] newarray = new Object[array.length*2]; 
			System.arraycopy(array, 0, newarray,0,array.length);
			array = newarray;   //重新赋值 
		}

我们数组,它固然是由长度限制的,所以我们应该在哪里进行扩容呢?放add()这个方法里面是最合理的也是最恰当的,因为只有我们数据添加的时候,集合的长度才会越来越长嘛,所以我们在add()方法这里加了一个等于的判断,当它符合扩容条件,也就是已经和数组长度相等时,就对这个数组扩容,扩容这里我们单独列出了一个方法,写了一个新的数组newarray,它的长度是原来数组长度的二倍,当然源码当中不是这么定义的,我们是为了方便理解,然后将这个数组新的数组复制到我们原来的数组当中即可。这里需要普及一个System的方法System.arrarcopy(参数1,参数2,参数3,参数4,参数5);
他们分别代表的含义如下:
参数1:原数组,
参数2:原数组的起始位置,
参数3:目标数组,
参数4:目标数组的起始位置,
参数5:需要复制的数组长度
每调用一次add()方法,我们的size也就自增一次,代表集合的长度正在发生变化

2. get()方法 获取集合指定下标的元素
        //获取指定下标的元素 
	@Override
	public Object get(Integer i) {
		 if (i<0 ||i>size-1) {  //数组溢出异常
	            throw new IndexOutOfBoundsException();
	     }
		 return array[i];
	}

我们知道数组是自带索引的,从0开始,所以我们获取的时候只需要判断一下给定的长度i是否符合条件,不符合条件的情况就抛出数组溢出的异常,否则就返回这个数组中这个下标的的数组内容出去。

3. remove()方法 获取集合指定下标的元素
        //删除集合指定的元素
	@Override
	public void remove(Object object) {
		for (int i = 0; i < array.length; i++) {
			if(object == array[i]) {  //如果内容相等  1.元素上移  2. 删除原元素
				// 拿到需要移动的个数  
				int index = size - i - 1 ;    
				System.arraycopy(array,i+1 , array,i,index);  //前移
				array[--size] = null;  //最后一个空值 也就登录删除了一个下标
				/* 假设集合总长度size = 8,然后我删除第5个,也就是下标 i = 4这个元素  
				这个时候要右边要移动的元素个数也就是index = 8 - 4 - 1 
				;也就是有3个元素要移动  
			        然后再熟练使用我们刚刚的System.arraycopy这个方法,对这个数组进行操作
				,我们可以知道我们的数组起始位置分别是i+1以及i,也就是从我们当前这个/
				下标为4+1这个位置开始,分别向前移动一个位置,将第五个元素的内容移动/
				到第四个,第六个移动到第五个的位置, 
				依次类推,最后达到补齐我们要删除的这个空缺。
				最后我们移动完之后,最后一个元素肯定会是空缺的,所以我们将它赋空值,并
				且size-1(集合长度变化)  这个时候才算完成。   
				*/
			}
		}
	}

这里我们只做了,根据内容进行删除,传一个内容进来,然后循环整个数组去判断,如果找到了这个内容我们就要把这个内容去除掉,这里可能会有一丢丢疑惑,因为我也看了好一会,这里要做的一个操作就是,我们要对这个数组进行移动,解释已经在注释当中标识。还请客观仔细品,为了方便理解,附上一张方向图。

4. size()方法 获取集合的长度
//获取数组的长度
	@Override
	public int size() {
		return size; 
	}

这个方法应该比较容易理解了把,不过还要强调的一点是 size != 数组长度,它是集合的长度,O不OK?
基本方法就这么结束了。


六、迭代器部分

等等,不要急不要躁。还记得我们上次我们提的迭代器嘛?OK,下面我们就在上面我们的基础上加一个迭代器。还记得迭代器是干嘛的嘛,迭代器可以用来循环我们这个集合,并且在循环的过程中可以对当前循环对象进行删除操作,有时候面试官就问你,怎么一边循环稽核一边删除,迭代器迭代器迭代器!
迭代器首先也是要一个接口

    /**
 * 迭代器的接口
 * @author Administrator
 *
 */
public interface newIterator {
	boolean hasNext(); //判断是否有下一个元素的方法 
	Object Next();  //获取元素  
	void remove();  //移除元素 和arraylist的指定移除remove(Object object)几乎一样 
	
}

接口定义好勒,我们继续在List这个接口下写上我们的获取迭代器的方法,让ArrayList重写

    public newIterator getIterator();

ArrayList当中需要这样写

        //返回新的迭代器 
		public newIterator getIterator() {
			return  new newArrayListIterator();
		}

沃特???疑问来了,返回的这个 newArrayListIterator在哪???别急嘛,继续看咯

        //实现迭代器接口 
		private class newArrayListIterator implements newIterator{

			private int index =0;   //用来判断是否有下一个
			private int lastindex =-1; //用来记住上一次的位置,可以理解为当前循环的数组下标
			
			@Override
			public boolean hasNext() {
				return index!=size; 
			}
 
			@Override
			public Object Next() {
				lastindex = index;
				return array[index++];
			}

			@Override
			public void remove() {
				 
				if(lastindex < 0) {   //如果没有元素  就抛出异常 
					throw new IllegalSelectorException();
				}
				
				int moveindex = size - lastindex -1 ;  
				System.arraycopy(array,lastindex+1, array, lastindex, moveindex);
				array[--size]  = null;  
				index --;  
				lastindex = -1 ;
			}
			
		}

这个私有的内部类同样在ArrayList当中去实现,index用来作比较,lastindex是我们上一次的位置,如果你不太理解,建议你先学会如何使用迭代器在看这部分。
其中:

1.hasNext() 判断条件

hasNext()这个方法,是在while循环中,用来判断true||false的条件,只有满足index!=size,也就是,当前循环对象不是最后一个,这个循环才能继续下去。

2.Next() 获取当前循环的元素内容

Next()方法,就是用来获取当前循环对象的内容的,返回一个index++下标的内容,这里不要将index比如是6,index++不要理解为这里是返回下标为7的内容,区分index++++index的区别,所以如果这里index = 6 ,他返回的也还是下标为6的内容

3.remove() 删除当前循环对象

这里不作太多重复的阐述,因为这里和我们之前的remove(Object object)原理类似。


七、泛型部分

最后在做最后一步的优化,给List集合加上泛型

public interface newList<T> {    //定义泛型,因为Object可以存储任意类型,有时候我们需要
   		                 //用泛型 代替Object 
   	public void add(T t);  //集合的添加方法 

同理,remove修改一下参数 变为泛型代表--T

使用泛型有一个好处,就是有些时候我们的在转换的过程中不需要强制转换
举个例子:
ArrayList中两个这样的方法

@Override
		public Object get2(Object object) {
			// TODO Auto-generated method stub
			return object;
		}

		@Override
		public <T> T get3(T t) { 
			// TODO Auto-generated method stub
			return t; 
		}

一个是返回Object,一个返回泛型
结果如下:

newList<Object> li2 = new newArrayList<Object>();
		Object ob = new Object();
		Object o = (newList<Object>) li2.get2(ob);
		Object o2 = li2.get3(ob);

你就会发现,使用Object是不是要强转,而使用泛型因为我最开始就指定了集合 T = Object 所以不再需要强转啦 奥利给!!!


终于要写完了,最后我们在做个小小的总结,以及解答一些面试中小小的疑惑。
到此为止我们要知道的是:
1.ArrayList是List接口下一个有序的集合
2.ArrayList通常用于查询功能,

答:我们查询出来的数据之后,调用add方法,add方法总是追加到最后一个元素后面,这里几乎没有什么多余的线性时间,当然你可能会说数组扩容消耗了这么大的性能,但你要知道ArrayList最突出的是获取的时候,直接用get()方法通过索引去获取元素,这样速度是非常快的,后续我们可以拿它个LinkedList作一个对比。
3.另外像一些集合克隆,就自己去琢磨把,思路(直接给ArrayList一个克隆方法返回整个集合或者加一个构造函数返回这个集合)


遇见您,是我们之间的缘分,三生有幸! \

  • WeChat:lljb1218
posted @ 2019-12-12 23:54  林果头发多  阅读(306)  评论(0编辑  收藏  举报