Java 实例 – 数组扩容(超详细)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观

在 Java 开发中,数组是一个基础且重要的数据结构。然而,数组一旦被初始化后,其长度便无法直接修改。当需要动态调整存储容量时,开发者常通过“数组扩容”这一技术来实现灵活的数据管理。本文将围绕 “Java 实例 – 数组扩容” 展开,从概念解析、手动实现、核心库源码分析到性能优化策略,逐步带领读者深入理解这一主题。


一、数组扩容的必要性

1.1 数组的基本特性与局限性

数组是静态数据结构,其长度在初始化时便被固定。例如,声明一个长度为 5 的整型数组时:

int[] numbers = new int[5];  

如果后续需要存储超过 5 个元素,程序会抛出 ArrayIndexOutOfBoundsException 异常。此时,扩容是解决问题的核心手段。

1.2 动态需求场景举例

假设我们正在开发一个日志记录系统,需要将每条日志存入数组。但无法预知日志数量,若初始数组过小,将导致存储失败;若初始数组过大,则浪费内存资源。此时,通过动态扩容的数组(如 ArrayList)能平衡空间与性能。


二、手动实现数组扩容

2.1 扩容的基本逻辑

扩容的核心思想是:

  1. 创建新数组:新数组的长度通常为原数组的 1.5~2 倍
  2. 数据迁移:将原数组中的元素复制到新数组。
  3. 替换引用:用新数组替换旧数组的引用,完成容量扩展。

以下是一个手动扩容的代码示例:

public class MyArrayList {  
    private int[] elements;  
    private int size;  

    public MyArrayList() {  
        elements = new int[10]; // 初始容量为 10  
    }  

    public void add(int element) {  
        if (size >= elements.length) {  
            // 扩容操作  
            int newCapacity = elements.length * 2;  
            int[] newArr = new int[newCapacity];  
            System.arraycopy(elements, 0, newArr, 0, elements.length);  
            elements = newArr;  
        }  
        elements[size++] = element;  
    }  
}  

2.2 扩容的比喻解释

可以将扩容想象成 “搬家”

  • 旧房子(原数组):空间有限,无法容纳新物品。
  • 新房子(新数组):空间更大,但需将所有物品(元素)搬入新位置。
  • 搬家成本:时间(复制数据)与资源(内存占用)。

三、Java 核心库中的数组扩容机制

3.1 ArrayList 的底层实现

Java 的 ArrayList 是动态数组的经典实现,其扩容逻辑与手动实现类似。通过分析源码片段,可发现其核心逻辑:

// ArrayList.add() 方法片段(简化版)  
public boolean add(E e) {  
    if (size >= elements.length)  
        grow();  
    elements[size++] = e;  
    return true;  
}  

private void grow() {  
    int oldCapacity = elements.length;  
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为 1.5 倍  
    // ... 其他逻辑 ...  
}  

此处,扩容倍数为 1.5 倍,而非手动示例中的 2 倍,这是为了平衡内存占用与扩容频率。

3.2 扩容触发条件

ArrayList 的扩容仅在以下情况发生:

  • 添加元素时:当 size 达到当前容量的阈值。
  • 初始化时:若通过 new ArrayList<>(initialCapacity) 指定初始容量,则首次扩容时仍遵循倍数规则。

四、数组扩容的性能影响

4.1 时间与空间的权衡

扩容操作的时间复杂度为 O(n)(需复制所有元素),而空间复杂度为 O(2n)(新数组占用额外内存)。因此,频繁扩容会导致性能下降。

4.2 性能案例分析

假设需要存储 1000 个元素:

  • 初始容量为 1:需扩容 9 次(1 → 2 → 4 → 8 → ... → 1024),总操作次数为 约 2000 次
  • 初始容量为 1000:无需扩容,总操作次数为 1000 次

通过表格对比扩容次数与时间开销:

初始容量扩容次数总操作次数
191999
10031300
100001000

五、优化扩容策略

5.1 合理设置初始容量

若能预估数据量,可通过构造函数指定初始容量,例如:

ArrayList<String> logs = new ArrayList<>(1000); // 预设容量为 1000  

这可减少扩容次数,提升性能。

5.2 批量操作的优先级

使用 addAll() 方法批量添加元素,比多次单个 add() 更高效。例如:

List<Integer> list = new ArrayList<>();  
List<Integer> elements = Arrays.asList(1, 2, 3, 4, 5);  
list.addAll(elements); // 一次扩容  
// 而非:  
for (int num : elements) list.add(num); // 可能触发多次扩容  

六、避免扩容的替代方案

6.1 静态数组的适用场景

若数据量固定,可直接使用静态数组,避免扩容开销。例如:

// 存储固定长度的传感器数据  
float[] sensorData = new float[1024];  

6.2 其他动态结构的选择

若需频繁插入或删除元素,可考虑 LinkedList(双向链表),其插入/删除操作的时间复杂度为 O(1)(不涉及扩容),但随机访问性能较差。


结论

通过本文对 “Java 实例 – 数组扩容” 的系统讲解,我们明确了数组扩容的核心逻辑、实现方法以及优化策略。无论是手动实现动态数组,还是通过 ArrayList 的封装,扩容的本质都是在 内存效率执行速度 之间寻找平衡点。开发者需根据实际场景选择合适的数据结构,并通过合理设置初始容量、减少扩容次数等手段,进一步提升程序性能。

掌握数组扩容不仅能让开发者理解 Java 核心库的设计思想,还能在实际开发中应对动态数据存储的挑战。希望本文能帮助读者在编程实践中更加灵活地运用这一技术!

最新发布