Collection集合体系
1. 集合简单介绍
1.1 集合的介绍
开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。我们一般通过 “容器”来容纳和管理数据。事实上,数组就是一种容器,可以在其中放置对象或基本类型数据。
数组的优势:是一种简单的线性序列,可以快速的访问数组元素,通过索引获取元素效率非常高。
数组的劣势:不灵活!容量事先定义好,不能随着需求的变化而扩容,并且插入和删除操作效率非常低。
比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这个用户有多少个?我们在写程序时是无法确定的。因此,也就不能使用数组。
由于数组远远不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活的,容量随时可扩的容器来装载我们的对象。 这就是我们今天要学习的容器或者叫集合,集合存储的数据必须是引用类型数据。
ArrayList是一种常见的集合,我们可以使用ArrayList 集合来管理和组织数据。
【示例】ArrayList存储和遍历元素案例
package com.huayu.demo;
import java.util.ArrayList;
/**
* 集合的入门案例:向集合中添加元素并遍历
*/
public class Demo1 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(111);
list.add(222);
list.add(333);
list.add(444);
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
1.2 集合的继承关系
查看 ArrayList 类源码,我们发现它继承了抽象类 AbstractList同时又实现了接口 List,而 List 接口又继承了Collection 接口。因此,Collection 接口为最顶层集合接口了。
ArrayList 继承于AbstractList抽象类并实现了List接口
List接口继承于Collection 接口
这说明我们在使用 ArrayList 类时,该类已经把所有抽象方法进行了重写。那么,实现 Collection接口的所有子类都会进行方法重写。
Collection接口位于java.util包中,常用的派生接口是List接口和Set接口:
(一)List接口常用的子类有:ArrayList类、LinkedList类、Vector类。
(二)Set接口常用的子类有:HashSet类、LinkedHashSet类、TreeSet类。
集合的继承关系简图:
1.3 Collection接口
既然 Collection 接口是集合中的顶层接口,那么 Collection 接口中定义的方法子类都可以使用。查询API,发现 Collection 接口中很多集合的操作方法,那么这些方法都具体能做什么呢?
| 方法名 | 描述 |
|---|---|
| int size(); | 容器中元素的数量。 |
| boolean isEmpty(); | 容器是否为空。 |
| boolean add(Object o); | 增加元素到容器中。 |
| boolean addAll(Collection c); | 将容器c中所有元素增加到本容器。 |
| boolean remove(Object o); | 从容器中移除元素。 |
| boolean removeAll(Collection c); | 移除本容器和容器c中都包含的元素。 |
| boolean retainAll(Collection c); | 取本容器和容器c中都包含的元素,移除非交集元素。 |
| boolean contains(Object o); | 容器中是否包含该元素。 |
| boolean containsAll(Collection c); | 本容器是否包含c容器所有元素。 |
| Iterator iterator(); | 获得迭代器,用于遍历所有元素。 |
| Object[] toArray(); | 把容器中元素转化成Object数组。 |
由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。
【示例】演示 Collection 接口中的方法演示
package com.huayu.demo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
public class Demo2 {
public static void main(String[] args) {
Collection coll1 = new ArrayList();
coll1.add("hello");
coll1.add("java");
System.out.println("coll1 = " + coll1);//coll1 = [hello, java]
// 添加元素
Collection coll2 = new ArrayList();
coll2.add(10);
coll2.add(20);
coll2.addAll(coll1);
System.out.println("coll2 = " + coll2);//coll2 = [10, 20, hello, java]
// 删除元素
boolean hello = coll2.remove("hello");
System.out.println("hello = " + hello);
System.out.println("coll2 = " + coll2);
coll2.removeAll(coll1);
System.out.println("coll2 = " + coll2);
// 没有修改方法,查询元素个数
int size = coll2.size();
System.out.println("size = " + size); // size = 2
// 其他方法
// 清除集合coll2中所有元素
coll2.clear();
System.out.println("coll2 = " + coll2);//coll2 = []
// 判断coll2集合是否包含10
boolean contains = coll2.contains(10);
System.out.println("contains = " + contains);//contains = false
// 判断coll2集合是否包含coll1集合中所有元素
boolean b = coll2.containsAll(coll1);
System.out.println("b = " + b); // b = false
// 判断集合是否为空
boolean empty = coll2.isEmpty();
System.out.println("empty = " + empty); // empty = true
// 获取集合迭代器对象
Iterator iterator = coll2.iterator();
System.out.println("iterator = " + iterator);//iterator = java.util.ArrayList$Itr@4554617c
//将集合转数组
Object[] objects = coll2.toArray();
System.out.println("objects = " + Arrays.toString(objects));
// 取出a和c中都包含的元素,存入c集合
Collection a = new ArrayList();
a.add("hello");
a.add("java");
a.add(1);
a.add(2);
Collection c = new ArrayList();
c.add(1);
c.add(2);
c.add(3);
boolean b1 = a.retainAll(c);
System.out.println("b1 = " + b1);
System.out.println("a = " + a);
System.out.println("c = " + c);
}
}
2. List接口
2.1 List接口简介
List是一个有序的、可以重复、可以为null 的集合(有时候我们也叫它“序列”)。
有序指的是:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
可重复指的是:List允许加入重复的元素。更确切地讲,List通常允许满足obj1.equals(obj2) 的元素重复加入容器。
List是Collection的子接口,除了Collection接口中的方法,List还多了一些跟顺序(索引)有关的方法:
| 方法名 | 说明 |
|---|---|
| void add(int index, Object obj); | 在指定位置插入元素。 |
| boolean addAll(int index, Collection c); | 在指定位置增加一组元素 |
| Object set(int index, Object element); | 修改指定位置的元素。 |
| Object get(int index); | 返回指定位置的元素。 |
| boolean remove(int index); | 删除指定位置的元素,后面元素通通前移一位。 |
| int indexOf(Object o); | 返回第一个匹配元素的索引。如果没有该元素,返回-1。 |
| int lastIndexOf(Object o); | 返回最后一个匹配元素的索引。如果没有该元素,返回-1。 |
| List subList(int fromIndex, int toIndex); | 取出集合中的子集合。 |
| ListIterator listIterator(); | 为ListIterator接口实例化。 |
List接口常用的实现类有3个:ArrayList、LinkedList、Vectorh.
代码示例:
public class Demo3 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
// list集合中跟索引有关的方法
list.add(1,4);
List newList = Arrays.asList(5, 6, 7,2);
boolean b = list.addAll(3, newList);
// 获取指定索引位置的元素
Object o = list.get(2);//o = 2
System.out.println("o = " + o);
//删除指定索引位置的元素
Object remove = list.remove(4);
// 获取列表迭代器
ListIterator listIterator = list.listIterator();
System.out.println("listIterator = " + listIterator);
// 修改指定索引位置的元素,返回旧值。
Object set = list.set(3, 55);
System.out.println("set = " + set);
// 截取指定索引范围内的元素,左闭右开区间,[1,3)
List list1 = list.subList(1, 3);
System.out.println("list1 = " + list1);//list1 = [4, 2]
// 从前往后获取指定元素的索引
int index1 = list.indexOf(2);
System.out.println("index1 = " + index1);//index1 = 2
// 从后往前获取指定元素的索引
int index2 = list.lastIndexOf(2);
System.out.println("index2 = " + index2);//index2 = 5
System.out.println("list = " + list);// list = [1, 4, 2, 55, 7, 2, 3]
}
}
2.2 ArrayList类详解
2.2.1 ArrayList源码分析
ArrayList是基于数组来实现的,并且ArrayList底层是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
ArrayList集合的特点为:查询效率高,增删效率低,线程不安全。
查看源码,我们可以看出ArrayList底层使用Object数组来存储元素数据。所有的方法,都围绕这个核心的Object数组来开展。
对ArrayList的操作,其实就是对数组的操作,下面我们来模拟ArrayList的底层实现:
2.2.2 私有属性介绍
ArrayList类只定义了两个私有属性:
public class ArrayList {
// 存放元素的数组
private Object[] elementData;
// 存放元素的个数
private int size;
}
很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。
2.2.3 构造方法介绍
ArrayList提供的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表。
public class ArrayList {
// ...省略私有属性...
// 无参构造方法,默认容量是10。
public ArrayList() {
this(10);
}
// 带容量大小的构造函数
public ArrayList(int capacity) {
// 如果参数非法,抛出异常
if(initialCapacity < 0) {
throw new IllegalArgumentException("capacity: 不能为负数 "+ capacity);
}
// 创建指定容量的数组
this.elementData = new Object[initialCapacity];
}
}
2.2.4 添加元素方法
我们知道,数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。 ArrayList的Object数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度更大的数组,并将原数组内容和新的元素一起加入到新数组中,源码如下:
public class ArrayList {
// ...省略私有属性和构造方法...
// 追加一个元素方法
public boolean add(Object element) {
// 1.判断数组是否需要扩容
ensureCapacityInternal();
// 2.把element添加进入数组中
elementData[size] = element;
// 3.更新size的值
size++;
return true;
}
/**
* 判断数组是否需要执行扩容操作
*/
private void ensureCapacityInternal() {
// 1.当数组的空间长度等于数组实际存放元素的个数时,这时就需扩容操作
if(elementData.length == size) {
// 2.创建一个比原数组空间长度更大的新数组
Object[] newElementData = new Object[elementData.length + elementData.length>>1];
// 3.把原数组中的元素拷贝进入新数组中
for(int i = 0; i < size; i++) {
newElementData [i] = elementData[i];
}
// 4.让原数组保存新数组的地址值
elementData = newElementData ;
}
}
}
2.2.5 获取元素方法
ArrayList的get方法就比较简单了,先检查index是否合法,此处index的合法索引取值范围在[0, size - 1]之间,然后执行获取元素的操作。
public class ArrayList {
// ...省略私有属性和构造方法...
// 根据索引获取元素
public Object get(int index) {
// 1.检查所有是否合法
rangeCheck(index);
// 2.根据索引获取元素
return elementData[index];
}
// 检查所有是否合法
private void rangeCheck(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("数组索引越界");
}
}
2.2.6 修改元素方法
ArrayList的set方法和获取元素方法比较类似,先检查index是否合法,此处index的合法索引取值范围在[0, size - 1]之间,然后执行修改操作。
public class ArrayList {
// ...省略私有属性和构造方法...
// 根据索引替换数组中的元素
public Object set(int index, Object element) {
// 1.检查所有是否合法
rangeCheck(index);
// 2.获取被替换的元素
Object oldValue = elementData[index];
// 3.替换元素
elementData[index] = element;
// 4.返回替换元素的值
return oldValue;
}
}
2.2.7 插入元素方法
将元素插入到列表中指定的位置,先检查传入的index索引是否合法(此处index的合法取值范围在[0, size]之间),然后判断数组是否需要扩容,接着将指定位置以及后续的元素向后移动一位,最后再插入元素。
public class ArrayList {
// ...省略私有属性和构造方法...
// 指定位置插入一个元素
public void add(int index, Object element) {
// 1.检查索引位置是否合法
rangeCheckForAdd(index);
// 2.检查是否需要扩容
ensureCapacityInternal();
// 3.将指定位置以及后续的元素向后移动一位
System.arraycopy(elementData, index,elementData, index + 1, size - index);
// 4.指定位置插入元素
elementData[index] = element;
// 5.数组元素个数累加
size++;
}
// 检查索引位置是否合法
private void rangeCheckForAdd(int index) {
if(index < 0 || index > this.size)
throw new IndexOutOfBoundsException("插入索引越界");
}
}
2.2.8 移除元素方法
根据索引来移除元素,首先先判断索引是否合法,然后将指定位置以及后续的元素向前移动一位,最后把数组中最后一个元素设置为null。
根据元素来移除元素,首先找到该元素在数组中所在的索引,如果没有找到则证明移除失败,如果找到则进行根据索引来移除元素的操作。
public class ArrayList {
// ...省略私有属性和构造方法...
// 根据索引移除数组元素
public Object remove(int index) {
// 1.检查索引位置是否合法
rangeCheck(index);
// 2.获取索引所在的元素
Object oldValue = elementData[index];
// 3.删除输出元素
fastRemove(index);
// 4.返回被删除的元素
return oldValue;
}
// 根据元素移除数组元素
public boolean remove(Object o) {
// 因为集合中可以存放null,所以判断之前需判断元素是否为null
if(o == null) {
for (int index = 0; index < size; index++)
// 判断元素是否为null
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) {
// 1.将指定位置后续的元素向前移动一位
System.arraycopy(elementData, index + 1, elementData, index, size - index - 1);
// 2.把数组最后一个元素设置为null,并数组元素个数减一
elementData[--size] = null;
}
}
集合扩容原理
要想详细了解集合的扩容原理,必须重点分析集合的添加元素的方法:add()
在JDK1.8版本时:
使用无参构造创建一个ArrayList集合时,底层创建的是一个空数组Object[] elementData={},容量为:0
当向集合中添加第1个元素时,集合第1次将容量扩容为:10
当向集合中添加第11个元素时,集合第2次将容量扩容为原来容量的1.5倍,也就是:15
当向集合中添加第16个元素时,集合第3次将容量扩容为原来容量的1.5倍,也就是:22
...
2.3 链式存储结构
2.3.1 单链表概述
2.3.1.1 单链表的定义
单链表采用的是链式存储结构,使用一组地址任意的存储单元来存放数据元素。在单链表中,存储的每一条数据都是以节点来表示的,每个节点的构成为:元素(存储数据的存储单元) + 指针(存储下一个节点的地址值),单链表的节点结构如下图所示:
另外,单链表中的开始节点,我们又称之为首节点;单链表中的终端节点,我们又称之为尾节点。如下图所示:
2.3.1.2 根据序号获取节点的操作
在线性表中,每个节点都有一个唯一的序号,该序号是从0开始递增的。通过序号获取单链表的节点时,我们需要从单链表的首节点开始,从前往后循环遍历,直到遇到查询序号所对应的节点时为止。
以下图为例,我们需要获得序号为2的节点,那么就需要依次遍历获得“节点11”和“节点22”,然后才能获得序号为2的节点,也就是“节点33”。
因此,在链表中通过序号获得节点的操作效率是非常低的,查询的时间复杂度为O(n)。
2.3.1.3 根据序号删除节点的操作
根据序号删除节点的操作,我们首先应该根据序号获得需要删除的节点,然后让“删除节点的前一个节点”指向“删除节点的后一个节点”,这样就实现了节点的删除操作。
以下图为例,我们需要删除序号为2的节点,那么就让“节点22”指向“节点44”即可,这样就删除了序号为2的节点,也就是删除了“节点33”。
通过序号来删除节点,时间主要浪费在找正确的删除位置上,故时间复杂度为O(n)。但是,单论删除的操作,也就是无需考虑定位到删除节点的位置,那么删除操作的时间复杂度就是O(1)。
2.3.1.4 根据序号插入节点的操作
根据序号插入节点的操作,我们首先应该根据序号找到插入的节点位置,然后让“插入位置的上一个节点”指向“新插入的节点”,然后再让“新插入的节点”指向“插入位置的节点”,这样就实现了节点的插入操作。
以下图为例,我们需要在序号为2的位置插入元素值“00”,首先先把字符串“00”封装为一个节点对象,然后就让“节点22”指向“新节点00”,最后再让“节点00”指向“节点33”,这样就插入了一个新节点。
通过序号来插入节点,时间主要浪费在找正确的插入位置上,故时间复杂度为O(n)。但是,单论插入的操作,也就是无需考虑定位到插入节点的位置,那么插入操作的时间复杂度就是O(1)。
2.3.2 双链表概述
2.3.2.1 双链表的定义
双链表也叫双向链表,它依旧采用的是链式存储结构。在双链表中,每个节点中都有两个指针,分别指向直接前驱节点(保存前一个节点的地址值)和直接后继节点(保存后一个节点的地址值),如下图所示。
所以,从双链表中的任意一个节点开始,都可以很方便地访问它的直接前驱节点和直接后继节点,如下图所示。
2.3.2.2 单链表和双链表的区别
逻辑上没有区别,他们均是完成线性表的内容,主要的区别是结构上的构造有所区别。
(1) 单链表
对于一个节点,有储存数据的data和指向下一个节点的next。也就是说,单链表的遍历操作都得通过前节点—>后节点。
(2) 双链表
对于一个节点,有储存数据的data和指向下一个节点的next,还有一个指向前一个节点的pre。也就是说,双链表不但可以通过前节点—>后节点,还可以通过后节点—>前节点。
2.3.2.3 根据序号插入节点的操作
以下图为例,我们需要在序号为2的位置插入元素值“55”
第一步: 封装新节点:data为55, pre 为 索引为1的节点内存地址, next为索引为2的节点内存地址
第二步:将索引为1节点的next改为新节点内存地址,将索引为2节点的pre改为新节点的内存地址
2.3.3 环形链表概述
环形链表依旧采用的是链式存储结构,它的特点就是设置首节点和尾节点相互指向,从而实现让整个链表形成一个环。在我们实际开发中,常见的环形链表有:
环形单链表
在单链表中,尾节点的指针指向了首节点,从而整个链表形成一个环,如下图所示:
环形双链表
在双链表中,尾节点的指针指向了首节点,首节点的指针指向了尾节点,从而整个链表形成一个环,如下图所示:
2.4 LinkedList类详解
2.4.1 LinkedList源码分析
LinkedList类底层用双向链表实现的存储,也就意味着LinkedList类采用的是链式存储结构。在双链表中,每个节点中都有两个指针,分别指向直接前驱节点(保存前一个节点的地址值)和直接后继节点(保存后一个节点的地址值)。
LinkedList集合的特点为**:查询效率低,增删效率高,线程不安全。我们打开LinkedList源码,可以看到里面包含了双向链表的相关代码:
源码中Node类中包含了item、next和prev三个成员变量,其中item保存了节点中的内容,next和prev分别指向了后一个节点和前一个节点,则意味着Node类就是双向链表的节点类。
将接下来,我们再继续看LinkedList包含了哪些属性,源码如下:
源码中first属性保存的就是双链表的首节点,last属性保存的就是双链表的尾结点,而size属性保存了双链表实际存放元素的个数。
因此,对LinkedList的操作,其实就是对双链表的操作,下面我们来模拟LinkedList的底层实现。
2.4.2 LinkedList增加的相关方法
| 方法名 | 说明 |
|---|---|
| void addFirst(Object obj); | 将指定元素插入该双向队列的开头 |
| void addLast(Object obj); | 将指定元素插入该双向队列的末尾 |
| Object getFirst(); | 获取,但不删除双向队列的第一个元素 |
| Object getLast(); | 获取,但不删除双向队列的最后一个元素 |
| boolean offerFirst(Object obj); | 将指定的元素插入该双向队列的开头 |
| boolean offerLast(Object obj); | 将指定的元素插入该双向队列的末尾 |
| Object peekFirst(); | 获取,但不删除该双向队列的第一个元素,如果些双向队列为空,则返回null |
| Object peekLast(); | 获取,但不删除该双向队列的最后一个元素,如果些双向队列为空,则返回null |
| Object pollFirst(); | 获取,并删除该双向队列的第一个元素,如果些双向队列为空,则返回null |
| Object pollLast(); | 获取,并删除该双向队列的最后一个元素,如果些双向队列为空,则返回null |
| Object pop(); | 弹出该双向队列所表示的栈中的第一个元素 |
| void push(Object e) | 将一个元素push进该双向队列所表示的栈中(头插入) |
| Object removeFirst() | 获取,并删除该双向队列的第一个元素 |
| Object removeFirstOccurrence(Object obj) | 获取,并删除该双向队列的第一次出现的元素obj |
| Object removeLast() | 获取,并删除该双向队列的最后一个元素 |
| Object removeLastOccurrence(Object obj) | 获取,并删除该双向队列的最后一次出现的元素obj |
代码示例:
public class Demo5 {
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.addLast(110);
list.addFirst(10);
list.addLast(100);
Object first = list.getFirst();
System.out.println("first = " + first);//first = 10
Object last = list.getLast();
System.out.println("last = " + last);//last = 100
list.offerFirst(1);
list.offerLast(2);
Object o = list.peekFirst();
System.out.println("o = " + o);
Object o1 = list.peekLast();
System.out.println("o1 = " + o1);
System.out.println("list = " + list);//list = [1, 10, 110, 100, 2]
Object o2 = list.pollFirst();
System.out.println("o2 = " + o2);
Object o3 = list.pollLast();
System.out.println("o3 = " + o3);
System.out.println("list = " + list);//list = [10, 110, 100]
Object pop = list.pop();
System.out.println("pop = " + pop);
list.push(99);
list.push(100);
list.push(99);
list.push(1);
System.out.println("list = " + list);//list = [1, 99, 100, 99, 110, 100]
Object o4 = list.removeFirst();
System.out.println("o4 = " + o4);
Object o5 = list.removeLast();
System.out.println("o5 = " + o5);
System.out.println("list = " + list);//list = [99, 100, 99, 110]
//list.removeFirstOccurrence(99);
list.removeLastOccurrence(99);
System.out.println("list = " + list);//list = [99, 100, 110]
}
}
2.4.3 节点类的定义
节点类很简单,element存放业务数据,previous与next分别存放前后节点的信息(在数据结构中我们通常称之为前后节点的指针)。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
2.4.4 私有属性介绍
LinkedList中之定义了三个属性:
// 集合中实际元素(节点)的个数
transient int size = 0;
// 集合中首节点
transient Node<E> firstNode;
// 集合中尾节点
transient Node<E> lastNode;
2.4.5 添加元素方法
在链表的尾部追加一个节点,但是要注意判断当前链表是否为空链表。
public void add(Object element) {
// 1.把元素内容包装为一个节点对象
Node node = new Node(null, element, null);
// 2.判断链表中尾节点是否存在,如果不存在则证明链表中还没有节点
if (lastNode == null) {
// 设置首节点都为node
firstNode = node;
lastNode = node;
}
// 3.链表中存在尾节点,那么把尾节点和node链接起来
else {
// 把尾节点和node链接起来
lastNode.next = node;
node.prev = lastNode;
}
// 4.更新尾节点
lastNode = node;
// 5.链表中节点递增
size++;
}
2.4.6 获取元素方法
获取链表中指定位置的元素,首先判断索引是否合法,然后通过循环来找到对应索引位置的节点,从而拿到节点中存放的内容。
// 根据索引获取元素
public Object get(int index) {
// 1.检查索引是否合法
checkElementIndex(index);
// 2.获取元素节点的内容
return node(index).element;
}
// 检查索引是否合法
private void checkElementIndex(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("索引越界异常");
}
// 根据索引获取节点
private Node node(int index) {
// 1.如果索引在前半区,则从前往后开始找
if (index < (size >> 1)) {
// 1.1准备开始从首节点开始查找
Node currentNode = firstNode;
// 1.2从前往后遍历节点,一直到index所在位置
for (int i = 0; i < index; i++)
// 找到index索引对应的节点
currentNode = currentNode.next;
// 1.3返回找到的节点
return currentNode;
}
// 2.如果索引在后半区,从后往前开始查找
else {
// 2.1准备开始从尾节点开始查找
Node currentNode = lastNode;
// 2.2从后往前遍历节点,一直到index所在位置
for (int i = size - 1; i > index; i--)
// 找到index索引对应的节点
currentNode = currentNode.prev;
// 2.3返回找到的节点
return currentNode;
}
}
2.4.7 修改元素方法
修改链表中指定位置的元素,首先判断索引是否合法,然后通过循环来找到对应索引位置的节点,最后再修改节点中存放的内容。
// 根据索引修改元素
public Object set(int index, Object element) {
// 1.检查索引是否合法
checkElementIndex(index);
// 2.根据索引获取元素节点
Node node = node(index);
// 3.获取节点以前存放的内容
Object oldVal = node.element;
// 4.修改节点中存放的内容
node.element = element;
// 5.返回节点修改之前的内容
return oldVal;
}
2.4.8 插入元素方法
在链表中指定位置插入元素,首先判断索引是否合法,然后通过循环来找到对应索引位置的节点,最后执行插入操作。
// 插入元素方法
public void add(int index, Object element) {
// 1.检查索引是否合法(index和size可以相同)
isPositionIndex(index); // 此处判断索引是否合法与checkElementIndex不同
// 2.节点插入操作
// 2.1如果index和size相等,那么直接就是节点追加
if (index == size) { // 此操作还包含空链表的情况
add(element);
}
// 2.2如果index和size不相等,则进行插入操作
else {
// 2.3根据索引获取链表中的节点
Node node = node(index);
// 2.4进行插入操作
linkBefore(element, node);
// 2.5链表中节点递增
size++;
}
}
// 插入节点操作
public void linkBefore(Object element, Node targetNode) {
// 1.获取目标节点的上一个节点
Node preNode = targetNode.prev;
// 2.把元素内容包装为一个节点对象
Node newNode = new Node(preNode, element, targetNode);
// 3.把targetNode节点的prev指向newNode
targetNode.prev = newNode;
// 4.把preNode的next指向newNode
// 4.1如果preNode存在,正常处理
if (preNode != null)
preNode.next = newNode;
// 4.2如果preNode不存在,则证明newNode为首节点
else
firstNode = newNode;
}
// 检查索引是否合法
private void isPositionIndex(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("索引越界异常");
}
// ...省略链表节点类...
2.4.9 移除元素方法
根据索引来移除元素,首先先判断索引是否合法,然后通过循环来找到对应索引位置的节点,最后删除该节点。
根据元素来移除元素,首先找到该元素在数组中所在的索引,如果没有找到则证明移除失败,如果找到则删除该节点。
// 移除元素方法
public Object remove(int index) {
// 1.检查索引是否合法
checkElementIndex(index);
// 2.找到需要移除的节点对象
Node node = node(index);
// 3.获取被删除节点中存放的内容
Object value = node.element;
// 4.执行移除节点操作
unlink(node);
// 5.返回被删除节点的内容
return value;
}
// 根据内容移除元素方法
public boolean remove(Object o) {
// 因为集合中可以存放null,所以判断之前需判断元素是否为null
if(o == null) {
for(Node node = firstNode; node != null; node = node.next) {
// 判断元素是否为null
if(node.element == null) {
// 执行移除节点操作
unlink(node);
return true;
}
}
}
else {
for(Node node = firstNode; node != null; node = node.next) {
// 判断元素是否和传入的元素相同
if(node.element.equals(o)) {
// 执行移除节点操作
unlink(node);
return true;
}
}
}
return false;
}
// 移除节点操作
public void unlink(Node node) {
// 1.获取被移除节点的上一个节点和下一个节点
Node prev = node.prev;
Node next = node.next;
// 2.判断prev是否为空
if(prev == null)
// 2.1prev为null,则证明删除node后,next就为链表的首节点
firstNode = next;
else
// 2.2prev不为null,则把prev.next设置为next
prev.next = next;
// 3.判断next是否为空
if(next == null)
// 3.1next为null,则证明删除node后,prev就为链表的尾节点
lastNode = prev;
else
// 3.2next不为null,则把next.prev设置为prev
next.prev = prev;
// 4.释放node对象的引用关系
node.next = null;
node.prev = null;
node.element = null;
// 5.链表中节点递减
size--;
}
// ...省略链表节点类...
2.5 Vector类详解
2.5.1 Vector类概述
Vector类和ArrayList类的用法几乎一模一样,底层都是采用了数组结构,很多情况下可以互用。只不过Vector类的方法都加了同步检查,因此“线程安全,效率低”。
比如:add(E e)方法就增加了synchronized同步标记。
Vector的源码(add方法)
相比较于ArrayList,Vector还包含了许多传统的方法,虽然这些方法不属于集合框架。
2.5.2 Vector类演示
public class Demo6 {
public static void main(String[] args) {
// 实例化一个Vector
Vector vector = new Vector();
// 添加元素,addElement()方法和add()方法类似
vector.addElement("java");
vector.addElement("HTML");
vector.addElement("JavaScript");
vector.addElement("CSS");
// 遍历集合, elements()方法类似于iterator()方法
Enumeration<String> elements = vector.elements();
while(elements.hasMoreElements()) {
System.out.println(elements.nextElement());
}
System.out.println("vector = " + vector);// vector = [java, HTML, JavaScript, CSS]
}
}
【新手建议】如何选用ArrayList、LinkedList、Vector?
- 需要保证线程安全时,建议选用Vector。
- 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
- 不存在线程安全问题时,增加或删除元素较多用LinkedList。