《面试与源码》LinkedList(和ArrayList对比)——两者的迭代器居然还存在不同?

那一天,我看着电脑里的一堆笔记陷入沉思,有一个声音在我脑海浮现
“Co Co Da Yo !”,“你走开!”
“想明白生命的意义吗?想真正的活着吗?”,“大哥你走错片场了吧?”
“要不把笔记整理一下,写点文章?” ,“试试?” ,“干就完事了!奥利给!”

引言

对于一个从小学到高中都在为作文凑字数的人来说,写文章真是一件困难的事情,现在还在一个字一个字往外扣,以后再也不催火星更新逆天邪神了,哈哈。

上一篇博文我们了解了ArrayList,这篇博文我们来了解一下LinkedList,细细体会它们之间的区别。

正文

“咦 你这个小伙子有点面熟 昨天那个ArrayList是你答的吧?”

“是的”

“不错不错,那今天一定要好好考考你(虐死你)”

“…”
在这里插入图片描述

简单聊聊LinkedList

LinkedList当初都是和ArrayList一起看的 easy~ easy~

  • LinkedList底层是双向链表,链表中的每一个node结点都存储了自己的前驱后驱。
  • LinkedList允许存入null,且有序(插入取出有序)。
  • LinkedList不是线程安全的。

源码分析

Node类,表示链表中的一个结点。

private static class Node<E> {
    E item;   //数据
    Node<E> next;  //后继
    Node<E> prev;  //前驱
      
    //构造方法
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

说一下linkedList插入一个数据的底层实现

这不是手到擒来~

linkedList包含 first 和 last 两个Node引用,分别用来指向链表的头结点和尾结点。
第一次add操作时,会将 first 和 last 都指向第一个结点,
随后每一次add操作都会将新结点的prev指向last引用的Node,将last引用的Node的next指向新结点,然后将last引用指向新结点。

举个例子:在一个LinkedList中依次插入值666,777,888
画的是真的丑
画的是真的丑
在这里插入图片描述

源码分析

add操作

	//头结点
	transient Node<E> first;
	//尾结点
	transient Node<E> last;
	
	public boolean add(E e) {
	    linkLast(e);  //在链表尾部插入
	    return true;
	}
	
	void linkLast(E e) {
	    final Node<E> l = last;  //last是链表的尾结点
	    final Node<E> newNode = new Node<>(l, e, null); //在尾结点后创建一个新结点
	    last = newNode; //新节点设置为尾节点
	    if (l == null) 
	        first = newNode; //first为链表的头结点,链表为空时,将插入的节点设置为头结点。
	    else
	        l.next = newNode; 
	    size++;  //链表的大小加1
	    modCount++;
	}
	
	public void add(int index, E element) {
	    checkPositionIndex(index); //检测下标是否越界
	
	    if (index == size)
	        linkLast(element);
	    else
	        linkBefore(element, node(index)); //在index下标节点前插入element结点
	}
	
	Node<E> node(int index) {   //遍历获取第index下标下的元素
	    // assert isElementIndex(index);
	    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;
	    }
	}

get操作时会判断当前索引的位置,如果在链表前半段则从前向后遍历,反之则从后向前遍历。

	public E get(int index) {
		//越界抛出异常
        checkElementIndex(index);
        return node(index).item;
    }

    // 返回指定索引下的元素的非空节点
    Node<E> node(int index) {
        // assert isElementIndex(index);
		/*	
			如果下标在前半段则从头部向后遍历
			如果下标在后半段则从尾部向前遍历
		*/
        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;
        }
    }

Tip :又出现了 modCount,之前在ArrayList简单提了一下,这里详细讲一下,这其实是集合的fail-fast 机制

fail-fast是Collection中的一种错误机制。

在用迭代器遍历一个集合对象时,如果遍历过程中对集合进行了增删(非迭代器自带的增删),会改变modCount,集合迭代器的 next( )会判断modCount是否发生改变,发生改变则会抛出Concurrent Modification Exception。

这种情况在多线程时也会发生,当一个线程在遍历时,另一个线程增删了集合,也会导致modCount发生改变,抛出Concurrent Modification Exception。

fail-fast防止在遍历集合时,集合内的数组发生改变。

大部分list,set在迭代时都会对modcount值进行判断, 但是CopyOnWriteArrayList、CopyOnWriteArraySet不会,原因后续会讲。

面试官脸上露出了熟悉的微笑 ~
在这里插入图片描述

如果我要往List里面存放数据,你怎么判断用ArrayList还是LinkedList

哼哼哼,我清了清嗓子,准备好之后的连环炮~

如果不能确定List的大小,在频繁插入的情况下建议使用LinkedList,因为ArrayList随着数组增大是需要对数组扩容的,而扩容就涉及到内存的分配,频繁的扩容会增大虚拟机的负荷(消耗时间和空间)。

如果顺序插入删除频繁,ArrayList理论上会快,因为ArrayList底层是数组,内存是提前分配的。而LinkedList每次都需要创建Node对象,所以理论上是慢于ArrayList的。

如果插入删除的在数组前半段,可以考虑使用LinkedList,如果插入删除的在数组后半段,可以考虑使用ArrayList,因为ArrayList会调用arraycopy方法,这个方法会拷贝插入删除位置后的数据,所以插入的位置越靠前,插入的需要拷贝的数据越多,而LinkedList只需要改变node结点前后的引用地址即可。

说说怎么遍历ArrayList和LinkedList

ArrayList建议使用普通的for循环遍历,LinkedList要使用foreach循环也就是使用迭代器遍历。

List<Integer> list = new ArrayList<>();
//普通for循环
for (int i=0; i<list .size(); i++){
    System.out.println(list .get(i));
}

//foreach循环,使用迭代器遍历
for (Integer i : list ){
    System.out.println(i);
}

ArrayList它实现了RandomAccess 标记接口,用于标明实现该接口的List支持快速随机访问。

LinkedList使用for循环每次调用get(i)都会从链表头部遍历链表到i结点处,效率低下。使用foreach循环,迭代器遍历时每一次调用next()方法都会保存当前结点的引用,每一次都是从当前结点向下遍历。

Tip: 标记接口的作用是当某个类实现了这个接口,我们就认为这个类拥有了某种功能,我们不需要实现任何的方法。常见的标记接口还有Cloneable(可调用clone()方法)、Serializable(可以实现序列化)。

这怎么难得倒我,我笑嘻嘻的看着面试官。
在这里插入图片描述

面试官(内心:被小瞧了!!不行!!),突然他灵光一闪~

说一下LinkedList的迭代器和ArrayList有什么不同

裂开! 这可真难到我了!还好我昨天和陈小志夜谈,他提到了这一点

ArrayList使用的是Iterator(ArrayList也有ListIterator)而 LinkedList 使用的是ListIterator。

  • ListIterator 在遍历的时候可以向 List 中添加和修改对象,而 Iterator 不能。
  • ListIterator 和 Iterator 都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。
  • ListIterator可以通过nextIndex()和previousIndex()定位当前索引的位置,Iterator没有此功能。

面试官(算了…只能下回找回场子了)

你之前说ArrayList和LinkedList不是线程安全的,那你说说怎么实现List的线程安全。

结束语

下一章会说说线程安全的List,希望大家能有所收获。

PS:上一篇博文一天的阅读量有三十,希望明天能上一百。
在这里插入图片描述


良师益友(推荐几个大佬)
@敖丶丙 @五月的仓颉 @占小狼

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值