热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

大话顺序结构之ArrayList

上篇回顾上一篇中讲解了线性表的相关概念,通过上一篇文章,希望可以对线性表有一个基本的认识。本篇开始讲述线性表的顺序表是怎么实现的。先来看一下顺序表的继承关系,这是一个简图。imag

上篇回顾

上一篇中讲解了线性表的相关概念,通过上一篇文章,希望可以对线性表有一个基本的认识。本篇开始讲述线性表的顺序表是怎么实现的。

先来看一下顺序表的继承关系,这是一个简图。

《大话顺序结构之ArrayList》 image.png

1. ArrayList

首先试想一下,我们如此熟悉的ArrayList,如果要你去实现, 你会怎么做?
答案是用数组。那么数组和集合的区别是什么呢?
1) 数组的长度是固定的,我们初始化一个数组,或者指定长度或者指定元素(同样指定了长度)
2) 集合的长度是动态规范的, 我们初始化一个集合,从来不需要处理长度相关的问题
3)数组是没法添加元素的, 集合可以无限制的从尾部添加新元素

其实仔细对比一下,可以发现, 删除一个元素, 修改一个元素, 查询一个元素等功能无论在数组还是ArrayList中实现方式都是一样的,什么?别告诉我在数组里怎么实现你不会。那么就剩下了添加一个元素,从以上的3条对比可以发现, ArrayList与数组的主要区别就在于添加一个元素,如何实现在尾部添加? 如何实现扩容? 其实ArrayList的核心可以讲的东西有两点扩容和移位。

这里我们把查询与修改的实现一笔带过;添加与删除涉及到移位,先讲移位再将删除;添加涉及到扩容,最后讲添加。至于最后的判空啊, 获取长度啊,我相信是不用说的。

ArrayList的实现

/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
// Android-note: Also accessed from java.util.Collections
transient Object[] elementData; // non-private to simplify nested class access

上面提到过, ArrayList的内部数据存储采用数组的形式{$elementData},而当我们初始化一个ArrayList时,我们会得到一个默认长度是10的数组。

ArrayList的查询

/**
* Returns the element at the specified position in this list.
*
* @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) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}

查询的实现从看到源码的瞬间就不用说了,谁不会的话,自己去打屁屁吧。 这里第一步是参数的校验, 这是一个优秀的编码习惯,值得我们去学习。一个全面的,有效的参数校验可以一定程度上保证代码的健康。什么时候需要抛异常,什么时候返回默认值也是个值得推敲的问题。

ArrayList的修改

/**
* Replaces the element at the specified position in this list with
* the specified element.
*
* @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) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}

同查询,普普通通的赋值操作,不值一提。不过可以稍微注意下,返回的值,是未修改之前的原值。

移位

ArrayList是线性表中顺序表的经典结构之一,顺序表的特点是连续的顺序的空间;因此当一个元素添加或删除后,ArrayList会如何做呢?删除之后,对应位置的元素会是空? 当然不是; 一个数组元素已经排位完毕,如何插入一个元素?答案是移位。

1.什么是移位?

当ArrayList插入或移除一个元素时, 指定元素之后的元素会统一向前或向后移动一个位置,称之为移位。那么先官方的解释一下! 假设有一个数组,如下

[0, 1, 2, 3, 4, 5, 6]

对应的位置存储着对应的数字, 此时我想在位置[4]处插入一个元素7,那么插入后的数组是

[0, 1, 2, 3, 7, 4, 5, 6]

我们根据这个现象观察ArrayList的插入操作。首先, 找到位置[4], 然后[4]后面的元素统一后移1位,最后在位置[4]插入指定的元素7。通俗的解释就是像我们日常排队,我们已经排好了一个队伍,这时候领导来了,当然让领导先嘛是不是,这时候我们要所有人往后退一步,腾出第一个位置,领导插进来,皆大欢喜。如图

《大话顺序结构之ArrayList》 image.png

再看来下删除操作。如果我想删除元素4,那么删除后的数组是

[0, 1, 2, 3, 5, 6]

我们根据这个现象观察ArrayList的删除操作。首先,找到元素4,位于位置[4],然后删除4, 最后[4]后面的元素统一往前移。还是以排队为例,我们已经排好了一个队伍,忽然队伍中的小王来大姨妈了,要去一趟卫生室。那么后面的人肯定要往前占据小王的位置的,不要说你的生活中不是这样的,你说了我也不信。如图

《大话顺序结构之ArrayList》 image.png

通过以上两个例子,我想应该都明白了什么是移位吧。那么我们开始来撸一下代码。

ArrayList的删除

/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
/*核心删除操作*/
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}

我们直接来看一下核心的删除实现。首先取到待删除值,这个是要返回的; numMoved是什么鬼?看名字就知道了吧,移动的数量,用于判断是否需要执行移位操作;如果没有需要移动的,比如删除最后一个元素,那么很明显就不用移位了嘛,因为移动的位置是从index + 1开始的。

上面提过,数组是长度是固定不变的,如果我想加一个元素到数组,你是怎么做的? 重新创建一个length+1的数组。 这里是通过arraycopy方法来将index(待删除元素的位置) + 1到最后一个元素复制到一个新书组中。
解析下这个API

public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
src: 现有的数组
srcPos: 从现有数组第几个开始复制
dest: 目标数组,要复制到哪个数组下
destPos: 复制到目标数组的开始位置
length: 复制多少个元素

总结一下以上方法,就是把现在数组从index+1开始到最后一个元素;整体复制到本数组中,复制元素从index开始。不好理解?还是以上面数组为例,原数组是

[0, 1, 2, 3, 4]

我想删除元素2, 那么我就把位置[2]后面的元素[3,4]复制到本数组中,从位置[2]开始放,如下
[0,1,3,4,2]
可以看出3和4从原来的位置[3]与位置[4], 移动到了位置[2]和位置[3]。 也就是把2后面的元素前移了一位。

再看源码的最后一句,最后一个元素置空。size是ArrayList的长度,删除后长度-1。 顺便看一下官方注释, 置空是为了释放最后一个元素的资源,因为最后一个元素不是ArrayList的有效元素了嘛。

好了,删除就说这么多,移位也不再多说了。添加元素是从尾部插入的,因此不需要位移的,前面提过;如果你需要向指定位置添加元素,OK, 还是需要移位的,不过方向与删除相反,也好理解。

ArrayList的添加与扩容

ArrayList的添加是从尾部开始的,因此不需要移位,这也是ArrayList比数组方便的原因之一。前面提到过,数组的长度是固定的,因此在添加元素时,我们要对数组进行扩容。那么扩容的策略如何,接下来看源码

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return true (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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:
elementData = Arrays.copyOf(elementData, newCapacity);
}

一看啊呀代码怎么这么多,别慌! 先看add方法,add方法只有两步: 1) 扩容 2) 扩容好了,尾插一个元素 我相信从数组最后插入一个元素不是困难的问题,size++, 扩容之后,数组的长度是+1的,无异议吧,因为插入一个元素。这个size++和之前的&#8211;size可是不同的, 前者是后运算, 因此扩容后的数组长度成了size+1,而插入的位置是size, 也就是size++在调用的时候,尚未执行++操作。 而&#8211;size正好相反,先运算,再调用。好的,来说下扩容

扩容

从以上添加代码可以看出,最终的扩容策略是在grow()中执行的。

int oldCapacity = elementData.length;

取出原数组的长度

int newCapacity = oldCapacity + (oldCapacity >> 1);

先执行系统的自动扩容, 扩容因子是(oldCapacity >> 1)。 这是一个位移运算,意思是右移1位。在二进制中,右移一位的意思自然是 / 2,因为二进制从左到右的位数越来越低。从右到左分别是2的0次方,2的1次方&#8230;那如果左移也就是很明显是x2了。从这句话得出,系统的扩容因子是原数组长度的一半。

if (newCapacity - minCapacity <0)
newCapacity = minCapacity;

如果系统扩容后,还不足以插入新元素,别忘了ArrayList是有addAll()的, 插入一个军队,这就非同小可了。这时候扩容后的长度就是旧的数组长度 + 新增元素的长度。 再往下一句是一个边界判断,不用解释了吧。

elementData = Arrays.copyOf(elementData, newCapacity);

到了最后的最后,哎?还是这个套路, 把我现在的数组elementData扩展成为一个长度为newCapacity的数组,我们的扩容就完成了。

面试中可能对扩容策略有提问,所以最好记住这个系统的扩容因子以及最终是如何实现扩容的。ArrayList还有一个add(Element, index)的方法,无非是先实现扩容,然后再进行移位。以上两个核心点搞明白了,这些都不是事儿。

好了,ArrayList篇到这了,是不是很简单呢? 这可是最简单的数据结构了。


推荐阅读
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Whatsthedifferencebetweento_aandto_ary?to_a和to_ary有什么区别? ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
author-avatar
嘟嘟酱
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有