有人问过我一个问题都这个年头了写“Java数组常见用法”还有人看吗我的回答是正因为常见才真正值得往深里刨一刨。数组是Java里最容易被“以为会了”的基础知识点。增删改查谁都能写两行可真到面试桌上、读框架源码、刷算法题的时候关于数组的细节能卡住一大批人。ArrayList底层是Object[]HashMap的table是Node[]String内部是byte[]线程池的阻塞队列虽然是Queue但它管理线程的核心数据结构也离不开数组。几乎你能叫得上名字的集合和组件底座都是数组。这篇内容我会把数组的声明初始化、Arrays工具类操作、扩容原理、与List和String的互转、二维数组的内存模型以及高频面试题一次性讲透。适合准备Java面试的开发者、刚学完语法想进阶的初学者也包括写了好几年业务代码想回头补地基的同学。1. 别被“基础”两个字骗了数组的声明与初始化边界1.1 四种写法背后的差异先说声明和初始化。数组在Java里最常见的写法有下面四种// 动态初始化只给长度元素取默认值 int[] arr1 new int[5]; // 静态初始化直接给元素长度由编译器推断 int[] arr2 {1, 2, 3}; // 静态初始化的另一种形式new 关键字 元素列表 int[] arr3 new int[]{1, 2, 3}; // 匿名数组没有变量名直接作为方法参数 printSum(new int[]{4, 5, 6});这里有个很容易踩的坑int[] arr {1, 2, 3};这种写法必须在声明的同时初始化不能先写int[] arr;再写arr {1, 2, 3};编译会直接报错。如果你想“先声明后初始化”只能用第二种形式int[] arr; arr new int[]{1, 2, 3}; // 合法int[] arr; arr {1, 2, 3}; // 编译错误为什么会这样因为{1, 2, 3}是数组初始化器它只在声明语句里被编译器识别。而new int[]{1, 2, 3}是一个完整的“创建数组对象”表达式随时可以用。这个区别看起来很细但面试时真的会有人被问到“为什么不能先声明再初始化”说不清楚。1.2 默认值、匿名数组与方法传参“动态初始化”只指定长度不指定内容那元素到底等于什么答案是各类型的默认值数组元素类型默认值byte / short / int / long0 / 0Lfloat / double0.0f / 0.0dchar\u0000空字符booleanfalse引用类型String、对象等null很多初学者会在这里犯错尤其是引用类型数组。String[] names new String[3];创建出来的三个元素全是null不是空字符串。你要是直接names[0].length()得到的不是0而是NullPointerException。所以在遍历初始化出来的数组之前养成先判空的习惯能省下很多排查时间。匿名数组的价值在于方法传参。比如你要调一个方法方法签名是void handle(int[] values)你用handle(new int[]{1, 2, 3})直接传不用单独定义一个变量。实际开发里这种写法不算多但刷算法题和写工具类时很常用。另外要注意匿名数组的写法里new int[]{1, 2, 3}里面不能写长度一旦写了new int[3]{1, 2, 3}编译立刻报错。长度要么由列表元素决定要么只给长度不写元素二者不能混用。1.3 数组是有length属性的特殊对象我在带新人时经常问一个看起来很简单的问题数组的length是属性还是方法答案是属性。String.length()是方法所以后面跟着括号但arr.length没有括号它是一个字段。原因在于Java里的数组是一种特殊的对象它不是通过new MyClass()这种方式由普通类实例化出来的而是由JVM在运行时创建的一种“数组类”对象。这个数组类由JVM动态生成自动带有一个公开的final int length字段用于记录数组长度。正因为没有对应的.java类定义所以数组无法像普通对象那样添加自定义方法也就没有length()方法。这一点还能引申出一个冷知识int[].class是存在的你甚至可以int[] arr new int[3]; System.out.println(arr.getClass());输出会带[I这样的前缀其中[表示这是一维数组I表示元素类型是int。数组对象还重写了clone()方法所以arr.clone()可以直接调用。但也因为toString()没有被重写System.out.println(arr)打出来的是一串像[I1b6d3586的哈希串并不是元素内容。想看内容得用Arrays.toString(arr)后面工具类部分会专门讲。2. Arrays工具类让sort、binarySearch、copy替你干活日常开发中真正徒手写数组遍历逻辑的场景没有想象中那么多因为java.util.Arrays这个工具类已经把最常见操作封装好了。但问题在于很多人只会用其中一两个方法遇到排序、查找、拷贝时会重复造轮子甚至造出带bug的轮子。下面按使用频率逐个讲。2.1 排序两个底层算法和自定义比较器排序是数组操作里的顶流。Arrays.sort有两个典型的重载方向int[] nums {5, 3, 8, 1}; Arrays.sort(nums); // 对基本类型数组排序底层是双轴快排 String[] names {Tom, jerry, Alice}; Arrays.sort(names); // 对对象数组排序底层是TimSort归并插入的混合为什么要分两套算法这里有个底层逻辑基本类型数组不要求排序稳定性稳定的意思是值相等的两个元素排序后相对位置不变而对对象数组来说稳定性常常是业务需要的——比如先按姓名排序再按年龄排序如果排序不稳定第二次排序会把第一次的结果打乱。所以JDK对基本类型直接上更快的双轴快排对对象则用稳定的TimSort。面试如果被问到“为什么Arrays.sort对不同类型有不同实现”这个稳定性理由就是标准答案。再看自定义排序。二维数组排序在业务里很常见比如我有一颗热搜词叫“二维数组”很多LeetCode题目和报表排序都会用到int[][] arr {{1, 9}, {2, 3}, {3, 6}}; Arrays.sort(arr, (a, b) - Integer.compare(a[1], b[1])); // 结果{{2, 3}, {3, 6}, {1, 9}}这里要特别提醒一下Comparator里不要直接写a[1] - b[1]。看着很简洁但如果两个值一正一负相减可能溢出导致排序结果错误。用Integer.compare(a[1], b[1])是更稳妥的写法。排序后想降序可以用Comparator.reverseOrder()或者比较器里调换前后项的位置。2.2 查找与填充binarySearch的返回值规则数组查找最容易被误解的方法就是Arrays.binarySearch。它要求数组必须是有序的否则结果不可靠。这个方法的返回值有固定规则找到就返回索引找不到就返回-(插入点) - 1。int[] nums {1, 3, 5, 7}; System.out.println(Arrays.binarySearch(nums, 5)); // 找到输出2 System.out.println(Arrays.binarySearch(nums, 4)); // 找不到插入点在下标2输出 -(2)-1 -3为什么设计成-(插入点)-1而不是直接返回负数原因很简单如果直接返回负数那0就会产生歧义——到底是找到了下标0还是找不到返回了0用-(插入点)-1之后所有“找不到”的结果都严格小于等于-1和“找到下标0”的返回值不会再混淆。这个设计在Java里到处都是理解后对你读源码很有帮助。填充方法Arrays.fill就直白多了把数组里所有元素替换成同一个值int[] counter new int[10]; Arrays.fill(counter, 0); // 初始化计数表它也可以指定区间Arrays.fill(counter, 1, 5, 42)把下标1到4的元素设为42右边界5是不包含的。这个区间语义和大量Java API保持一致别记反。2.3 拷贝三兄弟copyOf、copyOfRange与System.arraycopy数组长度固定所以“扩容”在底层必然涉及拷贝。Arrays.copyOf是把老数组复制到一个新长度的数组里int[] old {1, 2, 3}; int[] bigger Arrays.copyOf(old, 5); // 结果[1, 2, 3, 0, 0]多出来的位置填充默认值Arrays.copyOfRange是复制指定区间Arrays.copyOfRange(old, 1, 3)得到[2, 3]右边界同样不包含。它还有一个好处即使右边界超过原数组长度也会用默认值补齐不会抛越界异常。底层真正干重活的是System.arraycopy。这是一个native方法由JVM直接调用操作系统级别的内存复制效率比循环赋值高得多。Arrays.copyOf内部就是先创建一个新数组然后调用System.arraycopy把元素搬过去。在日常代码里优先用Arrays.copyOf和copyOfRange这类高级封装除非你确实需要对拷贝的起止位置做精细控制否则没必要直接碰System.arraycopy。但有一点要知道这些拷贝方法对引用类型数组都是浅拷贝拷贝的是“引用”而不是“对象本身”后面第5章会展开讲。3. 数组的“增删改”与遍历动态扩容不是黑魔法3.1 三种遍历方式与增强for的编译真相数组遍历有三大流派普通for循环、增强for、Java 8的Stream。先看前两种// 按索引遍历可以修改元素 for (int i 0; i arr.length; i) { arr[i] arr[i] * 2; } // 增强for适合只读遍历 for (int x : arr) { System.out.println(x); // 这里修改x不会影响arr }很多人以为增强for是“语法糖”底层就是用迭代器。对于数组来说这个理解不准确。编译时for (int x : arr)会被编译器翻译成普通的索引for循环因为数组没有Iterator直接下标访问反而高效。而对ArrayList这类集合增强for才会用到迭代器。这也是为什么在数组场景下增强for和普通for性能几乎没差别的原因。但对于集合增强for在遍历过程中如果结构被修改会抛ConcurrentModificationException数组则没有这个问题因为数组长度固定本身不能增删。值得注意的一个细节是增强for里面的循环变量是“值拷贝”。如果你遍历的是对象数组例如for (StringBuilder sb : sbArr)你拿到的是指向同一个对象的引用所以sb.append(x)能改动原对象但如果你写sb new StringBuilder(y)仅仅是把循环变量指向了新对象原数组里的引用不受任何影响。这个“变量赋值”和“对象内容修改”的区分是写代码时最常见的认知混乱点。3.2 数组增加元素扩容的本质与代价热搜词里有个很有代表性的问题数组增加。Java数组的长度从创建起就是固定的所以“增加元素”本质上只有一条路新建一个更长的新数组把旧元素搬过去在新位置写入值。直接用Arrays.copyOfint[] arr {1, 2, 3}; arr Arrays.copyOf(arr, arr.length 1); // arr变长一格 arr[arr.length - 1] 4; // 尾部追加这段代码背后发生的事情先申请一块新内存长度为4然后把1、2、3复制过去最后在最后一个下标写入4。原来的3个元素的数组对象如果没有其他引用指向它会被垃圾回收。这里要理解扩容的时间代价。每次“增加”都需要复制全部旧元素单次复杂度是O(n)。ArrayList之所以把每次扩容都干到1.5倍而不是只加一个位置就是为了摊销这个成本平摊下来每次尾部追加的代价接近O(1)。面试常考的“为什么ArrayList扩容是1.5倍而不是2倍”从内存和碎片化的角度都能聊但核心要点是扩容必须批量预留空间否则频繁拷贝会拖垮性能。所以如果你的数据规模会频繁增长优先用ArrayList而不是裸数组如果确定数据量不变或者对性能极其敏感才建议直接用数组。3.3 删除与替换System.arraycopy的精细操作删除数组中间的元素同样不能真的“删”只能覆盖。比如删除下标index的元素需要把index后面的元素整体向前移动一位int[] arr {1, 2, 3, 4, 5}; int index 2; System.arraycopy(arr, index 1, arr, index, arr.length - index - 1); arr[arr.length - 1] 0; // 把最后一个位置“清掉”第一行是把从index1开始的所有元素搬到从index开始的位置搬动数量是arr.length - index - 1。搬完之后数组末尾还剩一个重复值所以第二行手动把末尾重置为0。这个模式在实现自己的集合类时非常常见。如果数组元素是引用类型末尾那个位置重置成null还有一个作用帮助垃圾回收。因为旧对象还挂在数组上就是一个强引用GC没法回收它。虽然对局部变量来说方法结束变量就销毁了但如果这个数组本身是某个对象的成员长期存活里面堆积失效引用就会造成内存浪费。这也是为什么很多源码在删除元素后会把对应位置置空最典型的就是ArrayList.remove里那一行elementData[--size] null;。3.4 一个真实场景分割字符串数组并按条件筛选热搜词里有一条特别接地气的需求数组分割并显示包含某一字符。实际场景可能是拿到一串逗号分隔的配置要筛出包含特定关键词的项。完整示例String raw apple,banana,grape,avocado; String[] fruits raw.split(,); // Java 8 流式筛选 ListString matched Arrays.stream(fruits) .filter(s - s.contains(an)) .collect(Collectors.toList()); System.out.println(matched); // [banana] // 传统for循环版 for (String fruit : fruits) { if (fruit.contains(an)) { System.out.println(fruit); } }这里有几个会实际踩到的坑。第一split默认会丢弃尾部的空字符串。比如a,,b,.split(,)的结果是[a, , b]最后一个空串被去掉了。如果业务要求保留必须用重载方法split(,, -1)这样末尾的空串才会保留。第二split的参数是正则表达式如果分隔符是.、|、*这种元字符直接写会得到意想不到的结果必须转义成\\.或\\|。第三查出结果后要处理“没有匹配项”的情况否则后续拿着空列表做操作容易出问题。这个小需求看起来简单但能把字符串分割、正则转义、数组遍历、条件筛选全串起来非常适合作为面试的手写题。4. 数组与List/String互转几个踩了无数次的坑4.1 数组转List的三个坑数组转List最常用的方法是Arrays.asList但它有一堆反直觉的行为我几乎每次代码评审都能看到有人踩。第一个坑返回的List不支持增删。Arrays.asList(a, b)返回的其实是Arrays内部的一个私有ArrayList类它继承自AbstractList但add和remove没有实现调用就会抛UnsupportedOperationException。想要真正可增删的列表得再套一层ListString realList new ArrayList(Arrays.asList(a, b));第二个坑它和原数组共享数据。asList返回的List底层直接指向原数组改动List里的元素原数组也会跟着变反过来改数组List也变。对很多业务来说这个共享行为既违背直觉还容易埋下隐蔽bug。第三个坑基本类型数组转List不能直接用Arrays.asList。你写int[] nums {1, 2, 3}; Listint[] wrong Arrays.asList(nums); // 你以为ListInteger实际是Listint[]size等于1因为泛型只认引用类型int[]被整个当成一个对象塞进去了。正确的做法是用StreamListInteger right IntStream.of(nums).boxed().collect(Collectors.toList());数组转字符串是另一个高频操作。拼接可以用String.join或者StringBuilder循环但如果你只是想打印调试Arrays.toString(arr)是最省事的二维数组则用Arrays.deepToString(arr)。很多人直接System.out.println(arr)看到一串[Icafe就懵了其实只要记得数组没重写toString一切就都合理了。4.2 List转数组new String[0]还是new String[size]反过来List转数组也有讲究ListString list new ArrayList(List.of(a, b)); String[] arr1 list.toArray(new String[0]); String[] arr2 list.toArray(new String[list.size()]);两种写法都对但社区里普遍推荐new String[0]。原因在于如果你传入new String[list.size()]集合内部发现数组长度刚好够就会直接填充这个数组并返回。看起来省了一次复制但这个“正正好好”的数组是你在外部刚创建的新数组长度完全取决于调用瞬间的size()。如果之后List内容变化这个数组长度就过时了。传new String[0]时集合内部会自己判断长度不够就创建新数组长度够就用传入的。JDK开发者也在多个版本的方法文档里建议使用“零长度数组”这种形式。实际性能差异微乎其微但语义上更干净所以我建议统一写toArray(new String[0])。4.3 可变参数与main方法数组的隐藏身份每个Java程序员写下的第一个方法就是public static void main(String[] args)这里的args正是一个String[]数组。也就是说命令行参数本质上是JVM把参数列表打包成一个字符串数组传给了main方法。与此类似可变参数本质上也是数组public void log(String message, Object... args) { // args就是Object[] for (Object arg : args) { System.out.println(message : arg); } }编译之后Object... args会被当成Object[] args处理。所以一个方法声明为void a(String... s)调用时可以穿零个、多个参数或者直接传一个String[]。这里有个小陷阱重载时固定参数方法的优先级高于可变参数。如果你同时定义了a(String s)和a(String... s)调用a(x)永远走的是固定参数那个。理解了可变参数和数组的关系面试遇到“可变参数和数组重载问题”就不会慌。5. 二维数组、引用数组与内存视图5.1 二维数组是“数组的数组”不是矩阵二维数组在Java里的本质是一维数组的数组。int[][] matrix new int[3][4];做的事是先创建一个长度为3的外层数组外层数组每个元素都是一个长度为4的int[]引用。所以matrix.length // 3外层数组的长度即行数 matrix[0].length // 4第一行那个内层数组的长度即列数也正因为这个结构Java支持“不规则数组”也叫锯齿数组。每一行的长度可以不一样int[][] jagged new int[3][]; jagged[0] new int[2]; jagged[1] new int[5]; jagged[2] new int[3];这在C语言里做不到因为C的二维数组是连续内存块。Java这种设计换来了灵活性也带来了更清晰的“引用”语义。遍历二维数组的标准姿势就是双层循环外层循环取行内层循环取列。注意matrix[0]可能为null如果创建时只指定了外层长度、内层还没逐个初始化必须先对每一行做判空。热搜词里的“指针数组”其实是C语言的概念元素本身是指针的数组比如char *argv[]。Java没有指针但“引用数组”要解决的问题是一样的——数组里存的是对象的引用可以粗略理解成地址通过这个引用能访问到堆里的实际对象。二维数组就是这种引用的典型体现外层数组元素指向内层数组对象而不是把一整块数据平铺开。5.2 数组作为方法参数引用传递还是值传递这是个经典面试题。Java的参数传递只有一个规则值传递。但数组是一种对象所以传数组时传的是“引用变量的值”也就是对象地址的拷贝。结果就是在方法里修改数组元素外面的数组会跟着变因为两个变量指向同一个数组对象。但如果方法里把参数重新指向一个新数组外面的引用不受影响。public void change(int[] arr) { arr[0] 99; // 外部数组第0个元素也会变成99 arr new int[3]; // 外部引用仍然指向原数组不受影响 }这个“修对象内容”和“改引用指向”的区分比单纯背“值传递/引用传递”结论重要得多。理解了它你就知道为什么很多工具方法在内部不修改入参数组而是返回一个新数组比如Arrays.copyOf。也正是因为数组是对象才有clone()、getClass()这些对象方法这也是它和C数组最大的区别之一。5.3 浅拷贝与深拷贝对象数组的危险共享数组拷贝方法包括clone()和Arrays.copyOf对引用类型数组都是浅拷贝。什么意思看这个例子StringBuilder[] sa {new StringBuilder(a)}; StringBuilder[] sb sa.clone(); sb[0].append(x); System.out.println(sa[0]); // 输出a x不对输出ax原数组被影响了clone()只复制了数组里存的引用复制后的sb[0]和sa[0]指向同一个StringBuilder对象。你通过sb[0]修改对象内容sa[0]自然能看到变化。对不可变对象比如String、Integer来说浅拷贝无所谓但对于可变对象这就是一个隐蔽的共享陷阱。二维数组的拷贝同样危险。int[][] copy matrix.clone();只是把外层数组复制了一份内层数组还是原来的那批。你改copy[0][0]matrix[0][0]也会变。想要真正的深拷贝必须手动逐个拷贝内层数组int[][] deepCopy new int[matrix.length][]; for (int i 0; i matrix.length; i) { deepCopy[i] matrix[i].clone(); }搜索“数组拷贝”相关问题时只要看到“二维数组”“对象数组”“浅拷贝”这几个关键词同时出现基本就是在考察这一点。记住一个口诀拷贝数组本身很容易但拷贝数组里的对象永远要用循环。热搜词里还有一条“树状数组维护前缀和sum(11)”类型的算法题那个场景里数组是基础存储结构但算法操作不走拷贝路线而是通过索引直接更新和查询这就更依赖于你对“数组底层是连续内存”的理解了。5.4 跨语言视角Java数组少了什么这里插入一个对比帮我看得更清楚。Java数组和C/C数组的差异简而言之有三条第一条Java数组自带长度信息arr.length随时可查C/C的数组退化成指针后长度就丢了只能自己额外记第二条Java数组有运行时越界检查访问arr[5]长度为3会抛ArrayIndexOutOfBoundsExceptionC/C则是未定义行为可能读到垃圾内存第三条Java不允许指针算术你不能用arr 1跳到下一个元素这让数组操作安全得多但也少了那种底层操控感。这背后其实是Java的安全设计理念宁可要高一点的开销越界检查也不要让程序员把内存搞崩。JIT编译器会优化掉多数不必要的检查所以在循环热点里Java数组的性能并不会有想象中那么差。理解了这些差异再看热搜词里的“C多维数组指针”——它和Java的引用数组是两个世界一个是纯内存地址运算一个是带类型的对象引用模型。6. 高频面试题与实战避坑清单6.1 经典题数组有没有length()方法new T[10]能编译吗这两道题几乎每次面试都会出现。第一题前面已经聊过数组没有length()方法只有length属性因为数组不是普通类定义出来的对象JVM直接创建了带length字段的数组类。第二题更深入一点为什么不能写new T[10]创建泛型数组核心原因是类型擦除。Java的泛型在运行时会被擦除成它的上界或ObjectJVM根本不知道T具体是谁。而数组和泛型有一个根本冲突数组是“运行时自省”的它在创建时就必须知道具体元素类型并且每次赋值都会做运行时类型检查。比如Object[] objs new String[3]; objs[0] 1;这行代码能编译但运行时立刻抛ArrayStoreException原因就是数组记住了自己“实际上是String[]”这件事。泛型构造时无法提供运行时具体类型所以被禁掉了。妥协方案就是SuppressWarnings(unchecked) T[] arr (T[]) new Object[10];这种写法在很多框架源码里都会出现但需要靠SuppressWarnings压制编译器的警告。理解“数组协变但集合不变”的差异是Java类型系统里很有价值的一课。6.2 数组与ArrayList为什么底层是数组还要封装另一道高频题数组和ArrayList有什么区别标准答案无非是数组长度固定ArrayList长度可动态增长数组可以存基本类型ArrayList只能存引用类型泛型不能是基本类型数组访问和写入更快ArrayList多了封装和方法调用开销数组是Java语言内置的ArrayList是集合框架里的类。但我想补充一个更深层的理解角度ArrayList本质上就是把数组的“扩容、越界检查、增删改细节”都封装好了。它底层维护一个Object[] elementData当元素数量达到数组容量上限时就执行oldCapacity (oldCapacity 1)的扩容即1.5倍然后Arrays.copyOf一把梭。你背下这句话很容易但我建议你顺手打开JDK源码看一眼grow方法和ensureCapacity方法比看十篇面试题解析都有用。理解了这层关系面试被问到“ArrayList扩容为什么是1.5倍”时你可以从“为什么不每次扩容1个位置性能太差”和“为什么不直接2倍内存浪费和碎片化风险”两个方向回答这就能体现出你是真的理解而不是背答案。6.3 手写冒泡排序与算法场景从排序到前缀和热搜词里“冒泡排序java”热度一直不低说明手写排序仍然是面试常见题。虽然工程上你不会用冒泡排序——Arrays.sort快得多——但手写它能考察你对循环边界、交换逻辑和优化意识的理解。一个完整可运行的版本public static void bubbleSort(int[] arr) { if (arr null || arr.length 1) { return; } 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 tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; // 这一轮没有发生交换说明数组已经有序 } } }这里有两个容易写错的地方内层循环的终止条件是arr.length - 1 - i因为每一轮都会把当前最大值冒到末尾下一轮就不用再管末尾的元素了优化点在于提前退出当某一轮完整扫描后一次交换都没发生说明数组已经有序再继续扫描纯属浪费。这个swapped标志就是初级版和进阶版的区分点很多面试官会专门看你会不会加这个优化。数组在算法题里的角色远不止排序。热搜词里提到了“树状数组维护长度 n 16 的序列。查询前缀和 sum(11) 与单点修改 add(3, x)”——这类数据结构的基础存储就是数组。但即便不做树状数组前缀和本身就值得掌握int[] nums {1, 2, 3, 4}; int[] pre new int[nums.length 1]; for (int i 0; i nums.length; i) { pre[i 1] pre[i] nums[i]; } // 求下标l到r的区间和pre[r 1] - pre[l]为什么前缀和数组长度要加1因为这样pre[i]恰好表示“前i个元素的和”求sum(l, r)时可以直接用pre[r1] - pre[l]避免了在边界条件上做大量判断。这个设计思路在很多用数组模拟区间场景的算法里都是标配。6.4 一组实战避坑规则最后我把自己写代码时反复踩过的坑整理成几条规则不管你是新手还是老手都建议在提交前过一遍访问数组前先判断数组本身是不是null尤其是从外部接口、文件、网络拿到的数组。很多人只检查下标却在arr.length上抛了NullPointerException。能用arr.length就不要用魔法数字。数组重构后长度可能变化写死的数字是最隐蔽的bug来源。遍历时如果需要同时用多个下标统一用普通for循环。增强for适合只读别在增强for里尝试写回。对象数组之间做拷贝时先确认被拷贝对象是否可变。如果是要评估是否需要深拷贝。不要把Arrays.asList的结果直接当成普通List传给需要增删的方法除非你非常清楚它不支持结构性修改。基本类型数组转List时记得用IntStream.of(arr).boxed()别用Arrays.asList(arr)否则你会得到一个“只包含一个数组对象的列表”。二维数组打印用Arrays.deepToString一维用Arrays.toString直接打印数组变量只会得到哈希串。数组作为返回类型时如果不想让外部随便改内部状态可以返回arr.clone()而不是直接返回原数组引用。这在设计不可变工具类时非常关键。如果让我给新人一个学习建议我不会让他们去死背API。数组这玩意儿核心就一句话它是连续内存上一段带有类型信息的对象。把这个模型焊死在脑子里再去看ArrayList的扩容、HashMap的位运算、队列栈的实现、前缀和的推导基本是一马平川。日常开发里数组可能不像集合那么出镜但它是所有数据结构的底座。我文中所有这些例子都在实际项目和面试题里出现过照着敲一遍比看十遍总结有用得多。
阅读完成 · 觉得有帮助?