ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java数组从入门到进阶:定义、遍历、排序与常见坑全面解析

Java数组从入门到进阶:定义、遍历、排序与常见坑全面解析 1. 先聊清楚数组到底是干嘛的如果你刚开始学Java数组大概率是你在循环、判断之外遇到的第一个“真正有点数据结构味道”的东西。很多新手学到这里会有一个疑问我声明一堆变量不行吗为什么非要搞个数组出来假设你要存班里50个人的成绩不使用数组的话你得写50个变量score1、score2、score3……然后你想算平均分的时候代码会变成一场灾难——你得一个一个把它们加起来。更痛苦的是如果需求从50人变成500人你的代码就得膨胀10倍。数组解决的就是这个“同类型数据批量管理”的问题它把多个同类型的值放到一块连续的内存空间里用同一个名字加上下标去访问。从语言设计的角度说数组是几乎一切高级数据结构的基石。你后面学ArrayList、HashMap底层存储其实都在和数组打交道。ArrayList说白了就是一个“会自动扩容的数组”HashMap的桶数组本质上也是一个数组。所以现在把数组吃透你后面看集合源码会顺畅很多不然你连table[index]这种访问都反应不过来它在做什么。用生活里的例子来理解数组就像电影院的连排座位每个座位编号固定座位号从0开始你想找第几个观众直接按座位号过去就行不用从头一个个数。这听起来很朴素但背后藏着一个重要特性数组支持随机访问时间复杂度是O(1)。这一点的意义你现在可能感受不到等你以后写性能敏感的业务代码或者刷算法题用数组模拟栈、队列、哈希表的时候你会发现它快得离谱——因为它就是通过“首地址 下标 * 元素大小”这个公式直接算出内存地址的中间没有任何遍历过程。2. 定义与初始化这些基础操作里藏着不少坑2.1 声明数组的两种写法有什么讲究Java里声明数组有两种方式初学者经常搞混int[] arr1; // 推荐写法类型后面跟方括号 int arr2[]; // C语言风格变量名后面跟方括号第一种读起来更自然“int数组类型的arr1”。第二种是从C/C移植过来的老写法Java语法支持它但如果你去看Java官方规范或者主流开源项目几乎没人这么写。我建议你一开始就只用int[]这种形成肌肉记忆。因为后面涉及到方法返回值时两种写法的差异会让人崩溃// 这样返回一个int数组语义很清楚 public int[] getScores() { return new int[]{1, 2, 3}; } // 这种写法虽然合法但阅读起来很容易看成返回了一个int public int getScores2[]() { return new int[]{1, 2, 3}; }看到没有数组做方法返回值时把方括号写在方法名后面是合法的但可读性非常差。而如果把方括号放在类型后面方法声明就自然多了。所以不用纠结统一用“类型方括号”的写法可以避免很多无谓的混乱。2.2 三种初始化方式静态、动态和默认值初始化是新手接触数组时最容易踩坑的地方。梳理下来其实是三条路// 第一种静态初始化声明同时把值也给了 int[] scores {85, 92, 78, 90, 88}; // 第二种动态初始化先指定长度后面再逐个赋值 int[] scores2 new int[5]; scores2[0] 85; scores2[1] 92; // 第三种使用new关键字配合大括号实际上是匿名数组的变体 int[] scores3 new int[]{85, 92, 78, 90, 88};第一种和第三种效果一样区别在于第三种可以像下面这样分成两步写int[] scores; scores new int[]{85, 92, 78, 90, 88}; // 合法 // int[] scores2; // scores2 {85, 92, 78, 90, 88}; // 不合法静态初始化不能脱离声明语句单独存在这一点很多人不知道。静态初始化语法{...}只能跟在声明语句里如果你想先声明一个数组变量之后再给值就必须用new int[]{...}的完整形式。动态初始化的时候还有个隐藏的“默认值”规则。Java为了安全性会为每个数组元素赋默认值不同类型的默认值不一样数组类型默认值int、short、byte0long0Lfloat0.0fdouble0.0dchar\u0000空字符booleanfalse引用类型String、对象等null这就意味着你执行int[] arr new int[10];之后虽然没赋值但直接用arr[5]是安全的它会是0。不过引用类型数组就没这么省心了——String[] names new String[3];之后names[0]是null如果你直接调用names[0].length()立刻就是NullPointerException。工作中很多线上事故就是这种“以为数组有值结果是null”的情况。2.3 为什么数组下标从0开始而不是从1开始这是一个特别容易被问倒但特别基础的问题。数组访问的底层逻辑是数组的首地址假设叫base加上偏移量。如果下标从0开始那么第i个元素的地址就是base i * elementSize寻址时不需要做任何减法运算。如果下标从1开始第i个元素的地址就变成了base (i - 1) * elementSize每次访问都要做一次i-1的减法虽然对现代CPU来说这点开销几乎可以忽略但在数组这种最基础的结构上语言的底层设计者不愿意为这种“折损”买单。还有一个历史因素C语言从1960年代末期设计开始就用0下标Java的语法很多地方延续了C的风格所以这个“0起点”的传统就被继承下来了。理解这一点后你写循环遍历时就会下意识想到for (int i 0; i arr.length; i)而不是从1开始。面试时如果有人问你也能从“偏移量计算”这个角度回答而不是只会说一句“约定俗成”。2.4 length到底是属性还是方法——很多人栽在这里String类获取长度要写str.length()因为length是方法。而数组获取长度写的是arr.length没有括号因为数组的length是属性。很多人从String转到数组时在这里报错int[] arr new int[5]; int len arr.length; // 正确 // int len2 arr.length(); // 编译报错找不到符号 length()为什么会有这种区别因为String是一个类长度由内部方法计算返回而数组不是普通类它是Java语言层面直接支持的语法结构length字段由JVM在创建数组时自动维护。你没法修改arr.length的值——它是final的而且根本不对你暴露任何setter。这一点在写循环条件时要特别注意因为数组长度是固定不变的。3. 遍历、拷贝与对象数组实操中最常用的操作细节3.1 三种遍历方式性能差异和适用场景遍历数组是最高频的操作。常见写法有三种int[] arr {3, 7, 1, 9, 4}; // 方式一传统for循环可以拿到下标 for (int i 0; i arr.length; i) { System.out.println(下标 i 的值是 arr[i]); } // 方式二增强for循环foreach拿不到下标 for (int num : arr) { System.out.println(num); } // 方式三Java 8之后的Stream方式只是展示性能追求场景慎重 Arrays.stream(arr).forEach(System.out::println);传统for循环最灵活你可以控制从哪里开始到哪里结束比如只要偶数下标的值、从后往前遍历等。增强for写起来最简洁适合从头到尾全量处理且不需要下标的情形。很多人不知道增强for背后其实会被编译成传统for循环对于数组而言所以性能上两者差别很小不用过于纠结。但有一个容易出错的细节增强for循环里对循环变量赋值并不会改变原数组里的值。这点和C#的foreach类似int[] arr {1, 2, 3}; for (int num : arr) { num 100; // 这里改的是副本numarr里的值完全不变 } // arr依然是{1, 2, 3}因为Java的基本类型是值传递增强for循环每次都会把arr里的元素复制一份给num。如果你是引用类型数组foreach拿到的也是引用的副本通过这个引用修改对象的内部状态是有效的但直接对循环变量重新赋值则不会影响原数组。这个区别在很多复杂的对象数组操作中经常会让人困惑。3.2 数组拷贝引用复制会害了你要区分“浅拷贝”与“深拷贝”数组之间用等号赋值做的是引用传递不是值的拷贝int[] arr1 {1, 2, 3}; int[] arr2 arr1; arr2[0] 999; System.out.println(arr1[0]); // 输出999因为arr1和arr2指向同一个数组对象这在日常开发中是个大坑。比如你写了一个方法内部对传入的数组做排序如果不小心把参数直接赋值给一个“看起来像副本”的变量然后对这个变量做操作外部原来的数组其实已经被改了。很多时候你并不是故意要改调用方的数据但引用传递把这个意图模糊了。要真正复制一份数组内容常用方式有这几种int[] src {1, 2, 3, 4, 5}; // 方式一Arrays.copyOf最推荐可截取或扩容 int[] copy1 Arrays.copyOf(src, src.length); // 方式二System.arraycopy最底层常用于数组部分拷贝 int[] copy2 new int[src.length]; System.arraycopy(src, 0, copy2, 0, src.length); // 方式三clone方法 int[] copy3 src.clone();这三者核心都是native层的内存块拷贝效率都不错。日常工作最常用的还是Arrays.copyOf因为它简洁如果你要自定义拷贝范围比如从src的第2个元素开始拷3个元素到目标数组的第5个位置那就必须用System.arraycopy了它的参数体系支持这种精细控制。但你要记住以上三种方式对于基本类型数组是彻底的“值拷贝”拷贝后改任意一个数组都不影响对方。但如果数组里存的是对象那么拷贝出来的新数组和原数组里的对象引用是同一个——也就是说修改新数组里某个对象的字段原数组里对应的那个对象也会跟着变。这种情况如果你要的是完全独立的副本就得手动遍历对每个元素做深拷贝。在写对象数组的拷贝逻辑时一定要先想清楚你的场景需要的是“引用共享”还是“数据隔离”。3.3 打印数组的乱码问题为什么直接输出会是一串[Ixxxx新手最爱犯的错数组是不是可以直接打印int[] arr {1, 2, 3}; System.out.println(arr); // 输出结果类似[I1b6d3586很多人看到这个直接傻眼以为数据坏了。其实这串东西有固定格式[I1b6d3586里的[表示一维数组I表示int类型后面是对象的哈希码十六进制表示。也就是说数组直接打印默认调用的是Object的toString方法它根本不会去遍历你的数组内容。正确的打印方式是用Arrays工具类的toString方法import java.util.Arrays; int[] arr {1, 2, 3}; System.out.println(Arrays.toString(arr)); // 输出[1, 2, 3]这个设计初看很反直觉但背后有它的道理数组不是一个普通的“集合容器”Java没有为它重写toString可能是觉得数组是底层结构打印需求交给工具类统一处理更合理。以后看到[Ljava.lang.String;xxxx这种输出就明白了那是String数组直接toString的结果[L表示它是一个引用类型的一维数组。3.4 对象数组与Arrays.asList的一个经典陷阱对象数组和基本类型数组在转换的时候有个极容易踩的坑。看看这个String[] strArr {aa, bb, cc}; ListString list Arrays.asList(strArr); System.out.println(list.size()); // 3 int[] intArr {1, 2, 3}; Listint[] list2 Arrays.asList(intArr); System.out.println(list2.size()); // 1而不是3为什么同样是数组int数组转出来size是1因为Arrays.asList接受的是泛型可变参数它会把传入的东西当作元素放进List。String[]可以被当作String...处理所以展开成了3个元素。而int[]整体是一个对象没法自动装箱成Integer[]所以它把整个int数组当作了一个元素放进了List。这也是面试里高频的“八股”题目理解起来其实不复杂泛型不支持基本类型int[]作为一个Object恰好满足泛型的类型边界于是它就成了List里的单一项。现在的推荐做法是如果你要int[]转List老老实实写循环int[] arr {1, 2, 3}; ListInteger list new ArrayList(); for (int num : arr) { list.add(num); }另外还要注意Arrays.asList返回的是Arrays内部的一个ArrayList子类它继承了AbstractList但并没有实现add和remove。所以直接对asList的结果调用add会抛UnsupportedOperationException。想要一个真正可增删的List要包一层new ArrayList(Arrays.asList(strArr))。这个坑在真实项目中会碰到很多人在工具类里写了个“列表转换”结果测试的时候没测add上线后一调用就炸了。4. 二维数组它其实是一个“装着数组的数组”4.1 二维数组的内存结构和C语言完全不同Java里的二维数组从语言层面说其实是一个“数组的数组”。比如int[][] matrix new int[3][4];这行代码创建了4个数组对象最外层是一个长度为3的数组它的每个元素分别指向3个长度为4的一维int数组。如果你学C语言二维数组通常是一整块连续的内存区域类似int matrix[3][4]的大块内存但Java里是“外层数组多个内层数组对象”的嵌套结构。这一点会带来两个影响。第一Java二维数组不要求每一行的列数相同你可以自由创建“不规则数组”int[][] jagged new int[3][]; jagged[0] new int[2]; jagged[1] new int[4]; jagged[2] new int[3];这在某些场景里特别有用比如存储三角形矩阵、稀疏结构、每一行数据条数不一样的数据。如果你用规则二维数组那每一行都得按最大长度分配浪费内存空间。第二访问二维数组元素matrix[row][col]时JVM需要先通过外层数组找到第row行的一维数组对象再在这个一维数组里找第col个元素。这比C语言的连续内存寻址多一层间接跳转但因为数组对象本身很小实际性能影响有限不用担心。4.2 二维数组的初始化与遍历初始化二维数组也有好几种写法// 方式一动态初始化 int[][] matrix new int[3][4]; // 方式二静态初始化比较直观 int[][] matrix2 { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // 方式三只指定行数列数手动设置不规则数组 int[][] matrix3 new int[3][]; matrix3[0] new int[]{1, 2}; matrix3[1] new int[]{3, 4, 5, 6}; matrix3[2] new int[]{7};遍历二维数组的标准套路是双层循环int[][] matrix { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; 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(); }注意内层循环的终止条件用的是matrix[i].length而不是什么固定的“4”或者“matrix.length”。这样写的好处是即使某一行长度不一样代码也不会数组越界。如果你把条件写死成外层行数当作列数遇到不规则数组时会直接抛ArrayIndexOutOfBoundsException。打印二维数组有一个和一维数组对应的方法System.out.println(Arrays.deepToString(matrix)); // 输出[[1, 2, 3], [4, 5, 6], [7, 8, 9]]当然你也可以用增强for循环嵌套遍历for (int[] row : matrix) { for (int num : row) { System.out.print(num ); } System.out.println(); }4.3 二维数组的实际应用场景很多人觉得二维数组就是教材里的“矩阵打印”工作里用不上其实不然。最典型的一个场景是把表格数据按行和列存储比如Excel导入时的原始数据解析、游戏地图的格子坐标、图像处理里的像素矩阵这些全是二维数组的天下。我在实际项目里处理过Modbus报文解析有一段数据是按“寄存器地址通道号”组织的数值矩阵用int[][] data new int[registerCount][channelCount]来存然后通过嵌套循环把每个格子里的值取出来做二次计算。当时如果用一堆List 去存类型转换和取值反而非常繁琐。数组在这种“固定行列、批量数值访问”的场景下从代码可读性和内存占用来看都优于集合。还有一个例子是二维数组在算法竞赛或刷题里的经典用法——计算前缀和。假设你有一个二维矩阵想快速求任意子矩阵的和可以预先构建一个二维前缀和数组。核心思想就是利用数组的随机访问特性把区域求和从O(n*m)降到O(1)。这种题目在面试里不算少见如果二维数组不熟光是想清楚dp数组的下标映射就能花掉半天。5. 进阶实操排序、去重、查找与算法入门5.1 排序先用工具类但你要能写出冒泡排序Java里给数组排序最简单的方式是int[] arr {5, 2, 8, 1, 9}; Arrays.sort(arr); System.out.println(Arrays.toString(arr)); // [1, 2, 5, 8, 9]Arrays.sort用的底层是DualPivotQuicksort对基本类型数组性能很好。对于对象数组传入Comparator可以自定义排序规则String[] words {banana, apple, cherry}; Arrays.sort(words, (a, b) - b.compareTo(a)); // lambda表达式降序 System.out.println(Arrays.toString(words)); // [cherry, banana, apple]但事情没这么简单。Java面试里手写排序算法是经典保留节目尤其是冒泡排序。别觉得现在有现成的sort就万事大吉面试官让你写冒泡考察的是你对循环嵌套、数组交换、边界控制的敏感度。手写一个冒泡排序顺便加一个小优化public static void bubbleSort(int[] arr) { // 外层控制需要比较的轮数最后一轮只剩一个数不需要再比 for (int i 0; i arr.length - 1; i) { // 假设这一轮没有发生交换如果确实没交换说明数组已经有序可以提前退出 boolean swapped false; // 内层是相邻元素两两比较 for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }为什么内层循环要写成j arr.length - 1 - i因为每一轮排序后最大的数会像气泡一样“浮到”数组末尾末尾的这些位置已经排好了下一轮就没必要再比较它们。-i就是排除掉已经确定的i个最大的数。这个细节能看出你对排序过程的理解程度。5.2 查找从线性查找到二分查找对数组做查找最简单的就是遍历也叫线性查找时间复杂度O(n)。如果数组是有序的可以使用二分查找Java有现成的方法int[] arr {1, 3, 5, 7, 9}; int index Arrays.binarySearch(arr, 5); System.out.println(index); // 输出2如果没找到binarySearch返回的是一个负数具体值是-(insertion point) - 1。比如在{1, 3, 5, 7, 9}里查4插入点本来是下标2返回就是-3。很多人对这个返回值不太理解只要记住一个原则返回值小于0就说明没找到想要知道该插入到哪个位置来维持有序可以用-index - 1还原插入点。但真正面试手写的时候你要能独立写出来public static int binarySearch(int[] arr, int target) { int left 0; int right arr.length - 1; while (left right) { // 写成 left (right - left) / 2 可以防止 leftright 溢出 int mid left (right - left) / 2; if (arr[mid] target) { return mid; } else if (arr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }很多人就会问为什么不用(left right) / 2因为left和right如果都接近int最大值相加会溢出成负数。这个细节在二分查找、归并排序等很多算法场景里都有出现稍微有点经验的面试官会追问到这里。5.3 去掉数组里的重复元素基础版与进阶版“数组去重”是面试题里的常客。最简单粗暴的思路是借助SetInteger[] arr {1, 2, 3, 2, 4, 1, 5}; SetInteger set new LinkedHashSet(Arrays.asList(arr)); Integer[] result set.toArray(new Integer[0]); System.out.println(Arrays.toString(result));为什么要用LinkedHashSet因为它既能去重又能保持元素的插入顺序。如果你用HashSet去重虽然能完成但最终顺序没有保证。如果你要的是一个List那更简单ListInteger distinctList new ArrayList(new LinkedHashSet(Arrays.asList(arr)));对象数组去重有一点要格外留意如果元素是自定义对象光靠HashSet还不够你得给这个对象重写equals和hashCode。不然HashSet判断两个对象是不是相同走的是Object的默认实现——比较内存地址即使两个对象的业务字段完全一样也会被当成不同元素去重就会失效。如果面试要求不用额外的集合类就在原数组上做去重最朴素的方法就是两层循环找出重复项并标记或者先排序再去重因为排序后相同的元素相邻一遍遍历就能跳过重复。这些写法各有取舍核心是考察你对时间复杂度和空间复杂度的权衡能不能说清楚。5.4 数组与集合转换String.join、字符串数组转String等一个很常见的需求是把数组元素拼接成字符串比如把{2024, 05, 20}拼成2024-05-20。从Java 8开始有非常简洁的String.joinString[] parts {2024, 05, 20}; String date String.join(-, parts); System.out.println(date); // 2024-05-20如果你要把int数组元素拼成一个逗号分隔的字符串Arrays.stream配合Collectors也简单int[] numbers {1, 2, 3, 4}; String str Arrays.stream(numbers) .mapToObj(String::valueOf) .collect(Collectors.joining(,)); System.out.println(str); // 1,2,3,4这里顺便回答一个前后端联调中经常冒出来的问题前端传来一个JSON数组比如[1, 2, 3]后端解析成int[]之后又需要把它拼成1,2,3传给下一个系统。很多人会用for循环拼接再加上一堆if判断“是不是最后一个元素”其实用Collectors.joining就不需要处理末尾逗号的问题。反过来字符串拆成数组用的就是String.split方法String str apple,banana,cherry; String[] fruits str.split(,); System.out.println(Arrays.toString(fruits));如果想用多个分隔符比如逗号或分号split支持正则str.split([,;])就行。不过有一点要提醒split如果末尾有一些空字符串结果数组可能会丢弃它们。比如a,b,.split(,)得到的数组长度是2而不是3因为它默认会丢弃末尾的空字符串。如果业务需要保留要使用split(,, -1)。6. 高频异常与面试易错点把这些坑提前踩一遍6.1 ArrayIndexOutOfBoundsException是怎么产生的这是你接触Java后第一个高频异常。最常见的元凶有两类一是循环边界写错比如数组长度是5你写了i arr.length最后一次循环访问的就是arr[5]而合法下标是0到4二是动态计算下标时越界比如从外部传入一个index你没有校验就arr[index]取值一旦传入负数或者大于等于length的数运行到这里就抛异常。处理这种问题的核心习惯是遍历时用i arr.length而不是i arr.length如果是外部传入下标先做范围校验或者把防御逻辑放在统一的工具方法里看异常栈信息它会精确告诉你哪个类哪一行出现了问题比如java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5从JVM的角度看数组越界检查是Java对程序员的一种保护几乎每次数组访问都会由JVM隐式做边界检查越界就抛异常不会像C/C那样直接让程序产生未定义行为甚至内存破坏。很多人觉得“每次访问都检查会影响性能”实际上JVM的JIT编译器会做优化在循环场景里往往能把边界检查消除或合并所以正常业务代码不需要为Java的这个安全检查担心性能。6.2 数组为null与数组长度为0是两回事有一个新手经常弄混的概念。int[] arr null;表示arr这个引用没有指向任何数组对象你访问arr.length就会NullPointerException。而int[] arr new int[0];是创建了一个长度为0的数组对象它是真实存在的arr.length是0只是没有任何元素。代码里这两个状态不能混同public void printArray(String[] arr) { // 错误的写法直接判空然后就用了 if (arr ! null) { for (String s : arr) { System.out.println(s); } } // 更稳妥的写法同时处理null和可能为null的引用 if (arr null) { return; } // 到这里arr一定不为null但length可能为0增强for循环自然跳过 for (String s : arr) { System.out.println(s); } }实际项目中很多接口返回的数组要么是null要么是空数组调用方如果不加判断很容易写出一堆“if (arr ! null arr.length 0)”这种样板代码。如果Java 8以后的项目建议用Arrays.stream(arr)的方式处理它天然会对null元素做逻辑兼容但注意如果arr本身是nullstream还是会抛NPE因为stream无法处理数组引用本身为null。6.3 面试里关于数组的“八股”整理把工作中和面试里关于数组最常出现的考点整理成一张速查表方便你在复习时一眼过完常见问题原因处理/回答要点数组能不能用toString直接打印数组继承的是Object的toString使用Arrays.toString或用Arrays.stream数组长度是属性还是方法数组的length是JVM维护的字段arr.length没有括号为什么数组下标从0开始地址偏移量计算base index * elementSize避免减法数组和ArrayList怎么选数组长度固定、集合可变元素数量固定或追求性能用数组频繁增删用集合arrays.asList(int[])为何size为1泛型不支持基本类型int[]被当作单个对象用循环或Java 8的IntStream转换对象数组拷贝后改了为什么原数组也变了引用类型数组拷贝的是引用深拷贝需逐元素new新对象new int[5]之后元素是什么值数组初始化会赋类型的默认值int是0引用类型是null数组越界会不会被编译器提前发现只有运行期的动态检查循环边界写错是主要来源6.4 实际工作会踩到的“内存不足”问题热词里有一个很典型的报错Java中遇到java.lang.OutOfMemoryError: Insufficient memory。数组往往是诱导这个错误的元凶因为创建大数组需要连续的堆内存空间。比如你在代码里写byte[] buffer new byte[1024 * 1024 * 1024];也就是1GB的数组如果JVM堆没有这么大或者即使有但碎片化严重导致无法分配连续空间就会抛OutOfMemoryError。这类问题定位思路比较固定先用jmap -heap pid或借助VisualVM查看堆内存大小和实际使用量检查代码里是否有一次性创建超大数组的逻辑特别是读取文件、网络报文时的bytes数组如果是因为反复在循环里创建大数组导致内存堆积考虑复用缓冲区判断到底是不是“一次性分配太大”还是“持续累积最终不够用”。前者报错信息常有“Requested array size exceeds VM limit”字样后者往往是java heap space实际开发里还有一种隐蔽情况你用Listbyte[]存了很多下载的文件块每个块大小合理但总量失控导致堆内存被打满。这种问题不是单个数组分配失败而是数组引用被集合持有后无法回收。拿数组练手时多想想它背后的内存模型对以后排查这类问题很有帮助。6.5 循环里数组引用指向同一个对象的隐患假设你要建一个二维数组来当表格用像下面这样初始化int[][] table new int[5][5];这段代码没问题它确实创建了5个独立的一维数组。但如果你图省事写了int[] row new int[5]; int[][] table new int[5][]; for (int i 0; i table.length; i) { table[i] row; // 问题极大 }你会发现table[0]和table[1]其实指向同一个一维数组。你改table[0][2]table[1][2]也会跟着变。原因是循环每轮把同一个row引用塞给了不同的外层元素真正的数组只有一个。这种写法偶尔会在处理不规则数据时出现比如你想临时借用同一个数组做批量填充但忘了每次循环要重新new。同理用Arrays.fill填充多维数组时需要逐行填不要试图对整个二维数组做fill再期望它能按行生效因为你fill的其实是外层数组的引用内容。遇到这种逻辑给自己定一个规则每个内部数组都要独立创建不要偷懒复用同一个一维数组对象。这个问题的本质就是Java引用传递的经典体现。写代码的时候多问自己一句“这个对象是只有我一个人在用还是多处共享”能避免很多诡异的联调问题。7. 我建议你这样练一套入门到进阶的自测想法学数组光看不练等于白学。我建议你按下面这个顺序把每个小例子亲自跑一遍不打折扣地写出来。第一层基础语法。创建不同基本类型数组打印默认值用三种方式初始化分别证明它们等价写一个倒序遍历打印。第二层对象数组。创建一个Student类包含name和score字段用对象数组存5个学生初始化后打印每个人的信息然后找出分数最高的学生打印他的名字。这一步会逼你摆脱纯数值数组的舒适区开始习惯“数组里装的是引用”这个事实。第三层算法操作。随机生成10个0到100之间的整数放入数组写一个冒泡排序不要直接调用Arrays.sort再写一个二分查找要先排好序。算一下排序前和排序后数组里的值变化体会一下原地排序与返回新数组的区别。第四层二维数组练习。创建一个三角形矩阵第i行有i1个元素按金字塔形打印。注意要处理不规则数组的访问边界。再用二维数组模拟一个游戏地图比如用0表示空地、1表示障碍物写一个方法统计周围8个格子里有几个障碍物。这类小练习做完你对二维数组的理解会比刷十遍教程都深。我自己的体会是数组这一章虽然简单但它是后续所有数据结构的“思维模板”。你调试二维数组越界时积累的经验在以后排查HashMap扩容相关问题时依然适用。遇到这种基础主题别跳过、别眼高手低亲手写一遍是最快的路径。
返回列表