数组基础——Java“抽屉柜”

如果把 Java 中的数组比作生活中的东西,那它就像一个带编号的抽屉柜—— 每个抽屉大小一样,按顺序排好队,还有自己专属的编号(索引),存取东西贼方便(︶.̮︶✽)

数组,表示一块连续的内存空间,可用来存储多个数据(元素),要求元素类型要一致。

int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
int a5 = 5;
// int类型变量,用来标识1块内存,只能用来存放1个数值
//不够方便
int[] arr = {1,2,3,4,5};
// 这里可以使用一个数组来保存这5个元素值

数组定义

数组的定义有2种格式,分别如下:

数据类型[ ] 数组名;(推荐用法)

int[] arr;        
double[] arr;      
char[] arr;

数据类型 数组名[ ];(像c++不是吗)

int arr[];
double arr[];
char arr[];

对于数组定义,大家可能会说:ψ(`∇´)ψ有什么关系,想用啥用啥。还有学c的小伙伴看着第一个定义方式顺眼,不过这个毕竟不是只有我推荐,推荐原因我给你仔细说道一下:

虽然两种写法语法上完全等价,但几乎所有 Java 开发者和官方规范都推荐用写法 1(数据类型 [ ] 数组名),核心原因是这种写法更符合 Java 的设计逻辑,能避免歧义,让代码更易读。

1. 更直观地体现 “数组是一种类型”

Java 中,int[ ] 本身是一种独立的数据类型(“int 数组类型”),而不是 “int 类型的数组变量”。

写法 1(int[ ] arr)清晰表达了:arr 是一个 “int 数组类型” 的变量,变量类型是 int[ ]。
写法 2(int arr[ ])容易让人误解为:arr 是一个 “int 类型” 的变量,只是后面加了个 [ ] 表示数组(这更像 C/C++ 的语法习惯)。

声明多个数组时,差异更明显:

// 写法1:清晰表示 a、b 都是 int[] 类型
int[] a, b;  

// 写法2:容易误解为 b 是 int 类型(实际 b 也是 int[],但视觉上有歧义)
int a[], b;  

2. 符合 Java 官方编码规范

Java 官方文档(如《Java Language Specification》)和主流编码规范(如 Google Java 风格指南)都明确推荐 数据类型[ ] 数组名 的写法。

这种规范不是强制的,但遵循它能让代码在团队协作中更统一,减少沟通成本。就像大家都靠右行,不是因为左边不能走,而是统一规则能提高效率。

3. 避免 “类型与变量” 的混淆

对于引用类型数组,写法 1 的优势更突出:

// 写法1:明确是“String 数组”类型
String[] names;  

// 写法2:视觉上容易拆分理解,不够直观
String names[];  

为什么会有两种写法?
数据类型 数组名[ ] 是 Java 为了兼容 C/C++ 开发者的习惯而保留的语法(C 语言中数组声明就是 int arr[ ])。但 Java 作为一门新语言,在设计时更强调 “类型的清晰性”,因此推荐 数据类型[ ] 数组名 这种更符合自身设计理念的写法。

就像给变量起名要见名知意,数组声明的规范也是为了让代码 “说话”—— 一眼就能看出变量的类型和含义。


大家看下面代码,我们来分析下数组的内存构成。

public static void main(String[] args){
    // 定于变量a
    int a;
    // 定义变量b并初始化
    int b = 10;
    // 定义数组arr,注意未初始化
    int[] arr;
    
    // 编译报错
    //System.out.println(a);
    System.out.println(b);
    // 编译报错
    //System.out.println(arr);
}

 

注意1:数组是引用数据类型,用来存储一个引用值(地址值)

注意2:数组没有进行初始化,不可以直接使用


数组初始化

定义数组(开辟栈空间内存)的同时,给其赋上初值,就叫做数组的初始化。也就是我们为其开盘空间并存放数据这个过程。

动态初始化

指定长度,元素由系统赋默认值(数字 0、布尔 false、引用类型 null)。

完整格式:数据类型[ ] 数组名 = new 数据类型[数组长度];

int[] arr1 = new int[3];
double[] arr2 = new double[2];
String[] arr3 = new String[4];

注意:
new 是一个关键字,表示为数组开辟内存空间
等号两边的数据类型要一致
数组长度必须要有,可以>=0(一般大于0),但不能为负数

内存情况:

数组名标识的那块内存(栈空间),存放了一个引用值(地址值),通过该地址值可以找到堆空间相应内存(用来存放数组中所有元素)。

堆空间内存存在默认初始化:整形数初始化为0,浮点数0.0,引用类型 null,字符类型初始化 \u0000

可以用下面这段代码来验证,不过地址值不一样这个就得问你的计算机了ฅ(̳•·̫•̳ฅ)

public class TestOfArr{
 public static void main(String[] args) {
 // 定义并初始化数组arr
    int[] arr = new int[4];
    
        /* 验证:上图中堆空间的存在 */

    System.out.println(arr);
    System.out.println("-------------------");
    
        /* 验证:元素初始值为0 */
    // 访问数组元素可以通过数组下标来实现,具体格式:数组名[下标]
    // 注意,下标从0开始,最大值为 数组长度-1
    System.out.println(arr[0]); //第1个元素
    System.out.println(arr[3]); //第4个元素
    System.out.println("-------------------");
    
        //数组的遍历
    for(int i = 0; i < 4; i++) {
   System.out.println(arr[i]);
   }
 }
}

以我的地址值举例,这个[I@15db9742 什么意思呢?(。ヘ°)
[ : 当前的空间是一个数组类型    
I : 当前数组容器中所存储的数据类型    
@ : 分隔符  (固定格式)
15db9742 : 堆空间十六进制内存地址

其实十六进制数字才是数组真正的地址值,但是我们平时习惯把主题叫作它的地址值

默认值的 “隐藏陷阱”
动态初始化后,若未手动赋值就使用元素,可能因默认值导致逻辑错误。例如:

boolean[] flags = new boolean[1];
if (flags[0]) { // flags[0]默认是false,可能不符合预期逻辑
    // ...
}

静态初始化

直接指定元素,长度由元素个数决定(编译期确定)。

静态初始化格式:
完整版格式
数据类型[ ] 数组名 = new 数据类型[ ]{元素1,元素2,...};
简化版格式
数据类型[ ] 数组名 = {元素1,元素2,...};

int[] arr1 = {1, 2, 3}; // 正确
int[] arr2;
arr2 = {1, 2, 3}; // 错误!静态初始化语法只能在声明时使用

arr2 = new int[]{1, 2, 3}; // 正确(动态初始化的一种,可分开写)

和上面动态相比提前告诉你的条件不一样(和我上一篇说的 for、while 一样)


数组下标

数组的下标的区间为 [0, 数组长度-1] 。
如果数组长度为length,那么数组下标的最小值为0,下标最大值为 length-1 。

//数组长度为4,那么其下标就是0~3
int[] arr1 = new int[4];
//可以通过下标获取数组元素值
System.out.println(arr[0]);
System.out.println(arr[3]);
//也可以通过数组下标给数组元素赋值
int[] arr2 = new int[4];
arr[0] = 337;
arr[1] = 340;
arr[2] = 348;
arr[3] = 352;

数组下标是从0开始的,下标也叫作索引,角标,是数组每个元素的“编号”。

我们可以结合循环来赋值或者取值,这也是常见的数组操作

int[] arr = new int[4];
//数组下标的取值范围,从0开始,到数组长度-1
for(int i = 0; i < 4; i++){
    arr[i] = 10 + i;
}
//获取数组每个元素的值,并且输出
for(int i = 0; i < 4; i++){
    System.out.println(arr[i]);

“索引”(从 0 开始)并非单纯的 “编号”,而是内存偏移量的抽象:索引 0 对应偏移量 0(首元素),索引 n 对应偏移量 n× 元素大小,这种映射关系让开发者无需直接操作内存地址,即可通过简洁的语法(如arr[2])访问元素,实现了对底层内存的 “逻辑封装”。


数组长度

数组长度,是指在一个数组中,可以存放同一类型元素的最大数量。

获取数组长度固定格式: 数组名.length

数组长度注意事项:
数组长度,必须在创建数组对象的时候就明确指定
数组长度,一旦确定,就无法再改变
数组长度,可以>=0(一般大于0),但不能为负数

借助循环赋值或遍历的最终形式:

public class Test_Length {
 public static void main(String[] args) {
 int[] arr = new int[4];
 
 //遍历数组中初始元素值,默认为0
 for(int i = 0; i < arr.length; i++){
    System.out.println(arr[i]);
 }
 
 System.out.println("--------------");
 //逐个元素赋值,借助 数组名.length 完成
 for(int i = 0;i < arr.length; i++){
    arr[i] = 10 + i;
 }
 //遍历数组中所有元素值
 for(int i = 0; i < arr.length; i++){
    System.out.println(arr[i]);
 }
 }
}

数组的长度在创建时即被确定且不可修改,本质是因为连续内存块的大小无法动态调整(否则会覆盖相邻内存数据)。这种 “固定性” 看似限制,实则是对内存安全的保障 —— 通过预先分配确定大小的空间,避免内存碎片化。


数组赋值中的类型转换

数组声明和创建时,元素类型必须完全一致(int[ ] 对应 int[ ],double[ ] 对应 double[ ]),否则编译报错。

不同类型的数组之间不能直接赋值,需要转换元素类型

示例 1:将 int 元素存入 double 数组(隐式类型转换)

int[] intArr = {1, 2, 3};
double[] doubleArr = new double[intArr.length];

// 手动遍历,逐个将 int 转为 double(向上转型,安全)
for (int i = 0; i < intArr.length; i++) {
    doubleArr[i] = intArr[i]; // int 自动转为 double
}

示例 2:将 double 元素存入 int 数组(需强制类型转换)

double[] doubleArr = {1.5, 2.8, 3.0};
int[] intArr = new int[doubleArr.length];

// 手动遍历,逐个将 double 转为 int(可能丢失精度,需显式强制转换)
for (int i = 0; i < doubleArr.length; i++) {
    intArr[i] = (int) doubleArr[i]; // 强制转换,小数部分被截断
}
// 结果:intArr = [1, 2, 3]

单独存一个也是如此,不过要注意精度损失问题

这是 Java 强类型特性的体现,虽然看似严格,但能在编译阶段避免类型不匹配的逻辑错误。


数组拷贝

数组的长度确定后便不能修改,如果需要数组存放更多元素,可以通过创建长度更长的新数组,然后先复制老数组内容到新数组中,再往新数组中放入额外的元素。

在 java.lang.System 类中提供一个名为 arraycopy 的方法可以实现复制数组中元素的功能

//该方法的声明
public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
//参数1,需要被复制的目标数组
//参数2,从目标数组的哪一个位置开始复制
//参数3,需要把数据复制到另外一个新的数组中
//参数4,把数据复制到新数组的时候,需要把数据从什么位置开始复制进去
//参数5,复制的目标数组的长度
public class ArrayCopyDemo {
    public static void main(String[] args) {
        int[] src = {10, 20, 30, 40, 50}; // 源数组
        int[] dest = new int[6]; // 目标数组(长度6,比源数组长)
        
        // 初始化目标数组的部分元素
        dest[0] = 1;
        dest[1] = 2;
        
        /* 
         * arraycopy参数说明:
         * 1. 源数组(src)
         * 2. 源数组起始索引(从src的哪个位置开始复制)
         * 3. 目标数组(dest)
         * 4. 目标数组起始索引(复制到dest的哪个位置)
         * 5. 复制的元素个数
         */
        System.arraycopy(src, 1, dest, 2, 3);
        
        // 打印源数组(不变)
        System.out.print("源数组:");
        for (int i = 0; i < src.length; i++) { // 用索引i遍历,从0到数组长度-1
            System.out.print(src[i] + " "); // 通过索引访问元素
        }
        
        // 打印目标数组(变化后)
        System.out.print("\n目标数组:");
        for (int j = 0; j < dest.length; j++) { // 用索引j遍历
            System.out.print(dest[j] + " "); // 通过索引访问元素
        }
    }
}

数组异常

使用数组的过程中,经常会遇到以下2种异常

索引越界异常

public class Test01{
 public static void main(String[] args) {
    int[] arr = new int[4];
    //数组下标最大取值为3,现在取4,超出了范围,会产生索引越界异常
    System.out.println(arr[4]);
 }
}

程序运行后,将会抛出 ArrayIndexOutOfBoundsException 数组越界异常。在开发中,数组越界异常是不能出现的,一旦出现了,就必须要修改代码。
避坑技巧:遍历数组时,用arr.length控制边界(如for(int i=0; i<arr.length; i++)),避免硬编码数字。

空指针异常

public class Test02{
 public static void main(String[] args) {
    int[] arr = new int[3];
    //把null赋值给数组
    arr = null;
    System.out.println(arr[0]);
 }
}

arr = null 这行代码,意味着变量arr将不会在保存数组的内存地址,我们通过arr这个标识符再也找不到堆空间数组元素,因此运行的时候会抛出 NullPointerException 空指针异常。
在开发中,空指针异常也是不能出现的,我们给数组一个真正的堆内存空间引用即可。


内存中的数组(内存图

两个数组内存结构图

如果我们将 arr1 赋给 arr2 呢

这就体现了一个重要的本质:我们对数组的存储,本质是存了它的地址值,比如我们直接打印一个数组打印出来的就是其地址值,这也是 new 关键字的重要体现,我们如果不new一个空间出来,把 arr1 赋给 arr2 本质只是把地址值给了 arr1 保存 

索引我们对数组的 “相等判断”,用==直接比较数组时,比较的是引用(地址),而非元素是否相同。判断元素是否全相等需手动遍历,或使用Arrays.equals()

int[] arr1 = {1, 2};
int[] arr2 = {1, 2};
System.out.println(arr1 == arr2); // false(不同对象)
System.out.println(Arrays.equals(arr1, arr2)); // true(元素相同)

Arrays工具类

java.util.Arrays 类,是JavaSE API中提供给我们使用的一个工具类,这个类中包含了操作数组的常用方法,比如排序、查询、复制、填充数据等,借助它我们在代码中操作数组会更方便。

Arrays中的常用方法:

equals方法:判断两个数组的长度和对应位置的元素是否完全相同
toString方法:可以把一个数组变为对应的String形式
copyOf方法:可以把一个数组进行复制
该方法中也是采用了arraycopy方法来实现的功能
sort方法:可以对数组进行排序
binarySearch方法:在数组中,查找指定的值,返回这个指定的值在数组中的下标,但是查找之前需要在数组中先进行排序,可以使用sort方法先进行排序
copyOfRange方法:也是复制数组的方法,但是可以指定从哪一个下标位置开始复制
该方法中也是采用了arraycopy方法来实现的功能
fill:可以使用一个特定的值,把数组中的空间全都赋成这个值

import java.util.Arrays;
//导包不要忘了
public class Test_Arrays {
 public static void main(String[] args) {
 int[] a = {1,3,5,2,6,8};
 //获取数组的字符串形式并输出
 System.out.println(Arrays.toString(a));
 //借助工具类完成数组拷贝
 a = Arrays.copyOf(a,10);
 //思考:数组的长度不能修改,此时a长度变成了10
 //就是因为该方法中也是采用了arraycopy方法来实现的功能,其实就是new了一个新数组
 System.out.println(a.length);
 System.out.println(Arrays.toString(a));
 //对数组排序
 Arrays.sort(a);
 System.out.println(Arrays.toString(a));
 //二分查找
 int index = Arrays.binarySearch(a,5);
 System.out.println(index);
 //数组元素填充
 Arrays.fill(a,100);
 System.out.println(Arrays.toString(a));
 }
}

二维数组

如果把普通的数组(一维数组),看作一个抽屉柜的话,那么二维数组就是像一个抽屉柜里面可以存放很多文件袋 (一维数组),三维四维等等往上也是有的,不过基本用不到(´▽`).。o

二维数组固定定义格式有2种,具体如下:(动态初始化

格式1:
数据类型[ ][ ] 数组名 = new 数据类型[一维长度m][二维长度n];
m:表示二维数组的元素数量,即可以存放多少个一维数组 
n:表示每一个一维数组,可以存放多少个元素
格式2:
数据类型[ ][ ] 数组名 = new 数据类型[一维长度][ ];

import java.util.Arrays;
public class Test{
 public static void main(String[] args) {
 //一维长度2,代表这个二维数组里面包含2个元素,每个元素都是一个
一维数组
 //二维长度3,代表这个二维数组中元素,类型都是int[3]的一维数
组,存放3个int数据
 int[][] arr = new int[2][3];
 /*
 [[I@15db9742
 [[: 2个中括号就代表的是2维数组
 I: 数组中存储的数据类型为int
 15db9742: 十六进制内存地址
 */
 System.out.println(arr);
 //二维数组的每个元素值(第一维), 对应的是一维数组的内存地址值
 System.out.println(arr[0]); //[I@15db9742
 System.out.println(arr[1]); //[I@6d06d69c
 System.out.println("--------------");
 
 //第二种定义格式
 int[][] arr2 = new int[2][];
        
 //输出arr2中2个元素值,默认为null、null
 System.out.println(Arrays.toString(arr2));
 
 //给二维数组的每个元素赋值
 //arr[0] = new int[2];
 //arr[1] = new int[3];
 }
}

内存结构:

可以把二维数组看成一个一维数组,数组的每个元素对应的内存区域中,存放的是一维数组引用值,具体可参考下面2个图:

二维数组中元素的访问和赋值,也是通过数组下标实现的。
二维数组名[一维下标m][二维下标n];

二维数组的静态初始化,有点类似一维数组的初始化,具体格式如下:
完整格式 : 
数据类型[ ][ ] 数组名 = new 数据类型[][]{ {元素1, 元素2...} , {元素1, 元素2...};
简化格式 : 
数据类型[ ][ ] 数组名 = { {元素1, 元素2...} , {元素1, 元素2...} ...};

多维数组的 “不规则性”

Java 的多维数组本质是 “数组的数组”,可以是不规则的(每行长度不同):

int[][] matrix = new int[2][]; // 只指定行数,列数不固定
matrix[0] = new int[3]; // 第0行3列
matrix[1] = new int[5]; // 第1行5列(合法)

总结

本文系统梳理了 Java 数组的底层逻辑与实践要点,旨在帮助开发者全面掌握这一基础数据结构的本质与应用价值。
数组是连续内存空间的同类型数据容器,掌握数组的设计逻辑与实践细节,不仅能高效处理批量数据,更能为进阶编程奠定坚实基础。

数组的物理本质是一片连续的内存块,每个元素占用大小相等的内存空间(因元素类型统一,即 “同构性”)。这种设计使得计算机可通过 “基地址 + 偏移量” 直接计算任意元素的内存地址(公式:元素地址 = 基地址 + 索引 × 元素大小),从而实现O (1) 时间复杂度的随机访问—— 这是数组与链表等非连续结构的核心区别,也是其高效性的根源。
其“同构性” 并非限制,而是对 “批量处理” 场景的精准适配,当需要对一组类型相同的数据进行统一操作(排序、筛选、统计)时,数组的类型约束确保了操作的一致性与安全性。

更容易理解的话来说,数组就是 “规矩的集体宿舍”,它虽然死板(长度固定、类型单一),但胜在整齐有序,存取速度快,就像学校的标准化宿舍 —— 虽然不能改格局,但找人确实方便😌


感谢点赞、感谢收藏、感谢关注!也感谢大家在评论区里指出错误,希望这篇文章对你有帮助。之后的文章我会为大家总结面向对象基础的知识点,再次感谢大家的支持与持续关注,我会持续更新的❥(ゝω・✿ฺ)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值