【源码之下无秘密】ArrayList,在线面试指南

本文详细介绍了ArrayList的常用操作,包括添加、插入、删除、查找等方法,通过源码分析理解其工作原理。例如,`add(int index, E element)`方法用于按指定位置插入元素,`trimToSize()`用于缩小ArrayList容量以节省空间,`ensureCapacity(int minCapacity)`则用于预估并调整ArrayList的容量。同时,文章还提醒在处理对象时,重写`equals()`和`hashCode()`的重要性,以及不同添加方法的性能考量。" 132777733,19919375,Rust Shellcode 加密混淆技术详解,"['Rust', '后端开发', '安全', '编码混淆', 'Windows API']
摘要由CSDN通过智能技术生成

list:[小明, 卖托儿索的小火柴]

Process finished with exit code 0

我这里初始化的时候创建了一个无参构造,所以数组的初始大小为:10,添加两个元素的时候并不会触发扩容机制。

第二种:add(int index, E element)

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

}

故名思意,看参数就应该能大致的猜出这个方法是干什么的,没错,他就是插入指定位置元素,他的插入和第一个差不多,唯一的区别就是第一个是往后添加,这里是按index添加到这指定下表位置,然后将其他的元素往后移,也就是System.arraycopy(elementData, index, elementData, index + 1, size - index)。

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“卖托儿索的小火柴”);

list.add(“海阔天空”);

list.add(5,“逆天而行”);

System.out.println(“list:” + list);

你们可以猜到执行的结果吗?执行结果就是报错,为什么呢?源码里面有这么一个方法

private void rangeCheckForAdd(int index) {

if (index > size || index < 0)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

}

判断插入的位置是否大于elementData的数组长度或者是否小于0,由于我这里只添加了两个元素,所以size应该是3,我们添加的下标却是5,所以就会抛出异常

Exception in thread “main” java.lang.IndexOutOfBoundsException: Index: 5, Size: 2

at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)

at java.util.ArrayList.add(ArrayList.java:473)

at com.ymy.list.MyArrayList.main(MyArrayList.java:18)

Process finished with exit code 1

我们修改一下代码,将下表修改成1

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“卖托儿索的小火柴”);

list.add(“海阔天空”);

list.add(1,“逆天而行”);

System.out.println(“list:” + list);

这个时候我们再来看运行结果

list:[小明, 逆天而行, 卖托儿索的小火柴, 海阔天空]

Process finished with exit code 0

我们发现逆天而行被添加到了下标为1的位置,而卖托儿索的小火柴,和海阔天空相应的往后移了一位。

trimToSize()


之前没有说清楚size与elementData的关系,size表示的是elementData数组中已经存放了多少元素,而elementData.length表示ArrayList的初始数组大小,请不要搞混.。

public void trimToSize() {

modCount++;

if (size < elementData.length) {

elementData = (size == 0)

? EMPTY_ELEMENTDATA
Arrays.copyOf(elementData, size);

}

}

这个方法就是判断已经存在数组中的元素个数(size)和数组初始化的大小(elementData.length)做对比,如果小于初始化值就去掉多余的,返回一个elementData大小等于size,实现的方式就是通过拷贝的形式。

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“卖托儿索的小火柴”);

list.trimToSize();

System.out.println(“list:” + list);

为了能看到我说的,我们断点调试走一波

在这里插入图片描述

在这里插入图片描述

我们发现走到断点的那一行size=2,elementData.length= 10,下面就是判断了,很明显2<10,所以这里会执行数据拷贝,拷贝完成之后我们在看结果

在这里插入图片描述

清除了多余没用的元素下标,但是这个方法大家在使用的时候还是慎重比较好,如果你清除完成之后又想添加数据,这个时候ArrayList就会执行扩容操作了,这是需要进行数据拷贝的,慎重哦。

ensureCapacity(int minCapacity)


源码如下

public void ensureCapacity(int minCapacity) {

int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

// any size if not default element table

? 0

// larger than default for default empty table. It’s already

// supposed to be at default size.
DEFAULT_CAPACITY;

if (minCapacity > minExpand) {

ensureExplicitCapacity(minCapacity);

}

}

大致意思:当你初始化了一个大小为10的初始数组之后,并添加了5条数据,这个时候你发现10可能不够,要是数组大小在大一点就好了,ensureCapacity就是解决这个问题的,他会扩大你指定的大小,但是扩大之后数组的大小是不是你指定的大小这个是不确定的,因为ensureExplicitCapacity(minCapacity);的源码在上面也看到了,他会现在原来的数组大小的基础上扩大1.5倍,然后在和你传入的数值做对比,如果大于你传入的,那么使用旧数组(elementData)大小的1.5倍作为新数组的大小,如果小于你传入的数值,这个时候就会以你传入的大小作为数组(elementData)的大小,这点一定要搞清楚哦,不然的话,你会发现你明明设置了值,但是最后数组的大小却和你设置的不一样,就会感觉是你的代码写的有问题。

我们先来看一个扩容小于原数组大小1.5倍的数值:12

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“卖托儿索的小火柴”);

list.ensureCapacity(12);

System.out.println(“list:” + list);

在这里插入图片描述

我们发现elementData的大小并不是我们传入的12,而是15,要注意哦

我们再来看看扩容大于原始数组大小1.5倍的数值:20

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“卖托儿索的小火柴”);

list.ensureCapacity(20);

System.out.println(“list:” + list);

在这里插入图片描述

在这里我在贴出一下导致这两种原因的代码在哪里

在这里插入图片描述

size()


返回当前ArrayList已经添加了多少条元素,这个不用多说,相信大家都知道。

isEmpty()


public boolean isEmpty() {

return size == 0;

}

判断ArrayList是否添加了数据,但是这点需要注意一下,这里只能判断是否存在元素,不能判断ArrayList是否为空,这点需要注意,如果使用这个方法判断空的话就报错哦。

contains(Object o)


public boolean contains(Object o) {

return indexOf(o) >= 0;

}

判断ArrayList所有元素中是否存在当前元素,如果是对象,判断的就是引用地址了,这里需要注意,如果我们的ArrayList的泛型是对象,那么最好重写一下equals和hashcode方法,举个例子

没有重写equals()与 hashCode()

ArrayList list = new ArrayList(10);

//用户插入集合的数据

User user1 = new User();

user1.setUserName(“小明”);

user1.setSex(1);

user1.setAge(18);

//用于对比的数据

User user2 = new User();

user2.setUserName(“小明”);

user2.setSex(1);

user2.setAge(18);

list.add(user1);

System.out.println(“是否包含user1:” + list.contains(user1));

System.out.println(“是否包含user2:” + list.contains(user2));

是否包含user1:true

是否包含user2:false

Process finished with exit code 0

看到输出结果了吧,判断是否包含user1结果为:true;判断是否包含user2的结果为:false,那是因为往list中添加的是user1,所以比较user1的时候他们都是同一个引用地址,所以返回true,而user2是新new出来的,他们是两个完全不相同的对象,内存地址肯定也不相同,所以这个时候肯定就返回false,因为user1和user2里面存放的数据都是一样的,有时候我们只需要判断内容是否相等,并不需要判断内存地址是否相等的时候需要怎么做呢?

重写equals()与 hashCode()

改造一下我们的User对象

package com.ymy.entity;

import lombok.Getter;

import lombok.Setter;

import lombok.ToString;

import java.util.Objects;

@Getter

@Setter

@ToString

public class User {

private String userName;

private Integer age;

private Integer sex;

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

User user = (User) o;

return Objects.equals(userName, user.userName) &&

Objects.equals(age, user.age) &&

Objects.equals(sex, user.sex);

}

@Override

public int hashCode() {

return Objects.hash(userName, age, sex);

}

}

测试代码还是不变,我们在运行查看结果

是否包含user1:true

是否包含user2:true

Process finished with exit code 0

总结就是一句话,ArrayList引用对象的时候如果没有重写equals()与 hashCode()对比的就是内存地址,如果重写了equals()与 hashCode(),对比的就是实实在在的数据。请拿小本本记好,这个要考。

indexOf(Object o)


查找元素所在的下标,如果查找的是对象,默认比较的是内存地址这点和contains(Object o)一样。

源码如下

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;

}

如果查找的内容为空,那么这个就会返回第一个元素为空的下标,否者返回数组中第一次出现查找元素的下标。

没有重写equals()与 hashCode()

ArrayList list = new ArrayList(10);

//用户插入集合的数据

User user1 = new User();

user1.setUserName(“小明”);

user1.setSex(1);

user1.setAge(18);

//用于对比的数据

User user2 = new User();

user2.setUserName(“小明”);

user2.setSex(1);

user2.setAge(18);

list.add(user1);

System.out.println(“是否包含user1:” + list.indexOf(user1));

System.out.println(“是否包含user2:” + list.indexOf(user2));

是否包含user1:0

是否包含user2:-1

Process finished with exit code 0

很明显查找user1的时候是同一个内存地址,所以返回了对应的下标,而user2与user1不是同一个内存地址,所以返回了-1。

重写equals()与 hashCode()

重写的方法和contains()一样,我们直接看结果即可

是否包含user1:0

是否包含user2:0

Process finished with exit code 0

所以一定要区分你需要查找的是值相同还是地址相同,不然就会导致bug哦。

lastIndexOf(Object o)


与indexOf()效果一样,都是查找元素所在的下标,但是又有一点区别,那就是lastIndexOf()返回的是最后一次出现的下标位置。

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;

}

这个使用和indexOf一样,这里就不做demo展示了。

clone()


克隆一个ArrayList

源码如下

public Object clone() {

try {

ArrayList<?> v = (ArrayList<?>) super.clone();

v.elementData = Arrays.copyOf(elementData, size);

v.modCount = 0;

return v;

} catch (CloneNotSupportedException e) {

// this shouldn’t happen, since we are Cloneable

throw new InternalError(e);

}

}

使用方式

ArrayList list = new ArrayList(10);

//用户插入集合的数据

User user1 = new User();

user1.setUserName(“小明”);

user1.setSex(1);

user1.setAge(18);

list.add(user1);

System.out.println(“list1:”+list);

ArrayList list2 = (ArrayList) list.clone();

System.out.println(“list2:”+list2);

运行结果

list1:[User(userName=小明, age=18, sex=1)]

list2:[User(userName=小明, age=18, sex=1)]

Process finished with exit code 0

将list拷贝到list2,但是这里需要注意一点,这里的拷贝属于浅拷贝,list2和list1共享一个User对象,这是需要特别注意的。

toArray()


将ArrayList转换成数组

源码

public Object[] toArray() {

return Arrays.copyOf(elementData, size);

}

使用方式

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list1:” + list);

Object[] array = list.toArray();

System.out.println(“array:” + array);

array[2] = “海阔天空”;

运行结果

list1:[小明, 逆天而行]

array:[Ljava.lang.Object;@3a71f4dd

Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: 2

at com.ymy.list.MyArrayList.main(MyArrayList.java:17)

Process finished with exit code 1

ArrayList转数组没有问题,但是在数组赋值的时候却报错了,这一点需要注意,这里的数组长度就是ArrayList的数组实际长度,ArrayList的长度是2,下标最大为1,但是我们赋值的时候给的下标是2,所以就会抛出数组越界的错误。

get(int index)


根据下表获取元素信息

源码

public E get(int index) {

rangeCheck(index);

return elementData(index);

}

第一步校验下标是否越界,然后返回对应下标元素信息。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list1:” + list);

String name = list.get(1);

System.out.println(“name:”+name);

运行结果

Connected to the target VM, address: ‘127.0.0.1:62855’, transport: ‘socket’

list1:[小明, 逆天而行]

name:逆天而行

Disconnected from the target VM, address: ‘127.0.0.1:62855’, transport: ‘socket’

Process finished with exit code 0

这里面的下标一定不能大于elementData的size,否者就会抛出数组越界。

set(int index, E element)


在指定下标添加元素

源码

public E set(int index, E element) {

rangeCheck(index);

E oldValue = elementData(index);

elementData[index] = element;

return oldValue;

}

添加的下标不能越界,他会将你的元素添加到数组的指定下标,并且返回被替换的元素,这里是替换哦,被替换的元素不会往后移,这点需要特别注意。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list:” + list);

String name = list.set(1,“海阔天空”);

System.out.println(“name:”+name);

System.out.println(“修改后的list:”+list);

结果

list:[小明, 逆天而行]

name:逆天而行

修改后的list:[小明, 海阔天空]

Process finished with exit code 0

remove(int index)


删除指定下标的元素,并返回被删除的元素值

源码

public E remove(int index) {

rangeCheck(index);

modCount++;

E oldValue = 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;

}

他在删除了指定下标之后,那这个下标的位置就会处于空缺,这个时候ArrayList做了一件事,那就是将数组进行重新排序,实现的方式就是数据拷贝,使用一个新的数组接受两段数据,一段是删除下标之前的数据,一段是删除下表之后的数据,整合到一个新的数组,然后赋值到原数组中。这里只需要了解一下即可,最后返回了被删除的元素值。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list:” + list);

String name = list.remove(1);

System.out.println(“name:”+name);

System.out.println(“删除后的list:”+list);

运行结果

list:[小明, 逆天而行]

name:逆天而行

删除后的list:[小明]

Process finished with exit code 0

remove(Object o)


通过元素值删除数组中存在的元素,这种删除比较耗时间,为什么这么说呢?请看源码

public boolean remove(Object o) {

if (o == null) {

for (int index = 0; index < size; index++)

if (elementData[index] == null) {

fastRemove(index);

return true;

}

} else {

for (int index = 0; index < size; index++)

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

private void fastRemove(int index) {

modCount++;

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

}

首先,他会判断删除的元素是否为空,如果是空,那么它将删除数组中第一个空元素,然后直接返回,如果你要删除的元素不为空,那这个时候就会循环数组,找到你要删除的第一个元素进行删除,但是删除的时候有需要做数据拷贝,如果不做的话,数组下标就会错乱,最后返回删除结果。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list:” + list);

boolean remove = list.remove(“逆天而行”);

System.out.println(“是否删除成功:”+remove);

System.out.println(“删除后的list:”+list);

运行结果

list:[小明, 逆天而行]

是否删除成功:true

删除后的list:[小明]

Process finished with exit code 0

clear()


这个方法比较简单,就是将数组中所有的元素都设置为null,然后将size设置为0。

源码

public void clear() {

modCount++;

// clear to let GC do its work

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

elementData[i] = null;

size = 0;

}

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list:” + list);

list.clear();

System.out.println(“删除后的list:”+list);

运行结果

list:[小明, 逆天而行]

删除后的list:[]

Process finished with exit code 0

addAll(Collection<? extends E> c)


将其他的集合添加到当前集合,这里添加方式是通过拷贝实现的。

源码如下

public boolean addAll(Collection<? extends E> c) {

Object[] a = c.toArray();

int numNew = a.length;

ensureCapacityInternal(size + numNew); // Increments modCount

System.arraycopy(a, 0, elementData, size, numNew);

size += numNew;

return numNew != 0;

}

ensureCapacityInternal(size + numNew);这行代码是不是经常看到,不用我多说想必大家也知道了,没错,就是判断当前的集合是否可以装下这些数据,是否需要扩容,接下来就是数据添加了,添加的方式就是通过数据拷贝,这里的拷贝同样属于浅拷贝。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

System.out.println(“list:” + list);

ArrayList list2 = new ArrayList();

list2.add(“随风起舞”);

lis2t.add(“穷凶极恶”);

list.addAll(list2);

System.out.println(“添加之后的lsit:”+list);

运行结果

list:[小明, 逆天而行]

添加之后的lsit:[小明, 逆天而行, 随风起舞, 穷凶极恶]

Process finished with exit code 0

addAll(int index, Collection<? extends E> c)


这个方法其实和上面那个也是大同小异,就是添加集合,但是这里的添加方式有点区别,这里可以指定下标。

源码如下

public boolean addAll(int index, Collection<? extends E> c) {

rangeCheckForAdd(index);

Object[] a = c.toArray();

int numNew = a.length;

ensureCapacityInternal(size + numNew); // Increments modCount

int numMoved = size - index;

if (numMoved > 0)

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

numMoved);

System.arraycopy(a, 0, elementData, index, numNew);

size += numNew;

return numNew != 0;

}

它会往你指定的下标添加集合元素,原本属于当前下标的元素向后移动,移动方式也是通过数据拷贝事项的。

使用方法

ArrayList list = new ArrayList(10);

list.add(“小明”);

list.add(“逆天而行”);

list.add(“铁血无双”);

System.out.println(“list:” + list);

ArrayList list2 = new ArrayList();

list2.add(“随风起舞”);

list2.add(“穷凶极恶”);

list.addAll(1,list2);

System.out.println(“添加之后的lsit:”+list);

运行结果

list:[小明, 逆天而行, 铁血无双]

添加之后的lsit:[小明, 随风起舞, 穷凶极恶, 逆天而行, 铁血无双]

Process finished with exit code 0

我们插入的下标位置为1,这个时候ArrayList就将list2这两个元素从下标1开始往后田间,冲突的元素就往后移,直到没有冲突为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值