热门标签 | 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篇到这了,是不是很简单呢? 这可是最简单的数据结构了。


推荐阅读
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • MySQL初级篇——字符串、日期时间、流程控制函数的相关应用
    文章目录:1.字符串函数2.日期时间函数2.1获取日期时间2.2日期与时间戳的转换2.3获取年月日、时分秒、星期数、天数等函数2.4时间和秒钟的转换2. ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 本文探讨了使用JavaScript在不同页面间传递参数的技术方法。具体而言,从a.html页面跳转至b.html时,如何携带参数并使b.html替代当前页面显示,而非新开窗口。文中详细介绍了实现这一功能的代码及注释,帮助开发者更好地理解和应用该技术。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 重要知识点有:函数参数默许值、盈余参数、扩大运算符、new.target属性、块级函数、箭头函数以及尾挪用优化《深切明白ES6》笔记目次函数的默许参数在ES5中,我们给函数传参数, ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文对SQL Server系统进行了基本概述,并深入解析了其核心功能。SQL Server不仅提供了强大的数据存储和管理能力,还支持复杂的查询操作和事务处理。通过MyEclipse、SQL Server和Tomcat的集成开发环境,可以高效地构建银行转账系统。在实现过程中,需要确保表单参数与后台代码中的属性值一致,同时在Servlet中处理用户登录验证,以确保系统的安全性和可靠性。 ... [详细]
  • 2.2 组件间父子通信机制详解
    2.2 组件间父子通信机制详解 ... [详细]
  • 本文全面解析了JavaScript中的DOM操作,并提供了详细的实践指南。DOM节点(Node)通常代表一个标签、文本或HTML属性,每个节点都具有一个nodeType属性,用于标识其类型。文章深入探讨了DOM节点的创建、查询、修改和删除等操作,结合实际案例,帮助读者更好地理解和掌握DOM编程技术。 ... [详细]
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社区 版权所有