数组是Java语言中最基础也是最重要的数据结构之一。简单来说,数组就是一组相同类型数据的集合,这些数据在内存中连续存储,通过索引(下标)来访问每个元素。想象一下你有一个装鸡蛋的纸盒,每个格子只能放一个鸡蛋,而且所有格子的大小都一样 - 这就是数组的具象化表现。
在Java中,数组是对象类型,这意味着它继承了Object类的所有方法。但与其他对象不同的是,数组有固定的长度,一旦创建就不能改变。这也是为什么我们常说数组是"静态"的数据结构。数组可以存储基本数据类型(如int、char等),也可以存储对象引用。
数组的声明语法有两种形式:
java复制// 第一种声明方式
数据类型[] 数组名;
// 第二种声明方式(C语言风格,不推荐)
数据类型 数组名[];
虽然两种语法在Java中都是合法的,但第一种方式更符合Java的编码规范,因为它更清晰地表达了"这是一个数组类型"的概念。第二种方式主要是为了照顾从C/C++转过来的开发者,但在现代Java开发中已经很少使用了。
在Java中创建数组主要有三种方式:
java复制int[] numbers = {1, 2, 3, 4, 5};
java复制double[] prices = new double[10];
java复制String[] names;
names = new String[]{"Alice", "Bob", "Charlie"};
注意:第一种方式只能在声明数组的同时使用,不能分开写。如果分开声明和初始化,必须使用第三种方式。
当使用new关键字创建数组但未显式初始化时,数组元素会被赋予默认值:
这个特性在实际开发中非常有用,比如我们需要创建一个计数器数组时,可以直接使用默认值:
java复制int[] counters = new int[26]; // 26个0
Java支持多维数组,最常见的是二维数组,可以看作是"数组的数组":
java复制// 3行4列的二维数组
int[][] matrix = new int[3][4];
// 不规则二维数组
int[][] triangle = new int[3][];
triangle[0] = new int[1];
triangle[1] = new int[2];
triangle[2] = new int[3];
多维数组的初始化也可以使用简写形式:
java复制int[][] magicSquare = {
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
数组元素通过从0开始的索引访问,语法是数组名[索引]。例如:
java复制String[] fruits = {"Apple", "Banana", "Cherry"};
System.out.println(fruits[1]); // 输出"Banana"
需要注意的是,Java会对数组访问进行边界检查,如果尝试访问不存在的索引(如负数或超过数组长度),会抛出ArrayIndexOutOfBoundsException异常。
遍历数组有多种方式,最常见的是使用for循环:
java复制int[] numbers = {10, 20, 30, 40, 50};
// 传统for循环
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
// 增强for循环(for-each)
for (int num : numbers) {
System.out.println(num);
}
对于多维数组,需要使用嵌套循环:
java复制int[][] matrix = {{1, 2}, {3, 4}, {5, 6}};
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
在实际开发中,我们经常需要复制数组。Java提供了几种数组复制的方式:
java复制int[] source = {1, 2, 3, 4, 5};
int[] dest = new int[5];
System.arraycopy(source, 0, dest, 0, source.length);
java复制int[] source = {1, 2, 3, 4, 5};
int[] dest = Arrays.copyOf(source, source.length);
java复制int[] source = {1, 2, 3, 4, 5};
int[] dest = source.clone();
注意:这些方法都是浅拷贝。如果数组元素是对象,复制的是引用而不是对象本身。
Java提供了java.util.Arrays类,包含了许多操作数组的实用方法:
java复制int[] numbers = {5, 3, 9, 1, 7};
// 排序
Arrays.sort(numbers); // [1, 3, 5, 7, 9]
// 二分查找(数组必须先排序)
int index = Arrays.binarySearch(numbers, 5); // 2
java复制int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
// 比较数组内容
boolean equal = Arrays.equals(a, b); // true
// 填充数组
int[] c = new int[5];
Arrays.fill(c, 100); // [100, 100, 100, 100, 100]
java复制int[] array = {1, 2, 3};
System.out.println(Arrays.toString(array)); // [1, 2, 3]
对于多维数组,使用deepToString()方法:
java复制int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]
这是初学者最容易犯的错误之一:
java复制int[] arr = new int[5];
System.out.println(arr[5]); // 抛出ArrayIndexOutOfBoundsException
解决方案:
array.length而不是硬编码长度当数组未初始化就使用时:
java复制int[] arr;
System.out.println(arr[0]); // 编译错误
int[] arr2 = null;
System.out.println(arr2[0]); // 运行时NullPointerException
解决方案:
数组一旦创建,长度就固定了。如果需要动态大小的集合,应该使用ArrayList等集合类。
解决方案:
理解多维数组的内存布局很重要:
java复制int[][] arr = new int[3][];
arr[0] = new int[2];
arr[1] = new int[3];
arr[2] = new int[4];
这样的不规则数组在内存中不是连续的矩形区域,而是每个一维数组独立分配空间。
数组的随机访问时间复杂度是O(1),因为它是基于内存地址的直接计算。这使得数组在需要频繁随机访问的场景下非常高效。
数组在内存中是连续存储的,这带来了几个好处:
数组在中间位置插入或删除元素效率较低(O(n)),因为需要移动后续所有元素。如果应用场景需要频繁插入删除,链表可能是更好的选择。
数组非常适合存储和处理统计数据:
java复制// 计算平均温度
double[] temperatures = {22.5, 23.1, 24.3, 21.8, 20.5};
double sum = 0;
for (double temp : temperatures) {
sum += temp;
}
double average = sum / temperatures.length;
图像像素通常用二维数组表示:
java复制// 简单的图像灰度处理
int[][] imagePixels = loadImage();
for (int i = 0; i < imagePixels.length; i++) {
for (int j = 0; j < imagePixels[i].length; j++) {
int gray = (imagePixels[i][j] & 0xFF) / 3;
imagePixels[i][j] = (gray << 16) | (gray << 8) | gray;
}
}
游戏中的地图、棋盘等常用数组表示:
java复制// 简单的井字棋棋盘
char[][] board = new char[3][3];
Arrays.fill(board[0], ' ');
Arrays.fill(board[1], ' ');
Arrays.fill(board[2], ' ');
// 放置一个X
board[1][1] = 'X';
大多数算法都依赖数组作为基础数据结构:
java复制// 快速排序实现
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
虽然数组是Java中最基础的数据结构,但在现代Java开发中,集合框架(如ArrayList、HashSet等)使用更为广泛。下面是它们的主要区别:
| 特性 | 数组 | 集合类(如ArrayList) |
|---|---|---|
| 长度 | 固定 | 动态增长 |
| 类型安全 | 编译时检查 | 泛型提供运行时安全 |
| 性能 | 随机访问快 | 稍慢,但有优化 |
| 功能 | 基本操作 | 丰富的方法 |
| 内存 | 紧凑 | 有额外开销 |
| 多态 | 有限 | 更好的多态支持 |
选择建议:
Java 8引入的Stream API也支持数组操作:
java复制int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers);
java复制String[] words = Stream.of("Java", "Python", "C++")
.toArray(String[]::new);
java复制int[] numbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(numbers)
.filter(n -> n % 2 == 0)
.sum();
根据多年开发经验,总结以下数组使用的最佳实践:
优先使用单行初始化:对于已知元素的小数组,使用{...}语法更简洁。
明确数组长度:如果可能,在创建时就确定合适的长度,避免频繁扩容。
使用增强for循环:遍历数组时,除非需要索引,否则优先使用for-each循环。
防御性复制:当返回数组给客户端代码时,返回副本而不是原始数组。
考虑使用集合类:如果业务需求变化频繁,考虑使用ArrayList等动态集合。
多维数组注意内存:非常大的多维数组可能占用过多内存,考虑替代方案。
利用Arrays工具类:不要重复造轮子,充分利用标准库提供的数组操作方法。
文档化数组约定:如果数组有特殊结构或约定,务必在文档中说明。
考虑并行处理:对于大型数组,考虑使用并行流(parallelStream)提高处理速度。
测试边界条件:特别注意测试空数组、单元素数组等边界情况。