集合系列(二) -List接口详解

news2024/11/20 2:25:19

一、List简介

List 的数据结构就是一个序列,存储内容时直接在内存中开辟一块连续的空间,然后将空间地址与索引对应。

以下是List集合简易架构图

由图中的继承关系,可以知道,ArrayList、LinkedList、Vector、Stack都是List的四个实现类。

  • AbstractCollection 是一个抽象类,它唯一实现Collection接口的类。AbstractCollection主要实现了toArray()、toArray(T[] a)、remove()等方法。
  • AbstractList 也是一个抽象类,它继承于AbstractCollection。AbstractList实现List接口中除size()、get(int location)之外的函数,比如特定迭代器ListIterator。
  • AbstractSequentialList 是一个抽象类,它继承于AbstractList。AbstractSequentialList 实现了“链表中,根据index索引值操作链表的全部函数”。
  • ArrayList 是一个动态数组,它由数组实现。随机访问效率高,随机插入、随机删除效率低。
  • LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。
  • Vector 也是一个动态数组,和ArrayList一样,也是由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。
  • Stack 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。

下面对各个实现类进行方法剖析!

二、ArrayList

ArrayList实现了List接口,也是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。
除该类未实现同步外,其余跟Vector大致相同。

在Java1.5之后,集合还提供了泛型,泛型只是编译器提供的语法糖,方便编程,对程序不会有实质的影响。因为所有的类都默认继承至Object,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

常用方法介绍

2.1、get方法

get()方法同样很简单,先判断传入的下标是否越界,再获取指定元素。

public E get(int index) {
        rangeCheck(index);
        return elementData(index);
}

/**
 * 检查传入的index是否越界
 */
private void rangeCheck(int index) {
        if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
2.2、set方法

set()方法也非常简单,直接对数组的指定位置赋值即可。

public E set(int index, E element) {
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
}
2.3、add方法

ArrayList添加元素有两个方法,一个是add(E e),另一个是add(int index, E e)。
这两个方法都是向容器中添加新元素,可能会出现容量(capacity)不足,因此在添加元素之前,都需要进行剩余空间检查,如果需要则自动扩容。扩容操作最终是通过grow()方法完成的。

grow方法实现

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//原来的1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
}

添加元素还有另外一个addAll()方法,addAll()方法能够一次添加多个元素,根据位置不同也有两个方法,一个是在末尾添加的addAll(Collection<? extends E> c)方法,一个是从指定位置开始插入的addAll(int index, Collection<? extends E> c)方法。

**不同点:addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关,时间复杂度是线性增长!
**

2.4、remove方法

remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素;另一个是remove(Object o),通过o.equals(elementData[index])来删除第一个满足的元素。

需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。

  • remove(int index)方法
public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //赋null值,方便GC回收
        return oldValue;
}
  • 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;
}

三、LinkedList

在上篇文章中,我们知道LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。

LinkedList底层通过双向链表实现,通过firstlast引用分别指向链表的第一个和最后一个元素,注意这里没有所谓的哑元(某个参数如果在子程序或函数中没有用到,那就被称为哑元),当链表为空的时候firstlast都指向null。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
     /**容量*/
    transient int size = 0;

    /**链表第一个元素*/
    transient Node<E> first;

     /**链表最后一个元素*/
    transient Node<E> last;
    
    ......
}
/**
 * 内部类Node
 */
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;
    }
}

常用方法介绍

3.1、get方法

get()方法同样很简单,先判断传入的下标是否越界,再获取指定元素。

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
}

/**
 * 检查传入的index是否越界
 */
private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
3.2、set方法

set(int index, E element)方法将指定下标处的元素修改成指定值,也是先通过node(int index)找到对应下表元素的引用,然后修改Node中item的值。

public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
}
3.3、add方法

同样的,add()方法有两方法,一个是add(E e),另一个是add(int index, E element)。

  • add(E e)方法

该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间,只需要简单修改几个相关引用即可。

public boolean add(E e) {
        linkLast(e);
        return true;
}

/**
 * 添加元素
 */
void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            //原来链表为空,这是插入的第一个元素
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
}
  • add(int index, E element)方法

该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。

具体分成两步,1.先根据index找到要插入的位置;2.修改引用,完成插入操作。

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            //调用add方法,直接在末尾添加元素
            linkLast(element);
        else
            //根据index找到要插入的位置
            linkBefore(element, node(index));
}

/**
 * 插入位置
 */
void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
}

同样的,添加元素还有另外一个addAll()方法,addAll()方法能够一次添加多个元素,根据位置不同也有两个方法,一个是在末尾添加的addAll(Collection<? extends E> c)方法,另一个是从指定位置开始插入的addAll(int index, Collection<? extends E> c)方法。

里面也for循环添加元素,addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关,时间复杂度是线性增长!

3.4、remove方法

同样的,remove()方法也有两个方法,一个是删除指定下标处的元素remove(int index),另一个是删除跟指定元素相等的第一个元素remove(Object o)。

两个删除操作都是,1.先找到要删除元素的引用;2.修改相关引用,完成删除操作

  • remove(int index)方法

通过下表,找到对应的节点,然后将其删除

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
}
  • remove(Object o)方法

通过equals判断找到对应的节点,然后将其删除

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
}

删除操作都是通过unlink(Node<E> x)方法完成的。这里需要考虑删除元素是第一个或者最后一个时的边界情况。

/**
 * 删除一个Node节点方法
 */
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;
        
        //删除的是第一个元素
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        //删除的是最后一个元素
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
}

四、Vector

Vector类属于一个挽救的子类,早在jdk1.0的时候,就已经存在此类,但是到了jdk1.2之后重点强调了集合的概念,所以,先后定义了很多新的接口,比如ArrayList、LinkedList,但考虑到早期大部分已经习惯使用Vector类,所以,为了兼容性,java的设计者,就让Vector多实现了一个List接口,这才将其保留下来。

在使用方面,Vector的getsetaddremove方法实现,与ArrayList基本相同,不同的是Vector在方法上加了线程同步锁synchronized,所以,执行效率方面,会比较慢!

4.1、get方法
public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
}
4.2、set方法
public synchronized E set(int index, E element) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
}
4.3、add方法
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
4.4、remove方法
public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
      removeElementAt(i);
      return true;
    }
    return false;
}

五、Stack

在 Java 中 Stack 类表示后进先出(LIFO)的对象堆栈。栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成的;在现实生活中,手枪弹夹的子弹就是一个典型的后进先出的结构。

在使用方面,主要方法有push peek pop

5.1、push方法

push方法表示,向栈中添加元素

public E push(E item) {
        addElement(item);
        return item;
}
5.2、peek方法

peek方法表示,查看栈顶部的对象,但不从栈中移除它

public synchronized E peek() {
        int     len = size();
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
}
5.3、pop方法

pop方法表示,移除元素,并将要移除的元素方法

  public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
}

关于 Java 中 Stack 类,有很多的质疑声,栈更适合用队列结构来实现,这使得Stack在设计上不严谨,因此,官方推荐使用Deque下的类来是实现栈!

六、总结

  • ArrayList(动态数组结构),查询快(随意访问或顺序访问),增删慢,但在末尾插入,速度与LinkedList相差无几!
  • LinkedList(双向链表结构),查询慢,增删快!
  • Vector(动态数组结构),相比ArrayList都慢,被ArrayList替代,基本不在使用。优势是线程安全(函数都是synchronized),如果需要在多线程下使用,推荐使用并发容器中的工具类来操作,效率高!
  • Stack(栈结构)继承于Vector,数据是先进后出,基本不在使用,如果要实现栈,推荐使用Deque下的ArrayDeque,效率比Stack高!

七、参考

1、JDK1.7&JDK1.8 源码

2、CarpenterLee - Java集合分析

3、博客园 - 朽木 - ArrayList、LinkedList、Vector、Stack的比较

八、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1521035.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

B3620 x 进制转 10 进制(详解)

题目 思路 八进制数567怎么转化为十进制数。首先八进制就是逢八进一&#xff0c;也就是说这里面最大的数也就7&#xff0c;没有≥8的数。下面我们就讲一下567怎么转化为十进制&#xff1a;首先7是个位&#xff0c;可以直接写成十进制的7&#xff0c;6是十位&#xff0c;它是通…

springboot基于java的畅销图书推荐系统

摘 要 二十一世纪我们的社会进入了信息时代&#xff0c;信息管理系统的建立&#xff0c;大大提高了人们信息化水平。传统的管理方式对时间、地点的限制太多&#xff0c;而在线管理系统刚好能满足这些需求&#xff0c;在线管理系统突破了传统管理方式的局限性。于是本文针对这一…

AI_寻路系统_修改寻路网格体

学习笔记&#xff0c;仅供参考&#xff01; 一、完成创建关卡和AI代理的初步步骤&#xff0c;以演示可以修改导航系统的不同方法。 创建简单关卡&#xff0c;并通过在关卡中放入导航网格体边界体积Actor来添加导航。 将ThirdPersonCharacter蓝图修改为使用导航系统在关卡中四…

vuepress-theme-vdoing博客搭建教程

搭建流程 前言 这是笔者搭建个人博客所经历的流程&#xff0c;特附上笔记 笔者个人博客地址&#xff1a;沉梦听雨的编程指南 一、主题介绍 本博客使用的主题为&#xff1a;vuepress-theme-vdoing&#xff0c;相关介绍和使用方法可以参考该主题的官方文档 官方文档快速上手…

力扣趣味题:找不同

经典面向样例编程 char findTheDifference(char* s, char* t) {if(sNULL){return t[0];}for(int x0;x<strlen(s);x){for(int y0;y<strlen(t);y){if(s[x]t[y]){t[y]1;break;}}}for(int x0;x<strlen(t);x){if(t[x]!1){return t[x];}}return NULL; }

银发经济@315:消费、陷阱与孤独的老人

【潮汐商业评论/文】 又是一年315。 这一天&#xff0c;从品牌到消费者&#xff0c;从线下到网络&#xff0c;都不约而同地将目光锁定在大众消费生活和与其相伴的消费“陷阱”上。 这其中&#xff0c;作为“有闲又有钱”且与社会经济发展速度相对有一定“代沟”的老年消费者群…

新加坡大带宽服务器托管优势

在数字化快速发展的今天&#xff0c;服务器托管成为企业拓展业务、提高服务质量的关键环节。而新加坡作为一个国际性的金融、贸易和科技创新中心&#xff0c;其大带宽服务器托管服务在全球范围内享有盛誉。本文将为您科普新加坡大带宽服务器托管的诸多优势。 首先&#xff0c;新…

AXI CANFD MicroBlaze 测试笔记

文章目录 前言测试用的硬件连接Vivado 配置Vitis MicroBlaze CANFD 代码测试代码测试截图Github Link 前言 官网: CAN with Flexible Data Rate (CAN FD) (xilinx.com) 特征: 支持8Mb/s的CANFD多达 3 个数据位发送器延迟补偿(TDC, transmitter delay compensation)32-deep T…

VS Code上,QT基于cmake,qmake的构建方法(非常详细)

VS Code上,QT基于cmake&#xff0c;qmake的构建方法 1 前言2 QT基于cmake的构建方法2.1 VS Code关键插件安装2.2 系统环境变量配置2.3 VS Code中&#xff0c;环境变量配置2.4 Cmake新建一个新的Porject 3 QT基于qmake的构建方法 1 前言 最近&#xff0c;由于认证了github的学生…

RabbitMQ学习总结-延迟消息

1.死信交换机 一致不被消费的信息/过期的信息/被标记nack/reject的信息&#xff0c;这些消息都可以进入死信交换机&#xff0c;但是首先要配置的有私信交换机。私信交换机可以再RabbitMQ的客户端上选定配置-dead-letter-exchange。 2.延迟消息 像我们买车票&#xff0c;外卖…

PHP 生成图片

1.先确认是否有GD库 echo phpinfo(); // 创建一个真彩色图像 $image imagecreatetruecolor(120, 50);// 分配颜色 $bgColor imagecolorallocate($image, 255, 255, 255); // 白色背景 $textColor imagecolorallocate($image, 230, 230, 230); // 黑色文字// 填充背景 image…

MyFileServer

靶场下载地址 https://download.vulnhub.com/myfileserver/My_file_server_1.ova 信息收集 # nmap -sn 192.168.56.0/24 -oN live.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-24 22:07 CST Nmap scan report for 192.168.56.2 (192.168.56.2) Host is up (0.…

Java学习笔记(13)

阶段项目 拼图小游戏 JFrame JMenuBar JMenu JMenuItem 用add方法添加到不同的对象中 添加图片 先创建一个图片ImageIcon的对象&#xff0c;写入图片的路径 再创建JLabel管理容器对象&#xff0c;把图片放到这个容器中&#xff0c;再把容器添加到界面 界面坐标位置 改变图…

nmcli --help(nmcli -h)nmcli文档、nmcli手册

文章目录 nmcli --helpOPTION解释OBJECT解释1. g[eneral]&#xff1a;查看NetworkManager的状态2. n[etworking]&#xff1a;启用或禁用网络3. r[adio]&#xff1a;查看无线电状态&#xff08;例如&#xff0c;Wi-Fi&#xff09;4. c[onnection]&#xff1a;列出所有的网络连接…

openwrt下部署clouddrive2

在启动项上增加启动参数 在exit 0前面增加 mount --make-shared /mnt/data480g注意&#xff0c;后面的/mnt/data480g要替换成你设置的共享映射券。 拉取镜像 docker pull cloudnas/clouddrive2启动镜像 一定要用ssh在后台用docker run命令启动&#xff0c;因为openwrt前台…

AI技术崛起:数据可视化之路更近

在当今AI技术蓬勃发展的时代&#xff0c;数据可视化作为信息传达的重要手段&#xff0c;其门槛逐渐降低。然而&#xff0c;这并不意味着我们可以忽视学习数据可视化的重要性。即使不需要深入专业技术&#xff0c;对数据可视化的基础知识的了解也是至关重要的。那么&#xff0c;…

恢复u盘数据时可以关机吗?深入解析与操作建议

随着科技的飞速发展&#xff0c;U盘已成为我们日常生活中不可或缺的数据存储设备。然而&#xff0c;当U盘中的数据遭遇丢失或损坏时&#xff0c;如何安全有效地恢复数据成为了一个重要的问题。在这个过程中&#xff0c;许多用户可能会疑惑&#xff1a;恢复u盘数据时可以关机吗&…

基于matlab使用 fmincon 函数来进行有约束条件的最小化问题求解

一、一般步骤 生成带有噪声的正态分布数据&#xff1b;定义拟合模型。 model (params, x) normpdf(x, params(1), params(2)); 初始参数猜测 initial_guess [mu, sigma]; 设置约束条件 lb [0, 0]; % 参数的最小值 ub [10, 10]; % 参数的最大值 定义优化问题 opts …

Android studio SDK Manager显示不全的问题解决

发现SDK Manager中只显示已下载的SDK版本&#xff0c;想下载其他版本下载不到&#xff0c;尝试翻墙也没用&#xff0c;修改host文件成功 在多个地点Ping服务器,网站测速 - 站长工具 输入dl.google.com&#xff0c;进行ping检测。 选择一个地址&#xff0c;比如180.163.150.1…

Docker使用(三)Docker底层分析

Docker使用(三)Docker底层分析 四、底层分析 1、Docker镜像原理 1.1 commit镜像 docker commit 提交容器成为一个新的副本 # 命令和git原理类似 docker commit -m“提交的描述信息” -a“作者” 容器id 目标镜像名:[TAG] 实操&#xff1a; # 1、启动一个默认tomcat # …