JAVA类集源码分析和实现——List

19 篇文章 0 订阅
3 篇文章 1 订阅

越努力越幸运,所有好的人和事我相信都会发现在你身上,在成长的过程中,我会保持积极的人生态度和一颗满怀感恩的心,感谢默默指导和在背后支持我的人,让自己没有理由不对自己负责,不负众望,努力了明天就会更好,加油 !

在这里插入图片描述

ArrayList

  • ArrayList底层是数组实现,存取有序,有索引,能存储重复元素,是线程不安全的

  • 源码分析

  • 1、成员常量DEFAULT_CAPACITY初始值为10,代表集合中最小Object[]大小为10

  • 2、还分别定义了成员Object[] EMPTY_ELEMENTDATA数组初始为空;Object[] elementData未被实例化,在构造中会被实例化,这个临时对象数组才是真正存储元素的;

  • 3、还定义了一个成员变量size用于记录当前数组大小以及存储的索引,每添加一个元素就会+1,初始值为0.数组都是从0开始的;
    在这里插入图片描述

  • 有参构造和无参构造

public ArrayList() {
	super();
	//在这里实例化了一个空的数组
	this.elementData = EMPTY_ELEMENTDATA;
}
//有参构造,参数initialCapacty指定数组大小;
public ArrayList(int initialCapacty) {
	super();
	if(initialCapacty < 0) {
			//如果给的是一个负数,就给出异常
			throw new IllegalArgumentException("Illegal Capacty : " +  initialCapacty);
	}
	//正确参数,就直接创建指定大小的数组,通过指定大小数组可以减少扩容的次数;
	this.elementData = new Object[initialCapacty];
}
  • add()方法的实现
public boolean add(E e) {
	// 该方法用于对数组大小进行操作,确保数组大小够用,这里不会改变size的值;
	ensureCapacityInternal(size + 1);      //当前数组大小 + 1;就是用来判断下一个元素能不能存进去够不够用
	//添加的数据存储到数组中;
	elementData[size++] = e;    
	//这也是可以存储重复元素的象征,不管什么情况一直返回true;
	return true;
}
private void ensureCapaityInternal(int minCapacity) {
	//当通过空参构造实例化elementData时这里就返回true
	if(elementData == EMPTY_ELEMENTDATA) {
		//这里就可以看出返回的肯定是10;所以在没有指定大小的时候集合初始大小为10;
		minCapacity = Math.max(minCapacity, DEFAULT_CAPACITY);
	}
	//继续确保扩容
	ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
	modCount++;    //暂时先不管
	
	//也就是说当传进来的参数大于存储元素数组的大小就调用grow()方法对其进行扩容
	if(minCapacity - elementData.length > 0) {
		//	扩容的实现方法
		grow(minCapacity);
	}
}
private void grow(int minCapacity) {
	//这里是先保存原来数组的大小
	int oldCapacity = elementData.length;   
	//创建新的大小,是原来大小的1.5倍
	int newCapacity = oldCapacity + (oldCapacity>>1);
	//如果创建的新大小还不够,那么就直接把传进来的大小给新大小
	if(newCapacity - minCapacity < 0) {
		newCapacity = minCapacity; 
	}
	//当创建的新大小如果大于数组最大长度,就定义一个更大的
	//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8:
	//Interget.MAX_VALUE = 0x7fffffff = 2147483647
	if(newCapacity - MAX_ARRAY_SIZE > 0) {
			//	其实再大也只能大8个
			newCapacity = hugeCapacity(minCapacity);
	}
	//通过数组工具类copyOf()方法创建一个新的大小的数组,并把原数组数据拷贝给新数组,原来数组将变成垃圾;这也是要减少扩容的原因;最好先指定大小;
	elementData = Arrays.copyOf(elementData, newCapacity);
}

总结:add()方法每添加一个元素前都会对其进行判断和操作,如果当前数组大小,小于元素大小;就会创建一个比原数组大1.5倍的新数组,并拷贝原数组达到扩容;原数组会变成垃圾

  • get(int index)方法的实现
private void get(int index) {
	//该方法用于判断传进来的索引是否大于数组索引的最大值
	randgeCheck(index);
	//该方法用于通过索引返回其索引所指的值
	return elementData(index);
}
private void randgeCheck(int index) {
	//如果大于最大索引,给出异常
	if(index > size) {
		thorw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	}
}
//通过泛型技术
E elementData(int index) {
	//	将数据向下转型返回
	return (E) elementData[index];
}

总结:数组是有索引,存储是有序的,所以get()就是直接通过索引进行取得对应的值,在取前进行了简单的判断;

  • set(int index, E element)方法的实现:通过索引修改其值
private E set(int index, E element) {
	//判断索引是否越界
	rangeCheck(index);
	
	//获取要修改的值
	E oldValue = elementData(index);
	//直接修改值
	elementData[index] = element;
	//返回修改前的值
	return oldValue;
}

总结:数组索引的体现,通过索引直接赋值修改;

  • size()方法的实现
public int size() {
	//size是一个成员变量,每添加一个元素就会自增+1;所以直接返回size就可以了;就是数组元素实际的大小
	return size;
}
  • 通过索引进行删除->remove(int index)方法的实现
private E remove(int index) {
	//跟get()方法一样先判断传进来的是否越界
	rangeCheck(index);
	//通过索引获取将要删除的值;
	E oldValue = elementData(index);
	//通过数组元素大小减去索引值再减去1;获取要删除的值后面有多少个元素;
	int numMoved = size - index - 1;
	//做了一个判断要移动的元素个数肯定要大于0
	if(numMoced > 0) {
		//将指定源数组中的指定位置复制到目标数组的指定位置,最后一个参数是要复制的个数
		System.arraycopy(elementData, index + 1, elementData, index, numMoved);
	}
	//
	elementData[--size] = null;   //将数据大小size-1;并将原数据最后一个元素赋值为null
	//最后返回删除的元素
	return oldValue;
}

总结:就是计算出System.arraycopy()方法中需要的参数;然后进行复制向前移,最后将最后一个元素进行清空;size-1,还通过elementData()方法通过索引获取了要删除的值,并返回;

  • 通过对象内容删除–>remove(Object o)
public boolean remove(Object o) {
	//null不能调用equals方法,所以使用if-else解决
	if(o == null) {
		//遍历数组
		for(int index = 0; index < size; index++) {
			//判断该值是否存在
			if(elementData[index] == null) {
				//存在就通过索引调用其方法进行删除
				fastRemove(index);
				//返回true表示删除成功
				return true;
			}
		}
	}else {
		//同样遍历数组
		for(int index; index < size; index++) {
			//通过equals()方法判断是否存在
			if(elementData[index].equals(o)) {
				//存在就通过索引调用方法删除
				fastRemove(index);
				//返回true表示删除成功;
			}
		}
	}
	//执行到这步说明上面并没有进去,就是没有该元素,返回false;表示没有删除元素和没有找到元素
}
//	删除元素方法以及移动的实现
private void fastRemove(int index) {
	modCount++;
	//获取要删除的元素后面有多少个元素
	int numMoved = size - index - 1;
	//进行判断是否大于0
	if(numMoved > 0) {
		//将指定源数组的指定位置复制到指定目标数组的指定位置,最后 一个参数为复制多少个;
		System.arraycopy(elementData, index + 1, elementData, inedx, numMoved);	
	}
	//将size-1,并把源数组最后一个元素赋为null;
	elementData[--size] = null;
}

总结:删除的原理和通过索引删除方法一样,但是此方法是通过遍历找到相等的值,再获取其值对应的索引值,再调用通过索引删除方法一样的原理的方法进行删除;

  • clear()方法的实现
public void clear() {
	mouCount++;
	//遍历将所有元素赋为空
	for(int index = 0; index < size; index++) {
		elementData[index] = null;
	}
	//最后将size赋为0,达到清空效果
	size = 0;
}

总结:比较简单的清空

LinkedList

  • 由于LinkedList间接的实现了Queue接口,所以我在说原理的时候直接一同实现Queue中的方法;
  • LinkedList底层是双向链表是实现,存取有序,有索引,可以存储重复的;是线程不安全的;
  • 原码分析
  • 成员属性
    在这里插入图片描述
  • 节点的定义,静态内部类
private static class Node<E> {
	//定义了一个成员变量用于存储元素值;
	E item;
	//定义了一个节点变量用于存储当前节点的上一个节点
	Node<E> prev;
	//定义了一个节点变量用于存储当前节点的下一个节点
	Node<E> next;
	
	//定义构造方法,并对属性进行赋值操作
	Node(Node<E> prev, E element, Node<E> next) {
		//将添加的元素进行赋值
		this.item = element;	
		//传头节点
		this.prev = prev;
		//传尾节点
		this.next = next;
	}
}
  • 直接添加元素–>add(E e),实现Queue中的方法
public boolean add(E e) {
	//该方法是具体的添加操作,从命名可以看出add()方法默认是在尾节点添加元素
	linkLast(e);
	//不管什么情况都是返回true,所以可以存储重复元素
	return true;
}
private void linkLast(E e) {
	//定义一个临时节点常量存储尾节点
	final Node<E> l = last;
	//创建一个新节点,将尾节点赋给新节点的上一个节点,下一个节点为null;
	final Node<E> newNode = new Node<>(l, e, null);
	//将新节点赋值为尾节点
	last = newNode;
	//判断原尾节点是否为空,为空的话肯定是链表;
	if(l == null) {
		//就将新创建的节点做为头节点
		first = newNode;
	} else {
		//新创建的节点赋为原尾节点的下一个节点
		l.next = newNode();
	}
	//记录元素个数,每调用一次+1;
	size++;
	//
	modCount++;
}

总结:add()方法默认从尾进行添加,每添加一次就会新new一个节点, 然后将节点的引用赋给上一个节点的下一个节点;形成一个链表结构;

  • 指定位置添加元素–>add(int index, E element)
public void add(int index, E element) {
	//该方法用于判断传进来的索引是否越界
	checkPositionIndex(index);
	
	//如果刚好在最后一个索引位置进行插入
	if(index == size) {
		//就直接调用从尾添加该元素
		linkLast(element);
	}else {
		//否则就调用该方法进行实现插入,node()方法用获取索引所对应节点
		linkBefore(element, node(index));
	}
}
private void checkPositionIndex(int index) {
	//如果该索引不在链表长度0-size范围内就给出异常
	if(!isPositionIndex(index)) {
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	}
}
private boolean isPositionIndex(int index) {
	//传进来的索引是否在范围内
	return index >= 0 && index <= size;
}
//该方法用于找到指定索引的节点
Node<E> node(int index) {
	//为了提高效率,这也是双向链表的特点,先判断索引靠头一点还是尾一点
	//index是否小于size的一半
	if(index < (size >> 1)) {
		//	靠头肯定从头遍历,将头节点给临时变量
		Node<E> x = first; 
		for(int i = 0; i < index; i++) {
			x = x.next;
		}
		//找到索引,并返回;
		return x;
	}else {
		//	到这里肯定是靠尾一点,将尾节点赋给临时变量
		Node<E> x = last;
		//从尾遍历
		for(int i = size - 1; i > index; i--) {
			x = x.prev;
		}
		 //找到索引,并返回;
		return x;
	}
}
void linkBefore(E e, Node<E> succ) {
	//记住succ要插入节点的上一个节点
	final Node<E> pred = succ.prev;
	//创建新节点,将上一个节点赋为pred,下一个节点prev,连接起来
	final Node<E> newNode = new Node<>();
	//将原节点的上一个节点设为新节点
	succ.prev = newNode;
	//如果在0的索引添加或是一个空链表,那么肯定是null
	if(pred == null) {
		//	创建的节点设为头节点
		first = newNode;
	} else {
		//不是的话,就将新的节点设为pred的下一个节点
		pred.next = newNode;
	}
	//然后size+1;
	size++;
	modCount++;
}
  • addLast(E e)在尾添加
public void addLast(E e) {
	//直接调用写好的方法就可以了
	linkLast(e);
}
  • addFirst(E e)在头添加
public void addFirst(E e) {
	linkFirst(e);
}
private void linkFirst(E e) {
	//先用一个临时节点存储头节点
	final Node<E> f = first;
	//再创建一个新节点,做为头节点,连接原头节点
	final Node<E> newNode = new Node<>(null, e, f);
	//改变头节点
	first = newNode;
	//如果头节点是空,那肯定是空链表
	if(f == null) {
		//新节点做为头节点
		first = newNode;
	}else {
		//原头节点的上一个节点连新节点
		f.prev = newNode;
	}
	//size+1
	size++;
	modCount++;
}

总结:先获取头节点,再创建一个新节点,然后将头节点的上一个节点设置成新节点,再将新节点设置成头节点;

  • get(int index)
public E get(int index) {
	//先判断索引是否有效
	checkElementIndex(index);
	//通过node(index)获取索引对应的节点,再返回其值
	return node(index).item;
}

总结:把些重复方法实现原理实现了,其它方法通用

  • getFirst()获取头节点的值
public E getFirst() {
	//临时节点得到头节点
	final Node<E> f = first;
	if(f == null) {
		//空链表给出异常
		thorw new NoSuchElementException();
	}
	//返回值
	return f.item;
}

总结:getLast()方法同理,获取头节点,然后通过头节点直接进行返回

  • 删除节点–>remove(),实现Queue中的方法
public E remove() {
	//通过removeFirst()方法进行删除并返回删除的值
	//从方法命名看是从头开始删除的
	return removeFirst();
}
private E removeFirst() {
	//先获取头节点
	final Node<E> f = first.next;
	if(f == null) {
		//	如果链表为空链表给出异常
		throw new NoSuchElementException();	
	}
	//具体实现
	return unlinkFirst(f);
}
private E unlinkFirst(Node<> f) {
	//将要删除的头节点的值赋给临时常量
	final E element = f.item;
	//将要删除的头节点的下一个节点记住
	final Node<> next = f.next;
	//将要删除的节点的属性赋为空
	f.item = null;
	f.next = null;
	//然后将删除的头节点的下一个节点设置为头节点
	first = next;
	//也就是删除的是最后一个元素也就是尾节点
	if(next == null) {
		//	就将尾节点进行清空
		last = null;
	}else {
		//进入代表不止一个节点,就将新的头节点的上一个节点设为空
		next.prev = null;
	}
	//size -1 
	size--;
	modCount++;
	//返回删除的值
	return element;
}

总结:该方法默认是从头节点开始删除,每调用一次就删除一次,原理是将删除的头节点赋为空,删除的头节点的下一个节点设置为新的头节点

  • 通过索引进行删除–>remove(int index)
public E remove(int index) {
	//先对索引进行判断,是否在范围内
	checkElementIndex(index);
	//具体实现方法,通过索引找到要删除的节点
	return unlink(node(index));
}
private E unlink(Node<E> x) {
	//将要删除的节点的值取出来用于返回
	final E element = x.item;
	//保存要删除的节点的上一个节点
	final Node<E> prev = x.prev;
	//保存要删除的节点的下一个节点
	final Node<E> next = x.next;

	//判断上一个是否为空
	if(prev == null) {
		//说明删除的是表头,直接将下一个节点设置为表头
		first  = next;
	}else {
		//将要删除的节点的上一个节点的下一个节点连接到要删除的节点的下一个节点
		prev.next = next;
		//将要删除的节点的上一个节点赋为空
		x.prev = null;	
	}

	//判断下一个是否为空
	if(next == null) {
		//如果是空删除的就肯定是尾节点,然后将删除节点的上一个节点直接设成尾节点
		last = prev;	
	}else {
		//删除的节点的下一个节点的上一个节点设置成删除节点的上一个节点
		next.prev = prev;
	}
	//将值赋为空
	x.item = null;
	//将size - 1;
	size--;
	modCount++;
	//返回删除节点的值
	return element;
}

总结:索引肯定要判断的,然后先通过索引获取其要删除的节点,然后获取要删除节点上一个节点和下一个节点;然后将删除上一个节点的下一个节点改为删除节点的下一个节点;将删除下一个节点的上一个节点设为删除节点的上一个节点;就大功告成了;

  • 通过元素进行删除–>remove(Object o)
public boolean remove(Object o) {
	//判断传进来的元素是否为空,为空的话不能调用equals()方法
	if(o == null) {
		//从头开始遍历链表
		for(Node<E> x = first; x != null; x = x.next) {
			//进行判断
			if(x.item == null) {
				//然后通过该方法实现删除原理和通过索引删除一样都传的是节点
				unlink(x);
				//返回true
				return true
			}
		}
	}else {
		//从头开始遍历链表
		for(Node<E> x; x != null; x = x.next()) {
			//进行判断
			if(x.item.equals(o)) {
				//然后通过该方法实现删除原理和通过索引删除一样都传的是节点
				unllink(x);
				//返回true,删除成功
				return true;
			}
		}
	}
	//没有找到
	return false;
}
  • 从尾节点进行删除–>removeLast()
public E removeLast() {
	//先得到尾节点
	final Node<E> l = last;
	//判断该尾节点是否为空
	if(l == null) {
		throw new NoSuchElementException();
	}
	//具体的实现方法
	return nulinkLast(l);
}
private E nuLinkLast(Node<E> l) {
	//先得到要删除的值
	final E element = l.item;
	//得到要删除的节点上一个节点
	final Node<E> prev = l.prev; 
	//将尾节点打空
	l.tem = null;
	i.prev = null
	//将删除的节点的上一个节点赋值成尾节点
	last = prev;
	//如果删除上一个节点是空值,那么代表当前删除的是头节点
	if(prev == null) {
		//所以将头节点至空
		first = null;
	}else {
		//	如果不是,就将要删除的节点上一个节点的下一个节点赋为空
		prev.next = null;
	}
	//size--
	size--;
	modCunt++;
	return element;
}

总结:先获取尾节点,再将尾节点的上一个节点进行保存,然后将尾节点的属性赋为空,保存的那个节点设置成尾节点,并且该节点的下一个节点设null;

  • 通过索引进行修改–>set(int index, E element)
public E set(int index, E element) {
	//先判断索引是否越界
	checkElementIndex(index);
	//通过node()方法获取索引所在的节点
	Node<E> x = node(index);
	//在修改前先获取要修改的值,用于返回
	E oldVal = x.item;
	//再修改其值
	x.item = element;
	//返回修改前的值
	return oldVal;
}

总结:通过索引获取节点,然后直接通过节点进行修改;

  • LinkedList的实现Queue中的方法offer(E e)
public boolean offer(E e) {
	//其实就是调用了add(E e),默认在尾添加
	return add(e);
}
  • LinkedList的实现Queue中的方法poll()
public E poll() {
	//先获取头节点
	Node<E> f = first;
	//判断头节点是否为空,为空返回null,不为空将头节点进行删除,将删除的元素进行返回
	return f == null ? null : unlinkFirst(f);
}

LinkedList的实现Queue中的方法peek()

public E peek() {
	//先获取头节点
	final Node<E> f = first;
	//为空就返回null,不为空就返回当前元素
	return (f == null) ? null : f.item;
}
  • LinkedList实现Oueue中的方法element()方法
public E element() {
	//获取头节点;
	return getFirst();
}

自己实现ArrayList

在这里插入图片描述

自己实现LinkedList

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值