本节目标
- 理解数组的基本概念
- 掌握数组的创建、初始化、访问、遍历基本用法
- 理解数组是引用类型,掌握 JVM 栈、堆内存模型,搞懂引用赋值、
null空引用 - 掌握数组作为方法参数、作为方法返回值
- 熟练使用
Arrays工具类,掌握数组拷贝、顺序查找、二分查找、冒泡排序、数组逆序 - 掌握普通二维数组、不规则二维数组
1. 数组的基本概念
1.1 为什么要使用数组
场景:保存多名学生考试成绩。 如果不使用数组,就需要定义一堆独立变量:
public class TestStudent{
public static void main(String[] args){
int score1 = 70;
int score2 = 80;
int score3 = 85;
int score4 = 60;
int score5 = 90;
System.out.println(score1);
System.out.println(score2);
System.out.println(score3);
System.out.println(score4);
System.out.println(score5);
}
}
问题:如果 20 个、100 个学生,就要定义几十上百个独立变量,代码极其繁琐,不利于循环批量处理。 观察:这一批数据类型完全相同,Java 提供数组用来存储一组相同类型的数据。
1.2 什么是数组
数组是相同类型元素的集合,在内存中是一段连续的存储空间。

类比车库示意图: 连续的车位,每一个车位有编号,编号从 0 开始,这个编号叫做下标(索引 index)。
- 空间连续排布
- 下标从 0 开始,
0,1,2,3,4,5……
数组下标范围:
[0 , 数组长度),左闭右开,不包含数组长度本身。
1.3 数组的创建及初始化
数组创建语法
T[] 数组名 = new T[N];
T:数组里面存放元素的数据类型T[]:代表数组这个整体的类型N:数组的长度,数组里面可以存放多少个元素
示例:
int[] array1 = new int[10]; // 创建可以存放10个int元素的数组
double[] array2 = new double[5];// 创建可以存放5个double元素的数组
String[] array3 = new String[3];// 创建可以存放3个String字符串元素的数组
数组两种初始化方式:动态初始化、静态初始化
① 动态初始化
创建数组的时候,只指定数组的长度,不写具体元素内容,元素自动赋默认值。
int[] array = new int[10];
② 静态初始化
创建数组时,直接在大括号{}写出具体存放的元素,编译器根据大括号里面元素的个数自动推算数组长度。
完整写法:
int[] array1 = new int[]{0,1,2,3,4,5,6,7,8,9};
double[] array2 = new double[]{1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = new String[]{"hello", "Java", "!!!"};
简写格式(省略new T[]),只能定义同时初始化,不能拆分两行写:
int[] array1 = {0,1,2,3,4,5,6,7,8,9};
double[] array2 = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] array3 = {"hello", "Java", "!!!"};
❗注意简写格式不能拆分定义和初始化,下面代码编译报错
int[] array3;
// array3 = {1,2,3}; //错误!简写只能写在同一行定义处
不推荐 C 语言风格写法:
int arr[] = {1,2,3};虽然语法合法,但是容易误解arr才是数组类型;Java 推荐int[] arr,把[]和类型写在一起。
可以拆分的写法(不能用简写):
int[] array1;
array1 = new int[10];
int[] array2;
array2 = new int[]{10,20,30};
数组元素默认初始值
数组 new 出来之后,如果没有手动赋值,每个元素自带默认值。
表格
| 元素类型 | 默认值 |
|---|---|
| byte、short、int、long | 0 |
| float、double | 0.0 |
| char | \u0000(空字符) |
| boolean | false |
| 引用类型(String、对象) | null |
示例:
String[] words = new String[3]; //三个元素默认值都是null
1.4 数组的基本使用
1.4.1 通过下标访问数组元素
数组内存连续,支持随机访问,通过下标快速读写任意位置元素。
int[] array = new int[]{10, 20, 30, 40, 50};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
// 修改数组下标位置的元素
array[0] = 100;
System.out.println(array[0]);
⚠️下标越界异常 数组长度 N,合法下标范围
0 ~ N‑1。访问超出范围下标,运行抛出ArrayIndexOutOfBoundsException数组下标越界异常。
int[] array = {1,2,3};
// System.out.println(array[3]); //运行报错 ArrayIndexOutOfBoundsException
1.4.2 遍历数组
遍历:把数组里面每一个元素全部访问一遍。
❌不推荐:手动写一堆打印语句,数组长度一变代码全部要改。
方式 1:普通 for 循环,使用.length获取数组长度
数组名.length获取数组的元素长度,没有小括号,是属性,不是方法!
int[] array = new int[]{10,20,30,40,50};
for(int i = 0; i < array.length; i++){
System.out.println(array[i]);
}
方式 2:增强 for 循环 for‑each
语法:for(元素类型 变量 : 数组名)
只读取元素,不能修改数组里面的内容,适合遍历查看。
int[] array = {1,2,3};
for (int x : array) {
System.out.println(x);
}
for‑each 内部拿不到下标 i,如果需要下标操作,就使用普通 for 循环。
2. 数组是引用类型
2.1 JVM 内存五大区域简单介绍
JVM 把内存划分不同区域,各司其职:
- 程序计数器 PC Register:很小空间,保存下一条要执行指令的地址。
- 虚拟机栈 JVM Stack:每调用一个方法就创建一块栈帧;保存局部变量;方法执行结束,栈帧直接销毁。我们写的局部变量(int a、数组引用变量)都存在栈帧。
- 本地方法栈:给 native 本地 C/C++ 方法使用,HotSpot 中和虚拟机栈合并。
- 堆 Heap:所有 new 出来的对象全部放在堆上。数组就是 new 出来的对象,数组真实元素数据全部存放在堆;堆生命周期跟随整个程序。
- 方法区:存放加载的类信息、常量、静态变量、字节码。
现阶段重点分清两块:虚拟机栈(存局部变量)、堆(存 new 出来数组实体)。

2.2 基本类型变量 vs 引用类型变量
- 基本类型变量:栈帧里面的变量,空间直接存放真实的数据值。
int a=10;,栈内存直接存 10。 - 引用类型变量(数组就是引用类型):栈帧的变量存放的是堆上对象的地址,不是对象本身。通过这个地址去堆里面找到真实数组。
public static void func() {
int a = 10;
int b = 20;
int[] arr = new int[]{1,2,3};
}

内存图解说明:
a、b是基本类型,栈帧直接存 10、20;arr是数组引用变量,栈中存堆数组的起始地址;new int[]{1,2,3}数组实体真正存放在堆内存。
类比 C 语言指针,但 Java 引用不能做地址加减运算,更安全。
2.3 引用变量赋值(重点高频坑)
public static void func() {
int[] array1 = new int[3];
array1[0] = 10;
array1[1] = 20;
array1[2] = 30;
int[] array2 = new int[]{1,2,3,4,5};
array2[0] = 100;
array2[1] = 200;
array1 = array2; // 核心!!
array1[2] = 300;
array1[3] = 400;
array2[4] = 500;
for (int i = 0; i < array2.length; i++) {
System.out.println(array2[i]);
}
}
运行输出:100 200 300 400 500


内存原理:
array1 = array2;不是拷贝数组内容!只是把 array2 保存的堆地址赋值给 array1。- 赋值结束,
array1和array2两个引用变量指向堆上面的同一个数组对象。 - 通过任意一个引用修改数组元素,另一个引用访问看到的也是修改后的数据。
原来
array1最开始指向的数组,没有任何引用指向它,等待垃圾回收 GC 回收。
2.4 null 空引用
null代表:引用变量不指向堆中的任何对象。
int[] arr = null;
// System.out.println(arr[0]);
如果对
null的引用做下标访问,运行抛出NullPointerException空指针异常。 注意:Java 中 null 不代表 0 地址,和 C 语言 NULL 有区别。
3. 数组的应用场景
3.1 保存批量数据
public static void main(String[] args) {
int[] array = {1, 2, 3};
for(int i = 0; i < array.length; ++i){
System.out.print(array[i] + " ");
}
}
3.2 数组作为方法参数
区分:基本类型传参、数组引用类型传参。
案例 1:传基本类型,值拷贝
public static void main(String[] args) {
int num = 0;
func(num);
System.out.println("num = " + num);
}
public static void func(int x) {
x = 10;
System.out.println("x = " + x);
}
输出:
x = 10
num = 0
原理:基本类型值拷贝,形参 x 是副本,修改 x 不会影响 main 里面的 num。
案例 2:传数组(引用传递,传递的是地址值拷贝)
public static void main(String[] args) {
int[] arr = {1, 2, 3};
func(arr);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
a[0] = 10;
System.out.println("a[0] = " + a[0]);
}
输出:
a[0] = 10
arr[0] = 10
原理:数组传参,把堆数组的地址拷贝一份给形参 a。main 的 arr 和 func 的 a 指向同一个堆数组对象。在方法内修改数组里面元素,外面也可以看到变化。 好处:不用拷贝整个数组,避免大数据数组拷贝带来巨大内存开销。
⚠️注意:传数组只是拷贝地址;如果方法内部写
a = new int[100];,只是修改形参 a 的指向,不会改变外面实参 arr 的指向。
3.3 数组作为方法返回值
方法可以把数组对象(堆数组的地址)返回出去。示例:返回斐波那契前 n 项数组。
public class TestArray {
public static int[] fib(int n){
if(n <= 0){
return null;
}
int[] array = new int[n];
array[0] = array[1] = 1;
for(int i = 2; i < n; ++i){
array[i] = array[i-1] + array[i-2];
}
return array;
}
public static void main(String[] args) {
int[] array = fib(10);
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
4. Arrays 工具类 & 数组经典练习
需要导包
import java.util.Arrays;,JDK 提供的工具类,封装大量数组常用方法。
4.1 Arrays.toString () 数组转为字符串打印
直接打印数组变量名,输出是地址哈希;Arrays.toString把数组内容格式化成字符串,方便输出查看。
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
}
}
输出:[1, 2, 3, 4, 5, 6]
4.2 数组拷贝
错误写法:引用赋值,不是拷贝数组
int[] arr = {1,2,3,4,5,6};
int[] newArr = arr;
newArr[0] = 10;
// newArr 和 arr 指向同一个数组!修改newArr,arr也跟着变
System.out.println("newArr: " + Arrays.toString(arr));
Arrays.copyOf ():完整拷贝,生成全新数组
在堆上开辟一块全新数组,复制元素;新旧数组互相独立,修改互不干扰。
import java.util.Arrays;
public static void func(){
int[] arr = {1,2,3,4,5,6};
int[] newArr = Arrays.copyOf(arr, arr.length);
arr[0] = 10;
System.out.println("arr: " + Arrays.toString(arr));
System.out.println("newArr: " + Arrays.toString(newArr));
}
Arrays.copyOfRange (arr,from,to) 拷贝区间
左闭右开区间[from , to),包含 from 下标,不包含 to 下标。
int[] arr = {1,2,3,4,5,6};
int[] newArr2 = Arrays.copyOfRange(arr, 2, 4);
System.out.println("newArr2: " + Arrays.toString(newArr2));
手写实现简单版 copyOf
public static int[] copyOf(int[] arr) {
int[] ret = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
ret[i] = arr[i];
}
return ret;
}
注意:数组里面如果存放引用类型对象,这里只是浅拷贝,深浅拷贝后续面向对象讲解。
4.3 顺序查找
遍历数组逐个比对,找到返回下标,找不到返回‑1。
public static int find(int[] arr, int data) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == data) {
return i;
}
}
return -1; //代表没有找到
}
public static void main(String[] args) {
int[] arr = {1,2,3,10,5,6};
System.out.println(find(arr,10));
}
4.4 二分查找(只针对有序数组)
升序数组思路:
- left=0,right = 数组长度‑1;
- 取中间 mid,待查找数和 arr [mid] 比较;
- 小于中间值,去左边;大于中间值,去右边;相等找到返回下标;
- left>right 循环结束,代表没找到返回‑1。
public static int binarySearch(int[] arr, int toFind) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (toFind < arr[mid]) {
right = mid - 1;
} else if (toFind > arr[mid]) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(binarySearch(arr,6));
}
数据量越大,二分查找效率对比顺序查找优势巨大。
4.5 冒泡排序
升序思路:每一趟相邻两个元素比较,如果前面大于后面,交换;每一趟把最大的元素 “冒泡” 移动到数组末尾。
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 1; j < arr.length-i; j++) {
if (arr[j-1] > arr[j]) {
int tmp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
JDK 内置高效排序:
Arrays.sort(arr);,开发优先直接使用。
4.6 数组逆序
双指针法:left 指向最开头,right 指向末尾;交换两者元素,left++,right‑‑,直到left >= right结束。
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
5. 二维数组
二维数组本质:一维数组,数组里面每一个元素又是另外一个一维数组。
5.1 普通二维数组
语法格式:
数据类型[][] 数组名称 = new 数据类型[行数][列数] {初始化内容};
行数不能省略;普通二维数组列数可以省略(不规则数组)。
示例代码:
int[][] arr = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 双重循环遍历
for (int row = 0; row < arr.length; row++) {
for (int col = 0; col < arr[row].length; col++) {
System.out.printf("%d\t", arr[row][col]);
}
System.out.println("");
}
arr.length获取二维数组的行数arr[row].length获取第 row 这一行一维数组的列长度
5.2 不规则二维数组
各行拥有不一样的列数,定义的时候只指定行数,列不在外层写死,各行单独 new 一维数组。
int[][] array = new int[2][];
array[0] = new int[3]; //第一行3个元素
array[1] = new int[5]; //第二行5个元素

📝本篇完整核心考点总结
- 数组是一组相同类型元素的集合,内存连续,下标从 0 开始,下标越界抛出
ArrayIndexOutOfBoundsException。 - 初始化分动态初始化(只指定长度,元素给默认值)、静态初始化(写具体元素);简写格式不能拆分两行。
- 遍历两种方式:普通 for 循环可以拿到下标;for‑each 增强 for 只读取元素,拿不到下标。
- 数组属于引用类型:栈上存地址,堆存放真实数组实体;多个引用变量可以指向同一个堆数组;一个引用修改,全部引用可见。
null是空引用,不指向任何堆对象,访问下标报空指针NullPointerException。- 数组做方法传参:传递的是堆地址的拷贝;方法内部修改数组元素外部可见;给形参引用重新 new 数组,不影响外部实参。
Arrays.toString()打印数组;copyOf/copyOfRange生成全新拷贝数组,和原数组互不干扰。- 查找:顺序查找全部遍历;二分查找只可以用于有序数组。
- 冒泡排序相邻比较交换;
Arrays.sort()JDK 内置排序。 - 二维数组本质是一维数组,每个元素存另外一个一维数组;支持不规则二维数组,各行列数可以不同。
面试简答
Q:
int[] arr = array1;是拷贝数组吗?A:不是拷贝数组,只是拷贝堆地址,两个引用指向同一个数组对象。想要真正拷贝要用
Arrays.copyOf。Q:数组传参是值传递还是引用传递?
A:Java 全部都是值传递;数组传参是把引用(地址)的值拷贝一份给形参。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/wuqingyigexiaoba/article/details/165117632




