如果把 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) 时间复杂度的随机访问—— 这是数组与链表等非连续结构的核心区别,也是其高效性的根源。
其“同构性” 并非限制,而是对 “批量处理” 场景的精准适配,当需要对一组类型相同的数据进行统一操作(排序、筛选、统计)时,数组的类型约束确保了操作的一致性与安全性。
更容易理解的话来说,数组就是 “规矩的集体宿舍”,它虽然死板(长度固定、类型单一),但胜在整齐有序,存取速度快,就像学校的标准化宿舍 —— 虽然不能改格局,但找人确实方便😌
感谢点赞、感谢收藏、感谢关注!也感谢大家在评论区里指出错误,希望这篇文章对你有帮助。之后的文章我会为大家总结面向对象基础的知识点,再次感谢大家的支持与持续关注,我会持续更新的❥(ゝω・✿ฺ)
689

被折叠的 条评论
为什么被折叠?



