
做Java开发的如果你去翻那些基础面试题数组和字符串一定是绕不开的两块硬骨头。数组要考声明、拷贝、排序、工具类字符串要考不可变性、常量池、比较、拼接稍微问深一点就能看出一个人的基本功。这篇是“JAVA重点基础、进阶知识及易错点总结”的第二篇把数组和字符串这两大高频考点一次性讲透既适合刚接触Java基础的新手巩固语法也适合准备java面试题的开发者拿来查漏补缺。写这篇文章之前我把日常开发里踩过的坑、Code Review里见过的低级错误、以及面试中被反复追问的知识点都汇总了一遍尽量做到每一条都有代码、有原因、有解法。你会发现很多看似简单的写法背后其实牵扯到JVM内存布局和JDK源码设计把这些底层逻辑搞明白写起业务代码会顺手很多。1. 数组从声明到工具类的完整记忆链数组在Java里属于最基本的数据结构很多新手觉得简单但实际用起来坑不少尤其是内存布局、拷贝方式和工具类的正确使用这几个点。面试官从一维问到二维再问到扩容基本就能摸清你的底子。1.1 三种声明方式的底层差异Java数组的声明有几种常见写法很多人觉得都一样实际上是有区别的。直接看代码int[] a new int[3]; // 先分配空间默认值是0 int[] b {1, 2, 3}; // 静态初始化编译期确定内容 int[] c new int[]{1, 2, 3}; // 动态初始化加赋值第二种和第三种几乎等价区别在于第二种只能紧跟在声明语句中使用不能分两步写例如先写int[] b;再用b {1,2,3};是编译不过的。第一种方式下数组元素会有默认值基本类型int是0double是0.0boolean是false引用类型是null这个必须记牢因为很多题目或业务代码的初始化都依赖默认值。还有一个高频混淆点Java数组的length是属性不是方法字符串的length()是方法。一个用arr.length一个用str.length()写反了直接编译报错。面试手写代码时这个细节经常被盯上千万别在这种地方翻车。1.2 数组是引用类型浅拷贝与深拷贝要分清数组在Java里是对象存放在堆内存变量名只是引用。当你执行下面这段代码int[] src {1, 2, 3}; int[] dest src; dest[0] 100;此时src[0]也变成了100因为两个引用指向同一个数组对象这本质上不是拷贝而是引用共享。想真正复制一份独立的数组就需要用到拷贝方法。常见的数组拷贝方式有四种适用场景差异不小拷贝方式写法特点适用场景循环手写for逐元素赋值灵活、可控可以同步做转换需要同时处理元素内容时System.arraycopySystem.arraycopy(src,0,dest,0,3)本地方法性能最高大批量拷贝Arrays.copyOfArrays.copyOf(src,3)简洁自动扩容数组扩容clone()src.clone()最简单的浅拷贝快速复制这里最容易踩的坑是clone()和Arrays.copyOf对基本类型数组来说确实是深拷贝基本类型的值直接复制但对引用类型数组它们依然只是浅拷贝两个数组中的元素还是指向同一个对象。如果你要对一个对象数组做真正的深拷贝只复制数组本身不够还需要把每个元素也复制一遍。我最早是在写一个简单的对象缓存工具时踩进去的调试了半天发现数组内容被连带修改最终才意识到是浅拷贝问题从那以后碰到实体对象数组都改用循环逐元素复制。1.3 Arrays工具类的正确打开方式java.util.Arrays是被低估的工具类很多数组操作都可以通过它一行搞定。我把最常用的几个方法列出来并说明适合的场景Arrays.sort(arr)底层是双轴快排对基本类型和对象类型都能用默认升序。对象排序时可以传入Comparator但要注意基本类型数组不能直接传Comparator必须先把int装箱成Integer数组。Arrays.binarySearch(arr, key)前提是数组必须已经有序否则结果不可靠这个前提很容易被忽略。Arrays.fill(arr, val)把所有元素填充为指定值初始化二维数组时非常常用但要先确认每行数组已经创建。Arrays.equals(arr1, arr2)比较两个数组内容是否一致。注意数组本身继承的equals比较的是引用地址调arr1.equals(arr2)永远是false必须用Arrays.equals。Arrays.toString(arr)把一维数组转成[1, 2, 3]这种格式的字符串。二维数组别用它要改用Arrays.deepToString否则打印出来是一堆对象地址。说到Arrays.toString经常有人问数组转字符串到底怎么做。很多人习惯直接arr.toString()结果输出[I15db9742这种内码地址。正确处理要么用Arrays.toString(arr)要么自己循环拼接或者用String.join但int数组需要先转成字符串。我在写日志输出时70%的场景用Arrays.toString就够了只有需要自定义格式时才手写循环。1.4 二维数组的本质数组的数组二维数组在Java里并不是一个连续矩阵本质上是“数组的数组”。比如int[3][4]可以理解为一个长度为3的一维数组每个元素又是一个长度为4的一维数组。这意味着你可以构造不规则二维数组int[][] arr new int[3][]; arr[0] new int[1]; arr[1] new int[2]; arr[2] new int[3];这种“锯齿数组”在表示稀疏数据或者按行存储不同长度数据时很实用。不过需要注意初始化二维数组时如果写了new int[3][4]所有整数元素默认是0不要再对整个二维数组调用Arrays.fill这样会因为你传的是二维数组的引用而直接出问题。正确做法是遍历每一行对arr[i]使用Arrays.fill(arr[i], 0)或者干脆在声明时就初始化好每一行。在C语言里“指针数组存放字符串”通常对应char*[]在Java里对应的就是String[]。这个类比我经常跟新人讲Java虽然去掉了显式指针语法但数组引用本质上就是指向堆内存对象的指针。你把数组传给方法时方法内部修改数组元素会直接影响原数组所以在写工具函数时如果不想外部数据被改动要考虑防御性拷贝。2. 字符串不可变性、常量池与比较字符串在Java业务代码里几乎无处不在但平时用得多不代表理解得深。面试官特别爱追问String的底层设计比如为什么不可变、常量池怎么工作、new String(abc)创建了几个对象。这些不是死记硬背能应付的搞懂原理才是关键。2.1 String的不可变性是设计出来的String类在JDK中用final修饰核心存储private final char[] valueJDK9之后改成了byte数组加编码标记所以String对象一旦创建就不能被修改。你调用的substring、concat、replace等方法并不会改变原字符串而是返回一个新字符串对象。很多人误以为String s abc; s s d;是在修改原字符串实际上只是把引用重新指向了新的对象。为什么要设计成不可变核心理由有三个缓存字符串内容可以被常量池缓存同一个字面量可以复用同一个对象极大减少内存占用。比如hello在程序里出现100次也只需要一份对象。安全String常被用作文件名、路径、类名、网络连接参数如果内容可变恶意代码修改引用对象的内容会带来严重安全隐患。线程安全不可变对象天然线程安全多个线程共享同一个字符串引用不需要任何同步开销只要保证引用本身不发生并发修改。面试时把这三层讲出来基本就能证明你不只是背了“String是不可变的”这句话。2.2 常量池、new与几个对象的问题字符串常量池是高频考点我用最简洁的方式说清楚直接写String s abc;时JVM会先去常量池查找有没有abc有就直接返回地址没有就在常量池创建所以这种方式最多创建1个对象。写String s new String(abc);时abc本身会在常量池创建1个对象如果还没有new在堆中再创建1个对象所以一共创建了2个对象。这里的s指向堆中的对象常量池中的abc是另一个独立对象。调用s.intern()时如果常量池没有相同内容的字符串就把内容复制到常量池并返回池中的引用如果已有直接返回已有引用。这个机制直接引出一个经典题目String s1 ab; String s2 a b;这两个引用相等吗答案是相等的因为编译期常量折叠会把a b直接折叠成ab所以s1和s2都指向常量池同一个对象。而String s3 new String(ab);和 s1 用比较就是false因为s3指向堆里的对象s1指向常量池里的对象用equals比较就是true因为equals比的是内容。再看一个真实陷阱拼接变量时String a a; String b b; String c a b;这里a b不能编译期折叠底层实际上是通过StringBuilder拼接后再toString()生成新对象所以c ab为false。很多人觉得这题简单但能准确说出底层原理的人其实没那么多。2.3 判断字符串相等为什么必须用equals这是一个问烂的题目但真正踩过坑的人才会理解为什么必须用equals。举一个业务场景接口里经常要判断请求参数是否等于某个固定值String flag getFlag(); // 可能在别处是 new String(Y) if (flag Y) { }早期如果getFlag()返回的是字面量有可能返回true一切看起来很正常。但某天数据从数据库或者远程接口读取或者中间用new String(Y)包装过就返回false了业务逻辑悄悄失效。这种Bug很难在测试阶段发现等到出问题排查成本很高。所以判断字符串相等统一用equals忽略大小写用equalsIgnoreCase。如果确定一方是字面量常量建议写成Y.equals(flag)这样还能规避flag为null时的空指针。再补充一点String.equals的实现是先比较地址地址相同直接返回true否则逐字符比较内容所以即使两个对象地址不同只要内容相同equals也可靠。这也是为什么面试题里都默认用equals做字符串内容比较。2.4 StringBuilder与StringBuffer的选择当你需要在循环里拼接字符串时如果直接用String result ; for (int i 0; i 10000; i) { result i; }这段代码每次循环都会创建一个新的StringBuilder对象并调用toString()实际生成了上万个中间对象既慢又费内存。我在一个日志聚合场景里实测过用拼接一万次大约比用StringBuilder慢接近两个数量级GC压力明显增加。所以有大量拼接需求时优先用StringBuilder。StringBuilder和StringBuffer的区别很多人背过答案StringBuffer的方法加了synchronized线程安全但性能低StringBuilder不加锁性能高但多线程下不安全。日常开发99%是单线程操作直接用StringBuilder就好。顺便提一句编译器在方法内部对字符串做拼接时确实会优化成StringBuilder但这是局部优化。循环拼接这个常见场景不会被自动优化所以该手动用StringBuilder时别偷懒。2.5 字符串常用方法的几个细节字符串的方法不少有几个细节新手很容易忽略split(String regex)的参数是正则表达式不是直接字符。比如用点号切分192.168.1.1直接split(.)会返回空白数组因为.在正则里表示任意字符。正确写法是split(\\.)竖线、星号、空格同理。replace和replaceAll的区别replace的入参是CharSequence不做正则解析replaceAll的第一个参数是正则。想替换某个符号时用错replaceAll会遇到意外结果。substring是左闭右开区间hello.substring(0, 2)返回he不是hel。边界搞错会抛出IndexOutOfBoundsException或者多一个字母。indexOf找不到时返回-1很多人忘记判断-1就直接拿返回值去截取然后踩异常。getBytes()默认使用平台字符集跨环境时可能不一致。做网络传输或文件写入最好显式指定字符集比如getBytes(StandardCharsets.UTF_8)。判断字符串是否全由字母和数字组成可以用matches([a-zA-Z0-9])更高效的做法是用循环加Character.isLetterOrDigit逐个判断。枚举转字符串时可以直接调用name()或者重写toString()。如果日志要输出更多语义化信息重写toString更合适。Java没有JavaScript那种原生模板字符串但String.format(%s-%d, name, age)或MessageFormat.format可以实现格式化拼接构造SQL和日志时很常用。3. 高频易错点与性能陷阱这一节把我自己踩过的坑和从同事代码里看到的典型问题整理出来按照出现频率排个序。这些内容不论面试还是工作都能直接当备忘录用。3.1 数组下标越界与length使用数组下标越界是所有语言初学者绕不开的问题Java也不例外。实际工作中最常见的场景是循环条件写成i arr.length或者从1开始遍历导致最后一次访问越界。Java对数组越界有强约束一旦越界直接抛出ArrayIndexOutOfBoundsException不会像C语言那样允许你读到越界内存这其实是好事但习惯上还是要小心。另一个容易混淆的细节在main方法里运行时不带参数时args的长度是0而不是空指针所以判断应该是args.length 0而不应该判断args null。这个细节在处理命令行工具参数时经常出错。3.2 数组内联初始化与对象数组的默认值前面提过数组声明时如果只指定长度引用类型数组的元素默认是null。最容易翻车的场景是创建实体对象数组后忘了填充就调用元素方法User[] users new User[10]; users[0].getName(); // 空指针异常正确的做法是逐个new User()填充或者干脆用ListUser用ArrayList动态添加元素要更贴近多数业务场景。热词里有“数组增加”这其实就是数组长度固定的痛点。Java数组没有直接增加元素的方法想新增元素只有两种思路一是先扩容再赋值比如Arrays.copyOf(arr, arr.length 1)后在末尾放新值二是直接用List的add方法需要时再转回数组。我的建议是除非性能要求极高否则业务代码多用List数组更适合固定长度、结构简单的数据。3.3 数组转字符串的几个姿势数组转字符串在日志、接口对接、数据处理中太常见了我把常用姿势整理成表格需求推荐方案快速打印一维数组内容Arrays.toString(arr)打印多维数组内容Arrays.deepToString(arr)拼接成自定义分隔符字符串String.join(,, arrStr)int数组需先转String转成JSON格式用Jackson或Gson序列化只转部分元素循环或Stream处理字符串与数组互转abc,def.split(,)得到数组String.join(,, list)拼回这里有个实用细节String.join需要传入字符序列数组int数组不能直接使用。要把int[]拼成1,2,3这种格式用Stream很方便int[] arr {1, 2, 3}; String result Arrays.stream(arr) .mapToObj(String::valueOf) .collect(Collectors.joining(,));再补充几个转换写法char[]转字符串用new String(chars)byte[]转字符串用new String(bytes, StandardCharsets.UTF_8)字符串转char数组用str.toCharArray()转byte数组用str.getBytes(StandardCharsets.UTF_8)。3.4 字符串拼接的性能对比字符串拼接的性能问题在社区里讨论很多我这里把实测结论整理成表格拼接方式一万次耗时相对适用场景String 循环极慢创建大量对象不推荐String.concat中等也有对象开销少量拼接StringBuilder.append最快循环或复杂拼接首选StringBuffer.append略慢于StringBuilder多线程环境String.format较慢但可读性好日志或模板生成很多人写日志或SQL时图省事用拼接小规模没问题一旦放进循环或高频调用就会拖慢程序。举个例子构建一个上万行的批量SQL参数用会造成明显的GC压力改用StringBuilder后基本没有额外对象生成实测更稳。面试时能说出实测数据或的底层编译行为这题基本稳了。3.5 字符数组、字节数组与字符串互转的坑字符数组和字符串互转虽然简单但容易踩坑。比如String s abc; char[] chars s.toCharArray(); chars[0] x; System.out.println(s); // 输出 abc不受影响原因很简单toCharArray()返回的是新拷贝的数组修改它不会影响原字符串。这个特性经常被忽略有人想通过修改char数组来改字符串结果发现没生效正确做法是转成char数组后修改再创建新字符串char[] chars s.toCharArray(); chars[0] x; String newS new String(chars);还有一个跨平台的坑String.getBytes()在不同平台默认字符集不同Windows上可能是GBKLinux下是UTF-8字节长度和解析结果都可能不一样。凡是涉及网络传输、文件读写、加解密务必显式指定UTF-8。4. 实战手写题从排序到同构串联核心知识点这一节挑几个典型题目把数组和字符串的知识点串起来代码都是面试时可以直接手写出来的级别。数组排序、反转、字符串回文、同构字符串这些都是高频手写题。4.1 冒泡排序与Arrays.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; } } }这里加了一个swapped标志位如果某一趟没有发生交换说明数组已经有序提前退出最好情况时间复杂度可以降到O(n)。面试手写排序时能写出这种优化版本就算过关。但实际工程里直接用Arrays.sort(arr)就好。JDK针对基本类型做了专门优化比手写排序稳得多也不容易出错。字符串排序同理Arrays.sort(strArr)默认按字典序升序如果按长度排序可以传Comparator.comparingInt(String::length)。中文排序要注意用Collator.getInstance(Locale.CHINA)才能按拼音排序这个细节业务中容易忽略。4.2 数组反转与字符串反转的三种实现数组反转是高频手写题双指针写法最简洁public static void reverse(int[] arr) { int left 0, right arr.length - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } }字符串反转要比数组绕一点因为String本身不可变。常用实现有三种用StringBuilder的reverse()方法new StringBuilder(str).reverse().toString()最简单直接。转成char数组用双指针交换再new String(chars)。递归从末尾拼接适合考察递归思想但性能不好。如果需求是按单词反转而不是按字符反转比如把I am java变成java am I思路是先split( )得到单词数组再逆序拼回。注意多个空格时处理好分隔符否则结果会不符合预期。这道题看起来简单实际细节点很多手写时考虑边界情况是加分项。4.3 判断字符串是否同构“是否同构”是一道很典型的题目。题目描述大致是给定两个字符串s和t判断它们是否同构。如果s中的字符可以按某种映射关系替换得到t那么两个字符串同构每个字符只能映射到唯一字符且映射必须一一对应。例如egg和add同构因为e - a、g - d是一对一映射。foo和bar不同构因为o同时映射到a和r产生冲突。经典解法是双Mappublic boolean isIsomorphic(String s, String t) { MapCharacter, Character s2t new HashMap(); MapCharacter, Character t2s new HashMap(); for (int i 0; i s.length(); i) { char cs s.charAt(i); char ct t.charAt(i); if (s2t.containsKey(cs) s2t.get(cs) ! ct) { return false; } if (t2s.containsKey(ct) t2s.get(ct) ! cs) { return false; } s2t.put(cs, ct); t2s.put(ct, cs); } return true; }为什么需要两个Map因为映射必须双向唯一。如果只用一个Map比如ab和cca - c、b - c单方向是成立的但两个不同字符映射到同一个字符时其实已经不符合同构条件了。所以必须同时检查反向映射。这道题综合考察了Map、字符遍历、双向映射三件事面试价值很高。如果字符串只包含小写字母还有一种更隐蔽的数组写法分别记录每个字符上次出现的位置如果两个字符上次出现位置不一致说明无法构成映射。这种优化写法不用Map但更考验理解建议面试时先写双Map正确性优先。4.4 数字转字符串的蓝桥杯套路热词里有“蓝桥杯 数字 题目”这类编程比赛里很多题目会把整数当成字符串处理比如回文数判断、大数相加、进制转换、数字重排等。一个很实用的转换套路是int num 12345; String str String.valueOf(num); int[] digits str.chars().map(c - c - 0).toArray();回文数判断可以一行搞定boolean isPalindrome(int x) { if (x 0) return false; String s Integer.toString(x); return s.equals(new StringBuilder(s).reverse().toString()); }大数字相加时如果直接用int会溢出用字符串逐位相加反而更安全。进制转换用Integer.toString(num, radix)一行完成比如转二进制就是Integer.toString(255, 2)但性能要求高的场景还是自己写循环更可控。这些题目的本质就是把数值与字符串之间的转换、字符数组操作、长度与下标边界组合起来正好把数组和字符串两大模块串在一起。建议新手先把这些基础题型刷透刷题时先手写一遍再对比工具类实现理解会深很多。5. 常见问题速查表与排查思路最后整理一份速查表相当于给自己和团队留一个备忘录。数组和字符串的坑说多不多说少不少有张表能省去很多排查时间。5.1 高频问题与解决方案问题现象根本原因解决方案数组打印输出[I1b6d3586误用了数组继承的toString()用Arrays.toString或deepToStringsplit(.)返回空数组.是正则元字符用split(\\.)replaceAll(\\d, )没效果正则写法或转义错误确认正则必要时改用replace比较字符串返回false比较的是引用地址用equals或equalsIgnoreCasearr[arr.length]越界下标范围是0到length-1遍历条件写i arr.length循环拼接字符串很慢每次循环创建新对象用StringBuilder.append二维数组默认赋值空指针每行数组未初始化先new int[n][m]再逐行填充indexOf返回-1后继续截取未判断-1先判断index 0中文字符串字节长度不一致默认字符集不同显式getBytes(StandardCharsets.UTF_8)这张表几乎覆盖了日常Code Review里最常见的几类问题。每次看到有人踩split(\\.)的坑我都建议他把正则基础再补一补这类细节虽然小但排查起来非常花时间。5.2 排查思路与调试技巧如果数组或字符串相关的Bug不好定位推荐三个排查手段第一善用断点。IDEA里对数组变量直接点开能看到每个元素的值对字符串可以切到“View Text”标签大字符串也能完整显示。很多数组内容不对的问题加一行System.out.println(Arrays.toString(arr))就能秒杀但断点查看更直观。第二按顺序排查字符串比较逻辑。先看是不是用了再看有没有可能为null然后看字符集和大小写问题。按这个顺序走下去绝大多数字符串问题都能定位。第三看字节码。如果你真的想彻底搞懂字符串拼接和常量池可以用javap -c 类名反编译class文件会看到ldc、new StringBuilder、invokespecial这些指令。当年我研究ab与变量拼接的差异时就是靠javap彻底看明白的。新版JDK的字符串拼接可能还会用到invokedynamic和makeConcatWithConstants指令比老教程里讲的更复杂但排查思路是一样的。5.3 面试中的几个延伸方向如果面试官继续深挖数组和字符串还会碰到下面这些方向提前准备会更有底气如何实现一个可变长数组和ArrayList相比有什么优势和劣势。核心是理解Arrays.copyOf扩容逻辑但ArrayList封装了迭代器、fail-fast机制等整体更成熟。字符串的hashCode为什么选31作为乘数。31是奇素数乘法可以被移位和减法优化分布也比较均匀这个细节答出来会让面试官眼前一亮。为什么String适合当Map的Key而不是StringBuilder。因为String不可变且hashCode会被缓存语义稳定StringBuilder可变hashCode会随内容变化拿它当Key会导致HashMap查不到数据。JDK8和JDK9之后String存储的变化。JDK8用char[]每个字符2字节JDK9之后用byte[]加编码标记Latin-1编码的纯英文字符串可以压缩一半空间。能说出这个区别说明你关注过新版本底层变化。我在带新人的时候也发现能把数组和字符串分开用得很熟的人不少但能把两者结合起来理解数组对象在堆上的布局、理解字符串常量池与堆对象关系的人就少一截。如果你能在写代码之余顺手跟踪一次内存变化理解会进步得非常快。6. 写在最后的一点个人体会数组和字符串表面上是最基础的内容实际上是Java一切数据结构和业务处理的地基。我见过不少工作三五年的同事框架用得很溜结果在字符串比较和数组拷贝上栽跟头原因就是平时没把基础当回事。我自己后来养成了两个习惯凡是循环拼接字符串先问自己一句为什么不用StringBuilder凡是拿到数组先问自己一句要不要防御性拷贝。这些微小习惯的长期收益非常大。如果你正在准备面试或者刷题建议不要只盯着高级框架花半天时间把数组和字符串的这些知识点全部手敲一遍尤其是用javap看一次字节码层面的常量池和拼接过程再回头读那些源码实现就会豁然开朗。下一篇我会整理集合框架里的高频易错点那些内容和数组、字符串的底层理解直接挂钩到时候你会有一种知识串起来的感觉。