Java集合常见面试题(二)

news2024/9/17 7:23:02

Collection 子接口之 List

ArrayList 和 Vector 的区别?

  • ArrayList 是 List 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 ;
  • Vector 是 List 的古老实现类,底层使用Object[] 存储,线程安全的。

Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍,这样ArrayList就有利于节约内存空间。

ArrayList 与 LinkedList 区别?

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。
  • 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构。(JDK1.6 及之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  • 插入和删除是否受元素位置的影响:
    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    • LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst() 、 removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element),remove(Object o)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

注意:LinkedList 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的时间复杂度都是 O(n) 。

双向链表和双向循环链表

双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
在这里插入图片描述

双向循环链表: 最后一个节点的 next 指针指向 head 节点,而 head 节点的 prev 指针指向最后一个节点,构成一个环。

在这里插入图片描述

RandomAccess 接口

public interface RandomAccess {
}

源码注释:
Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. The primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good performance when applied to either random or sequential access lists.

翻译:
List 实现使用的标记接口,以指示它们支持快速(通常是恒定时间)随机访问。这个接口的主要目的是允许通用算法改变它们的行为以在应用于随机或顺序访问列表时提供良好的性能。

从 RandomAccess 源码可以看出,这个接口什么都没有定义,结合源码注释我们可以认为 RandomAccess 接口仅仅起到标识作用,表示这个接口的实现类支持快速(通常是恒定时间)随机访问。

Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?

  1. Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。

  2. Array 大小是固定的,ArrayList 的大小是动态变化的。

  3. ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

ArrayList 简介

ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

ArrayList继承于 AbstractList ,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

}
  • RandomAccess 是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。

  • ArrayList 实现了 Cloneable 接口 ,即覆盖了函数clone(),能被克隆。

  • ArrayList 实现了 java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

ArrayList 扩容机制

先来看一看 ArrayList 的构造函数

    /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例的共享空数组实例)
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例。
     * 我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     *ArrayList 所包含的元素个数
     */
    private int size;


  	/**
     *带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
     */
	public ArrayList(int initialCapacity) {
		//如果传入的参数大于0,创建initialCapacity大小的数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        	//如果传入的参数等于0,创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
        	//其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

   	/**
     *默认无参构造函数
     *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.
     *初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 (后续会介绍)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
     */
    public ArrayList(Collection<? extends E> c) {
    	//将指定集合转换为数组
        elementData = c.toArray();
         //如果elementData数组的长度不为0
        if ((size = elementData.length) != 0) {
            // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
            if (elementData.getClass() != Object[].class)
            	 //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }


通过观察源码可以发现:以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

扩容机制核心


public boolean add(E e) {
		
		
		/**
    	  *首先调用 ensureCapacityInternal()方法
	      *判断是否可以容纳e,若能,则直接添加在末尾;若不能,则进行扩容,然后再把e添加在末尾
    	  */
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        
        //将e添加到数组末尾
        elementData[size++] = e;
        return true;
}


/**
  *每次在add()一个元素时,arraylist都需要对这个list的容量进行一个判断。
  *通过ensureCapacityInternal()方法确保当前ArrayList维护的数组具有存储新元素的能力,
  *经过处理之后将元素存储在数组elementData的尾部
  *
  *minCapacity 最小容量: 添加元素前 ArrayList 元素个数加1,即新添加一个元素 ArrayList 需要的最小容量
  */
private void ensureCapacityInternal(int minCapacity) {
		
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}


/**
  * 计算容量
  */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
		/**
  		  * 使用无参构造函数创建的 ArrayList 其 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  		  */
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	//选取默认的容量和最小容量的较大值 这里对应前面(向数组中添加第一个元素时,数组容量扩为 10)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
}



//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        /**
          * 如果需要的最小容量 minCapacity 小于此时 elementData 数组的长度,则需要调用 grow() 函数进行扩容
          * 否则 ensureCapacityInternal() 函数执行结束,直接将新元素添加到 elementData 数组的末尾
          */ 
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}


private void grow(int minCapacity) {
        // 旧容量 oldCapacity , 数组 elementData 的长度
        int oldCapacity = elementData.length;
        // 新容量 newCapacity , 数组 elementData 长度的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
       	//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            
        /**
          * 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZ
          */
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        
        elementData = Arrays.copyOf(elementData, newCapacity);
}


/**
  * 如果 minCapacity 大于最大容量 , 则新容量则为 Integer.MAX_VALUE 
  * 否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8
  */
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
            
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

    

System.arraycopy() 和 Arrays.copyOf() 方法

System.arraycopy() 方法


public void add(int index, E element) {
		//越界检查
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
}

private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


/**
  * System.arraycopy()
  * 
  * src – the source array. 
  * srcPos – starting position in the source array. 
  * dest – the destination array. 
  * destPos starting position in the destination data. 
  * length – the number of array elements to be copied.
  *
  * 将 src 数组 从 srcPos 位置开始的元素复制到 dest 数组 desPos 位置,复制元素长度为 length
  */
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
                                       

Arrays.copyOf() 方法


/**
  * Arrays.copyOf()
  * 
  * original – the array to be copied 
  * newLength – the length of the copy to be returned 
  * newType – the class of the copy to be returned
  *
  * 以正确的顺序返回一个包含 original 列表中所有元素的数组(从第一个到最后一个元素)
  * 返回的数组的运行时类型是指定数组的运行时类型——newType
  */

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
}

联系: 看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法

区别: arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

ensureCapacity方法

ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢?

/**
    如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
     *
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

理论上来说,最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

我们通过下面的代码实际测试以下这个方法的效果:

public class EnsureCapacityTest {
	public static void main(String[] args) {
		ArrayList<Object> list = new ArrayList<Object>();
		final int N = 10000000;
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < N; i++) {
			list.add(i);
		}
		long endTime = System.currentTimeMillis();
		System.out.println("使用ensureCapacity方法前:"+(endTime - startTime));

	}
}

运行结果:

使用ensureCapacity方法前:2971
public class EnsureCapacityTest {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<Object>();
        final int N = 10000000;
        long startTime1 = System.currentTimeMillis();
        list.ensureCapacity(N);
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1));
    }
}

运行结果:

使用ensureCapacity方法后:2665

通过运行结果,我们可以看出向 ArrayList 添加大量元素之前使用ensureCapacity 方法可以提升性能。不过,这个性能差距几乎可以忽略不计。而且,实际项目根本也不可能往 ArrayList 里面添加这么多元素。

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

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

相关文章

谷粒学院复习

一、Mybatis Plus复习分布式系统唯一ID主键策略(面试)面试的时候就说知道有以下四种策略&#xff0c;分别介绍一下每一种&#xff0c;然后说一下项目中用的是雪花算法分类自动增长 AUTO INCREMENT就是自动增长&#xff0c;每次都会自动加一。缺点&#xff1a;如果在分库分表的场…

VUE: Vue3+TS的项目搭建及基础使用

简介 通过 Vue-cli4 创建的 Vue3TS 的项目&#xff0c;并进行一些基础使用的举例。 项目搭建 1. 进入命令提示符窗口 在要搭建项目的文件夹中&#xff0c;点击路径&#xff0c;输入CMD并按回车 2. 查看node版本、Vue-cli版本 2.1 node版本&#xff08;14.x以上&#xf…

基于域控的SSO单点登录

大家好&#xff0c;好久不见&#xff0c;今天老吕给大家来一篇偏冷门知识的文章。一、需求大型集团企业内部会有许多业务系统&#xff0c;工作人员也往往需要登录多个业务系统才能完成工作&#xff0c;这就可能会存在一些问题1、多套账号与密码需要记录或者记忆2、多次登录&…

14.live555mediaserver-setup请求与响应

live555工程代码路径 live555工程在我的gitee下&#xff08;doc下有思维导图、drawio图&#xff09;&#xff1a;https://gitee.com/lure_ai/live555/tree/master 学习demo live555mediaserver.cpp 学习线索和姿势 1.学习的线索和姿势 网络编程 流媒体的地基是网络编程&…

Git 的常用命令

Git 的常用命令 目录Git 的常用命令帮助初始化配置提交远程仓库管理版本控制删除分支管理查看文件提交、状态帮助 查看常用命令 git help查看某个命令的使用帮助 git help [命令]查看 git 使用指南&#xff08;这个命令会详细展示 git 的使用周期&#xff09; git help tut…

【BP靶场portswigger-客户端13】跨来源资源共享(CORS)-4个实验(全)

前言&#xff1a; 介绍&#xff1a; 博主&#xff1a;网络安全领域狂热爱好者&#xff08;承诺在CSDN永久无偿分享文章&#xff09;。 殊荣&#xff1a;CSDN网络安全领域优质创作者&#xff0c;2022年双十一业务安全保卫战-某厂第一名&#xff0c;某厂特邀数字业务安全研究员&…

2022.12 青少年机器人技术等级考试理论综合试卷(三级)

2022年12月 青少年机器人技术等级考试理论综合试卷&#xff08;三级&#xff09; 分数&#xff1a; 100 题数&#xff1a; 30 一、 单选题(共 20 题&#xff0c; 共 80 分) 1.舵机接到 Arduino UNO/Nano 主控板的 2 号引脚&#xff0c; 下列选项中&#xff0c; 实现舵机在 0 度…

4、字符串处理

目录 一、字符串的构造 二、字符串比较 三、字符串查找 四、字符串替换 五、字符串——数值转换 Matlab中的字符串函数有&#xff1a; 一、字符串的构造 字符串或字符串数组的构造可以通过直接给变量赋值来实现&#xff0c;具体表达式中字符串的内容需要写在单引号内。如…

ESP8266 ArduinoIDE 搭建web服务器与客户端开发

一、wifi 相关配置 1.1 无线终端 wifi 模式 此模式中&#xff0c;esp8266 会连接到指定 wifi 进行工作。 #include <ESP8266WiFi.h> // 本程序使用ESP8266WiFi库const char* ssid "home"; // 连接WiFi名&#xff08;此处使用home为示例&…

Vue2-Vue开发环境搭建

一、IDE编辑器&#xff1a;Vscode&#xff0c;自行下载安装即可 二、三种引入方式&#xff0c; 教程使用方式一引入 Vue官网&#xff1a;https://v2.cn.vuejs.org/v2/guide/installation.html 方式一&#xff1a;直接script引入 教程下载开发版本&#xff0c;下载到本地&…

使用人工智能机器人提高农业效率| 数据标注

人工智能技术创新不仅仅蔓延到智慧城市、智能建筑或新的混合工作模式&#xff1b;机器人还通过人工智能、自动拖拉机、实时监测农作物的传感器、无人机或水果和蔬菜收获机器人来彻底改变农业。今天&#xff0c;我们将向您介绍一些已经在农业中使用的最有趣的AI技术&#xff0c;…

微信小程序textarea的placeholder的行高怎么修改

目前不支持修改行高。如果你的内容设置了行高但是placeholder没有行高会导致输入内容的时候感觉不是对齐的&#xff0c;想要解决这个问题怎么办呢/ 我们可以自己写个text假装是placeholder的内容。然后textarea获取焦点输入内容的时候就不显示这个text的内容。 <view class…

新入公司 git基本命令使用(二) 小乌龟版

git命令行的操作复杂不直观,且容易出错. 这里推荐大家使用 git版小乌龟插件进行使用 下载地址 :https://tortoisegit.org/download/ 安装一路next即可 创建本地仓库 右键点击克隆, 然后输入项目地址,确认 拉取代码 右键点击同步 , 然后再界面中选择好对应的分支, 点击拉取 …

朴素贝叶斯分类算法和实例演示

文章目录贝叶斯公式算法原理实例演示代码实现本文开始&#xff0c;我们来学习一种新的机器学习方法&#xff1a;贝叶斯算法。 这次从最基础的朴素贝叶斯分类算法出发&#xff0c;了解相关的算法原理。 考虑如下一种分类问题&#xff1a;样本中只包含2类特征&#xff0c;标签只…

接口返回数据实体类属性大写变成小写

问题背景 今天遇到一个特别恶心的事情&#xff0c;我的返回实体类遵循了字段属性明明规则&#xff0c;驼峰命名法&#xff0c;在接口返回数据给前端的时候&#xff0c;所有数字那个字母全部自动变为了小写字母&#xff01; 错误的返回示例&#xff1a; 正确的返回示例&#x…

【鸟哥杂谈】腾讯云 CentOS8 Linux环境搭建docker

忘记过去&#xff0c;超越自己 ❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-01-15 ❤️❤️ 本篇更新记录 2023-01-15 ❤️&#x1f389; 欢迎关注 &#x1f50e;点赞 &#x1f44d;收藏 ⭐️留言&#x1f4dd;&#x1f64…

当青训营遇上码上掘金之主题四-攒青豆

theme: juejin 攒青豆 现有 n 个宽度为 1 的柱子&#xff0c;给出 n 个非负整数依次表示柱子的高度&#xff0c;排列后如下图所示&#xff0c;此时均匀从上空向下撒青豆&#xff0c;计算按此排列的柱子能接住多少青豆。&#xff08;不考虑边角堆积&#xff09; 以下为上图例子…

【JavaEE初阶】第一节.计算机是如何工作的

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 文章目录 前言 一、计算机发展历史 二、冯诺依曼体系 三、CPU 3.1 浅谈CPU 3.2 寄存器 3.3 指令 3.4 CPU的操作流程 3.5 时钟周期 四、编程语言 总结…

vue.js客服系统实时聊天项目开发(四)引入iconfont图标代码

普通引入模式下是这样的 首先&#xff0c;您需要在iconfont.cn上创建一个账号并添加图标。 然后&#xff0c;将iconfont的链接代码加入到页面的head标签中&#xff0c;例如&#xff1a; <link rel"stylesheet" href"//at.alicdn.com/t/font_123456_abcdefghi…

Docker为什莫方便(学习的记录)

Docker为什莫方便&#xff08;学习的记录&#xff09; 程序 — apk— 发布到商城------下载安装即可使用 程序----打包项目带上环境&#xff08;创建一个项目的镜像&#xff09;-----发布到docker仓库当中------下载安装运行即可 &#x1f315; docker的核心思想 将各个环境…