【List篇】ArrayList 详解(含图示说明)

news2024/9/23 17:19:34

Java中的ArrayList是一个动态数组,可以自动扩展容量以适应数据的添加和删除。它可以用来存储各种类型的数据,例如String,Integer,Boolean等。ArrayList实现了List接口,可以进行常见的List操作,例如添加、插入、删除和访问元素等。

在这里插入图片描述

ArrayList具有以下特点:

  • 有序的 :元素按照添加顺序排列。
  • 可重复的 :同一元素可以重复出现在不同位置。
  • 可变的 :可以动态调整数组大小。
  • 线程不安全 :在多线程环境下需要进行额外的同步措施。
  • 查询效率快。这是由于底层的数据结构是基于Object 数组实现的,且数组在内存中是一块连续的空间,所以每次执行get方法获取元素时,可以通过索引 + 地址的方式能够快速的在数组上的指定位置获取元素
  • 增删效率低。在执行add方法时,可能存在 扩容 ,就需要生成一个新的数组,然后将旧数组中的元素复制到新数组中,最后将新增的元素放在数组上;执行remove方法时,将指定位置元素删除后,后面所有元素向 左移 动一位;因为增删需要对数组的结构进行调整,所以效率低
  • 支持序列化: 实现 Serializable标记性接口。
  • 支持克隆功能: 实现 Cloneable 标记性接口。
  • 支持随机访问功能: 实现 RandomAccess 标记性接口。也就是通过下标获取元素对象的功能
  • 继承 Iterable 接口,可以使用 for-each 迭代

源码分析(JDK1.8)

成员变量属性

/**
  * 默认长度为10
  */
 private static final int DEFAULT_CAPACITY = 10;

 /**
  * 用于空实例的共享空数组实例
  * 是为了优化创建ArrayList空实例时产生不必要的空数组,使所有的ArrayList空实例底
  * 层数据结构都指向同一个空数组
  * 例如:当构造参数是指定的长度,且为0 
  *      当构造参数是一个集合,且该参数集合中的元素个数为0
  */
 private static final Object[] EMPTY_ELEMENTDATA = {};

 /**
  * 用于默认大小的空实例的共享空数组实例。
  * 将其与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要扩容多少
  * 用于标记当前ArrayList是使用空参构造的,在第一次使用add添加元素时,数组的最大长度直接使用默认的10
  */
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 /**
  * ★ 存储ArrayList元素的数组缓冲区,ArrayList的容量就是这个数组缓冲区的长度
  * 添加第一个元素时,任何elementData==DEFAULTCAPACITY_empty_elementData的空ArrayList
  * 都将扩展为DEFAULT_CAPACITY(默认初始容量10)
  */
 transient Object[] elementData; 

 /**
  * ★ 记录ArrayList 的元素个数
  */
 private int size;

 /**
  * ★ 数组最大的长度
  */
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造方法

ArrayList 有三个构造函数,空参构造方法,指定初始容量值构造方法和指定集合元素列表的构造方法,如果集合中的元素不为空,新建的ArrayList集合中的元素顺序就是构造参数中的元素顺序

  • 空参构造
    之前在网上看到一个说法" 使用空参构造一个ArrayList时,默认会创建一个容量为10的数组",这个说法其实不准确。在JDK1.8以前是这样的,但是为了节省内存资源,在JDK1.8版本之后,使用空参构造方法,只会创建一个空底层数据结构长度为0的空数组结构,当第一次执行add添加元素时,才会将数组容量扩充到默认10
 public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }
  • 指定初始容量值构造
    如果在构建ArrayList之前,就能明确元素的个数,那么就可以在构造ArrayList时,指定容量大小,这样就可以节省因扩容而造成的资源浪费
 public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
         //当指定参数大于0时,真实存储数据的数据长度就是指定值
         this.elementData = new Object[initialCapacity];
     } else if (initialCapacity == 0) {
         //当指定参数等于0时,使用空数组
         this.elementData = EMPTY_ELEMENTDATA;
     } else {
          //指定的长度参数不能小于0
         throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
     }
 }
  • 构造参数是一个集合,并按照参数集合中元素个数和顺序返回新的ArrayList
 public ArrayList(Collection<? extends E> c) {
     Object[] a = c.toArray();
     if ((size = a.length) != 0) {
         if (c.getClass() == ArrayList.class) {
             //如果构造参数集合的类型是ArrayList,那么构造参数的数组结构直接使用
             elementData = a;
         } else {
             //如果构造参数集合的类型不是ArrayList
             //先初始化的数组长度 = 构造参数集合元素个数
             //再将构造参数集合中的数据信息,copy到新的数组中
             elementData = Arrays.copyOf(a, size, Object[].class);
         }
     } else {
         //如果构造参数
         elementData = EMPTY_ELEMENTDATA;
     }
 }

ArrayList 的增删查

add(), 插入元素方法

ArrayList 的add方法有两个,尾部插入指定位置插入

  • 末尾添加
    顾名思义就是直接在数组中最后一个元素的下一个位置上存放新添的元素,在添加元素之前,会校验数组容量是否已经到达临界值,如果到达临界值了,就先扩容,再将新元素添加到扩容后的新数组结构中的最后一个元素后面
public boolean add(E e) {
  //确定底层elementData的数组长度,且校验是否需要扩容
  //size + 1 可以理解为假设长度,后面会和当前数组的实际长度相比较,以判断是否需要扩容
  ensureCapacityInternal(size + 1);   // ★ 扩容核心代码
  //添加元素,实际上就是赋值的操作
  elementData[size++] = e;
  return true;
}

在这里插入图片描述

  • 指定位置插入
    也叫插队添加,会打乱原本数组中元素的存储顺序。大致步骤如下:
    1.校验数组是否有足够的空间
    2.如果空间足够,直接将index及其之后的所有元素向后移动一位
    3.如果空间不够,那么先进行扩容,扩容后的新数组长度是旧数组长度的1.5倍,然后将index位置之前的元素全部等位copy到新数组中,index之后的元素全部以index + 1 的形式偏移一位copy到新数组中
public void add(int index, E element) {
   //校验参数index值是否大于数组的长度
   rangeCheckForAdd(index);
   
    //确定底层elementData的数组长度,且校验是否需要扩容
    //size + 1 可以理解为假设长度,后面会和当前数组的实际长度相比较,以判断是否需要扩容
    ensureCapacityInternal(size + 1);  

    //指定位置后面的元素,全部copy一份,并向右移动一位,以便于空出位置
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    //最后对指定位置赋值
    elementData[index] = element;
    size++;
}

在这里插入图片描述

扩容机制

上面有描述,ArrayList底层是一个动态数组,何为动态?就是在ArrayList每次在执行add方法时,都会先计算一下,假设当前元素添加成功后,数组的长度是否已经超过实际长度,如果超过,那么就自动进行扩容
简单看一下扩容机制源码:

/**
  * 计算数组长度
  */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
   if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
       //这里可以看到,当使用空参或者指定长度参数,指定构造参数集合元素为0//计算结果:此处所需的最小数组长度为10
       return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
   modCount++;  //记录数组长度修改次数
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //当所需最小数组长度 大于 当前实际长度时,进行扩容
        grow(minCapacity);  //★ 核心扩容机制
}

 /**
 * 扩容机制
   */
 private void grow(int minCapacity) {
     // overflow-conscious code
     int oldCapacity = elementData.length; //扩容前的数组长度
     //扩容后的新数组长度 = 扩容前的数组长度 * 1.5
     int newCapacity = oldCapacity + (oldCapacity >> 1); 
     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:
     //按照新的长度创建一个新数组,然后将原数组的数据copy到新数组中,并将add的元素加入到数组中
     elementData = Arrays.copyOf(elementData, newCapacity);
 }

remove(), 删除元素方法

ArrayList 的remove方法有两个,指定元素删除指定位置删除

  • 指定元素删除
    由于ArrayList 允许元素重复,所以指定元素删除方法可能存在删除多个。
public boolean remove(Object o) {
    if (o == null) {
        //如果指定删除的元素是null,那么循环去校验是由有元素为null
        //如果存在为null的元素,那么就匹配上,然后在内存中新开辟一个数组空间,将旧数组上,
        //从匹配上位置左边全部   按原index位置copy到新数组中,右边的全部按照index -1 的位置copy到新数组中
        //匹配几次,就需要重新修改几次数组的数组结构
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        //如果指定删除的元素不是null,那么循环去校验去匹配
        //同理
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/*
 * 快速删除方法
 */
private void fastRemove(int index) {
   modCount++;  //记录数组结构修改的次数
    int numMoved = size - index - 1;  //统计需要移动的元素个数
    if (numMoved > 0)
        //参数1:elementData  旧数组
        //参数2:index+1  源数组起点
        //参数3:elementData 新数组
        //参数4:index  新数组起点
        //参数5:numMoved 复制多少位
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    //数组中的最后一位置为null,地址引用删除,方便GC清除    
    elementData[--size] = null; 
}

在这里插入图片描述

  • 指定位置删除
    最多只会删除一个元素,如果指定位置index超出数组结构长度,报错 IndexOutOfBoundsException
  public E remove(int index) {
      //校验指定index是否超出数组结构长度
      rangeCheck(index);

      modCount++;  //记录数组结构发生调整的次数
      //通过指定索引index,获取数组元素信息
      E oldValue = elementData(index);
      //统计需要移动的元素个数
      int numMoved = size - index - 1;
      if (numMoved > 0)
          System.arraycopy(elementData, index+1, elementData, index,numMoved);
      //数组中的最后一位置为null,地址引用删除,方便GC清除    
      elementData[--size] = null; // clear to let GC do its work
      return oldValue;
  }

/**
  * 校验指定index是否超出数组结构长度
  */
 private void rangeCheck(int index) {
     if (index >= size)
         throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
 }

在这里插入图片描述
ArrayList在执行remove()方法做删除之后,数组中可能会出现大量的空闲空间。而ArrayList没有自动缩容机制,导致底层大量的空闲空间不能被释放,造成内存浪费。对于这种场景,ArrayList也提供了相应的处理方法,如下:

/**
 * 将此ArrayList实例的容量修改为列表的当前大小。
 */
 public void trimToSize() {
     //记录数组结构发生调整的次数
     modCount++;
     //size 是当前数组中的元素个数
     //当元素个数小于数组实际长度时,做缩容处理; 比例按照元素个数进行缩容
     if (size < elementData.length) {
         //实际元素个数为0,直接将数组置为空数组
         elementData = (size == 0)
           ? EMPTY_ELEMENTDATA
           : Arrays.copyOf(elementData, size);  //生成新数组, 长度 = 元素个数,将元素copy到新数组中
     }
 }

ArrayList 的增删方法总结, 为什么ArrayList是增删慢的?
从上文介绍可知, add() 方法会存在扩容场景, remove() 会存在移动元素的场景, 这些都会对性能产生很大的影响。用时间复杂度来表示是O(n),所以增删效率低

get(), 取元素方法

ArrayList 的get方法只有一个,只能通过索引下标index获取

public E get(int index) {
    //校验指定index是否超出数组结构长度
    rangeCheck(index);
    
    //直接通过索引index获取指定位置的value值
   return elementData(index);
}

/**
  *  校验index的有效性
  *  index 不能超过数组元素的个数,否则会报数组越界异常
  */
private void rangeCheck(int index) {
    if (index >= size)
         throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
 }

首先数组是存在堆内存中,一块连续的内存空间,并且ArrayList的底层是Object[]数组, 那么数组中的所有元素所占用的字节大小是一致的,所以想要从堆内存中获取元素信息,就必须知道index位置元素在堆内存中的内存地址。数组上元素的内存地址,主要就是看index下标对应的偏移量,这里就需要计算:
公式: 数组上元素的内存地址 = 数组的起始地址 + index * 元素的大小(单位是字节,)
计算出index对应的元素内存地址后, 即可获取到对应数组上元素信息, 而数组中存放的是元素实际数据信息的内存地址,再通过此地址获取到实际元素数据信息
在这里插入图片描述
由于get()取元素方法,是通过下标index 精准定位,继而获取元素信息的, 这期间没有所谓的扩容,copy,迁移等等场景, 用时间复杂度来表示是O(1),所以效率高。

set(), 修改元素方法

ArrayList 中的修改方法只有一个,通过index下标来精准修改, 修改元素信息的步骤,其实就是同一个位置的信息覆盖操作

 public E set(int index, E element) {
     //校验指定index是否超出数组结构长度
     rangeCheck(index);

     //通过index,获取旧的元素信息
     E oldValue = elementData(index);

     //然后覆盖替换
     elementData[index] = element;
     return oldValue;
 }

/**
  *  校验index的有效性
  *  index 不能超过数组元素的个数,否则会报数组越界异常
  */
private void rangeCheck(int index) {
    if (index >= size)
         throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
 }

在这里插入图片描述

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

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

相关文章

ping与Traceroute是如何工作的

ping 是基于 ICMP 协议工作的。ICMP 全称 Internet Control Message Protocol&#xff0c;就是互联网控制报文协议。 ICMP 报文是封装在 IP 包里面的。因为传输指令的时候&#xff0c;肯定需要源地址和目标地址。它本身非常简单。 ICMP 报文有很多的类型&#xff0c;不同的类型…

最近读书了吗?林曦老师与你分享来自暄桐课堂的读书方法

近来&#xff0c;大家有在开心读书吗&#xff1f;对于读书&#xff0c;有一个很生动的说法&#xff1a;“无事常读书&#xff0c;一日是四日。若活七十年&#xff0c;便二百八十。”读书帮助我们超越个体生命经验的限制&#xff0c;此时此地的我们&#xff0c;也可借由书本&…

JS-DOM BOM(未完)

一.Web APIs 1.Web APIs和JS基础关联性 1.1 JS组成 1.2 JS基础阶段以及Web APIs阶段 2.API和Web API 2.1 API 2.2 Web API 2.3 API和Web API总结 二.DOM 1.DOM简介 1.1 什么是DOM 1.2 DOM树 2.获取元素 2.1 如何获取页面元素 2.2 根据ID获取 使用getElementById()方法获…

爬虫系统的核心:如何创建高质量的HTML文件?

在网页抓取或爬虫系统中&#xff0c;HTML文件的创建是一项重要的任务。HTML文件是网页的基础&#xff0c;包含了网页的所有内容和结构。在爬虫系统中&#xff0c;我们需要生成一个HTML文件&#xff0c;以便于保存和处理网页的内容。 在这种情况下&#xff0c;可以使用Java函数…

deepfm内容理解

对于CTR问题&#xff0c;被证明的最有效的提升任务表现的策略是特征组合(Feature Interaction)&#xff1b; 两个问题&#xff1a; 如何更好地学习特征组合&#xff0c;进而更加精确地描述数据的特点&#xff1b; 如何更高效的学习特征组合。 DNN局限 &#xff1a;当我们使…

ESXI主机扩容(VCSA)

原因分析SCSI扩容 VMware为ESXI虚拟机硬盘扩容(需要先关闭ESXI) ESXI扩容前ESXI扩容 https://blog.csdn.net/tongxin_tongmeng/article/details/132652423 ESXI扩容后

2023年高教社杯数学建模国赛 赛题浅析

2023年国赛如期而至&#xff0c;为了方便大家尽快确定选题&#xff0c;这里将对赛题进行浅析&#xff0c;以分析赛题的主要难点、出题思路以及选择之后可能遇到的难点进行说明&#xff0c;方便大家尽快确定选题。 难度排序 B>A>C 选题人数 C>A>B (预估结果&…

软件工程课件

软件工程 考点概述软件工程概述能力成度模型能力成熟度模型集成软件过程模型逆向工程![ ](https://img-blog.csdnimg.cn/425cea8190fb4c5ab2bf7be5e2ad990e.png) 考点概述 重点章节 软件工程概述 之前老版教程的&#xff0c;之前考过 能力成度模型 记忆 能力等级 和 特点 能力…

【Java】基础练习 --- Stream练习

1.拼接 给定一个字符串数组,使用 Stream 把所有字符串拼接成一个字符串。 String[] arr {"a", "b", "c"}; 输出: abc&#xff08;1&#xff09;源码&#xff1a; package swp.kaifamiao.codes.Java.d0907;import java.util.stream.Stream;/*…

2023高教社杯全国大学生数学建模竞赛E题代码解析

2023高教社杯全国大学生数学建模竞赛E题 黄河水沙监测数据分析 代码解析 因为一些不可抗力&#xff0c;下面仅展示部分python代码&#xff08;第一问的部分&#xff09;&#xff0c;其余代码看文末 首先导入包&#xff1a; import numpy as np import pandas as pd import m…

【Python】matplotlib分格显示

参考&#xff1a;matplotlib图形整合之多个子图一起绘制_matplotlib多子图_王小王-123的博客-CSDN博客 方式一&#xff1a; import matplotlib.pyplot as plt import matplotlib.gridspec as gridspecplt.figure() # 方式一: gridspec # rowspan:行的跨度&#xff0c;colspan…

【代码随想录】Day 49 动态规划10 (买卖股票Ⅰ、Ⅱ)

买卖股票的最佳时机 https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/ dp[i]表示在第i天时&#xff0c;卖/不卖股票能获得的最大利润&#xff1a; 1、卖股票&#xff1a;dp[i] prices[i] -minPrice&#xff08;i天以前的最低价格&#xff09; 2、不卖股票&am…

Python web自动化测试 —— 文件上传

文件上传三种方式&#xff1a; &#xff08;一&#xff09;查看元素标签&#xff0c;如果是input&#xff0c;则可以参照文本框输入的形式进行文件上传 方法&#xff1a;和用户输入是一样的&#xff0c;使用send_keys 1 2 3 4 5 步骤&#xff1a;1、找到定位元素&#xff0c…

ICCV2021 Exploring Cross-Image Pixel Contrast for Semantic Segmentation (Oral)

Exploring Cross-Image Pixel Contrast for Semantic Segmentation 探索语义分割的跨图像像素对比度 Paper&#xff1a;https://openaccess.thecvf.com/content/ICCV2021/html/Wang_Exploring_Cross-Image_Pixel_Contrast_for_Semantic_Segmentation_ICCV_2021_paper.html Co…

k8s 入门到实战--部署应用到 k8s

k8s 入门到实战 01.png 本文提供视频版&#xff1a; 背景 最近这这段时间更新了一些 k8s 相关的博客和视频&#xff0c;也收到了一些反馈&#xff1b;大概分为这几类&#xff1a; 公司已经经历过服务化改造了&#xff0c;但还未接触过云原生。公司部分应用进行了云原生改造&…

Spring MVC拦截器

拦截器&#xff08;Interceptor&#xff09;是 Spring MVC 提供的一种强大的功能组件。它可以对用户请求进行拦截&#xff0c;并在请求进入控制器&#xff08;Controller&#xff09;之前、控制器处理完请求后、甚至是渲染视图后&#xff0c;执行一些指定的操作。 在 Spring MV…

springboot设置热部署

一、常见的三种方式&#xff1a; Springboot中常见的热部署方式有3种: 1.使用springloaded配置pom.xml文件&#xff0c;使用mvn spring-boot:run启动 2.使用springloaded本地加载启动&#xff0c;配置jvm参数 3.使用devtools工具包&#xff0c;操作简单&#xff0c;但是每次…

python开发基础篇1——后端操作K8s API方式

文章目录 一、基本了解1.1 操作k8s API1.2 基本使用 二、数据表格展示K8s常见资源2.1 Namespace2.2 Node2.3 PV2.4 Deployment2.5 DaemonSet2.6 StatefulSet2.7 Pod2.8 Service2.9 Ingress2.10 PVC2.11 ConfigMap2.12 Secret2.13 优化 一、基本了解 操作K8s资源api方式&#xf…

第11篇:ESP32vscode_platformio_idf框架helloworld点亮LED

第1篇:Arduino与ESP32开发板的安装方法 第2篇:ESP32 helloword第一个程序示范点亮板载LED 第3篇:vscode搭建esp32 arduino开发环境 第4篇:vscodeplatformio搭建esp32 arduino开发环境 ​​​​​​第5篇:doit_esp32_devkit_v1使用pmw呼吸灯实验 第6篇:ESP32连接无源喇叭播…

酷克数据推出AI开发工具箱HashML 加速企业级AI应用落地投产

近日&#xff0c;业界领先的国产企业级云数仓厂商酷克数据发布了下一代In-Database高级分析和数据科学工具箱HashML&#xff0c;在业内率先实现为企业提供随数仓部署一步到位、开箱即用的AI能力。 在数字经济时代&#xff0c;描述性分析已经非常成熟并被企业广泛采纳。然而&am…