【Java SE】数组的定义与使用

本节目标

  1. 理解数组的基本概念
  2. 掌握数组的创建、初始化、访问、遍历基本用法
  3. 理解数组是引用类型,掌握 JVM 栈、堆内存模型,搞懂引用赋值、null空引用
  4. 掌握数组作为方法参数、作为方法返回值
  5. 熟练使用Arrays工具类,掌握数组拷贝、顺序查找、二分查找、冒泡排序、数组逆序
  6. 掌握普通二维数组、不规则二维数组

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 把内存划分不同区域,各司其职:

  1. 程序计数器 PC Register:很小空间,保存下一条要执行指令的地址。
  2. 虚拟机栈 JVM Stack :每调用一个方法就创建一块栈帧;保存局部变量;方法执行结束,栈帧直接销毁。我们写的局部变量(int a、数组引用变量)都存在栈帧。
  3. 本地方法栈:给 native 本地 C/C++ 方法使用,HotSpot 中和虚拟机栈合并。
  4. 堆 Heap所有 new 出来的对象全部放在堆上。数组就是 new 出来的对象,数组真实元素数据全部存放在堆;堆生命周期跟随整个程序。
  5. 方法区:存放加载的类信息、常量、静态变量、字节码。

现阶段重点分清两块:虚拟机栈(存局部变量)、堆(存 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};
    }

内存图解说明:

  1. ab是基本类型,栈帧直接存 10、20;
  2. arr是数组引用变量,栈中存堆数组的起始地址;
  3. 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

内存原理:

  1. array1 = array2; 不是拷贝数组内容!只是把 array2 保存的堆地址赋值给 array1
  2. 赋值结束,array1array2 两个引用变量指向堆上面的同一个数组对象
  3. 通过任意一个引用修改数组元素,另一个引用访问看到的也是修改后的数据。

原来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 二分查找(只针对有序数组

升序数组思路:

  1. left=0,right = 数组长度‑1;

  2. 取中间 mid,待查找数和 arr mid 比较;

  3. 小于中间值,去左边;大于中间值,去右边;相等找到返回下标;

  4. 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个元素

📝本篇完整核心考点总结

  1. 数组是一组相同类型元素的集合,内存连续,下标从 0 开始,下标越界抛出ArrayIndexOutOfBoundsException
  2. 初始化分动态初始化(只指定长度,元素给默认值)、静态初始化(写具体元素);简写格式不能拆分两行。
  3. 遍历两种方式:普通 for 循环可以拿到下标;for‑each 增强 for 只读取元素,拿不到下标。
  4. 数组属于引用类型:栈上存地址,堆存放真实数组实体;多个引用变量可以指向同一个堆数组;一个引用修改,全部引用可见。
  5. null是空引用,不指向任何堆对象,访问下标报空指针NullPointerException
  6. 数组做方法传参:传递的是堆地址的拷贝;方法内部修改数组元素外部可见;给形参引用重新 new 数组,不影响外部实参。
  7. Arrays.toString()打印数组;copyOf/copyOfRange生成全新拷贝数组,和原数组互不干扰。
  8. 查找:顺序查找全部遍历;二分查找只可以用于有序数组
  9. 冒泡排序相邻比较交换;Arrays.sort()JDK 内置排序。
  10. 二维数组本质是一维数组,每个元素存另外一个一维数组;支持不规则二维数组,各行列数可以不同。

面试简答

Q:int[] arr = array1; 是拷贝数组吗?

A:不是拷贝数组,只是拷贝堆地址,两个引用指向同一个数组对象。想要真正拷贝要用Arrays.copyOf

Q:数组传参是值传递还是引用传递?

A:Java 全部都是值传递;数组传参是把引用(地址)的值拷贝一份给形参

相关推荐
旋生万物1 小时前
圈道裂痕——用 Python 验证素数分布的螺旋生成论判据
开发语言·python·数论·素数·cci·可计算数学
qeen871 小时前
【C++】智能指针介绍
开发语言·c++·笔记·指针
hanchenxing1 小时前
本地AI绘画网关: 用 30 行 Python 把 NVIDIA FLUX.2 Klein 接入 OpenAI 兼容客户端Python
开发语言·python·ai作画·ai绘画·nvidia
摆烂z1 小时前
定时任务同步数据--续跑&重试
java·mybatis·ai编程
CAE虚拟与现实2 小时前
docker desktop中的build功能是要build什么
java·docker·容器
PHP实战开发录3 小时前
PHP脚本手动能跑定时任务却失败
开发语言·php·开发
郝学胜-神的一滴4 小时前
Effective Python 条款 10 :海象运算符_=
开发语言·python·程序人生·开源
wuyk5554 小时前
Python零基础入门第十四章:异常处理(try-except)
开发语言·python
wuyk5554 小时前
【Socket 进阶之路】第 3 章 TCP 三次握手 & 四次挥手深度剖析|连接建立、断开、状态机、TIME_WAIT 核心工程问题
服务器·开发语言·网络·物联网·网络协议·tcp/ip