单列集合&链表

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值