乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

一维数组

白歌:"数组是 Java 中最基础的数据结构,也是理解 JVM 内存模型的第一个窗口。我在面试中经常发现——很多候选人能写出排序算法,却说不清数组在堆栈中的布局。这一节,我们从底层开始构建数组的完整认知。"


什么是数组

数组(Array) 是 Java 中最基本的数据结构之一,它是一组相同数据类型、连续存储、固定长度的数据元素的集合。

在 Java 中,数组本身是引用类型——即使数组中存储的是基本类型数据(如 int),数组对象本身也分配在 JVM 堆上,栈中只保存对其的引用。

特性说明类比
同类型性数组中所有元素必须是同一类型(或兼容类型)一列火车车厢,每节装同一种货物
连续存储元素在堆内存中地址连续排列小区里紧挨着的连排别墅
零下标下标从 0 开始,最大下标为 length - 1楼层的"第0层"思维模型
长度不可变数组一旦创建,length 属性就固定不变建好的楼房,层数不会变
引用类型数组变量是引用,数组对象在堆上遥控器 vs 电视机

数组的声明方式

Java 支持两种声明语法:

声明方式语法说明推荐度
类型在前int[] arr;C/C++ 风格的改良,可读性好★★★★★
类型在后int arr[];兼容 C/C++ 传统语法,混合声明时易混淆★★
混合声明(反例)int[] arr1, arr2; → 两者都是数组易误读仅作了解
混合C风格(反例)int arr1[], arr2; → arr1 是数组,arr2 是 int容易出错不推荐

白歌的建议:永远使用 int[] arr 风格。它不仅读起来清晰("int数组 arr"),而且避免了混合声明时的歧义。团队代码规范中应统一这一条。

// ✅ 推荐:类型在前
int[] scores;           // 学生成绩数组
String[] names;         // 学生姓名数组
double[] salaries;      // 员工薪资数组

// ⚠️ 可用但不推荐:类型在后
int scores2[];          // 兼容C语法,但可读性差

// ❌ 混合声明的陷阱
int arr1[], arr2;       // arr1是数组,arr2只是int!极易引起误解

数组的三种初始化方式

方式一:静态初始化

在声明的同时用花括号指定所有元素的值,由 JVM 自动推断数组长度。

// 语法格式
数据类型[] 数组名 = {元素1, 元素2, ...};

// 示例:飞翔科技学生成绩
int[] scores = {85, 92, 78, 95, 88};

注意:静态初始化必须在声明的同时完成。不能先声明再静态赋值。

// ❌ 错误:声明与初始化分离时不能使用花括号
int[] scores;
scores = {85, 92, 78};   // 编译错误!Array initializer is not allowed here

// ✅ 正确:声明与初始化分离时,必须使用 new
int[] scores;
scores = new int[]{85, 92, 78};   // 匿名数组语法

方式二:动态初始化

指定数组长度,由 JVM 自动为每个元素赋予默认值(零值)。

// 语法格式
数据类型[] 数组名 = new 数据类型[长度];

// 示例:飞翔科技——预留10个学生成绩的位置
int[] scores = new int[10];
数组元素类型默认值
byte / short / int / long0
float / double0.0
char'\u0000'(空字符,不是空格)
booleanfalse
引用类型(类 / 接口 / 数组)null

方式三:默认初始化

当数组作为类的实例变量或静态变量时,即使不显式赋值,JVM 也会在类加载或对象创建时自动赋予默认值。

public class ScoreManager {
    private int[] scores = new int[5];  // 实例变量数组,元素自动初始化为 0
    // ...
}

三种初始化方式对比:

初始化方式适用场景长度来源是否可分离声明与赋值
静态初始化 {a,b,c}数据已知且固定由花括号内元素个数确定否(必须同时)
动态初始化 new int[n]数据未知,但容量确定由 new 指定的长度确定是
默认初始化(成员变量)数组作为类的字段由 new 指定的长度确定是

数组的内存模型

这是本章最重要的部分。理解数组在内存中的布局,是诊断 NullPointerException 和画面试内存图的前提。

关键原理:

  1. 栈上存引用,堆上存对象:int[] scores = new int[]{85, 92, 78, 95, 88} 执行后,栈上的 scores 变量只存储一个指向堆中数组对象的引用地址(32位JVM中为4字节,64位JVM中为8字节,若开启指针压缩则为4字节)。

  2. 数组对象的内存结构:堆上的数组对象由三部分组成:

    组成部分说明大小
    对象头(Object Header)包含 Mark Word(锁信息、GC分代年龄、hashCode)和 Class Pointer(指向 int[] 的类元数据)12~16字节(64位)
    length 字段存储数组长度值4字节(int)
    数据区连续存储每个数组元素元素类型大小 × 数组长度
  3. 连续存储的优势:

    • O(1) 随机访问:元素的地址可以通过公式 基地址 + 下标 × 元素字节大小 直接计算,不需要遍历
    • 缓存友好:连续内存使得 CPU 缓存行(Cache Line)可以被充分利用,遍历时预取效率极高
    • 对比 ArrayList:ArrayList 底层也是数组,但数组本身比 ArrayList 少一层封装,访问更快
// 内存地址计算示例(概念演示,非实际代码)
// 假设 scores 数据区起始地址为 0x1000,每个 int 占 4 字节
// scores[0] 地址 = 0x1000 + 0 × 4 = 0x1000
// scores[1] 地址 = 0x1000 + 1 × 4 = 0x1004
// scores[2] 地址 = 0x1000 + 2 × 4 = 0x1008
// ...
// scores[n] 地址 = 0x1000 + n × 4

length 属性

length 是数组对象的 public final 成员变量,不是方法。它存储了数组的长度,创建时确定,不可更改。

int[] scores = {85, 92, 78, 95, 88};
System.out.println(scores.length);  // 输出 5 —— 注意:没有括号,不是 length()

小崔的疑问:"为什么 String 用 length() 方法,而数组用 length 属性?"

白歌解答:String.length() 是一个方法,因为 String 类的实现可能变化(如 JDK 9 后改用 byte[] 替代 char[] 存储),方法提供了封装。而数组的 length 是 JVM 在创建数组对象时写入的一个不可变字段,直接暴露即可。这是语言层面的设计——数组在 Java 中是特殊的对象,length 由 JVM 直接维护。


数组遍历

方式一:标准 for 循环

适用于需要操作下标的场景(修改元素、部分遍历、逆序遍历等)。

// 语法格式
for (int i = 0; i < 数组名.length; i++) {
    // 通过 数组名[i] 访问元素
}

// 示例:计算飞翔科技第一季度员工工资总和
double[] salaries = {12000.0, 15000.0, 9800.0, 13200.0, 16800.0};
double total = 0.0;
for (int i = 0; i < salaries.length; i++) {
    total += salaries[i];
}
System.out.println("第一季度工资总额:" + total);

方式二:增强 for 循环(for-each / foreach)

JDK 5 引入,适用于只读遍历全部元素的场景。

// 语法格式
for (数据类型 变量名 : 数组名) {
    // 变量名代表当前元素的值(副本)
}

// 示例:打印所有员工姓名
String[] names = {"大翔", "白歌", "小崔", "孔蓝"};
for (String name : names) {
    System.out.println(name);
}

两种遍历方式对比:

对比维度标准 for 循环增强 for 循环
是否可获取下标是(i)否
是否可修改元素是(arr[i] = xxx)否:操作的是副本
是否可部分遍历是(控制 i 的范围)否:遍历全部
是否可逆序遍历是否
代码简洁度一般简洁
适用场景需要下标操作只读遍历全部元素
底层实现直接访问编译后仍为普通 for(数组类型)或 Iterator(Iterable 类型)

底层原理:增强 for 循环在编译时对数组类型会退化为标准 for 循环(索引遍历),对 Iterable 类型则使用迭代器。因此对数组而言,增强 for 循环没有性能损耗,但失去了下标控制能力。


完整代码示例

示例一:学生成绩分析系统

场景:飞翔科技为某 985 高校开发学生成绩管理系统。小崔需要用数组存储 6 名学生的 Java 课程成绩,并完成统计分析。

/**
 * 飞翔科技 —— 学生成绩分析系统
 * 演示一维数组的基本操作:遍历、求和、求最值、求平均
 */
public class ScoreAnalyzer {
    public static void main(String[] args) {
        // 静态初始化:6名学生的Java成绩
        int[] scores = {85, 92, 78, 95, 88, 73};

        // 1. 使用标准 for 循环计算总分和平均分
        int sum = 0;
        int max = scores[0];
        int min = scores[0];

        for (int i = 0; i < scores.length; i++) {
            sum += scores[i];
            if (scores[i] > max) {
                max = scores[i];
            }
            if (scores[i] < min) {
                min = scores[i];
            }
        }
        double average = (double) sum / scores.length;

        System.out.println("========== 飞翔科技学生成绩分析 ==========");
        System.out.println("学生人数:" + scores.length);
        System.out.println("总  分:" + sum);
        System.out.printf("平均分:%.2f\n", average);
        System.out.println("最高分:" + max);
        System.out.println("最低分:" + min);

        // 2. 使用增强 for 循环统计及格人数
        int passCount = 0;
        for (int score : scores) {
            if (score >= 60) {
                passCount++;
            }
        }
        System.out.println("及格人数:" + passCount);
        System.out.println("及格率:" + String.format("%.1f%%", 100.0 * passCount / scores.length));

        // 3. 打印成绩详情(带序号)
        System.out.println("\n====== 成绩详情 ======");
        for (int i = 0; i < scores.length; i++) {
            String level;
            if (scores[i] >= 90) {
                level = "优秀";
            } else if (scores[i] >= 80) {
                level = "良好";
            } else if (scores[i] >= 70) {
                level = "中等";
            } else if (scores[i] >= 60) {
                level = "及格";
            } else {
                level = "不及格";
            }
            System.out.printf("学号 %d:%d 分 (%s)\n", i + 1, scores[i], level);
        }
    }
}
运行输出:
========== 飞翔科技学生成绩分析 ==========
学生人数:6
总  分:511
平均分:85.17
最高分:95
最低分:73
及格人数:6
及格率:100.0%

====== 成绩详情 ======
学号 1:85 分 (良好)
学号 2:92 分 (优秀)
学号 3:78 分 (中等)
学号 4:95 分 (优秀)
学号 5:88 分 (良好)
学号 6:73 分 (中等)

示例二:飞翔科技"工资条"系统

场景:技术总监大翔要求小崔写一个程序,输入各部门的月度工资数据,计算工资排名(升序展示),并找出中位数。

import java.util.Arrays;

/**
 * 飞翔科技 —— 工资条系统
 * 演示动态初始化、排序(手动冒泡)、中位数计算
 */
public class SalaryReport {
    public static void main(String[] args) {
        // 动态初始化:预留10个工资数据的位置,先填充5个
        double[] salaries = new double[10];
        salaries[0] = 12500.0;  // 大翔
        salaries[1] = 15000.0;  // 白歌
        salaries[2] = 8200.0;   // 小崔
        salaries[3] = 10500.0;  // 孔蓝
        salaries[4] = 9300.0;   // 其他员工
        int employeeCount = 5;  // 实际员工数

        System.out.println("========== 飞翔科技2024年6月工资条 ==========");
        String[] names = {"大翔", "白歌", "小崔", "孔蓝", "李明"};
        System.out.println("\n原始工资数据:");
        for (int i = 0; i < employeeCount; i++) {
            System.out.printf("  %s:¥%.2f\n", names[i], salaries[i]);
        }

        // 手动冒泡排序(升序)—— 理解排序本质
        double[] sortedSalaries = Arrays.copyOf(salaries, employeeCount);
        for (int i = 0; i < sortedSalaries.length - 1; i++) {
            for (int j = 0; j < sortedSalaries.length - 1 - i; j++) {
                if (sortedSalaries[j] > sortedSalaries[j + 1]) {
                    double temp = sortedSalaries[j];
                    sortedSalaries[j] = sortedSalaries[j + 1];
                    sortedSalaries[j + 1] = temp;
                }
            }
        }

        System.out.println("\n工资排名(升序):");
        for (int i = 0; i < sortedSalaries.length; i++) {
            System.out.printf("  第%d名:¥%.2f\n", i + 1, sortedSalaries[i]);
        }

        // 计算中位数
        double median;
        int mid = sortedSalaries.length / 2;
        if (sortedSalaries.length % 2 == 0) {
            median = (sortedSalaries[mid - 1] + sortedSalaries[mid]) / 2.0;
        } else {
            median = sortedSalaries[mid];
        }
        System.out.printf("\n工资中位数:¥%.2f\n", median);

        // 计算总支出和平均工资
        double total = 0.0;
        for (int i = 0; i < employeeCount; i++) {
            total += salaries[i];
        }
        System.out.printf("本月工资总支出:¥%.2f\n", total);
        System.out.printf("平均工资:¥%.2f\n", total / employeeCount);
    }
}
运行输出:
========== 飞翔科技2024年6月工资条 ==========

原始工资数据:
  大翔:¥12500.00
  白歌:¥15000.00
  小崔:¥8200.00
  孔蓝:¥10500.00
  李明:¥9300.00

工资排名(升序):
  第1名:¥8200.00
  第2名:¥9300.00
  第3名:¥10500.00
  第4名:¥12500.00
  第5名:¥15000.00

工资中位数:¥10500.00
本月工资总支出:¥55500.00
平均工资:¥11100.00

数组下标越界:ArrayIndexOutOfBoundsException

这是 Java 初学者最常遇到的运行时异常之一。

根本原因

数组下标超出了 [0, length-1] 的合法范围。这是 JVM 在运行时抛出的异常——编译器无法提前发现下标越界问题(因为下标通常是运行时计算出来的)。

易错场景

// ❌ 反例一:for 循环条件写成 <= length
int[] scores = {85, 92, 78};
for (int i = 0; i <= scores.length; i++) {  // 错误!应该是 i < scores.length
    System.out.println(scores[i]);          // i=3 时抛出 ArrayIndexOutOfBoundsException
}

// ❌ 反例二:循环条件中错误使用 length - 1
for (int i = 0; i < scores.length - 1; i++) {  // 漏掉了最后一个元素!
    System.out.println(scores[i]);              // 不会报异常,但数据不完整
}

// ❌ 反例三:空数组访问
int[] empty = new int[0];       // 长度为0的数组(合法,但不是null)
System.out.println(empty[0]);   // ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0

白歌的经验:写出 <= 是新手最常见的错误。记住口诀——"零下标,开区间":[0, length),左闭右开,永远是 i < length。


空指针异常:NullPointerException

这是 Java 中最"臭名昭著"的异常。对于数组而言,空指针的发生场景非常明确。

数组中的三种空指针场景

// ❌ 场景一:声明了但未初始化
int[] scores;
System.out.println(scores[0]);
// 编译错误!Variable 'scores' might not have been initialized
// 局部变量必须显式初始化

// ❌ 场景二:显式赋值为 null
int[] scores = null;
System.out.println(scores.length);
// NullPointerException: Cannot read the array length because "scores" is null

// ❌ 场景三:方法返回 null 数组
public static int[] getScores() {
    return null;  // 某些情况下返回了 null
}
int[] result = getScores();
System.out.println(result[0]);  // NullPointerException!

防御性编程

// ✅ 正确:访问数组前进行 null 检查
int[] scores = getScores();
if (scores != null && scores.length > 0) {
    System.out.println(scores[0]);
} else {
    System.out.println("暂无成绩数据");
}

// ✅ 更好:让方法返回空数组而非 null
public static int[] getScores() {
    // 没有数据时返回空数组,而不是 null
    return new int[0];  // 长度0的空数组,不是 null
}

白歌的建议:在团队代码规范中,约定方法返回数组时永远不要返回 null,返回空数组 new int[0] 即可。调用方不需要防御 null,代码更简洁。


易错场景汇总

反例一:数组的赋值是引用传递

小崔以为数组赋值是复制,结果两个变量指向了同一个数组对象:

// ❌ 错误:数组赋值是引用复制,不是元素复制
int[] scores1 = {85, 92, 78};
int[] scores2 = scores1;   // scores2 指向了 scores1 的同一个数组对象!
scores2[0] = 100;          // 修改 scores2 的同时也修改了 scores1
System.out.println(scores1[0]);  // 输出 100,而非 85!

// ✅ 正确:使用 Arrays.copyOf 或手动复制元素
int[] scores1 = {85, 92, 78};
int[] scores2 = Arrays.copyOf(scores1, scores1.length);  // 真正的复制
scores2[0] = 100;
System.out.println(scores1[0]);  // 输出 85,不受影响

反例二:增强 for 循环中的"无效修改"

// ❌ 错误:增强 for 循环中修改元素无效
int[] scores = {85, 92, 78};
for (int score : scores) {
    score = score + 10;   // score 是局部变量副本,不影响数组元素
}
System.out.println(scores[0]);  // 仍是 85!

// ✅ 正确:用标准 for 循环修改
for (int i = 0; i < scores.length; i++) {
    scores[i] = scores[i] + 10;
}
System.out.println(scores[0]);  // 输出 95

反例三:混淆 length 和 length()

// ❌ 错误:对数组调用 length() 方法
int[] arr = {1, 2, 3};
System.out.println(arr.length());  // 编译错误!数组没有 length() 方法

// ✅ 正确
System.out.println(arr.length);    // 输出 3 —— 注意没有括号

// 对比:String 是 length() 方法
String str = "hello";
System.out.println(str.length());  // 输出 5 —— 有括号

面试考点

Q1:Java 中数组是基本类型还是引用类型?为什么?

数组是引用类型。证据有三: ① 数组变量存储在栈上,数组对象存储在堆上,变量存的是引用; ② 数组赋值是引用传递(浅拷贝),修改副本会影响原数组; ③ 数组可以赋值为 null,基本类型不能。

即使数组存储的是 int 等基本类型元素,数组本身也仍是引用类型——int[] 是一个对象类型,它的 Class 对象是 int[].class。

Q2:int[] arr = new int[0] 和 int[] arr = null 有什么区别?

  • new int[0]:在堆上创建了一个长度为 0 的数组对象,占用一些内存(对象头 + length字段),arr 指向该对象,可以安全地调用 arr.length(返回 0)和在增强 for 循环中使用(不执行循环体)。
  • null:arr 不指向任何对象,任何访问操作(arr.length、arr[0]、for-each)都会抛出 NullPointerException。

最佳实践:返回空数组时使用 new int[0],调用方无需防御 null。

Q3:数组的 length 属性可以改变吗?为什么?

不能。length 是数组对象的一个 public final 字段,由 JVM 在数组创建时写入,之后不可修改。这也意味着数组大小一旦确定就无法改变。如果需要动态扩容,应使用 ArrayList 或手动 Arrays.copyOf() 创建新数组。

Q4:增强 for 循环遍历数组时,底层是如何实现的?

对数组类型,增强 for 循环在编译后会被 javac 翻译为等价的标准 for 循环(索引遍历),不存在性能差异:

// 源代码
for (int x : arr) { System.out.println(x); }
// 编译后等价于
for (int i = 0; i < arr.length; i++) {
    int x = arr[i];
    System.out.println(x);
}

对 Iterable 类型(如 ArrayList),则翻译为迭代器模式。数组走索引方式,因此数组的 for-each 效率与原生的 for-i 完全一致。

Q5:如何判断一个引用是数组类型?

使用 instanceof 或反射:

Object obj = new int[]{1, 2, 3};
System.out.println(obj instanceof int[]);    // true
System.out.println(obj.getClass().isArray());  // true
System.out.println(obj.getClass().getComponentType());  // int

白歌的结语:"一维数组是 Java 数据结构的起点。当你真正理解了数组在堆栈中的布局、length 的不可变性、增强 for 循环的本质以及空指针的防御,你就已经迈过了新手阶段。下一节,我们将把维度扩展——进入多维数组的世界。"

上一页
章节导读
下一页
多维数组