首页 文章 精选 留言 我的

精选列表

搜索[Jdk],共7534篇文章
优秀的个人博客,低调大师

Java源码阅读之ArrayList - JDK1.8

阅读优秀的源码是提升编程技巧的重要手段之一。 如有不对的地方,欢迎指正~ 转载请注明出处https://blog.lzoro.com。 前言 当你对某件事情很感兴趣的时候,时间的流逝在感知中都模糊了(是不是很文艺,绕口得都快听不懂了),通俗来说,就是时间过得很快。 而且,只有感兴趣才能驱动你继续下去,不然读源码,写解析博客这么高(Ku)大(Zao)上的事,是很难坚持的。 详细地写一篇源码解析博客少则半天一天,比如本篇,多则几天,比如红黑树在Java - HashMap中的应用,又要画图又要注释,还要排版,时不时要加点表情,开个车什么的,你说要是没兴趣,怎么坚持呢,还不如吃个鸡实在(啊,暴露了我是吃鸡选手)。 image 闲话少说,打开你的IDE,挽起袖子,开撸代码,加上注释,总计1461行代码。 基本介绍 常量 相比HashMap来说,ArrayList的常量算是短小精悍了,只有几个。 其中包含一个默认容量和两个空数组等,如下。 /** * 默认初始化容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 空数组共享实例 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 缺省大小的空数组共享实例 * 与 EMPTY_ELEMENTDATA 区分开来,以便知道第一个元素添加时如何扩容 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 最大可分配大小 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 成员变量 成员变量也是简单到令人发指,一个负责实际存储的缓冲数组和一个表示大小的变量。 /** * 实际负责存储的缓冲数组 * ArrayList的容量就是缓冲数组的长度 * * 空的ArrayList(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)在第一个元素添加时将会以默认容量扩容 */ transient Object[] elementData; // 非私有,以简化嵌套类的访问 /** * 大小 */ private int size; 构造函数 三个构造函数,分别是利用默认初始容量/给定初始容量/给定特定集合来构造ArrayList。 /** * 根据给定初始容量构造一个空的list * * @param initialCapacity list的初始容量 * @throws IllegalArgumentException 当给定的初始容量非负时抛异常 */ public ArrayList(int initialCapacity) { //判断给定初始化容量是否合法 if (initialCapacity > 0) { //创建数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** * 按默认初始容量(10)构造一个空的list */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 根据给定集合构造一个list,将按集合迭代的顺序存储 * * @param c 集合 * @throws NullPointerException 集合为null时抛异常 */ public ArrayList(Collection<? extends E> c) { //集合转数组后赋值给缓冲数组 elementData = c.toArray(); //判断大小 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) //c.toArray方法可能不会返回Object[]形式的数组 //下面做一层判断 if (elementData.getClass() != Object[].class) //做拷贝操作 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { //如果是空集合,则替换成共享空数组实例 // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 功能 看完了基本介绍,应该会觉得Just so so。 接下来就要逐一介绍各个功能的具体实现了。 ArrayList中,我们常用的功能有add/remove/get等。 无外乎增删改查,下面一一介绍~ add 在ArrayList中,添加操作还分为几种 从尾部添加元素 指定位置添加元素 从尾部添加集合 从指定位置添加集合 /** * 从尾部添加指定元素 * * @param e 元素 * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { //确保内部容量,有一系统调用链但不复杂,下面分析 ensureCapacityInternal(size + 1); // Increments modCount!! //存储元素 elementData[size++] = e; return true; } /** * 在指定位置插入元素 * 移动当前位置的元素 (如果存在) 和后继元素到右边 * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { //判断边界,可能会产生数组越界 rangeCheckForAdd(index); //确保内部容量,同上 ensureCapacityInternal(size + 1); // Increments modCount!! //调用效率较高的System.arraycopy进行数组复制 //目的是为了空出指定位置 System.arraycopy(elementData, index, elementData, index + 1, size - index); //指定位置上放入指定元素 elementData[index] = element; //容量+1 size++; } 在添加的元素的时候,有一个关键方法ensureCapacityInternal是来确保内部缓存数组的容量,当容量不够时进行扩容,下面具体看下这个方法的调用链 /** * 私有方法 */ private void ensureCapacityInternal(int minCapacity) { //判断是否是默认空实例,如果是的话,计算扩容容量 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } //调用ensureExplicitCapacity ensureExplicitCapacity(minCapacity); } ... private void ensureExplicitCapacity(int minCapacity) { //操作计算+1 modCount++; // overflow-conscious code //只有当容量不够时才扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * 缓冲数组扩容以确保能够存储给定元素 * * @param minCapacity 期望的最小容量 */ private void grow(int minCapacity) { // overflow-conscious code //现有元素长度 int oldCapacity = elementData.length; //新容量为 旧容量 + 旧容量的一半 //如 10 + 5 = 15 int newCapacity = oldCapacity + (oldCapacity >> 1); //如果计算的新容量比期望的最小容量小,则采用期望的最小容量作为扩容参数 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //判断是否超过最大数组容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: //最小扩容容量通常是接近size的,所以这是一场胜利 //这么臭美的吗 elementData = Arrays.copyOf(elementData, newCapacity); } /** * 取得最大容量 */ private static int hugeCapacity(int minCapacity) { //溢出 if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //取最大容量 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } set 这里的set其实可以理解为修改,将指定位置的元素替换为新元素。 /** * 修改指定位置的元素 * * @param index index of the element to replace * @param element element to be stored at the specified position * @return the element previously at the specified position * @throws IndexOutOfBoundsException {@inheritDoc} */ public E set(int index, E element) { //范围检查 rangeCheck(index); //取出旧值用以返回 E oldValue = elementData(index); //放入新的值 elementData[index] = element; return oldValue; } remove 数组的移除和添加一样,也分为几种方式 根据下标移除 根据对象移除 根据集合移除 根据过滤器移除 根据范围移除(受保护的方法) /** * 删除指定位置的元素,后继元素左移(前移) * * @param index 下标 * @return the 被移除的元素 * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { //下标检查 rangeCheck(index); //操作计数 + 1 modCount++; //获取指定位置的元素(用以返回) E oldValue = elementData(index); int numMoved = size - index - 1; //用system.arraycopy的方式移动元素 //相当于是index位置后的元素前移 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); //最后一个数组元素引用置为null,方便GC elementData[--size] = null; // clear to let GC do its work //返回 return oldValue; } /** * 当元素存在的时候,删除第一个找到的指定元素 * * 如果元素不存在,则list不会变动 * * @param o element to be removed from this list, if present * @return <tt>true</tt> if this list contained the specified element */ public boolean remove(Object o) { //o是否是null元素(数组允许存储null) if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { //调用内部的fastRemove方法,后面分析 fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) //这里跟上面不一样的是,是用equals来比较,而不是比较地址 if (o.equals(elementData[index])) { //同上 fastRemove(index); return true; } } return false; } /** * 根据给定的集合,将list中与集合相同的元素全部删除 * * @param c collection containing elements to be removed from this list * @return {@code true} if this list changed as a result of the call * @throws ClassCastException if the class of an element of this list * is incompatible with the specified collection * (<a href="Collection.html#optional-restrictions">optional</a>) * @throws NullPointerException if this list contains a null element and the * specified collection does not permit null elements * (<a href="Collection.html#optional-restrictions">optional</a>), * or if the specified collection is null * @see Collection#contains(Object) */ public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); //调用批量删除,后续分析 return batchRemove(c, false); } /** * 通过一个过滤器接口实现,来实现删除 */ @Override public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); // figure out which elements are to be removed // any exception thrown from the filter predicate at this stage // will leave the collection unmodified int removeCount = 0; //用bitset来存储哪些下标对应的元素要删除,哪些下标对应的元素要保存 //这里不清楚BitSet的用法的,可以先行了解一下 final BitSet removeSet = new BitSet(size); //判断并发修改用 final int expectedModCount = modCount; final int size = this.size; //按顺序遍历且没有并发修改 for (int i=0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; //利用过滤器匹配元素,如果匹配,则删除计数+1,并将下标进行存储 if (filter.test(element)) { removeSet.set(i); removeCount++; } } //判断是否存在并发修改 if (modCount != expectedModCount) { //抛出并发修改异常 throw new ConcurrentModificationException(); } // shift surviving elements left over the spaces left by removed elements //判断是否有要删除的元素 final boolean anyToRemove = removeCount > 0; if (anyToRemove) { final int newSize = size - removeCount; for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) { //下一个要保存的元素 i = removeSet.nextClearBit(i); //存放到新数组 elementData[j] = elementData[i]; } //把实际要保存的元素之后的全部置为null,用以GC //实际上,上面的操作已经将要保留的元素全部前移了,后面的元素都是不保留的,所以要置为null来帮助gc for (int k=newSize; k < size; k++) { elementData[k] = null; // Let gc do its work } //设置size this.size = newSize; //判断是否并发修改 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove; } /** * 删除list中指定范围的元素 * * Shifts any succeeding elements to the left (reduces their index). * This call shortens the list by {@code (toIndex - fromIndex)} elements. * (If {@code toIndex==fromIndex}, this operation has no effect.) * * @throws IndexOutOfBoundsException if {@code fromIndex} or * {@code toIndex} is out of range * ({@code fromIndex < 0 || * fromIndex >= size() || * toIndex > size() || * toIndex < fromIndex}) */ protected void removeRange(int fromIndex, int toIndex) { modCount++; //范围删除时,其实数组被分成三个部分 //分别为[保留区 - 删除区 - 保留区] //实际操作,则是计算出最后那部分保留区的长度 //利用arraycopy拷贝到第一个保留区的后面 //然后置空多余部分,帮助GC int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // clear to let GC do its work int newSize = size - (toIndex-fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; } //最后,来看一下批量删除这个私有方法 /** * 批量删除 */ private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) //这里其实有可能抛异常的 //complement //为false时,则证明下标r的元素不在删除集合c中,所以这个时候存储的是不删除的元素 //为true时,则证明下标r的元素在删除集合c中,所以这个时候存储的是要删除的元素 //这个布尔值的设计很巧妙。所以本方法可以供removeAll、retainAll两个方法来调用 if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. //所以这里要实际进行判断循环是否正常 if (r != size) { //如果不正常的话,需要挪动元素 System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } //如果有需要删除的元素的话,则证明有一部分位置需要只为null,来帮助GC //并且设置是否有修改的标志为true if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; } 至此,删除相关的方法都已经分析完毕。 有几个比较有意思的应用 BitSet 标志哪些下标要删除,哪些不删除 batchRemove 方法中的布尔值很巧妙 get 作为数组型的list,获取方法时比较简单的,只需要根据给定下标,读取指定下标的数组元素即可。 public E get(int index) { //范围检查 rangeCheck(index); return elementData(index); } contains 当前list是否包含指定元素 /** * 返回布尔值表示是否包含 */ public boolean contains(Object o) { //实际上是判断下标是否存在 return indexOf(o) >= 0; } /** * 指定元素在list中首次出现的下标,不存在则返回-1 */ public int indexOf(Object o) { //通过遍历的方式查找 if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; } //另外,还有一个,最后一次出现的下标 public int lastIndexOf(Object o) { //跟上面的类似,只不过遍历方式是从尾部开始 if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; } clear 清空缓冲数组。 public void clear() { //修改计数 + 1 modCount++; // clear to let GC do its work //全部置为null,帮助GC for (int i = 0; i < size; i++) elementData[i] = null; //size设置为0 size = 0; } 以上相关方法基本都已经介绍完毕。 总结 Array相比其他集合框架,如Map、Set之类的,还是比较简单的。 只需要了解相关方法的应用和原理,注意下标越界问题,以及内部的缓冲数组是如何扩容的,基本上就OK了。 溜了溜了。有帮助的话给格子点个赞呗~3Q 我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。

优秀的个人博客,低调大师

Java源码阅读之LinkedList - JDK1.8

阅读优秀的源码是提升编程技巧的重要手段之一。 如有不对的地方,欢迎指正~ 转载请注明出处https://blog.lzoro.com。 前言 前文基于缓冲数组的ArrayList已经分析过,那么同样作为List实现的LinkedList又有什么不一样呢? image 在阅读LinkedList源码之前,开头处先简单总结一下两者的区别 ArrayList 基于缓冲数组进行数据存储 查询/修改方便,因为基于下标容易定位数据 插入/删除不方便,需要移动数据 LinkedList 基于双向链表进行数据存储 查询/修改不方便,因为要移动指针 插入/删除方便,因为基于指针,不需要移动数据 带着这些概念,再次打开你的IDE,挽起袖子,开撸代码,加上注释,总计1262行代码,比ArrayList还少呢。 基本介绍 静态常量 嗯,没有,你没看错,LinkedList内部没有含业务属性的静态常量。 image 成员变量 工欲善其事,必先利其器。 虽然没什么太大关系,但为了提供逼格还是来了个引用。 要透彻理解整个LinkedList,那首先得先了解下它的内部提供了哪些成员变量,分别是做什么用的,这样有助于我们在看功能方法时提高效率。 其实,LinkedList内部定义的成员变量也少,但是没办法,谁让我为了提升篇幅,多说两句了。 image /** * 大小 */ transient int size = 0; /** * 首节点 * 恒定的: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * 尾节点 * 恒定的: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last; 可以看出来首节点/尾节点都是Node<E>的实例,那么Node<E>是何方神圣呢 它是一个私有的静态内部类,内部定义了当前元素和前置/后继指针,和一个构造函数,是整个双向链表的基础。 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; } } 构造函数 /** * 构造一个空的List */ public LinkedList() { } /** * 根据给定的集合构造一个List * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public LinkedList(Collection<? extends E> c) { //调用上面的构造函数 this(); //添加集合到List中 addAll(c); } 简洁明了。你应该也注意到了第二个构造函数中的addAll方法,看名字也知道是将集合c中的所有元素添加到LinkedList中。所以不能错过,往下看 image 可以看到,addAll(Collection<? extends E> c)是调用addAll(int index, Collection<? extends E> c)的,而这两个方法都是public的,第一个方法是在链表尾部添加指定的集合,而第二个方法比第一个方法多了一个参数,用来指定在某个位置添加指定集合。 /** * 将指定集合的所有元素添加都list尾部 * * 如果操作过程中指定的集合被修改,则此操作的行为未定义。 * (注意,如果指定的集合是该列表,并且它不是空的,则会发生这种情况。) * 其实就是线程不安全。 * * @param c collection containing elements to be added to this list * @return {@code true} if this list changed as a result of the call * @throws NullPointerException if the specified collection is null */ public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } /** * * 从指定位置插入指定集合的所有元素 * * @param index index at which to insert the first element * from the specified collection * @param c collection containing elements to be added to this list * @return {@code true} if this list changed as a result of the call * @throws IndexOutOfBoundsException {@inheritDoc} * @throws NullPointerException if the specified collection is null */ public boolean addAll(int index, Collection<? extends E> c) { //检查指针是否合法 checkPositionIndex(index); //集合转数组 Object[] a = c.toArray(); //集合长度 int numNew = a.length; //如果是空集合,则返回false if (numNew == 0) return false; //定义前置/继任节点 Node<E> pred, succ; //如果指定的位置是尾部(index==size) //无论当前链表是不是空的,只要index==size,就是往尾部插入元素 if (index == size) { //继任节点为null succ = null; //前置节点就是最后一个节点 pred = last; } else { //根据下标找出节点作为继任节点 succ = node(index); //设置前置节点 pred = succ.prev; //相当于在指定位置把当前链表断开 } //遍历集合元素进行插入(修改指针) for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } //如果没有继任节点 if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } //修改大小 size += numNew; //修改操作计数 modCount++; return true; } 这里的addAll添加一个集合的元素操作,整体逻辑还是比较清晰的,包含了指定集合c的非空判断,插入位置判断,断开链表,修改引用,后续判断和修改计数。 功能方法 了解了一些基础之后,那就该上大菜了。 接下来阅读,平时我们用的比较频繁的一些功能方法的源码。 还是老生常谈,对于这种集合框架来说,常用方法无外乎增/删/改/查。 另外,由于LinkedList不仅实现了List接口,还实现了Deque双端队列接口,所以也提供了队列相关方法。 add 跟ArrayList一样,LinkedList的添加也分为几类 尾部添加单个元素 指定位置添加单个元素 尾部添加集合元素 指定位置添加集合元素 首位添加 由于集合元素的添加,在上面构造函数章节已经提过,这里就不再赘述。 着重看一下单个元素的添加。 /** * 尾部添加元素,返回true * * <p>This method is equivalent to {@link #addLast}. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { //调用linkLast,后续分析 linkLast(e); return true; } /** * 指定位置添加元素 * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */ public void add(int index, E element) { //检查指针 checkPositionIndex(index); //判断是不是从尾部添加 if (index == size) linkLast(element); else //不是尾部添加的,调用linkBefore,后续分析 linkBefore(element, node(index)); } /** * 头部插入 * * @param e the element to add */ public void addFirst(E e) { linkFirst(e); } /** * 尾部插入 * * @param e the element to add */ public void addLast(E e) { linkLast(e); } 方法都很简单,没有什么操作逻辑,可以看出来,是linkLast/linkFirst/linkBefore在提供实际实现。 /** * 作为首节点 */ private void linkFirst(E e) { //取出当前首节点 final Node<E> f = first; //创建新节点 final Node<E> newNode = new Node<>(null, e, f); //用新节点替换首节点 first = newNode; //如果原来的首节点不存在的话 if (f == null) //当前只有一个节点,则首位节点都是同一个 last = newNode; else //原来的首节点后移 f.prev = newNode; //修改计数 size++; modCount++; } /** * 作为尾节点 */ void linkLast(E e) { //取出当前尾节点 final Node<E> l = last; //根据给定元素创建新节点 final Node<E> newNode = new Node<>(l, e, null); last = newNode; //判断原来的尾节点是否存在 if (l == null) //与上面同理 first = newNode; else //原来的尾节点前移 l.next = newNode; //修改计数 size++; modCount++; } /** * 在非null继任节点前插入 */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); //其实就是从指定节点断开连接,修改指针引用 succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } 看完上面内容,大概也就能了解为什么LinkedList适合插入/删除节点了,因为插入操作对于LinkedList来说,不需要移动数据,只需要在指定位置修改指针引用即可,即,断开->插入->修改引用。 移除其实也是同样的道理,即,断开->移除->修改引用。 remove 移除分为以下几种 根据下标移除 根据对象移除 移除头部(实现Deque接口的方法) 移除尾部((实现Deque接口的方法) 移除首个匹配的对象(实现Deque接口的方法) 移除最后一个匹配的对象(实现Deque接口的方法) /** * 根据下标移除元素,并移动相关指针 * * @param index 要移除元素的下标 * @return 返回删除元素的前一个元素 * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { //下标合法性判断 checkElementIndex(index); //调用node进行节点查找之后调用unlink进行移除 //后续分析这两个方法 return unlink(node(index)); } /** * 如果存在的话从list中移除第一个匹配的元素 * 如果不存在,则list不改变 * * 更正式点说,是移除在低位匹配到的元素 * 如下所示 * <tt>(o==null?get(i)==null:o.equals(get(i)))</tt> * (假如元素存在). * Returns {@code true} 如果列表存在元素 (等价理解,该链表被改变). * * @param o 要移除的元素 * @return {@code true} 如果存在要移除的元素 */ public boolean remove(Object o) { //先判断要移除的元素是不是null if (o == null) { //从首节点遍历查找 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { //匹配到null,在调用unlink移除(之后分析这个方法) unlink(x); return true; } } //要移除的元素不是 null } else { //仍然是遍历,不过比较方法换成equals for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } /** * 移除并返回首个元素 * * @return 返回list首元素 * @throws NoSuchElementException list为空时,抛异常 */ public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); //调用unlinkFirst,后面和unlink一起分析 return unlinkFirst(f); } /** * 移除并返回尾部元素 * * @return 返回list尾元素 * @throws NoSuchElementException list为空时,抛异常 */ public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); ////调用unlinkLast,后面和unlink一起分析 return unlinkLast(l); } /** * 从list头部->尾部进行遍历,如果存在指定元素的话,则移除第一个匹配的元素,如果不存在,则list不改变 * * @param o 要移除的元素 * @return {@code true} 如果存在的话,返回true * @since 1.6 */ public boolean removeFirstOccurrence(Object o) { //直接调用remove(o),因为remove(o)就是从头部遍历并移除第一个匹配的元素 return remove(o); } /** * 从list尾部->头部进行遍历,如果存在指定元素的话,则移除第一个匹配的元素,如果不存在,则list不改变 * * @param o 要移除的元素 * @return {@code true} 如果存在的话,返回true * @since 1.6 */ public boolean removeLastOccurrence(Object o) { //判断o是不是null,如果是null,则用==比较 if (o == null) { //从尾部遍历 for (Node<E> x = last; x != null; x = x.prev) { if (x.item == null) { //移除 unlink(x); return true; } } } else { for (Node<E> x = last; x != null; x = x.prev) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } //上面这个方法,通过条件分支,循环在两个分支里都有,看似可以抽离循环,然后再循环内部判断o==null来精简代码。 //但是实际上,把o==null抽离出来循环之外,虽然多写了些代码,但是不用在每次循环中做两次判断,可以提供效率。 //如果有类似的场景,我们也可以参考这种写法。 好了,看完上面的remove类方法,遗留了几个实际实现node、unlink、unlinkFirst、unlinkLast未阅读,下面继续 /** * 返回非指定位置的非null节点 */ Node<E> node(int index) { // assert isElementIndex(index); //判断下标在list的上游/下游 //如果是上游的话,从头部进行查找 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; } } /** * 移除非null节点x */ E unlink(Node<E> x) { // assert x != null; //取出元素待返回 final E element = x.item; //取出x的前/后节点 final Node<E> next = x.next; final Node<E> prev = x.prev; //如果前置节点不存在,则证明x是首节点 if (prev == null) { //首节点指向x的后置节点 first = next; } else { //如果x不是首节点 //则将x的前置节点与后置节点相连 prev.next = next; //x与前置节点断开 x.prev = null; } //如果x的后置节点不存在,则证明x是尾节点 if (next == null) { //尾节点指向x的前置节点 last = prev; } else { //如果不是尾节点 //则将x的尾首节点相连 //修改引用 next.prev = prev; //x与后置节点断开 x.next = null; } //x的元素置null x.item = null; //size - 1 size--; //操作计数 + 1 modCount++; return element; } /** * 移除非null的f首节点. */ private E unlinkFirst(Node<E> f) { // assert f == first && f != null; //老规矩,取出f节点元素,待返回 final E element = f.item; //取出f的后置节点 final Node<E> next = f.next; //下面两个置null帮助垃圾收集器进行GC f.item = null; f.next = null; // help GC //首节点指向f的后置节点 first = next; //如果后置节点不存在,证明list只有一个节点 if (next == null) //置null last = null; else //list不只一个节点 //后置节点变成首节点了,所以首节点的prev置null next.prev = null; //常规操作 size--; modCount++; return element; } /** * 移除非null的l尾节点 */ private E unlinkLast(Node<E> l) { // assert l == last && l != null; //取出元素待返回 final E element = l.item; //取出l的前置节点 final Node<E> prev = l.prev; //两个置null帮助垃圾收集器GC l.item = null; l.prev = null; // help GC //尾节点指向l的前置节点 last = prev; //如果前置节点尾null,证明list只有一个节点 if (prev == null) //首节点置null,此时list为空 first = null; else //list不只一个节点 //前置节点变成尾节点了,所以尾节点的后置为null prev.next = null; //常规操作 size--; modCount++; return element; } set 设置/修改元素操作,需要提供下标和对应的元素,逻辑比较简单。 /** * 提供下标和元素来替换指定位置的元素 * * @param index index of the element to replace * @param element element to be stored at the specified position * @return the element previously at the specified position * @throws IndexOutOfBoundsException {@inheritDoc} */ public E set(int index, E element) { //下标合法性判断 checkElementIndex(index); //获取指定节点(在remove章节已经分析过该方法) Node<E> x = node(index); //进行替换 E oldVal = x.item; x.item = element; return oldVal; } get LinkedList的查找元素方式跟ArrayList有点不同,由于它是双端链表形式存储数据,所以额外提供了getFirst和getLast,方法实现都很简单,下面看一下这三个方法的实现 /** * 返回指定位置的元素 * * @param index index of the element to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { //这里其实也是调用node(index)方法进行定位 checkElementIndex(index); return node(index).item; } /** * 返回list的首元素 * * @return the first element in this list * @throws NoSuchElementException if this list is empty */ public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; } /** * 返回list的尾元素 * * @return the last element in this list * @throws NoSuchElementException if this list is empty */ public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; } contains 判断list是否包含指定元素,跟ArrayList一样,通过查找元素的下标后判断下标是否存在,来判断元素是否存在,不一样的是元素的查找方法。 LinkedList是双端链表实现,所以查找方法时从首节点进行遍历。 public boolean contains(Object o) { return indexOf(o) != -1; } public int indexOf(Object o) { int index = 0; if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; } 其他方法 其他还有一些方法,如clear以及Deque接口中定义的方法实现如offer等,避免篇幅过长,这里不一一分析,有兴趣的可自行阅读源码,实现逻辑都相对比较简单。 clear offer peek ... 只要了解了双端链表的基本原理,和常规操作,基本上内部的方法实现都能掌握得差不多,所以。 我犯懒了,就差不多分析到这里。 image 最后 惯例求点赞~~ 3Q~

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。