Java经典面试题——对比 Vector、ArrayList、LinkedList 有何区别?

news2024/9/29 15:11:38

典型回答

这三者都是实现集合框架中的 List ,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。

Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别, Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。

LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

  • Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
  • LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

知识扩展

关于 Vector

Vector 底层是用数组实现的,相关的方法都加了同步检查,因此 “线程安全,效率低”。比如,indexOf 方法就增加了 synchronized 同步标记。

public synchronized int indexOf(Object o, int index) {
        if (o == null) {
            for (int i = index ; i < elementCount ; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = index ; i < elementCount ; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

Vector 下有一个子类 Stack ,本质上也是由动态数组实现的,只不过还实现了先进后出的功能(在 get、set、add 方法的基础上追加了 pop、peek 等方法),所以叫栈。

不过,由于 Stack 执行效率比较低(方法上同样加了 synchronized 关键字),就被双端队列 ArrayDeque 取代了。

关于 ArrayList

数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。 ArrayList 在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。

public class ArrayListTest {
    public static void main(String[] args) {
        //实例化ArrayList容器
        List<String> list  = new ArrayList<>();
        
        //添加元素
        boolean flag1 = list.add("a");
        boolean flag2 = list.add("b");
        boolean flag3 = list.add("c");
        boolean flag4 = list.add("d");
        System.out.println(flag1+"\t"+flag2+"\t"+flag3+"\t"+flag4);
		
		//删除元素
        boolean flag4 = list.remove("a");
        System.out.println(flag4);
        
        //获取容器中元素的个数
        int size = list.size();
        System.out.println(size);
        
        //判断容器是否为空
        boolean empty = list.isEmpty();
        System.out.println(empty);
        
        //容器中是否包含指定的元素
        boolean value = list.contains("b");
        System.out.println(value);
                
        //清空容器
        list.clear();
        Object[] objects1 = list.toArray();
      
		System.out.println(Arrays.toString(objects1));
   }
}

ArrayList 增删改查的时间复杂度

1)通过下标(也就是 get(int index))访问一个元素的时间复杂度为 O(1),因为是直达的,无论数据增大多少倍,耗时都不变。

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

2)默认添加一个元素(调用 add() 方法时)的时间复杂度为 O(1),因为是直接添加到数组末尾的,但需要考虑到数组扩容时消耗的时间。

3)删除一个元素(调用 remove(Object) 方法时)的时间复杂度为 O(n),因为要遍历列表,数据量增大几倍,耗时也增大几倍;如果是通过下标删除元素时,要考虑到数组的移动和复制所消耗的时间。

4)查找一个未排序的列表时间复杂度为 O(n)(调用 indexOf() 或者 lastIndexOf() 方法时),因为要遍历列表;查找排序过的列表时间复杂度为 O(log n),因为可以使用二分查找法,当数据增大 n 倍时,耗时增大 logn 倍(这里的 log 是以 2 为底的,每找一次排除一半的可能)

ArrayList 和 Vector 区别

  • Vector 是线程安全的,源码中有很多的 synchronized 可以看出,而 ArrayList 不是。导致 Vector 效率比 ArrayList 低;

  • ArrayList 和 Vector 都采用线性连续存储空间,当存储空间不足的时候,ArrayList 默认增加为原来大小的 1.5 倍,Vector 默认增加为原来大小的 2 倍;

  • Vector可以设置 capacityIncrement ,而 ArrayList 不可以,从字面理解 capacity 就是容量,Increment 是容量增长的参数。

  • Arraylist 和 Vector 默认情况下,初始化大小为 10 的 object 数组 。

关于 LinkedList

LinkedList 底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。

双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。

在 LinkedList 中有一个私有的静态内部类,叫 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;
    }
}

它由三部分组成:

  • 节点上的元素
  • 下一个节点
  • 上一个节点
    在这里插入图片描述
  • 对于第一个节点来说,prev 为 null;
  • 对于最后一个节点来说,next 为 null;
  • 其余的节点呢,prev 指向前一个,next 指向后一个。
如何选用ArrayList、LinkedList、Vector?
  • 需要线程安全时,用 Vector。
  • 不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)
  • 不存在线程安全问题时,增加或删除元素较多用LinkedList
LinkedList容器的使用(非List标准)

在这里插入图片描述

public class LinkedListTest {
    public static void main(String[] args) {
        System.out.println("-------LinkedList-------------");
        
        //将指定元素插入到链表开头
        LinkedList<String> linkedList1 = new LinkedList<>();
        linkedList1.addFirst("a");
        linkedList1.addFirst("b");
        linkedList1.addFirst("c");
        
        for (String str : linkedList1){
            System.out.println(str);
       }
        System.out.println("----------------------");
        
        //将指定元素插入到链表结尾
        LinkedList<String> linkedList = new LinkedList<>();
        linkedList.addLast("a");
        linkedList.addLast("b");
        linkedList.addLast("c");
        
        for (String str : linkedList){
            System.out.println(str);
       }     
        System.out.println("---------------------------");
        
        //返回此链表的第一个元素      
		System.out.println(linkedList.getFirst());
		
        //返回此链表的最后一个元素
        System.out.println(linkedList.getLast());
        System.out.println("-----------------------");
        
        //移除此链表中的第一个元素,并返回这个元素
        linkedList.removeFirst();
        
        //移除此链表中的最后一个元素,并返回这个元素
        linkedList.removeLast();
        
        for (String str : linkedList){
            System.out.println(str);
       }
        System.out.println("-----------------------");
        
        linkedList.addLast("c");
        //从此链表所表示的堆栈处弹出一个元素,等效于removeFirst
        linkedList.pop();
        for (String str : linkedList){
            System.out.println(str);
       }
        System.out.println("-------------------");
        
        //将元素推入此链表所表示的堆栈 这个等效于addFisrt(E e)
        linkedList.push("h");
        for (String str : linkedList){
            System.out.println(str);
       }
   }
}

ArrayList 和 LinkedList 遍历元素时究竟谁快?

1)ArrayList
遍历 ArrayList 找到某个元素的话,通常有两种形式:

  • get(int),根据索引找元素
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}

由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。

  • indexOf(Object),根据元素找索引
public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。

2)LinkedList
遍历 LinkedList 找到某个元素的话,通常也有两种形式:

  • get(int),找指定位置上的元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

既然需要调用 node(int) 方法,就意味着需要前后半段遍历了。

  • indexOf(Object),找元素所在的位置
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

需要遍历整个链表,和 ArrayList 的 indexOf() 类似。

那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用迭代器(Iterator)。

如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 node(int) 方法进行前后半段的遍历。

LinkedList.Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        LinkedList.Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        LinkedList.Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

那如果使用的是迭代器呢?

LinkedList<String> list = new LinkedList<String>();
for (Iterator<String> it = list.iterator(); it.hasNext();) {
    it.next();
}

迭代器只会调用一次 node(int) 方法,在执行 list.iterator() 的时候:先调用 AbstractSequentialList 类的 iterator() 方法,再调用 AbstractList 类的 listIterator() 方法,再调用 LinkedList 类的 listIterator(int) 方法,如下图所示。
在这里插入图片描述
最后返回的是 LinkedList 类的内部私有类 ListItr 对象:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new LinkedList.ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private LinkedList.Node<E> lastReturned;
    private LinkedList.Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
}

执行 ListItr 的构造方法时调用了一次 node(int) 方法,返回第一个节点。在此之后,迭代器就执行 hasNext() 判断有没有下一个,执行 next() 方法下一个节点。

由此,可以得出这样的结论:遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。

也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。

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

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

相关文章

详解CRC原理以及C语言实现

CRC检验原理 CRC&#xff08;Cyclic Redundancy Check&#xff09;校验是一种常用的数据校验方法&#xff0c;它通过计算数据的校验码来检测数据在传输过程中是否出现了错误。 CRC校验的基本原理是将数据按照一定的规则进行计算&#xff0c;得到一个固定长度的校验码&#xf…

JavaScript内改变this指向

之前我们说的都是代码内 this 的默认指向 今天我们要来说一下如何能改变 this 指向 也就是说, 你指向哪我不管, 我让你指向哪, 你就得指向哪 开局 在函数的原型( Function.prototype ) 上有三个方法 call apply bind 既然是在函数的原型上, 那么只要是函数就可以调用这三个方…

React(七):Router基本使用、嵌套路由、编程式导航、路由传参、懒加载

React&#xff08;七&#xff09;一、React-Router的基本使用1.安装和介绍2.路由的配置和跳转3.Navigate的使用4.如果找不到对应的路由路径&#xff1f;二、嵌套路由的用法三、编程式路由导航1.类组件中使用useNavigate2.函数式组件中使用useNavigate四、路由跳转传参1.设置好路…

小白学Pytorch 系列--Torch API(1)

小白学Pytorch 系列–Torch API Torch version 1.13 Tensors TORCH.IS_TENSOR 如果obj是PyTorch张量&#xff0c;则返回True。 注意&#xff0c;这个函数只是简单地执行isinstance(obj, Tensor)。使用isinstance 更适合用mypy进行类型检查&#xff0c;而且更显式-所以建议使…

【计组】主存储器有关知识梳理

一、主存储器 主存储器可以直接和CPU进行通信&#xff0c;但是只能保存临时数据&#xff0c;在断电后数据就消失。还有一个特点是&#xff0c;主存储器的容量小&#xff0c;速度快&#xff0c;造价高。 1.构成 2.主存中存储体的构造 最小的存储单位是存储元&#xff0c;存储元…

近亿美元:人工心脏龙头永仁心医疗完成超大额A轮融资

近日&#xff0c;永仁心医疗器械有限公司&#xff08;以下简称“永仁心医疗”&#xff09;完成近一亿美元A轮融资&#xff0c;由北京科兴中维生物技术有限公司&#xff08;SINOVAC科兴&#xff09;领投&#xff0c;太平&#xff08;深圳&#xff09;医疗健康产业私募股权投资基…

腾讯IM h5版本,在安卓原生和IOS原生支持情况的调查以及踩坑、解决办法

介绍 公司准备基于腾讯IM进行开发即时通讯功能&#xff0c;想用H5来开发&#xff0c;这样方便以后移植&#xff0c;在原生app里直接加载&#xff0c;通过三天时间的调查&#xff0c;以及与腾讯客服&#xff0c;技术来回沟通&#xff0c;已经有一定的成果&#xff0c;现将调查成…

【Redis】Redis持久化(一)

目录 1.Redis持久化 1.1.RDB持久化 1.1.1.执行时机 1.1.2.RDB原理 1.1.3.小结 1.2.AOF持久化 1.2.1.AOF原理 1.2.2.AOF配置 1.2.3.AOF文件重写 1.3.RDB与AOF对比 1.Redis持久化 Redis有两种持久化方案&#xff1a; RDB持久化 AOF持久化 1.1.RDB持久化 RDB全称Red…

SpringBoot - 什么是跨域?如何解决跨域?

什么是跨域&#xff1f; 在浏览器上当前访问的网站&#xff0c;向另一个网站发送请求&#xff0c;用于获取数据的过程就是跨域请求。 跨域&#xff0c;是浏览器的同源策略决定的&#xff0c;是一个重要的浏览器安全策略&#xff0c;用于限制一个 origin 的文档或者它加载的脚本…

Doris集成其他系统——ODBC外部表

Doris集成其他系统——ODBC外部表 文章目录Doris集成其他系统——ODBC外部表0. 写在前面1. 正文1.1 ODBC外部表介绍1.2 使用方式2. 使用 ODBC 的 MySQL 外表2.1 前置芝士2.2 安装 unixODBC2.3 安装MySQL 对应版本的 ODBC2.3.1 安装方式2.3.2 检查安装结果2.3.3 其他节点的安装及…

prometheus 配置服务器监控、服务监控、容器中服务监控与告警

最近公司有几个服务遇到了瓶颈&#xff0c;也就是数据量增加了&#xff0c;没有人发现&#xff0c;这不是缺少一个监控服务和告警的系统吗&#xff1f; 主要需求是监控每个服务&#xff0c;顺带监控一下服务器和一些中间件&#xff0c;这里采集的2种&#xff0c;zabbix和prom…

Kafka 消费者组

Kafka 消费者组Consumer数位移重平衡消费者组 (Consumer Group) : 可扩展且容错性的消费者机制 一个组内可能有多个消费者 (Consumer Instance) : 共享一个公共 ID (Group ID)组内的所有消费者协调消费订阅主题 (Subscribed Topics) 的所有分区 (Partition)每个分区只能由同个…

【1】熟悉刷题平台操作

TestBench使用 与quartus中testbench的写法有些许。或者说这是平台特有的特性&#xff01;&#xff01; 1 平台使用谨记 &#xff08;1&#xff09;必须删除&#xff1a;若设计为组合逻辑&#xff0c;需将自动生成的clk删除 若不删除&#xff0c;会提示运行超时错误。 &#…

git推送指定的提交到远程分支详细方法

默认情况下&#xff0c;git push会推送暂存区所有提交&#xff08;也即HEAD及其之前的提交&#xff09;到远程库&#xff0c;实际开发中可能因为功能没有开发完成&#xff0c;但是又必须提交部分修改好的代码就需要用到推送指定commit到远程分支。第一种方式&#xff1a;即符合…

CSS流动布局-页面自适应

项目中经常会碰到页面自适应的问题&#xff0c;例如&#xff1a;商城的列表展示、分类列表展示等页面&#xff0c;如下&#xff1a; 该页面会随着页面的放大缩小而随之发生变化&#xff0c;这种自适应的页面布局在大屏幕、小屏幕、不同的浏览器设备上都应该呈现出与设计匹配的…

【STM32MP157应用编程】4.串口接收、发送数据

目录 串口文件 指令操作串口 程序操作串口 程序说明 程序代码 4_ChuanKou_2.c 启动交叉编译工具 编译 拷贝到开发板 测试 串口文件 在/dev目录下&#xff0c;存放了串口的文件。 文件名对应的串口ttySTM0CH340ttySTM1com2&#xff08;公头&#xff09;ttySTM2com1&a…

java版云HIS系统源码 微服务架构支持VUE

云his系统源码 一个好的HIS系统&#xff0c;要具有开放性&#xff0c;便于扩展升级&#xff0c;增加新的功能模块&#xff0c;支撑好医院的业务的拓展&#xff0c;而且可以反过来给医院赋能&#xff0c;最终向更多的患者提供更好地服务。 私信了解更多&#xff01; 本套基于…

【C语言】结构体和共用体

目录一、结构体&#xff08;一&#xff09;结构体声明&#xff08;二&#xff09;结构体变量定义&#xff08;三&#xff09;结构体变量的初始化&#xff08;四&#xff09;结构体的引用&#xff08;五&#xff09;结构体数组二、共用体&#xff08;一&#xff09;共用体定义&a…

全球商城库存系统架构设计与实践

业务背景 商城原库存系统耦合在商品系统&#xff0c;考虑到相关业务逻辑复杂度越来越高&#xff0c;库存做了服务拆分&#xff0c;在可售库存管理的基础上新增了实物库存管理、秒杀库存、物流时效 、发货限制、分仓管理等功能&#xff0c;满足了商城库存相关业务需求。 系统架构…

GitLab 存储型XSS漏洞 (CVE-2023-0050)

漏洞描述 GitLab 是由GitLab公司开发的、基于Git的集成软件开发平台。kroki是一款集成在GitLab的基于文本的图表描述自动转为图片的开源工具&#xff0c;在GitLab 13.7引入。 由于Kroki中lib/banzai/filter/kroki_filter.rb对接收的image_src过滤不严&#xff0c;具有AsciiDo…