1. 集合简单介绍
1.1 集合的介绍
开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。我们一般通过 “容器”来容纳和管理数据。事实上,数组就是一种容器,可以在其中放置对象或基本类型数据。
数组的优势:是一种简单的线性序列,可以快速的访问数组元素,通过索引获取元素效率非常高。
数组的劣势:不灵活!容量事先定义好,不能随着需求的变化而扩容,并且插入和删除操作效率非常低。
比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这个用户有多少个?我们在写程序时是无法确定的。因此,也就不能使用数组。
由于数组远远不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活的,容量随时可扩的容器来装载我们的对象。 这就是我们今天要学习的容器或者叫集合,集合存储的数据必须是引用类型数据。
ArrayList是一种常见的集合,我们可以使用ArrayList 集合来管理和组织数据。
【示例】ArrayList存储和遍历元素案例
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; /* @author xiangge @date 2023/7/3 19:42 */ 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接口实例化。 |
tips: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还包含了许多传统的方法,虽然这些方法不属于集合框架。
【注意】如何选用ArrayList、LinkedList、Vector?
-
需要保证线程安全时,建议选用Vector。
-
不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
-
不存在线程安全问题时,增加或删除元素较多用LinkedList。