ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java数组应用全解:初始化、内存模型、排序查找与封装实战

Java数组应用全解:初始化、内存模型、排序查找与封装实战 “数组应用”这四个字放在Java基础语法里似乎平平无奇但真正把11到22这十几道题刷完、再回头看的时候你会发现它其实是整个面向对象语法线里最容易被低估的一块隐形地基。很多人觉得数组不就是一连串同类型变量嘛声明、赋值、遍历三步走五分钟就完事。可真到了OJ上做数组应用题、到了面试八股文里聊集合和排序、到了工作中去调试一段涉及数组拷贝的代码才意识到当初基础阶段欠下的账后面全都要还。这篇就结合我在实际教学和项目里反复遇见的问题把数组应用这份“账”从头梳理一遍。先说清楚这篇内容适合谁看正在学Java基础和面向对象的在校生、准备Java面试的求职者、以及写了几年代码但对数组底层细节仍然模糊的开发者。我会从数组为什么值得认真学开始一路讲到初始化、内存模型、排序查找、工具类、面向对象封装和排错实战每一块都会配上可运行的代码和容易翻车的细节。1. 数组为什么是Java面向对象语法线里的“隐形地基”很多初学者会有个疑问面向对象课程讲的是类、对象、继承、封装数组这种接近C语言风格的东西怎么会被安排在面向对象的章节里答案很简单——在Java里数组本身就是对象。它不是简单的一组变量而是一个在堆内存中真实存在的对象实例。这一点如果理解不到位后面的集合框架、泛型、Stream流等概念全都会蒙上一层阴影。除了语言层面的定位数组在现代Java开发里的渗透率远超想象。Java后端接口里接收批量参数会用到数组或集合的底层转换数据库批量操作的结果集转对象数组、Excel导入导出的行数据处理、算法题中排序和查找、IO流里缓冲区的读写处处都有数组的身影。甚至你在用ArrayList的时候它内部就是Object[]数组。学透数组应用表面上是应付OJ的11到22题本质上是在为理解整个Java技术栈打桩。数组要解决的最核心问题可以用一句话概括当你有大量同类型的数据需要统一存储和批量处理时数组是内存利用率最高、访问速度最快的数据结构。它支持通过下标进行O(1)时间复杂度的随机访问这是它相比链表的先天优势。在学习集合之前数组也是你手头唯一能用来组织数据的武器。这个阶段如果能把数组用熟后面学List、Map的时候会轻松很多因为你已经理解了“容器”是怎么一回事。2. 数组的初始化和内存模型三个把新手绕晕的细节数组应用题大多不考背诵但初始化方式和内存分配这两个知识点是绝对的送分题和送命题。很多同学能写出代码却不知道自己写的到底创建了几个对象也不知道为什么数组变量之间互相赋值之后一个变了另一个也跟着变。2.1 静态初始化和动态初始化差别不只是写法数组初始化分为静态初始化和动态初始化代码上的差别很小背后的语义差别却很大// 静态初始化创建数组的同时将元素放入 int[] a {1, 2, 3, 4, 5}; // 动态初始化先创建数组此时元素为默认值后续再手动赋值 int[] b new int[5]; b[0] 1; b[1] 2;我在实际批改作业时发现很多同学分不清什么时候用哪种。简单总结如果你在写代码时就已经知道要存什么数据用静态初始化如果数据是运行时通过用户输入、文件读取或计算产生的用动态初始化。动态初始化的数组元素会被赋予该类型的默认值int类型是0double是0.0boolean是false引用类型是null。这个默认值机制在OJ题里是常考的点尤其是判断“数组没赋值时输出什么”这类的题。int[] arr new int[3]; System.out.println(arr[0]); // 0 System.out.println(arr[2]); // 0还要注意一个坑静态初始化不能分两步写。下面这种写法编译直接报错int[] arr; arr {1, 2, 3}; // 错误Array initializer is not allowed here必须写成int[] arr; arr new int[]{1, 2, 3}; // 正确2.2 length是属性不是方法一个字母的差距数组的length是属性不是方法。String类的length()要有括号数组的length不需要。这个区别在OJ题里不会直接考但在你写代码时会一直碰到。更常见的场景是foreach循环和普通for循环之间的选择foreach不需要关心下标纯遍历的时候推荐用普通for循环需要访问下标或者对下标做运算时用。int[] nums {10, 20, 30, 40}; // 纯遍历用foreach for (int n : nums) { System.out.println(n); } // 需要下标时用for for (int i 0; i nums.length; i) { System.out.println(i - nums[i]); }2.3 内存模型数组变量和数组对象是两回事这是整个数组章节最核心的概念也是后面理解引用传递的基础。Java内存分为栈和堆栈里存基本类型变量和引用变量堆里存真正的对象数据。数组变量名本质上是一个引用它指向堆里的数组对象。int[] x {1, 2, 3}; int[] y x; y[0] 99; System.out.println(x[0]); // 99上面这段代码很多人第一次写的时候会懵我只改了y为什么x也变了原因就是x和y指向的是堆里同一个数组对象它们存的只是对象的地址。把这个模型画一遍胜过背十遍“数组是引用类型”。二维数组更是一个重灾区。Java的二维数组本质上是数组的数组也就是说int[][]第一维的每个元素是另一个int[]的引用。int[][] matrix new int[3][4];这个声明创建了1个二维数组对象和3个一维数组对象一共4个对象。如果你只写了new int[3][]不指定第二维的长度那matrix[0]初始是null必须手动给每一行单独newint[][] matrix new int[3][]; for (int i 0; i matrix.length; i) { matrix[i] new int[4]; }这样一来每一行长度也可以不一样这就是不规则数组。很多OJ题里涉及三角形输出、杨辉三角用的就是这个机制。3. 数组应用题的五类高频场景从11-22题出题逻辑反推核心能力数组应用这一节的题目虽然编号从11到22题各不相同但考察的能力点逃不出五类遍历求最值、累加平均值、数组反转、排序、查找。把这几类吃透不仅作业稳过面试里手撕算法题的基本功也牢固了。3.1 遍历求和与求最值一切计算的起点没有任何技巧就是遍历但这里恰恰是初学者最容易出错的地方。求最大值时初始值应该设成数组中第一个元素而不是0。如果设成0而数组里全是负数最后结果就是错的。这是我每年都能见到的高频错误。public static int findMax(int[] arr) { if (arr null || arr.length 0) { throw new IllegalArgumentException(数组不能为空); } int max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } } return max; }这里有个很多人都忽略的细节为什么从i 1开始遍历因为max已经取了arr[0]就没必要再从0开始比一遍了。虽然从0开始也不会错但从1开始是更严谨的写法体现的是对循环边界条件的理解。求和类似的道理初始值是0然后循环累加。求平均值前先判断length不为0避免除零异常。这些都是OJ题里不会直接提示但实际运行时一定会遇到的坑。3.2 数组反转理解“双指针”思想的第一课数组反转有个最简单的写法和一个更优的写法。最简单的写法是创建一个新数组倒序遍历塞进去再赋值回原数组。但更经典的是双指针原地反转空间复杂度O(1)这是面试中经常考察的思路。public static void reverse(int[] arr) { int left 0; int right arr.length - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } }这个思路的巧妙之处在于用一个临时变量完成两个位置的交换然后左右指针向中间收缩只需要遍历一半的次数就能完成反转。理解了双指针思想后很多字符串处理、链表操作的题目也会变得容易很多。3.3 冒泡排序和选择排序算法思维的起点排序是数组应用章节里的重头戏。虽然实际开发中可以调用Arrays.sort()但面试和考试里依然要能手写基本排序算法。其中最常考的是冒泡排序它的核心思想是相邻元素两两比较大的往后移每一轮都让一个最大的数“冒泡”到末尾。public static void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }冒泡排序有两个容易错的点外层循环是n - 1次因为n个数最多只需要n - 1轮就能排完内层循环是n - 1 - i因为每完成一轮末尾就有一个元素已经就位不需要再参与比较。优化的方法是在某一轮中没有发生交换时直接break说明已经有序这个优化在题目中偶尔会作为加分点出现。选择排序思路略有不同每一轮在内层找到最小值的下标然后和当前位置交换。它的交换次数比冒泡少但比较次数是一样的。3.4 顺序查找和二分查找从暴力到优化查找是数组应用另一个重要的方向。顺序查找最简单从第一个元素开始挨个比较时间复杂度O(n)。但如果是已经排好序的数组二分查找可以将时间复杂度降为O(log n)。public static int binarySearch(int[] sortedArr, int target) { int left 0; int right sortedArr.length - 1; while (left right) { int mid left (right - left) / 2; if (sortedArr[mid] target) { return mid; } else if (sortedArr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }这里有一个细节值得记一下mid的计算写成left (right - left) / 2而不是(left right) / 2。原因是当数组很大时left right可能溢出int范围。虽然OJ里的测试数据通常不会大到触发这个边界但养成这个习惯是专业的体现。二分查找的循环条件用left right因为相等时那个位置还没有检查过必须再进一次循环。3.5 数组拷贝和合并理解深拷贝与浅拷贝实际工作中数组拷贝比很多人想象的更常用。Java自带的System.arraycopy是最高效的拷贝方式它是native方法内存直接进行操作int[] src {1, 2, 3, 4, 5}; int[] dest new int[5]; System.arraycopy(src, 0, dest, 0, src.length);数组拷贝题里最容易混淆的是引用拷贝和深拷贝。直接int[] b a是引用拷贝它们指向同一个对象用arraycopy、clone或循环赋值才能得到独立的数组副本。这一块在后面的集合、对象拷贝中会扩展成更大的主题现在就把概念理清楚会很有帮助。两个数组合并本质上是目标数组容量足够然后分两次arraycopy过去public static int[] merge(int[] a, int[] b) { int[] result new int[a.length b.length]; System.arraycopy(a, 0, result, 0, a.length); System.arraycopy(b, 0, result, a.length, b.length); return result; }4. 用面向对象思维重新理解数组为什么说数组是对象的“容器”这一节是很多同学理解的断层地带。语法上数组存储的是基本类型但Java面向对象的核心在于“万物皆对象”数组也不例外。更重要的是数组可以存放对象这是从数组语法迈向面向对象编程的一座关键桥梁。4.1 对象数组的创建和初始化名字叫数组装的却是对象引用假设我们有一个Student类class Student { String name; int age; public Student(String name, int age) { this.name name; this.age age; } Override public String toString() { return Student{ name name \ , age age }; } }创建一个Student数组并填充数据Student[] class1 new Student[3]; class1[0] new Student(张三, 20); class1[1] new Student(李四, 21); class1[2] new Student(王五, 22);或者更简洁的静态初始化Student[] class1 { new Student(张三, 20), new Student(李四, 21), new Student(王五, 22) };这里有个常常被忽略的细节new Student[3]只是创建了一个能容纳3个Student引用的数组这个数组的值都是null并没有创建任何Student对象。要遍历数组给每个元素new对象才算真正把数据放进去for (Student s : class1) { if (s ! null) { System.out.println(s.name s.age); } }由于数组元素可能为null遍历对象数组时加上判空是一种稳妥的习惯。OJ题不会这么要求但在真实项目中这个习惯能帮你避开大量空指针异常。4.2 用类封装数组操作从面向过程到面向对象的转变既然学了面向对象就不应该再像C语言那样直接在外面写一堆静态方法操作数组。更面向对象的方式是把数组操作封装进工具类或与数据相关的类中。class IntArrayUtil { private int[] arr; public IntArrayUtil(int[] arr) { this.arr arr; } public int max() { int max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) max arr[i]; } return max; } public int sum() { int sum 0; for (int n : arr) sum n; return sum; } public double avg() { if (arr.length 0) return 0; return (double) sum() / arr.length; } }这样做的好处是数据和操作数据的逻辑被聚合在了一起职责边界更清晰。这也是从“以函数为中心”过渡到“以对象为中心”的过程。学完数组应用再来看这段代码你应该能体会到数组只是载体真正体现设计水平的是你怎么组织围绕数组的数据和行为。4.3 数组作为方法参数值的复制和引用传递之争数组作为方法参数时传的是引用。这与基本类型不同基本类型传的是值的拷贝public static void main(String[] args) { int num 10; int[] arr {1, 2, 3}; change(num); change(arr); System.out.println(num); // 10num并未改变 System.out.println(Arrays.toString(arr)); // [99, 2, 3]arr改变了 } public static void change(int n) { n 99; } public static void change(int[] a) { a[0] 99; }这个现象背后的原因是方法中拿到的是和主调方法相同的引用地址通过这个引用修改堆中对象的内容修改自然会被主调方法看到。如果方法内部对参数重新赋值参数变量指向新的数组对象主调方法中的引用不会受到影响。5. Arrays工具类与扩容思想能优雅就优雅写数组代码最高境界不是手写每个功能而是知道什么时候该调用jdk自带的能力。java.util.Arrays是数组最强大的助手。5.1 Arrays.toString解决数组输出乱码的痛点新手第一次用System.out.println直接打印int[]数组输出结果是[I1b6d3586这种看起来像乱码的东西。因为println调用的是Object的toString方法而数组没有重写toString。解决方案就是Arrays.toStringint[] arr {3, 1, 4, 1, 5}; System.out.println(Arrays.toString(arr)); // [3, 1, 4, 1, 5]二维数组要用Arrays.deepToStringint[][] matrix {{1, 2}, {3, 4}}; System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]5.2 Arrays.sort、binarySearch和fillArrays.sort用的是双枢轴快速排序对基本类型数组直接原地排序效率很高对对象数组排序时要求对象实现Comparable接口或传入一个Comparator。Arrays.binarySearch要求数组必须先排好序否则结果不确定。这一点必须小心实际项目里出过很多次问题直接对未排序的数组调用binarySearch得到的结果是负值或者根本错误的索引排查半天才发现是忘了排序。int[] arr {5, 3, 1, 4, 2}; Arrays.sort(arr); // 先排序 int index Arrays.binarySearch(arr, 3); // 再二分查找 System.out.println(index); // 2Arrays.fill可以用来快速填充数组在初始化测试数据的时候非常方便int[] arr new int[5]; Arrays.fill(arr, 7); System.out.println(Arrays.toString(arr)); // [7, 7, 7, 7, 7]5.3 数组扩容思想为什么ArrayList可以随便add数组一旦创建长度就不可变。要“扩容”只能创建一个更大的新数组把旧数据拷贝过去。这个思想非常基础也非常重要因为ArrayList的底层就是这样工作的。int[] arr {1, 2, 3}; int[] newArr Arrays.copyOf(arr, arr.length 2); newArr[3] 4; newArr[4] 5; System.out.println(Arrays.toString(newArr)); // [1, 2, 3, 4, 5]ArrayList扩容时不是每次加1而是按照约1.5倍的速度增长以此减少频繁扩容带来的性能开销。理解了数组的固定长度特性自然就能理解为什么集合框架里会有扩容机制这个概念。5.4 数组和集合之间的转换数组转List用Arrays.asList但有几个坑必须知道String[] strArr {a, b, c}; ListString list Arrays.asList(strArr);这段代码生成的List是固定长度的不能add也不能remove。很多人第一次用的时候想往里面加元素结果抛UnsupportedOperationException。如果想得到一个真正可变的ArrayList需要这样写ListString list new ArrayList(Arrays.asList(strArr));集合转数组则用List.toArray方法ListString list new ArrayList(); list.add(a); list.add(b); String[] arr list.toArray(new String[0]);这段代码里的new String[0]在Java 8以后也可以写成new String[list.size()]但在并发环境下使用0长度数组做参数有个好处是无需分配大数组空间现代JVM也会做优化。6. 排错经验数组代码最常见的四个报错与调试技巧刷题和写代码的过程中数组会带来一系列非常典型的报错。看清楚这些报错背后真正的原因比记住报错信息本身更有价值。6.1 ArrayIndexOutOfBoundsException最经典也最容易犯数组越界异常大概是Java初学者遇到的第一类运行时异常。根因就是对数组访问时下标超出合法范围[0, length-1]。遇到这个异常最直接的方法是在报错信息中定位到具体行号然后检查那一行访问的下标值是怎么计算出来的。常见的越界场景有三个一是循环条件写成i arr.length在i等于length时越界二是把下标从1开始导致最后一个元素没访问到或者越界三是在处理二维数组时行列下标搞反或者某一行长度和其他行不一致。我的建议是看到ArrayIndexOutOfBoundsException第一反应不是读代码而是看堆栈信息里的行号再到那一行分析下标。大多数情况下问题出在循环边界而不是数组本身。6.2 空指针和默认值是孪生兄弟引用类型的数组元素默认值是null如果直接调用引用类型元素的方法或访问属性会触发NullPointerException。很多题目要求先初始化引用类型数组的每个元素就是在考察这一点。Student[] students new Student[3]; System.out.println(students[0].name); // NullPointerException正确做法是for (int i 0; i students.length; i) { students[i] new Student(); }6.3 foreach里修改变量不生效foreach循环的迭代变量是一个局部副本修改它不会影响数组元素int[] arr {1, 2, 3}; for (int n : arr) { n n * 2; } System.out.println(Arrays.toString(arr)); // [1, 2, 3]没有变化如果想修改数组元素必须用带下标的普通for循环或者用引用类型数组时修改对象内部的字段因为引用指向的对象是同一个。6.4 用调试器看数组内容而不是一行行println最后分享一个非常实用的小技巧。在IDEA中调试运行时在数组变量上打上断点调试器会显示数组的全部分元素。如果你只想快速查看一个大数组可以在条件断点中设置i 某个值非常方便。而很多初学者遇到数组输出不完整或结果不对时喜欢在循环里System.out.println一串中间结果打完又不删最后代码全被输出淹没。适当使用调试器把println留给真正需要的地方效率会高很多。写在最后说实话数组这个知识点在Java技能树里确实谈不上高深但它像是语法和数据结构之间的一个转接点。写排序算法的时候你在训练逻辑写对象数组的时候你在为面向对象建模做准备排查数组越界的时候你在培养定位问题的敏感度。这个阶段多花一点时间把基础打牢进入集合框架、泛型、Stream之后你会发现一切都顺理成章。后面如果你们用到List和Map再回头对比数组和集合的取舍也会理解得更深。我给学生的建议一直没变过不要觉得数组简单就跳过把11到22这类题目老老实实刷完再过一遍源码级别的Arrays工具类这个基础就真的扎实了。
返回列表