👏作者简介:大家好,我是小白,一名Java练习生,喜欢唱跳rap篮球
🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
📝联系方式:19177258062,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
目录
E.解析抛出异常的checkForComodification方法
B.ArrayList(int initialCapacity)构造方法
D.addAll(int index, Collection c)
3.ArrayList频繁扩容导致添加性能急剧下降,如何处理?
4.ArrayList插入或删除元素一定比LinkedList慢吗
1.引言
欢迎来到我们的Java之旅第六篇!今天,我们将深入探讨Java集合框架中的一个重要部分——List集合,重点介绍ArrayList。我们不仅会了解它的基本概念,还会深入其源码,揭示其内部工作原理。并且也会介绍LinkedList集合以及其和ArrayList的区别,最后会通过编写一个学生管理系统来巩固之前所学的知识。干货满满,准备好了吗?让我们开始吧!🚀
2.集合的概念【理解】
在Java世界中,集合(Collection)就像是储物箱,可以用来存放各种物品,无论是袜子、玩具还是旧报纸。集合与数组的主要区别在于:集合的大小是可以动态变化的,而数组的大小是固定的。也就是说,集合更像是一个可以拉伸的魔法箱子,而数组则像是一个固定大小的收纳盒。
3.集合体系结构 【理解】
Java集合框架的结构就像一个庞大的家族,家族里有三个主要分支:List、Set和Map。
- List:有序的、可重复的元素集合。就像你按顺序收藏的漫画书,可以有重复的。
- Set:无序的、不重复的元素集合。像你妈的钥匙链,每把钥匙都独一无二。
- Map:键值对集合,键不能重复,值可以重复。想象一下你的电话本,每个人的名字(键)都是唯一的,但电话号码(值)可以重复。
4.常用List集合 【理解】
以下的源码解读都是基于JDK1.8
(1)List集合的特点【理解】
- 有序:元素按照插入顺序排列,就像你按顺序放在书架上的书。
- 可重复:允许存储重复的元素,就像你可以有好几本同样的书。
- 允许空元素:可以包含空元素,就像书架上可以有空位置。
(2)ArrayList【理解】
在本模块我们首先会介绍集合中的ArrayList集合以及教大家其基本使用,最后会带着大家一起探讨ArrayList的底层源码实现,有同学会问为什么要看源码呢?答:因为面试必问
A.ArrayList类概述【理解】
ArrayList
是Java集合框架中的一个动态数组实现。它就像一个会自动扩展的魔法背包,当你往里面塞更多东西时,它会自动变大。
B.ArrayList类常用方法【应用】
1.构造方法
方法名 | 说明 |
---|---|
public ArrayList() | 创建一个空的集合对象 |
2.成员方法
方法名 | 说明 |
---|---|
public boolean remove(Object o) | 删除指定的元素,返回删除是否成功 |
public E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
public E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
public E get(int index) | 返回指定索引处的元素 |
public int size() | 返回集合中的元素的个数 |
public boolean add(E e) | 将指定的元素追加到此集合的末尾 |
public void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
3.示例代码
public class ArrayListDemo02 {
public static void main(String[] args) {
//创建集合
ArrayList<String> array = new ArrayList<String>();
//添加元素
array.add("hello");
array.add("world");
array.add("java");
//public boolean remove(Object o):删除指定的元素,返回删除是否成功
// System.out.println(array.remove("world"));
// System.out.println(array.remove("javaee"));
//public E remove(int index):删除指定索引处的元素,返回被删除的元素
// System.out.println(array.remove(1));
//IndexOutOfBoundsException
// System.out.println(array.remove(3));
//public E set(int index,E element):修改指定索引处的元素,返回被修改的元素
// System.out.println(array.set(1,"javaee"));
//IndexOutOfBoundsException
// System.out.println(array.set(3,"javaee"));
//public E get(int index):返回指定索引处的元素
// System.out.println(array.get(0));
// System.out.println(array.get(1));
// System.out.println(array.get(2));
//System.out.println(array.get(3)); //?????? 自己测试
//public int size():返回集合中的元素的个数
System.out.println(array.size());
//输出集合
System.out.println("array:" + array);
}
}
C.常用的遍历方法【应用】
1.使用索引遍历
通过for循环+索引遍历
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("呀");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
2.使用迭代器遍历
(1)基本使用
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("啊");
Iterator<String> it = list.iterator();//创建迭代器对象
while (it.hasNext()){//hasNext()判断是否有下一个元素
String s = it.next();//next()获取下一个元素
System.out.println(s);
}
}
(2)迭代器图解
指针一开始在list前面,it.hasNext()判断下一个是“你”即有值,
it.next()取出下一个值"你",指针往后移到“你”的位置
it.hasNext()判断下一个值是“好”即有值,it.next()取出下一个值"好",指针往后移到“好”的位置
(3)并发修改异常
何时出现:当我们通过迭代器遍历对应集合时,对这个集合进行了增删等操作,就会出现并发修改异常
解决办法:创建一个用于储存需要删除元素的新集合removeList,在用list集合的迭代器遍历list集合时,当出现”好“字就把其加入到removeList中,此时并不会报并发修改异常,这是因为我们在list的迭代器遍历中对removeList集合进行增加元素,改变的是removeList的长度,并不是改变list的长度,最后通过removeAll删除removeList集合即可
import java.util.ArrayList;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("你");
list.add("好");
list.add("呀");
Iterator<String> iterator = list.iterator();
ArrayList<String> removeList = new ArrayList<>();//用来存储需要删除的元素
//当遍历出现“好”字时,从集合中删除
while (iterator.hasNext()){
String next = iterator.next();
if (next.equals("好"))
removeList.add(next);//将需要删除的元素添加到removeList中
}
list.removeAll(removeList);//将removeList传入list的removeAll方法进行删除
System.out.println(list);
}
}
(4)并发修改异常源码讲解
在每次使用next()方法时,都会校验modCount与expectModCount是否相等,若是中间有对集合的remove和add等操作,则modCount会进行加1操作,下次校验不会通过,抛出并发修改异常。foreach底层使用的是迭代器遍历,因此依然会出现此异常。普通for循环不使用迭代器,因此可以在遍历过程中修改集合。即只要是迭代器遍历,就不允许在迭代过程中使用除迭代器自己的remove方法之外的方法修改集合结构,除非设法越过检测。
A.异常信息解释
当我们用迭代器遍历集合时,并对集合进行删除操作会出现并发修改异常,下图显示堆栈错误信息,
- IteratorTest.main(IteratorTest.java:13) //最外层在IteratorTest类下的main方法中的13行出现问题
- java.util.ArrayList$Itr.next(ArrayList.java:859)//第二层在ArrayList的类中的Itr类的next方法中第859行出现问题
- java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)//最里面的第三层在ArrayList的类中的Itr类 的checkForComodification方法中第909行出现问题
- 由上面的报错信息,我们可以知道Itr类是ArrayList类的成员内部类,在Itr类中有next方法和checkForComodification方法,其中的checkForComodification方法出现问题,所以抛出异常
B.解析Iterator接口
在main方法中第13行报错,问题出现在iterator.next()这,点击next方法进入内部,发现这个方法从属于Iterator这个接口,接口作用是定义抽象方法,关键的是其实现的子类,所以现在就去找其实现子类
C.解析Iterator接口的实现子类
回到main方法中,iterator.next和iterator.hasNext都属于Iterator接口,所以继续往上找,有个list.iterator(),进入list.iterator()方法
这个方法属于ArrayList类的成员方法,返回值的类型是Iterator接口,返回的值是一个Itr的对象,此时就可以知道这个Itr就是Iterator接口的实现子类,此时我们再去看Itr这个类
D.解析Iterator接口的实现子类Itr
由下图可以看到,这个Itr类是ArrayList类的成员内部类,并且这个类实现了Iterator接口,这个类里有在堆栈错误信息出现的next和checkForComodification方法,
E.解析抛出异常的checkForComodification方法
从next方法左键点击快速找到这个方法,其抛出异常的原因是因为modCount!=expectedModCount,那么此时就需要去解决两个问题
-
1.这两个值表示含义是什么
-
2.这两个值何时出现不相等的情况
F.解析expectedModCount变量
在checkForComodification方法中点击expectedModCount变量快速找到其位置,发现这个变量是Itr类的成员变量,他的初始值为modCount,那么此时modCount=expectedModCount=0,就不会抛出异常,那么肯定是后面我们的modCount变量发生了改变,从而与expectedModCount变量不等
G.解析modCount变量含义
在checkForComodification方法中点击modCount变量快速找到其位置,发现这个变量是AbstractList类的成员变量,我们将其上面的注解去翻译一下,提炼出下面画黄线的信息,这个变量用来表示此列表的结构修改次数,一旦这个值发生更改,就会抛出并发修改异常,此时我们去看这个变量什么时候发生改变
H.解析modCount变量何时改变
由上面的翻译我们可以知道这个变量用来表示此列表的结构修改次数,结构修改是指哪些改变列表大小或以其他方式干扰列表的修改,像remove,add等方法都可以改变列表大小,即修改modCount的值,那我们在main方法中,通过list.remove方法改变了list的大小,从这入手,进入remove方法内部
进入看到的第一个remove方法并没有modCount变量,往上后往下看看还有没有其他的方法,刚好在其上面也有一个remove方法,里面由modCount++,那么就说明,我们执行了remove方法,这个modCount就会自增,发生改变,那么此时modCount!=expectedModCount,所以抛出并发修改异常
3.使用foreach遍历
foreach是java提供的一个语法糖(封装好的方法)。可以让我们更方便的遍历集合或数组。
格式如下:
for(元素数据类型 变量名 : 遍历的集合或者数组){
//遍历的时候会把遍历到的元素赋值给我们上面定义的变量
}
例如:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("啊");
for (String s : list) {
System.out.println(s);
}
}
String[] arr = {"三","更","草","堂"};
for(String s : arr){
System.out.println(s);
}
4.转换为数组遍历
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("啊");
//把list集合转换为数组 参数列表 返回值类型 []
// Object[] objects = list.toArray();
//遍历数组
// for (int i = 0; i < objects.length; i++) {
// System.out.println(objects[i]);
// }
// public <T> T[] toArray(T[] a)
String[] strings = list.toArray(new String[0]);
for (String string : strings) {
System.out.println(string);
}
}
D.ArrayList存储字符串并遍历【应用】
1..案例需求
创建一个存储字符串的集合,存储3个字符串元素,使用程序实现在控制台遍历该集合
2.代码实现
/*
思路:
1:创建集合对象
2:往集合中添加字符串对象
3:遍历集合,首先要能够获取到集合中的每一个元素,这个通过get(int index)方法实现
4:遍历集合,其次要能够获取到集合的长度,这个通过size()方法实现
5:遍历集合的通用格式
*/
public class ArrayListTest01 {
public static void main(String[] args) {
//创建集合对象
ArrayList<String> array = new ArrayList<String>();
//往集合中添加字符串对象
array.add("刘正风");
array.add("左冷禅");
array.add("风清扬");
//遍历集合,其次要能够获取到集合的长度,这个通过size()方法实现
// System.out.println(array.size());
//遍历集合的通用格式
for(int i=0; i<array.size(); i++) {
String s = array.get(i);
System.out.println(s);
}
}
}
E.ArrayList存储学生对象并遍历【应用】
1.案例需求
创建一个存储学生对象的集合,存储3个学生对象,使用程序实现在控制台遍历该集合
2..代码实现
/*
思路:
1:定义学生类
2:创建集合对象
3:创建学生对象
4:添加学生对象到集合中
5:遍历集合,采用通用遍历格式实现
*/
public class ArrayListTest02 {
public static void main(String[] args) {
//创建集合对象
ArrayList<Student> array = new ArrayList<>();
//创建学生对象
Student s1 = new Student("林青霞", 30);
Student s2 = new Student("风清扬", 33);
Student s3 = new Student("张曼玉", 18);
//添加学生对象到集合中
array.add(s1);
array.add(s2);
array.add(s3);
//遍历集合,采用通用遍历格式实现
for (int i = 0; i < array.size(); i++) {
Student s = array.get(i);
System.out.println(s.getName() + "," + s.getAge());
}
}
}
F.ArrayList存储学生对象并遍历升级版【应用】
1.案例需求
创建一个存储学生对象的集合,存储3个学生对象,使用程序实现在控制台遍历该集合,学生的姓名和年龄来自于键盘录入
2.代码实现
/*
思路:
1:定义学生类,为了键盘录入数据方便,把学生类中的成员变量都定义为String类型
2:创建集合对象
3:键盘录入学生对象所需要的数据
4:创建学生对象,把键盘录入的数据赋值给学生对象的成员变量
5:往集合中添加学生对象
6:遍历集合,采用通用遍历格式实现
*/
public class ArrayListTest {
public static void main(String[] args) {
//创建集合对象
ArrayList<Student> array = new ArrayList<Student>();
//为了提高代码的复用性,我们用方法来改进程序
addStudent(array);
addStudent(array);
addStudent(array);
//遍历集合,采用通用遍历格式实现
for (int i = 0; i < array.size(); i++) {
Student s = array.get(i);
System.out.println(s.getName() + "," + s.getAge());
}
}
/*
两个明确:
返回值类型:void
参数:ArrayList<Student> array
*/
public static void addStudent(ArrayList<Student> array) {
//键盘录入学生对象所需要的数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入学生姓名:");
String name = sc.nextLine();
System.out.println("请输入学生年龄:");
String age = sc.nextLine();
//创建学生对象,把键盘录入的数据赋值给学生对象的成员变量
Student s = new Student();
s.setName(name);
s.setAge(age);
//往集合中添加学生对象
array.add(s);
}
}
G.ArrayList源码探究【掌握】
这一块一定要好好看,吃透了随便面试官怎么问,要读懂ArrayList我们首先要知道ArrayList的体系结构,如下图:ArrayList会继承AbstractList
实现了List
、RandomAccess
、Cloneable
、Serializable
接口,接下来我们将讲解每个接口的含义及其作用
1.Serializable
接口解读
Serializable接口是Java中的一个标记接口,它的作用是让一个类的对象能够被序列化和反序列化。用通俗的话来说,就是让对象可以像快递包裹一样被打包和拆包,以便传输或者保存。
想象一下,你有一个非常珍贵的玩具(对象),你想把它寄给住在另一个城市的朋友(在网络上传输对象)。你需要做的就是把这个玩具装进一个盒子(序列化),然后寄给朋友。朋友收到包裹后,把玩具从盒子里拿出来(反序列化),就可以像你一样玩这个玩具了。
实现了Serializable接口的类就相当于打包玩具的盒子,这个盒子告诉Java:这个玩具(对象)可以被安全地打包和寄送。
举个栗子:
import java.io.*;
// 定义一个实现Serializable接口的类
class Dog implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{name='" + name + "', age=" + age + '}';
}
}
public class SerializationDemo {
public static void main(String[] args) {
Dog dog = new Dog("Buddy", 3);
// 序列化对象
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dog.ser"))) {
oos.writeObject(dog);
System.out.println("序列化成功:" + dog);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dog.ser"))) {
Dog deserializedDog = (Dog) ois.readObject();
System.out.println("反序列化成功:" + deserializedDog);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
代码解释
- 定义类:
Dog
类实现了Serializable
接口。为了保证序列化的版本一致性,定义了一个serialVersionUID
。 - 创建对象:在
main
方法中,创建了一个Dog
对象dog
。 - 序列化:
- 使用
ObjectOutputStream
将dog
对象写入文件dog.ser
。 writeObject(dog)
方法执行对象的序列化,把对象转换成字节流写入文件。
- 使用
- 反序列化:
- 使用
ObjectInputStream
从文件dog.ser
中读取对象。 readObject()
方法将字节流转换回Dog
对象。
- 使用
通俗解释
- 序列化:把
Dog
对象装进盒子里(转换成字节流),并把这个盒子保存到文件中。 - 反序列化:把保存的盒子从文件中取出来,再把
Dog
对象从盒子里拿出来(从字节流恢复成对象)。
这就像是把你的宠物狗拍成快照保存下来,然后再用这张快照把狗“还原”出来。
2.Cloneable
接口解读
- Cloneable接口是Java中一个标记接口,它的作用是允许对象通过克隆(即复制)来创建一个新的实例。通俗地说,Cloneable接口就像是一个标志,告诉Java这个对象可以被复制。
- 想象一下,你有一个玩具,你想要一个一模一样的副本。实现Cloneable接口的类,就像是告诉Java:“嘿,我这个玩具可以被复制,给我来一个一模一样的副本吧!”。
- 说到拷贝,在Java中,拷贝对象主要有两种方式:浅拷贝和深拷贝。他们需要实现Cloneable接口并重写clone方法
- 浅拷贝:复制对象时,只复制对象的基本类型字段,对于引用类型字段,只复制引用,而不复制引用指向的对象。这就像是把一张照片复印了一份,但照片里的人还是同一个人。
- 深拷贝:不仅复制对象本身,还递归地复制所有引用类型字段所指向的对象。这就像是复制了一份一模一样的房子,房子里的每个家具也都一一复制了。
举个浅拷贝的例子
class Dog implements Cloneable {
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
@Override
public String toString() {
return "Dog{name='" + name + "', age=" + age + '}';
}
}
public class ShallowCopyDemo {
public static void main(String[] args) {
try {
Dog dog1 = new Dog("Buddy", 3);
Dog dog2 = (Dog) dog1.clone();
System.out.println("Dog1: " + dog1);
System.out.println("Dog2: " + dog2);
// 修改dog1的属性,查看dog2是否变化
dog1.name = "Max";
System.out.println("After modifying Dog1");
System.out.println("Dog1: " + dog1);
System.out.println("Dog2: " + dog2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
//运行效果
Dog1: Dog{name='Buddy', age=3}
Dog2: Dog{name='Buddy', age=3}
After modifying Dog1
Dog1: Dog{name='Max', age=3}
Dog2: Dog{name='Buddy', age=3}
解释
Dog
类实现了Cloneable
接口,并重写了clone
方法,进行浅拷贝。- 创建
dog1
对象并克隆为dog2
。 - 修改
dog1
的name
属性,dog2
的name
属性没有变化,证明是浅拷贝,引用类型字段没有被复制。
举个深拷贝的例子
class Dog implements Cloneable {
String name;
int age;
Owner owner; // 引用类型字段
public Dog(String name, int age, Owner owner) {
this.name = name;
this.age = age;
this.owner = owner;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Dog clonedDog = (Dog) super.clone(); // 浅拷贝
clonedDog.owner = (Owner) owner.clone(); // 深拷贝
return clonedDog;
}
@Override
public String toString() {
return "Dog{name='" + name + "', age=" + age + ", owner=" + owner + '}';
}
}
class Owner implements Cloneable {
String name;
public Owner(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
@Override
public String toString() {
return "Owner{name='" + name + "'}";
}
}
public class DeepCopyDemo {
public static void main(String[] args) {
try {
Owner owner = new Owner("John");
Dog dog1 = new Dog("Buddy", 3, owner);
Dog dog2 = (Dog) dog1.clone();
System.out.println("Dog1: " + dog1);
System.out.println("Dog2: " + dog2);
// 修改dog1的owner属性,查看dog2是否变化
dog1.owner.name = "Mike";
System.out.println("After modifying Dog1's owner");
System.out.println("Dog1: " + dog1);
System.out.println("Dog2: " + dog2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
//运行效果
Dog1: Dog{name='Buddy', age=3, owner=Owner{name='John'}}
Dog2: Dog{name='Buddy', age=3, owner=Owner{name='John'}}
After modifying Dog1's owner
Dog1: Dog{name='Buddy', age=3, owner=Owner{name='Mike'}}
Dog2: Dog{name='Buddy', age=3, owner=Owner{name='John'}}
解释
Dog
类和Owner
类都实现了Cloneable
接口,并重写了clone
方法。- 在
Dog
类的clone
方法中,调用了owner
对象的clone
方法,实现了深拷贝。 - 创建
dog1
对象并克隆为dog2
。 - 修改
dog1
的owner
属性,dog2
的owner
属性没有变化,证明是深拷贝,引用类型字段被递归复制。
3.RandomAccess
接口解读
(1)接口解读
RandomAccess
接口是Java集合框架中的一个标记接口,用于标记那些支持快速(常数时间)随机访问的列表(如ArrayList
)。实现这个接口的列表可以快速地通过索引访问其元素。
想象一下你在图书馆里找一本书。如果图书馆的书是按照字母顺序排列的,那么你可以很快地找到你想要的那本书,因为你知道它大概在什么位置。这就类似于实现了RandomAccess
接口的列表,可以很快地通过索引(位置)找到你想要的元素。与之相对,如果图书馆的书是按照借书的顺序堆在一起的,那么找到某本书就需要翻阅每一本书,这种情况下随机访问的速度就会很慢。
以下是一个示例代码,展示如何利用这个接口来区分不同的列表实现。并且由结果可以看到,相同数据量下,实现了RandomAccess
接口的ArrayList遍历完数据的速度更快
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.RandomAccess;
public class RandomAccessDemo {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// 填充列表
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
linkedList.add(i);
}
// 测试ArrayList
testList(arrayList);
// 测试LinkedList
testList(linkedList);
}
private static void testList(List<Integer> list) {
long startTime = System.nanoTime();
if (list instanceof RandomAccess) {
System.out.println("This list supports fast random access: " + list.getClass().getSimpleName());
// 快速随机访问
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
} else {
System.out.println("This list does NOT support fast random access: " + list.getClass().getSimpleName());
// 顺序访问
for (Integer integer : list) {
// 顺序访问
}
}
long endTime = System.nanoTime();
System.out.println("Time taken: " + (endTime - startTime) + " ns");
}
}
//运行效果
This list supports fast random access: ArrayList
Time taken: 536148 ns
This list does NOT support fast random access: LinkedList
Time taken: 538656 ns
解释
ArrayList
实现了RandomAccess
接口,所以它支持快速的随机访问。LinkedList
没有实现RandomAccess
接口,所以它不支持快速的随机访问。testList
方法通过instanceof
关键字检查列表是否实现了RandomAccess
接口,并根据结果选择不同的访问方式。- 通过比较两种访问方式的时间,可以看出
ArrayList
在随机访问时的性能优势。
(2)RandomAccess
接口的使用场景(重要)
实际开发过程中:可能遇到的情况
- 已有的接口数据返回的是所有数据,但是我们只想获取其中的一条数据,也就是说要进行遍历
- 已有的多组数据需要进行合并,需要进行再次封装,同样可能需要遍历(建议通过sql返回的数据就是我们想要的数据)
鉴于上面的访问效率对比,建议能使用sql解决的问题不要用java代码进行遍历!如果实在没办法必须遍历,则对返回的数据进行instanceof判断
如果实现了RandomAccess接口。则使用集合的索引进行遍历
for (int i=0, n=list.size(); i < n; i++)
list.get(i); //随机获取
如果没有实现该接口则通过迭代器遍历
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();//顺序获取
也就是说代码应该这样写 推荐的代码
List list = new ArrayList();/* 通过dao获取的集合数据 */
if (list instanceof RandomAccess) { //判断是否实现了RandomAccess接口
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i);
/**
* 逻辑代码
*/
}
} else {
for (Iterator i = list.iterator(); i.hasNext(); ) {
Object obj = i.next();
/**
* 逻辑代码
*/
}
}
4.ArrayList中的各个变量
大家要对ArrayList的这几个成员变量有点印象,后续都会围绕这些变量去讲解
//长度为0的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量为0的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//实际上存储元素数据的数组
transient Object[] elementData;
//集合的长度
private int size;
//集合默认容量
private static final int DEFAULT_CAPACITY = 10;
5.真正开始源码解读
我猜小伙伴们已经迫不及待了,前面铺垫了这么久,终于开始讲源码了,话不多说,我们这就开始。
(1)构造方法源码阅读
在使用ArrayList时,我们通过new关键字去调用其构造方法来创建对象,所以我们先从ArrayList的构造方法源码看起,ArrayList有三个构造函数,分别如下
Constructor | Constructor描述 |
ArrayList() | 构造一个空列表 |
ArrayList(int initialCapacity) | 构造具有指定初始容量的空列表 |
ArrayList(Collection<? extends E> c) | 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序 |
A.ArrayList()构造方法
/**
* Constructs an empty list with an initial capacity of ten.
*/
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认的空容量的数组
//elementData:集合真正存储数据的容器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这个构造方法是ArrayList
的无参构造方法。当你使用无参构造方法创建一个ArrayList
时,Java会执行以下步骤:
-
调用无参构造方法:当你写
new ArrayList<>()
时,Java会调用这个无参构造方法。 -
初始化
elementData
:this.elementData
是一个数组,存储ArrayList
中的所有元素。DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一个空数组(具体来说,是一个长度为0的数组)。
所以,这个无参构造方法的作用就是初始化一个空的ArrayList
,准备好一个空数组来存放将来可能添加的元素。
举个例子
import java.util.ArrayList;
public class ArrayListDemo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(); // 使用无参构造方法
System.out.println("初始大小: " + list.size()); // 初始大小为0
list.add("Java");
list.add("Python");
list.add("C++");
System.out.println("添加元素后的大小: " + list.size()); // 添加元素后的大小为3
System.out.println("列表内容: " + list); // 列表内容: [Java, Python, C++]
}
}
//运行结果
初始大小: 0
添加元素后的大小: 3
列表内容: [Java, Python, C++]
B.ArrayList(int initialCapacity)构造方法
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传进来的变量大于0,则初始化一个指定容量的空数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//传进来的变量=0,则不去创建新的数组,直接将已创建的EMPTY_ELEMENTDATA空数组传给
//ArrayList
this.elementData = EMPTY_ELEMENTDATA;
} else {
//传进来的指定数组容量不能<0
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
这个构造方法允许你在创建ArrayList
时指定初始容量。让我们分步解释这个方法的逻辑:
-
传入初始容量:当你创建
ArrayList
时,可以指定一个初始容量,例如new ArrayList<>(10)
。 -
判断初始容量:
- 初始容量 > 0:
- 如果传入的初始容量大于0,构造方法会创建一个长度为初始容量
initialCapacity
的数组elementData
- 例如,传入10,就会创建一个长度为10的数组,用来存储将来的元素。
- 如果传入的初始容量大于0,构造方法会创建一个长度为初始容量
- 初始容量 == 0:
- 如果传入的初始容量是0,构造方法会将
elementData
设置为一个预定义的空数组EMPTY_ELEMENTDATA
。这表示列表最初是空的,没有预分配空间。
- 如果传入的初始容量是0,构造方法会将
- 初始容量 < 0:
- 如果传入的初始容量是负数,构造方法会抛出
IllegalArgumentException
异常,提示"非法容量"。这防止了创建一个无效大小的数组。
- 如果传入的初始容量是负数,构造方法会抛出
- 初始容量 > 0:
举个例子
import java.util.ArrayList;
public class ArrayListWithCapacityDemo {
public static void main(String[] args) {
try {
ArrayList<String> listWithCapacity = new ArrayList<>(10); // 初始容量为10
System.out.println("初始容量为10的ArrayList创建成功");
ArrayList<String> emptyList = new ArrayList<>(0); // 初始容量为0
System.out.println("初始容量为0的ArrayList创建成功");
ArrayList<String> invalidList = new ArrayList<>(-5); // 初始容量为负数
} catch (IllegalArgumentException e) {
System.out.println("捕获异常: " + e.getMessage());
}
}
}
//运行结果
初始容量为10的ArrayList创建成功
初始容量为0的ArrayList创建成功
捕获异常: Illegal Capacity: -5
C.ArrayList(Collection<? extends E> c)构造方法
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
//将构造方法中的参数转换成数组形式,其底层是调用了System.arraycopy()
elementData = c.toArray();
//将数组的长度赋值给size
if ((size = elementData.length) != 0) {
//检查elementData是不是Object[]类型,不是的话将其转换成Object[].class类型
if (elementData.getClass() != Object[].class)
//数组的创建与拷贝
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
//size为0,则把已创建好的空数组直接给它
this.elementData = EMPTY_ELEMENTDATA;
}
}
这个构造方法允许你使用一个现有的集合来初始化ArrayList
。让我们分步解释这个方法的逻辑:
-
传入集合:当你创建
ArrayList
时,可以传入一个现有的集合,例如new ArrayList<>(existingCollection)
。 -
转换为数组:使用集合的
toArray()
方法将其转换为数组,并将这个数组赋值给elementData
。elementData
是存储ArrayList
元素的内部数组。toArray()
方法将集合中的所有元素复制到一个新数组中。
-
设置大小:将数组的长度赋值给
size
,表示ArrayList
的当前大小。 -
检查数组类型:
- 如果
elementData
的类型不是Object[]
,则将其转换为Object[]
。 - 这是为了确保内部数组的类型始终是
Object[]
,以便在以后可以存储任何类型的对象。
- 如果
-
处理空集合:
- 如果传入的集合是空的(
size
为0),则将elementData
设置为一个预定义的空数组EMPTY_ELEMENTDATA
。
- 如果传入的集合是空的(
举个例子
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class ArrayListFromCollectionDemo {
public static void main(String[] args) {
// 创建一个现有的集合
Collection<String> existingCollection = Arrays.asList("Java", "Python", "C++");
// 使用现有集合初始化ArrayList
ArrayList<String> listFromCollection = new ArrayList<>(existingCollection);
// 打印ArrayList的内容
System.out.println("列表内容: " + listFromCollection);
// 创建一个空的集合
Collection<String> emptyCollection = Arrays.asList();
// 使用空集合初始化ArrayList
ArrayList<String> emptyList = new ArrayList<>(emptyCollection);
// 打印空ArrayList的内容
System.out.println("空列表内容: " + emptyList);
}
}
//运行结果
列表内容: [Java, Python, C++]
空列表内容: []
(2)添加方法
方法名 | 描述 |
add(E e) | 将指定的元素追加到此列表的末尾 |
add(int index, E element) | 在此列表中的指定位置插入指定的元素 |
addAll(Collection<? extends E> c) | 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾 |
addAll(int index, Collection<? extends E> c) | 将指定集合中的所有元素插入到此列表中,从指定的位置开始 |
我们将会围绕下面代码开始add方法的源码讲解,注意此时数组所能接受数据的个数我们初始化为5,也就是他目前可以存储5个数据,之后我们会往数组中添加1个数据,那么实际上数组大小为1
A.add(E e)
ArrayList的添加方法有四种形式,不过我们一般都是用第一种形式。在源码中,add(E e)方法,先执行的是ensureCapacityInternal()方法,这个方法是进行扩容判断,如果需要扩容就先进行扩容操作。扩容完之后再将我们需要添加的元素加入到elementData数组中,最后返回true表明添加成功
//将指定的元素追加到此列表的末尾
public boolean add(E e) {
//每增加1个元素,数组所需容量+1,并检查增加数组容量后是否要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//添加元素
elementData[size++] = e;
return true;
}
当我们调用add方法时,此时的size也就是数组实际大小是0,此时会进入到ensureCapacityInternal()方法中,并且实参为size+1也就是1
接下来我们来看ensureCapacityInternal(),先由calculateCapacity()方法计算出所需要的最小空间大小,再由ensureExplicitCapacity方法来判断是否需要扩容,计算应该扩容成多大,扩容方法就在ensureExplicitCapacity方法中.
/**
* minCapacity表示list所需要的最小空间,比如当前list里面放了5个数,需要的空间大小就是5,你执行add方法添加一个数,需要的空间就是6了,需要加一
* @param minCapacity
*/
private void ensureCapacityInternal(int minCapacity) {
// eg1:第一次新增元素,calculateCapacity方法返回值为DEFAULT_CAPACITY=10
// 扩容操作就在这个方法里面, elementData就是存放数据的数组,minCapacity就是存放数据所需的最小地址
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
此时elementData就是我们实际保存数据的数组,minCapacity表示list所需的最小空间,此时为1,也就是上一步传过来的size+1
我们再来看calculateCapacity(elementData, minCapacity) 是如何计算出存放元素需要的最小空间
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA为常量 {} ,判断elementData是不是初始状态,也就是数组大小为0的时候,是true的话,表示这是第一次扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 初始状态扩容,第一次扩容默认是变成10,但是如果所需要的最小空间大于10,第一次扩容就不是变成10了。比如你直接调用addAll()方法,传入一个大小大于10的list进去.
return Math.max(DEFAULT_CAPACITY, minCapacity); // eg1:满足if判断,DEFAULT_CAPACITY=10
}
return minCapacity;
}
因为在没有调用add()方法之前,我们是没有给数组进行初始化操作的,也就是数组中没任何的数据,也就是个空数组,那么就满足
elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时就会进行初始化状态扩容,判断第一次扩容所需要的数组大小是多少,大家要记住,只要一开始数组为空,在第一个初始化扩容时,所需要的最小空间都是默认为10,那如果一开始数组不为空,我们给数组塞了几个数据,那么此时所需的最小空间就是这个minCapacity,如下面第二张图,我们一开始往数组中塞了3个数据,所以此时的minCapacity=size+1=4
当计算出最小空间后,将返回的结果当作参数传入到ensureExplicitCapacity()方法中,执行ensureExplicitCapacity()方法,判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
// eg1: modCount++后,modCount=1
modCount++; // 并发相关
/** 前面的方法都是在计算需要的最小空间,这一步才是关键,判断需要的最小空间是不是大于当前数组大小,如果是那就得扩容数组,这样才能放的下元素 */
if (minCapacity - elementData.length > 0) { // eg1:10-0=10,满足扩容需求
// eg1:minCapacity=10
grow(minCapacity); // 扩容方法
}
}
在ensureExplicitCapacity()方法中,会判断需要的最小空间是不是大于当前数组大小,如果是那就得扩容数组,这样才能放的下元素,此时minCapacity - elementData.length的结果为10-0即是大于0的,说明此时需要扩容,就会进入到grow()方法中
在ensureExplicitCapacity()方法中会通过grow(minCapacity)来进行扩容,接下来详细看grow(minCapacity);扩容方法,计算出数组应该扩容之后的大小
// minCapacity需要的最小空间大小
private void grow(int minCapacity) {
/** 当前数组elementData的长度*/
int oldCapacity = elementData.length; // eg1:oldCapacity=0
/**
* oldCapacity >> 1 相当于除以2(这是右移)
* 000100 >> 1 = 000010
* 000100 << 1 = 001000
*/
/** 新增oldCapacity的一半整数长度作为newCapacity的额外增长长度 */
// 得到新数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1); // eg1:newCapacity=0+(0>>1)=0
/** 如果新的长度newCapacity依然无法满足需要的最小扩容量minCapacity,则新的扩容长度为minCapacity */
if (newCapacity - minCapacity < 0) {
// eg1:newCapacity=10
newCapacity = minCapacity;
}
/** 新的扩容长度newCapacity超出了最大的数组长度MAX_ARRAY_SIZE huge:巨大的 */
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 调用Arrays工具类,创建一个指定大小的数组,再把元素放到新数组上
elementData = Arrays.copyOf(elementData, newCapacity);
}
在grow()方法中会涉及到二进制右移的运算,也就是代码中oldCapacity >> 1,举个例子大家就知道右移了,我们拿十进制11来说,先转成二进制1011,右移规则为:每一位向右移动一位,最左侧补0,最右侧的位丢弃,所以最后的结果为0101,我们再从源码来看,此时需要的最小空间大小minCapacity为10,旧数组长度oldCapacity为0,那么新数组长度newCapacity = 0 + 0 >>1还是0,newCapacity - minCapacity是小于0的,就会重新给新数组长度newCapacity赋值为需要的最小空间大小minCapacity,也就是10,此时我们会调用Arrays工具类,创建一个指定大小的数组,再把元素放到新数组上,由此可知,扩容的本质再创建一个新的数组,并将元素放入到新数组中,只不过新数组的长度需要通过一些规则计算出来
当add,ensureCapacityInternal()指定完成之后,直接把元素加入到数组,返回true.
public boolean add(E e) {
/** 确定是否需要扩容,如果需要,则进行扩容操作. size表示当前list的大小,size+1就是list所需要的空间大小 */
ensureCapacityInternal(size + 1); // Increments modCount!!
// eg1:size=0,elementData[0]="a1",然后a自增为1
elementData[size++] = e;
return true;
}
我们总结一下流程:
- 每增加一个元素,都要判断增加后容量是否达到了规定的最大容量值,如果达到就触发扩容操作grow
- 在扩容操作中,设置新容量为老容量的1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1)),如果新容量小于所需容量,则新容量等于所需容量(newCapacity = minCapacity),如果新容量比规定的最大值还要大,则新容量等于最大容量newCapacity = hugeCapacity(minCapacity);
- 将原数组拷贝进长度为新容量的新数组中
B.add(int index, E element)
在指定索引处添加元素
public void add(int index, E element) {
//判断所传入的索引是否符合规则(既不能<0,也不能大于原数组容量)
rangeCheckForAdd(index);
//数组所需容量+1,并判断增加元素之后是否需要扩容
ensureCapacityInternal(size + 1);
//数组元素拷贝,index之后的元素向后移一位,把index的位置空了出来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在index位置上填充要插入的元素
elementData[index] = element;
//元素个数+1
size++;
}
//判断index是否符合规则
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
我们总结一下流程:
- 检查传入index是否符合规则
- 判断数组是否需要进行扩容
- 创建新数组,并将原数组元素进行拷贝,拷贝原理是使index之后的元素向后移动一位,将index位置空出,再在该位置填充元素
- 元素个数+1
C.addAll(Collection<? extends E> c)
按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾
public boolean addAll(Collection<? extends E> c) {
//将有数据的集合c转换成数组形式
Object[] a = c.toArray();
//将数据集合长度赋值给numNew
int numNew = a.length;
//判断增加元素后是否需要扩容
ensureCapacityInternal(size + numNew);
//将a拷贝进elementData最后
System.arraycopy(a, 0, elementData, size, numNew);
//数组中元素个数=原数组中元素个数+新数组中元素个数
size += numNew;
//c容量为0则返回false,容量不为0则返回true
return numNew != 0;
}
我们总结一下流程:
- 将原集合转换为数组形式
- 判断数组是否需要扩容
- 将传进来的集合元素拷贝到原集合的末尾
- 元素个数变化
D.addAll(int index, Collection<? extends E> c)
将指定集合中的所有元素插入到此列表中,从指定的位置开始
public boolean addAll(int index, Collection<? extends E> c) {
//判断index是否符合规则
rangeCheckForAdd(index);
//将要添加的集合转为数组形式
Object[] a = c.toArray();
//将数组长度赋给numNew
int numNew = a.length;
//判断增加元素之后是否需要扩容
ensureCapacityInternal(size + numNew); \
//记录有多少位元素需要向后移动
int numMoved = size - index;
//如果有元素需要移动
if (numMoved > 0)
//进行数组拷贝,这一步只是进行移动操作,并没有添加数据,将数组中index之后的所有元素向后移动numNew位
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//进行数组填充,将集合c中的元素从数组index处开始填充
System.arraycopy(a, 0, elementData, index, numNew);
//数组中元素个数=原数组中元素个数+新数组中元素个数
size += numNew;
//返回c集合是否为空的布尔值
return numNew != 0;
}
我们总结一下流程:
- 将原集合转换为数组形式
- 判断数组是否需要扩容
- 记录下需要有多少元素进行移动
- 如果有元素需要移动,新数组中index之后的所有元素向后移动numNew位
- 对数组进行填充
- 数组元素个数变化
(3)set(int index, E element)
根据索引修改集合元素
public E set(int index, E element) {
//判断索引是否符合规则,索引不能超过数组长度
rangeCheck(index);
//获得下标处的元素
E oldValue = elementData(index);
//修改索引处的元素值
elementData[index] = element;
//将旧的元素值返回
return oldValue;
}
//校验索引
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//获得下标处的元素
E elementData(int index) {
return (E) elementData[index];
}
(4)get(int index)
获得索引处的元素值
/**
* 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) {
rangeCheck(index);
return elementData(index);
}
//校验索引范围
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//返回索引处元素
E elementData(int index) {
return (E) elementData[index];
}
(5)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 remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
//对集合的实际修改次数+1
modCount++;
//计算要移动的元素的个数
int numMoved = size - index - 1;
//如果移动的元素的个数>0
if (numMoved > 0)
//移动元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将要删除的元素置为null,就是为了尽快被垃圾回收机制回收
elementData[--size] = null;
}
(6)clear()
public void clear() {
//修改次数
modCount++;
//将数组中的每个元素都设为null
for (int i = 0; i < size; i++)
elementData[i] = null;
//数组元素个数清空
size = 0;
}
H.ArrayList常见面试题
1.当使用无参构造创建ArrayList其大小是多少
这里有一个细节,当使用无参构造创建出来的ArrayList就是个空数组,只有当我们调用add()方法添加第一个元素时,才会扩容到10
2.ArrayList是如何扩容的
第一次扩容10 以后每次都是原容量的1.5倍
3.ArrayList频繁扩容导致添加性能急剧下降,如何处理?
我们通过一个案例解答
public class Test04 {
public static void main(String[] args) {
//未指定容量的list
ArrayList<String> list = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
list.add(i+"");
}
long endTime = System.currentTimeMillis();
System.out.println("未指定容量时添加100W条数据用时:"+(endTime-startTime));
//指定容量的list1
ArrayList<String> list1 = new ArrayList<>(1000000);
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
list.add(i+"");
}
long endTime1 = System.currentTimeMillis();
System.out.println("指定容量时添加100W条数据用时:"+(endTime1-startTime1));
}
}
//运行结果
未指定容量时添加100W条数据用时:95
指定容量时添加100W条数据用时:69
由此我们可得,在数据量较大时,为了避免扩容,我们最好创建ArrayList时便指定容量
4.ArrayList插入或删除元素一定比LinkedList慢吗
首先我们要确定ArrayList和LinkedList的底层数据结构
- ArrayList数据结构是·动态数组·
- LinkedList数据结构是·链表·
我们从源码层面去分析
观察LinkedList的remove源码
public E remove(int index) {
//对索引的校验,我们这里不做讨论
checkElementIndex(index);
//主要看node(int index)方法
return unlink(node(index));
}
//根据索引虽然可以直接去除一半的元素参与判断,但是效率依然很低,需要不断遍历
Node<E> node(int index) {
//这里是判断index在链表的前半部分还是后半部分,因为是双向的
//判断index是否小于集合长度的一半
if (index < (size >> 1)) {
//在前半部分的话,第一个节点就为x,从前往后找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//在后半部分的话,最后一个节点为x,从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//通过node(int index)方法找到了index所在节点的位置
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
//获取下一个节点
final Node<E> next = x.next;
//获取上一个节点
final Node<E> prev = x.prev;
//如果x在链表的开头
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//如果x在链表的末尾
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
由此我们可以得出结论:
- 要删除的节点位于集合中间位置,则LinkedList的速度慢
- 要删除的节点位于集合的头部和尾部,则LinkedList的速度快(如果不从添加元素时就开始计算时间的话是这样,但是如果要把添加元素的时间计算在内那么ArrayList快,因为LinkedList中的last节点每次在add元素时都会向后移动一位,也是需要花费时间的)
5.ArrayList是线程安全的吗
先回答问题:ArrayList不是线程安全的,我们使用一个案例来演示
package ArrayListProject.CloneTest;
import java.util.ArrayList;
import java.util.List;
public class CollectionTask implements Runnable {
private List<String> list;
public CollectionTask(List<String> list) {
this.list = list;
}
@Override
public void run() {
try {
Thread.sleep(50);
}catch (Exception e){
e.printStackTrace();
}
list.add(Thread.currentThread().getName());
}
}
class MyTest{
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
CollectionTask ct = new CollectionTask(list);
//开启50条线程
for (int i = 0; i < 50; i++) {
new Thread(ct).start();
}
//确保子线程执行完毕
Thread.sleep(3000);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("集合长度:"+list.size());
}
}
Thread-47
Thread-45
Thread-44
Thread-41
Thread-34
Thread-35
Thread-39
Thread-33
Thread-27
Thread-26
Thread-40
Thread-32
Thread-20
Thread-21
Thread-15
Thread-29
Thread-14
Thread-9
Thread-8
Thread-28
Thread-23
Thread-3
null
null
null
null
null
null
null
Thread-22
Thread-38
Thread-49
Thread-48
null
null
null
null
null
null
null
null
null
null
null
null
null
null
null
Thread-43
集合长度:49
由输出结果可知:ArrayList是线程不安全的
那么如何避免线程安全问题呢?
方案一:加同步代码块,保证代码在执行时不被其他线程干扰
@Override
public void run() {
synchronized (this){
try {
Thread.sleep(50);
}catch (Exception e){
e.printStackTrace();
}
list.add(Thread.currentThread().getName());
}
}
方案二:使用Vector集合代替ArrayList
- Vector类实现了可扩展的对象数组,并且其add方法上有synchronized关键字,但是其效率相比ArrayList较低,如果不需要线程安全的实现,建议使用ArrayList代替Vector
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
方案三:利用Collections.synchronizedList(List list)方法
List<String> list = new ArrayList<>();
List<String> list1 = Collections.synchronizedList(list);
ArrayList什么情况下需要加同步锁?
ArrayList作为局部变量时不需要加锁,因为它是专属以某一线程的,而成员变量被所有线程共享,ArrayList作为成员变量时需要加同步锁
(3)LinkedList【理解】
A.创建对象
LinkedList list = new LinkedList<>();//不限定集合中存放元素的数据类型
LinkedList<集合元素的数据类型> list2 = new LinkedList<>();//限定集合中存放元素的数据类型
B.常用方法
boolean add(E e) //添加元素,直接添加到集合的末尾 返回值代表是否添加成功
void add(int index, E element) //往指定索引位置添加元素
boolean remove(Object o)// 删除元素
E remove(int index) //删除指定索引位置的元素,返回值是被删除的元素
E set(int index, E element) //修改指定索引位置的元素 返回值为修改之前的元素值
E get(int index) //获取指定索引位置的元素 返回值为对应的元素
int size() //获取集合中元素的个数
boolean contains(Object o) //判断集合中是否存在某个元素 ,返回值代表是否存在
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
//添加元素
list.add("家");
list.add(0,"大");
//删除元素
list.remove("大");
//修改元素
list.set(0,"大家好啊");
//获取元素
String s = list.get(0);
//获取集合大小
int size = list.size();
//判断元素是否存在
boolean flag = list.contains("大家好啊");
}
我们发现上面这些方法其实和ArrayList中的常用方法都是相同的。因为LinkedList和ArrayList都是List接口的实现类,上面的很多方法都是他们共同的接口中定义的方法,所以都会有。
下面是LinkedList的一些特有方法:
void addFirst(E e) //把元素添加到集合的最前面
void addLast(E e) //把元素添加到集合的最后面
E removeFirst() //删除集合最前面的一个元素,返回值代表被删除的元素
E removeLast() //删除集合最后面的一个元素,返回值代表被删除的元素
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("啊");
list.addFirst("[");
list.addLast("]");
String s = list.removeFirst();
System.out.println(s);
String s1 = list.removeLast();
System.out.println(s1);
}
C.遍历
同ArrayList。
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("大");
list.add("家");
list.add("好");
list.add("啊");
//遍历集合
//for循环遍历
// for (int i = 0; i < list.size(); i++) {
// System.out.println(list.get(i));
// }
//迭代器
// Iterator<String> it = list.iterator();
// while (it.hasNext()){
// String s = it.next();
// System.out.println(s);
// }
//foreach
// for(String s : list){
// System.out.println(s);
// }
//转换为数组遍历
String[] strings = list.toArray(new String[0]);
for (int i = 0; i < strings.length; i++) {
System.out.println(strings[i]);
}
}
D.源码分析
推荐看"温文艾尔"博主发的【源码那些事】LinkedList底层源码有那么难吗,一文让你学会它
(4)ArrayList和LinkedList的区别
都是实现了List接口,不同点是底层存储数据的数据结构不同。ArrayList底层是用数组来存储,而LinkedList是链表。所以各自的特点也和数据结构的特点一样。
ArrayList : 查找快,增删慢
LinkedList: 增删快,查找慢
5.学生管理系统
(1)学生管理系统实现步骤【理解】
-
案例需求
针对目前我们的所学内容,完成一个综合案例:学生管理系统!该系统主要功能如下:
添加学生:通过键盘录入学生信息,添加到集合中
删除学生:通过键盘录入要删除学生的学号,将该学生对象从集合中删除
修改学生:通过键盘录入要修改学生的学号,将该学生对象其他信息进行修改
查看学生:将集合中的学生对象信息进行展示
退出系统:结束程序
-
实现步骤
-
定义学生类,包含以下成员变量
private String sid // 学生id
private String name // 学生姓名
private String age // 学生年龄
private String address // 学生所在地
-
学生管理系统主界面的搭建步骤
2.1 用输出语句完成主界面的编写 2.2 用Scanner实现键盘输入 2.3 用switch语句完成选择的功能 2.4 用循环完成功能结束后再次回到主界面
-
学生管理系统的添加学生功能实现步骤
3.1 定义一个方法,接收ArrayList<Student>集合 3.2 方法内完成添加学生的功能 ①键盘录入学生信息 ②根据录入的信息创建学生对象 ③将学生对象添加到集合中 ④提示添加成功信息 3.3 在添加学生的选项里调用添加学生的方法
-
学生管理系统的查看学生功能实现步骤
4.1 定义一个方法,接收ArrayList<Student>集合 4.2 方法内遍历集合,将学生信息进行输出 4.3 在查看所有学生选项里调用查看学生方法
-
学生管理系统的删除学生功能实现步骤
5.1 定义一个方法,接收ArrayList<Student>集合 5.2 方法中接收要删除学生的学号 5.3 遍历集合,获取每个学生对象 5.4 使用学生对象的学号和录入的要删除的学号进行比较,如果相同,则将当前学生对象从集合中删除 5.5 在删除学生选项里调用删除学生的方法
-
学生管理系统的修改学生功能实现步骤
6.1 定义一个方法,接收ArrayList<Student>集合 6.2 方法中接收要修改学生的学号 6.3 通过键盘录入学生对象所需的信息,并创建对象 6.4 遍历集合,获取每一个学生对象。并和录入的修改学生学号进行比较.如果相同,则使用新学生对象替换当前学生对象 6.5 在修改学生选项里调用修改学生的方法
-
退出系统
使用System.exit(0);退出JVM
-
(2)学生类的定义【应用】
public class Student {
//学号
private String sid;
//姓名
private String name;
//年龄
private String age;
//居住地
private String address;
public Student() {
}
public Student(String sid, String name, String age, String address) {
this.sid = sid;
this.name = name;
this.age = age;
this.address = address;
}
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(3)测试类的定义【应用】
public class StudentManager {
/*
1:用输出语句完成主界面的编写
2:用Scanner实现键盘录入数据
3:用switch语句完成操作的选择
4:用循环完成再次回到主界面
*/
public static void main(String[] args) {
//创建集合对象,用于保存学生数据信息
ArrayList<Student> array = new ArrayList<Student>();
//用循环完成再次回到主界面
while (true) {
//用输出语句完成主界面的编写
System.out.println("--------欢迎来到学生管理系统--------");
System.out.println("1 添加学生");
System.out.println("2 删除学生");
System.out.println("3 修改学生");
System.out.println("4 查看所有学生");
System.out.println("5 退出");
System.out.println("请输入你的选择:");
//用Scanner实现键盘录入数据
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
//用switch语句完成操作的选择
switch (line) {
case "1":
addStudent(array);
break;
case "2":
deleteStudent(array);
break;
case "3":
updateStudent(array);
break;
case "4":
findAllStudent(array);
break;
case "5":
System.out.println("谢谢使用");
System.exit(0); //JVM退出
}
}
}
//定义一个方法,用于添加学生信息
public static void addStudent(ArrayList<Student> array) {
//键盘录入学生对象所需要的数据,显示提示信息,提示要输入何种信息
Scanner sc = new Scanner(System.in);
String sid;
while (true) {
System.out.println("请输入学生学号:");
sid = sc.nextLine();
boolean flag = isUsed(array, sid);
if (flag) {
System.out.println("你输入的学号已经被占用,请重新输入");
} else {
break;
}
}
System.out.println("请输入学生姓名:");
String name = sc.nextLine();
System.out.println("请输入学生年龄:");
String age = sc.nextLine();
System.out.println("请输入学生居住地:");
String address = sc.nextLine();
//创建学生对象,把键盘录入的数据赋值给学生对象的成员变量
Student s = new Student();
s.setSid(sid);
s.setName(name);
s.setAge(age);
s.setAddress(address);
//将学生对象添加到集合中
array.add(s);
//给出添加成功提示
System.out.println("添加学生成功");
}
//定义一个方法,判断学号是否被使用
public static boolean isUsed(ArrayList<Student> array, String sid) {
//如果与集合中的某一个学生学号相同,返回true;如果都不相同,返回false
boolean flag = false;
for(int i=0; i<array.size(); i++) {
Student s = array.get(i);
if(s.getSid().equals(sid)) {
flag = true;
break;
}
}
return flag;
}
//定义一个方法,用于查看学生信息
public static void findAllStudent(ArrayList<Student> array) {
//判断集合中是否有数据,如果没有显示提示信息
if (array.size() == 0) {
System.out.println("无信息,请先添加信息再查询");
//为了让程序不再往下执行,我们在这里写上return;
return;
}
//显示表头信息
//\t其实是一个tab键的位置
System.out.println("学号\t\t\t姓名\t\t年龄\t\t居住地");
//将集合中数据取出按照对应格式显示学生信息,年龄显示补充“岁”
for (int i = 0; i < array.size(); i++) {
Student s = array.get(i);
System.out.println(s.getSid() + "\t" + s.getName() + "\t" + s.getAge() + "岁\t\t" + s.getAddress());
}
}
//定义一个方法,用于删除学生信息
public static void deleteStudent(ArrayList<Student> array) {
//键盘录入要删除的学生学号,显示提示信息
Scanner sc = new Scanner(System.in);
System.out.println("请输入你要删除的学生的学号:");
String sid = sc.nextLine();
//在删除/修改学生操作前,对学号是否存在进行判断
//如果不存在,显示提示信息
//如果存在,执行删除/修改操作
int index = -1;
for (int i = 0; i < array.size(); i++) {
Student s = array.get(i);
if (s.getSid().equals(sid)) {
index = i;
break;
}
}
if (index == -1) {
System.out.println("该信息不存在,请重新输入");
} else {
array.remove(index);
//给出删除成功提示
System.out.println("删除学生成功");
}
}
//定义一个方法,用于修改学生信息
public static void updateStudent(ArrayList<Student> array) {
//键盘录入要修改的学生学号,显示提示信息
Scanner sc = new Scanner(System.in);
System.out.println("请输入你要修改的学生的学号:");
String sid = sc.nextLine();
//键盘录入要修改的学生信息
System.out.println("请输入学生新姓名:");
String name = sc.nextLine();
System.out.println("请输入学生新年龄:");
String age = sc.nextLine();
System.out.println("请输入学生新居住地:");
String address = sc.nextLine();
//创建学生对象
Student s = new Student();
s.setSid(sid);
s.setName(name);
s.setAge(age);
s.setAddress(address);
//遍历集合修改对应的学生信息
for (int i = 0; i < array.size(); i++) {
Student student = array.get(i);
if (student.getSid().equals(sid)) {
array.set(i, s);
}
}
//给出修改成功提示
System.out.println("修改学生成功");
}
}
6.参考博文
【源码那些事】LinkedList底层源码有那么难吗,一文让你学会它
一文搞定ArrayList、LinkedList、HashMap、HashSet -----源码解读之ArrayList【源码那些事】LinkedList底层源码有那么难吗,一文让你学会它
7.总结与下一步
今天,我们深入了解了Java集合框架中的List集合,重点介绍了ArrayList
和LinkedList
。通过这些知识,你可以更好地理解和使用它们,根据不同的需求选择合适的集合类型。下一篇文章,我们将继续探讨Java集合框架中的其他重要类。保持好奇心,继续前进吧!🚀😄
希望你享受今天的学习过程,继续保持这种好奇心和热情。Happy coding! 😄🚀
如果你能看到这,那博主必须要给你一个大大的鼓励,谢谢你的支持!喜欢的可以点个关注
我们下期再见。