arraylist改变指定位置数值_面试题:Java容器之ArrayList全解析

本文深入解析Java中的ArrayList,从结构图、类简介到常见问题,详细阐述ArrayList如何改变指定位置的数值,包括扩容机制、赋值操作以及在遍历中删除元素的注意事项。还介绍了手动实现ArrayList的基本思路,并提供了相关参考资料。
摘要由CSDN通过智能技术生成

点击上方“程序员面试圈”,选择“置顶或者星标”

与你一起成长~

99aa4749b2233a59c0264afd23afe74a.png

公众号回复[ 资源 ] 领取888G学习资源

ArrayList结构图

ArrayList 是 java 集合框架中比较常用的数据结构了。继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。

26714eeb3e73f2642d0bdd3e217bf1d6.png

ArrayList类简介

  • 1、ArrayList是内部是以动态数组的形式来存储数据的、知道数组的可能会疑惑:数组不是定长的吗?这里的动态数组不是意味着去改变原有内部生成的数组的长度、而是保留原有数组的引用、将其指向新生成的数组对象、这样会造成数组的长度可变的假象。

  • 2、ArrayList具有数组所具有的特性、通过索引支持随机访问、所以通过随机访问ArrayList中的元素效率非常高、但是执行插入、删除时效率比较地下、具体原因后面有分析。

  • 3、ArrayList实现了AbstractList抽象类、List接口、所以其更具有了AbstractList和List的功能、前面我们知道AbstractList内部已经实现了获取Iterator和ListIterator的方法、所以ArrayList只需关心对数组操作的方法的实现、

  • 4、ArrayList实现了RandomAccess接口、此接口只有声明、没有方法体、表示ArrayList支持随机访问。

  • 5、ArrayList实现了Cloneable接口、此接口只有声明、没有方法体、表示ArrayList支持克隆。

  • 6、ArrayList实现了Serializable接口、此接口只有声明、没有方法体、表示ArrayList支持序列化、即可以将ArrayList以流的形式通过ObjectInputStream/ObjectOutputStream来写/读。

基础属性

ArrayList部分源码如下:

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable

{

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private transient Object[] elementData;

private int size;

//...省略部分代码

}

如上代码中为ArrayList的主要属性:

  • DEFAULT_CAPACITY:默认容量,即为初始值大小

  • EMPTY_ELEMENTDATA:共享的空数组,用于初始化空实例

  • elementData:ArrayList内部结构,是一个Object[]类型的数组

  • size:数组长度大小

构造方法

如下为ArrayList的构造方法:

1.public ArrayList(int initialCapacity)

2.public ArrayList()

3.public ArrayList(Collection extends E> c){

elementData = c.toArray();

size = elementData.length;

// c.toArray might (incorrectly) not return Object[] (see 6260652)

if (elementData.getClass() != Object[].class)

elementData = Arrays.copyOf(elementData, size, Object[].class);

}

  • 1.构造方法1,表示接受指定地容量值,初始化创建数组,建议在可估算数组大小时,创建ArrayList可指定

  • 2.构造方法2,是默认的构造方法,它将创建一个空数组

  • 3.构造方法3,接收一个Collection的实体,将该Collection实体转换为ArrayList对象

主干流程

1.添加指定元素代码如下

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

可以看到实际上只有3行代码,其流程主要如下:

1.扩容 (这里便解释了,在介绍时提出的问题):

主要源码如下

private void ensureCapacityInternal(int minCapacity) {

if (elementData == 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);

}

//最大数组容量

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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);

}

  • 第一个方法的逻辑为:判断是不是第一次添加元素,若为第一次,则设置初始化大小为默认的值10,否则使用传入的参数

  • 第二个方法的逻辑为:若长度大于数组长度,则扩容

  • 第三个方法的逻辑为:

1·扩容的大小为3/2倍原数组长度

2.若值newCapacity比传入值minCapacity还要小,则使用传入minCapacity,若newCapacity比设定的最大数组容量大,则使用最大整数值

3.实际扩容,使用了Arrays.copyof(elementData, newCapacity) (此处有两个问题 1.为啥扩容是原来的3/2倍原数组的长度? 2.调用Arrays.copyOf(elementData, newCapacity)方法具体做了什么操作? )

2.赋值:将添加的值放置到size++的位置上
3.返回:返回true

2.添加指定元素到指定的位置上代码如下:

public void add(int index, E element) {

rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!

System.arraycopy(elementData, index, elementData, index + 1,

size - index);

elementData[index] = element;

size++;

}

其流程为:

  • 1.校验下标:调用rangeCheckForAdd方法进行下标校验,不正确则会抛出IndexOutOfBoundsException异常

  • 2.扩容:详见上部分中做的介绍

  • 3.移动数据:将数据index后面的数据,都向后移动

  • 4.赋值:将加入的值放置到index位置中

  • 5.长度增加:长度增加

常见问题

1.问题描述

在使用ArrayList比较常见的一个问题就是在遍历ArrayList的时候调用remove()方法进行元素的删除操作,从而得到意想不到的结果,本人在开发过程中也遇到过这样的问题,所以在这里提出了,希望能够帮助到大家。

2.实例及分析

如下代码中,在遍历List时,调用了remove方法,删除元素a

//arrayList中的值为 [a,a,c,a,a]

for (int i = 0; i < arrayList.size(); i++) {

if (arrayList.get(i) == "a") {

arrayList.remove(i);

}

}

System.out.println(arrayList);

  • 这段代码看似解决了删除列表中所有的a元素,但是删除后得出List的结果为[a, c, a],为什么这种方式没有达到想要的效果,其实仔细分析后会发现,在调用remove()方法时List的长度会发生变化而且元素的位置会发生移动,从而在遍历时list实际上是变化的,例如

  • 当i=0时,此时list中的元素为[a,a,c,a,a],

  • 但当i=1时,此时List中的元素为[a,c,a,a],元素的位置发生了移动,从而导致在遍历的过程中不能达到删除的效果

3.解决方案

通过上述的分析可以看出,出现问题的原因是元素的位置发生了移动,从而导致异常的结果 方案一、逆向遍历List删除,代码如下,这种做法可行主要是因为remove()方法删除index处的元素时,是将index+1到size-1索引处的元素前移,而逆向遍历可以避免元素位置的移动

for (int i = arrayList.size()-1; i >=0 ; i--) {

if (arrayList.get(i) == "a") {

arrayList.remove(i);

}

}

System.out.println(arrayList);

方案二、使用迭代器中的remove方法,迭代器具体参考Iterator详解,主要代码如下(这种方式比较推荐)

Iterator<String> ite = arrayList.listIterator();

while (ite.hasNext()){

if(ite.next() == "a")

ite.remove();

}

System.out.println(arrayList);

手写一个ArrayList

自己手写一个ArrayList,代码如下:

public class MyArrayList<T> implements Iterable<T> {

private T[] theItems;

private int theSize;

private static final int DEAULT_CAPACITY=10;

public MyArrayList(){

theSize=0;

ensureCapacity(DEAULT_CAPACITY);

}

public void add(T data){

if(size()==theItems.length){

ensureCapacity(size()*2+1);

}

theItems[size()]=data;

theSize++;

}

public void add(int index,T data){

if(size()==theItems.length){

ensureCapacity(size()*2+1);

}

for(int i=theSize;i>index;i--){

theItems[i]=theItems[i-1];

}

theItems[index]=data;

theSize++;

}

public T get(int index){

if(index<0|index>=size()){

throw new IndexOutOfBoundsException("index error");

}

return theItems[index];

}

public T remove(int index){

T removeData=get(index);

for(int i=index;i<size()-1;i++){

theItems[i]=theItems[i+1];

}

theSize--;

return removeData;

}

public int size(){

return theSize;

}

private void ensureCapacity(int newCapacity){

if(theSize>newCapacity){

return;

}

T[] old=theItems;

theItems= (T[]) new Object[newCapacity];

for(int i=0;i<size();i++){

theItems[i]=old[i];

}

}

@Override

public Iterator<T> iterator() {

return null;

}

@Override

public void forEach(Consumer super T> action) {

}

@Override

public Spliterator<T> spliterator() {

return null;

}

}

总结

  • 1.ArrayList是基于数组实现的,它的内存储元素的数组为 elementData;elementData的声明为:transient Object[] elementData;

  • 2.ArrayList中EMPTYELEMENTDATA和DEFAULTCAPACITYEMPTY_ELEMENTDATA的使用;这两个常量,使用场景不同。前者是用在用户通过ArrayList(int initialCapacity)该构造方法直接指定初始容量为0时,后者是用户直接使用无参构造创建ArrayList时。

  • 3.ArrayList默认容量为10。调用无参构造新建一个ArrayList时,它的elementData = DEFAULTCAPACITYEMPTYELEMENTDATA, 当第一次使用 add() 添加元素时,ArrayList的容量会为 10。

  • 4.ArrayList的扩容计算为 newCapacity = oldCapacity + (oldCapacity >> 1);且扩容并非是无限制的,有内存限制,虚拟机限制。

  • 5.ArrayList的toArray()方法和subList()方法,在源数据和子数据之间的区别;

  • 6.注意扩容方法ensureCapacityInternal()。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则不建议使用。

参考资料

https://blog.csdn.net/crave_shy/article/details/17436773

https://www.jianshu.com/p/92373a603d42

https://juejin.im/post/5a1bc1006fb9a045030fce0e

- end -

d2366086f5005dbc583ab1eb72d1e21a.png

用心分享面试知识,做有温度的攻城狮

每天记得对自己说:你是最棒的!

往期推荐:

888G面试资源分享

面试题:Java面向对象的特征

面试题:请你说说“面向对象六大原则”?

面试题:谈谈final、finally、finalize的区别

面试题:java 内部类和静态内部类的区别

面试题:java 泛型详解-绝对是对泛型方法讲解最详细的

面试题:Java 中的 ==, equals 与 hashCode 的区别与联系

面试题:java中重载与重写的区别

面试题:int与Integer的区别

面试题:关于Java的反射机制,你需要理解这些..

                     每一个“好看”,都是对我们最大的肯定!417e745916ea3d5e282c80090df4271f.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值