资讯详情

资讯详情

Java基础语法(二):分支循环数组方法的边界陷阱与工程实践

1. 先把“基础语法(二)”的边界划清楚很多人学 Java 的时候会有个错觉变量、运算符、数据类型学完后面分支循环数组方法这些“看一眼就懂”于是直接跳到集合和面向对象去了。等到写业务代码的时候switch忘了写break导致穿透for循环里改了循环变量结果跑飞数组拷贝用了结果两个变量指向同一块内存方法传参以为能改掉外面那个变量结果没变——这类问题几乎每个新手都要踩一遍。这篇 Java 基础语法(二)就是专门讲这些“看着简单、用起来有坑”的东西分支结构、循环结构、数组、方法定义与调用以及围绕它们的一堆边界情况。我写这篇的目的不是复述教科书目录而是把我在带新人和做代码审查时反复见到的真实误区摊开讲清楚。每个语法点我会说明三件事它在 JVM 里到底怎么执行、为什么这么设计、实际写代码时哪些地方最容易翻车。读者对象很明确——已经能写出Hello World和简单变量声明正在往“能独立写出逻辑正确的代码”这个阶段走的人当然如果你已经工作两三年也可以拿这篇当一份自查清单看看自己有没有在某个细节上一直用错而不自知。之所以值得单独花篇幅讲这一批语法是因为它们构成了代码的“控制骨架”。一个类里写得最多的不是类声明而是if、for、方法调用线上事故里占比最高的也不是什么高深的并发问题而是边界判断少了一个等号、循环少跑一次、数组下标多了一。骨架歪了后面学再多框架也是歪的。2. 分支结构选哪种写法和怎么判断边界2.1 if-else 的边界写法与嵌套控制if的语法三分钟就能背下来但真正决定代码质量的是边界条件怎么写。最常见的两种错误一是把写成二是把“区间判断”写成了顺序颠倒的条件链。举个例子判断分数等级很多人会这么写int score 85; if (score 60) { System.out.println(及格); } else if (score 80) { System.out.println(良好); } else if (score 90) { System.out.println(优秀); }这段代码逻辑上是死的只要分数大于等于 60第一个分支就命中后面两个分支永远不会执行85 分会被打印成“及格”。正确的写法是从严格到宽松排列先判断 90再判断 80最后判断 60。这个坑我见过不止一次出现在生产代码里而且因为逻辑不报错、只是结果错测试覆盖不到就一直潜伏着。所以写条件链有个简单原则条件互斥优先不能互斥就按“最严格优先”排序。另一个高频问题是对null的处理。Java 里if (str.equals(abc))和if (abc.equals(str))在str为null时结果完全不同前者抛NullPointerException后者安全返回false。这个技巧叫“常量前置”是老一辈程序员传下来的习惯现在有了Objects.equals(a, b)可以更优雅地解决但常量前置依然值得保留在肌肉记忆里——它不依赖任何工具类一眼就能看出谁可能为 null。嵌套层级也是需要克制的。三层以上的if嵌套读起来非常痛苦解决办法是“提前返回”或者“卫语句”public String checkUser(User user) { if (user null) { return 用户不存在; } if (user.getStatus() ! 1) { return 用户状态异常; } if (!user.isVerified()) { return 用户未认证; } return 校验通过; }对比把这三段塞进嵌套if里的写法扁平化的版本可读性提升是数量级的。我的经验是只要一个方法的正常流程被缩进到了第三层就应该考虑抽方法或者改写结构了这跟具体业务无关纯粹是给人眼看的。注意if后面跟单条语句时省略大括号是合法的但强烈不建议。我处理过一次线上问题起因就是有人在省略大括号的if后面加了一行日志结果那行日志变成了无条件执行排查了两个小时。2.2 switch 的穿透机制与表达式写法switch的核心机制是“跳转表 顺序匹配”理解这一点才能理解它为什么快、以及为什么会穿透。传统switch支持的类型是有限的byte、short、char、int及其包装类加上枚举Java 5和StringJava 7。long、float、double都不支持这一点新手经常忘。String的switch底层并不是真的拿字符串做跳转表编译器会把它翻译成“先算hashCode定位、再用equals二次确认”的两段式逻辑。这个细节的实际意义在于如果你在case里写的是null值会在hashCode()调用时直接抛 NPE。所以对可能为null的字符串做分支必须先判空。穿透fall-through是switch最容易被误解的特性。它本来是 C 语言留下的设计允许“多个 case 共享一段逻辑”switch (day) { case 1: case 2: case 3: case 4: case 5: System.out.println(工作日); break; case 6: case 7: System.out.println(休息日); break; default: System.out.println(输入有误); }这种用法是合理的、也是推荐保留的。但如果是漏写break导致的意外穿透那就是 bug。判断标准很简单如果你是有意让两个 case 共用逻辑就正常写如果是忘了 break那就是事故。后者在代码审查里几乎必被指出我在团队里定的规矩是“每个 case 结束时必须有 break 或注释说明为何穿透”成本极低但效果很好。Java 14 之后switch表达式正式转正箭头写法彻底消除了穿透问题还支持返回值String type switch (day) { case 1, 2, 3, 4, 5 - 工作日; case 6, 7 - 休息日; default - { System.out.println(异常输入); yield 未知; } };这里yield是块内返回值的写法注意它不是returnreturn会直接结束整个方法。如果你的项目 JDK 版本在 14 以上新代码优先用表达式写法如果是 8 或者 11那就老老实实用传统写法并保证每个分支都有break。另外提一句default分支虽然语法上可选但涉及枚举和外部输入的场景一定要写因为别人后续往枚举里加值的时候没有default的switch会静默什么都不做这种“安静的 bug”最难查。3. 循环结构三种循环不是随便挑的3.1 for、while、do-while 的适用边界三种循环在功能上可以互相替代但适用场景差别很大。for适合“次数已知”或者“有明确迭代器”的场景因为初始化、条件、更新三要素写在头部一眼能看完整个生命周期while适合“次数未知、依赖运行时条件”的场景比如读取输入流直到结束do-while适合“至少执行一次”的场景最典型的是命令行菜单循环——先展示菜单用户操作完再判断是否继续。do-while是新手里存在感最低的一个但它在“重试”类场景里非常好用先发起一次请求失败了再看要不要重试。用while写就得把请求代码复制一份到循环外面用do-while就干净得多int retry 0; boolean success; do { success doRequest(); retry; if (!success retry 3) { System.out.println(第 retry 次失败准备重试); } } while (!success retry 3);注意这里循环条件必须同时判断success和重试次数只写!success就是死循环。这也是do-while要注意的点条件写在尾部容易被忽略读代码的人视线是往下的但逻辑判断在最后所以条件里包含的变量最好在循环体里有明显变化。3.2 循环变量的修改禁忌与标签跳转在for循环体里手动修改循环变量是个危险动作。下面这段代码看着没问题实际输出只有偶数for (int i 0; i 10; i) { if (i % 2 0) { i; // 想跳过下一个数结果把自己坑了 } System.out.println(i); }原因是i在循环体执行后还会再执行一次两次自增叠加跳过的数比预想的多。这类问题不会报错只会让结果对不上排查起来非常折磨。我的建议是循环变量只在for头部的第三段里修改循环体里想跳过元素就用continue想提前结束就用break。说到break嵌套循环里想跳出外层循环Java 提供了带标签的写法outer: for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (i * j 6) { System.out.println(找到: i i , j j); break outer; } } }标签就是一个合法的标识符加冒号break outer会直接跳出被标记的那层循环。这个语法被不少人认为“不优雅”但在二维数组查找、矩阵扫描这类场景里它比设置一个 flag 变量再层层判断要清晰得多。同理还有continue outer用来跳过外层循环的当前轮次。用不用看团队习惯但至少要知道它存在别看到别人的代码里有标签就以为是语法错误。3.3 死循环排查与循环性能的几个直觉死循环是循环结构里最经典的故障。常见成因有四种循环变量压根没变、条件永远为真、浮点数精度导致条件永不满足、以及while(true)忘写出口。前两种好查第三种最隐蔽for (double d 0.0; d ! 1.0; d 0.1) { // 永远不会结束 }原因是0.1在二进制里无法精确表示累加十次得到的是0.9999999999999999而不是1.0!判断永远成立。这类写法必须改成用范围比较比如d 1.0或者干脆用整数循环再除以精度因子。关于循环性能有些说法被传得很广但需要澄清。比如“for循环里把list.size()提出来能提速”在早期 JDK 里确实有影响现在 JIT 早就优化掉了代码可读性优先不必为此折腾。真正值得注意的反而是循环内部的重复计算和方法调用尤其是那些带 IO 或者数据库访问的操作把它们提到循环外能省下数量级的时间。另一个实际经验是大循环里避免频繁字符串拼接String是不可变对象每次都会产生新对象几千次以上就该换StringBuilder了。4. 数组最朴素也最容易出错的数据结构4.1 声明初始化的三种写法和默认值规则Java 数组的声明有三种写法int[] arr、int arr[]、int[] arr new int[5]。第一种是推荐写法因为类型信息集中在前面int[]读作“整型数组”更符合直觉第二种是 C 语言遗留风格能编译但不推荐第三种是真正分配内存的写法。这里要区分“声明”和“初始化”int[] arr;只是在栈上放了一个引用此时访问arr.length会直接编译报错局部变量未初始化而arr[0]更是无从谈起。数组创建后元素会有默认值这个规则必须记牢int、short、byte、long为0float、double为0.0char为\u0000空字符不是空格boolean为false所有引用类型为null。这跟局部变量必须显式赋值才能使用的规则形成对比——数组元素有默认值局部变量没有很多新手在这里混淆写int[] a new int[3]; if (a[0] 0)觉得理所当然却不知道这个0是 JVM 帮忙填的。数组的内存模型也值得说清楚数组对象本身在堆上包含一块连续的内存区域和记录长度的头部栈上的变量只是一个指向堆的引用。所以int[] b a;这个赋值操作复制的是引用不是内容。修改b[0]会同步反映到a[0]这一点和基本类型变量赋值完全不同也是数组相关 bug 的第一大来源。4.2 越界、拷贝与工具类的正确用法ArrayIndexOutOfBoundsException是新手最常见的运行时异常合法下标范围是0到length - 1。写循环的时候用而不是是经典错误int[] arr {1, 2, 3}; for (int i 0; i arr.length; i) { // 错会越界 System.out.println(arr[i]); }这个错误的隐蔽之处在于如果数组元素刚好访问完没报错你可能以为代码是对的换个长度就炸了。我的习惯是写循环时先确认一遍“长度和下标的关系”必要的话用增强forfor (int value : arr) { System.out.println(value); }增强for没有下标自然不存在越界问题也不需要关心长度。它的局限是不能在遍历中修改元素修改的是副本以及拿不到索引需要索引时还是得用传统for。数组拷贝的三种方式区别很大方式是否新对象元素是否复制适用场景b a否否共享同一数组只是想要个别名慎用System.arraycopy是目标需预先创建浅拷贝性能要求高、需要指定区间Arrays.copyOf是浅拷贝常用可扩容截断clone()是浅拷贝简单场景int[] src {1, 2, 3, 4, 5}; int[] dst new int[5]; System.arraycopy(src, 1, dst, 0, 3); // 从 src[1] 开始拷 3 个到 dst[0] System.out.println(Arrays.toString(dst)); // [2, 3, 4, 0, 0] int[] copy Arrays.copyOf(src, 3); // 只拷前 3 个长度变 3 System.out.println(Arrays.toString(copy)); // [1, 2, 3]System.arraycopy的参数有五个顺序是“源数组、源起始下标、目标数组、目标起始下标、拷贝长度”记不住的时候记住“源在前、目标在后、最后是长度”。它是 native 方法性能很好但要求做边界检查越界会抛IndexOutOfBoundsException而不是静默截断。还有一个必须强调的点这些拷贝都是浅拷贝。对于int[]这种基本类型数组没区别但对于String[]或者对象数组复制的是引用两个数组里的元素指向同一批对象。想深拷贝得自己遍历或者用序列化这是另一个话题了。4.3 二维数组与不规则数组Java 的二维数组本质是“数组的数组”不是矩阵。声明int[][] matrix new int[3][4]会创建 3 个长度 4 的一维数组内存上这些一维数组不保证连续。这个模型带来的直接好处是可以创建不规则数组int[][] triangle new int[4][]; for (int i 0; i triangle.length; i) { triangle[i] new int[i 1]; for (int j 0; j triangle[i].length; j) { triangle[i][j] i j; } }注意new int[4][]只创建了外层数组内层还是null直接访问triangle[0][0]会 NPE。遍历二维数组时外层的length是行数每行的length可能不同所以内层循环条件必须写triangle[i].length而不是triangle[0].length。Arrays.deepToString()是打印二维数组的正确方式Arrays.toString()只会打印出一堆类似[I1b6d3586的地址字符串。这个细节在调试时非常影响效率我见过有人盯着地址串看了半天以为数据没填进去。注意数组的长度是length属性没有括号字符串是length()方法集合是size()方法。这三者混用是新手期最常见的编译错误之一。5. 方法从“能跑起来”到“可维护”5.1 方法签名与值传递的真实含义方法是 Java 里组织逻辑的基本单元签名由“方法名 参数类型列表”决定与返回值类型无关。所以int foo(int a)和String foo(int a)不能共存编译器会报“方法重复定义”。这个规则解释了为什么重载不能靠返回值区分。参数传递是 Java 里被误解最深的机制之一。准确的说法是Java 只有值传递。对于基本类型传的是值的副本方法内修改不影响外部对于引用类型传的是引用的副本方法内可以通过这个引用修改对象内部状态但让引用指向新对象不会影响外部的引用。用一个图景来理解引用是一把钥匙传参是把钥匙复制一把给对方对方能用这把钥匙开门改屋里的东西但如果对方把手里这把钥匙换成另一把你手里那把并不会变。public static void modify(int[] arr) { arr[0] 99; // 修改对象内容外部可见 arr new int[]{7, 8}; // 重新赋值引用外部不可见 } public static void main(String[] args) { int[] data {1, 2, 3}; modify(data); System.out.println(data[0]); // 99不是 7 }理解了这个很多“为什么我方法里改了半天外面没变”的困惑就消失了。反过来如果你确实需要方法返回新的数组就老老实实通过返回值传出去别指望修改参数。5.2 重载匹配规则与可变参数的坑重载解析有一套优先级先找精确匹配再找自动类型提升int→long→float→double然后是装箱拆箱最后才是可变参数。这套规则决定了下面这段代码的输出public static void print(int x) { System.out.println(int); } public static void print(long x) { System.out.println(long); } public static void print(Integer x) { System.out.println(Integer); } public static void main(String[] args) { print(1); // int精确匹配 print(1L); // long print(Integer.valueOf(1)); // Integer }变化一下如果只有print(long)和print(Integer)两个重载传入int会选谁答案是long因为“自动类型提升”的优先级高于“装箱”。这个规则不算常用但遇到莫名的方法选择问题时知道去哪儿查能省不少时间。可变参数String... args本质是一个数组参数它在编译期被转换成String[]。两个注意点一是可变参数必须是参数列表的最后一项二是它可以接收null或者直接传数组这时候要小心args null的判断否则遍历时直接 NPE。还有一个容易忽略的地方——可变参数方法重载时编译器可能产生歧义比如同时定义foo(int...)和foo(Object...)调用foo()就会报“引用不明确”。这类问题不建议硬绕直接改名最省事。5.3 递归的边界与栈溢出预防递归在基础语法阶段就要理解到位因为后面学树、图、分治算法全靠它。递归三要素终止条件、递归调用、状态收敛。写递归最常见的问题是终止条件写错或者根本忘了写直接StackOverflowError。public static int factorial(int n) { if (n 0) { throw new IllegalArgumentException(n 不能为负数); } if (n 1) { return 1; } return n * factorial(n - 1); }这里n 1而不是n 1因为n 0时 0 的阶乘是 1同时也顺手挡住了负数虽然前面已经做了参数校验。这种“多重防线”在递归里是值得的因为一旦漏掉边界故障表现是栈溢出堆栈信息几十层定位成本很高。递归的深度受栈空间限制默认线程栈大小在 512KB 到 1MB 之间每层递归占用几十到上百字节实际能撑住的深度通常在几千到一万层。这个量级做普通树遍历够用但如果输入数据可能是十万级链表递归就会爆栈必须改写成迭代加显式栈。还有一个容易被忽视的点递归写成尾递归也救不了 JavaJava 不支持尾调用优化别指望编译器帮你把递归转成循环。6. 这些坑我踩过常见问题与排查速查表把上面这些内容里最容易出问题的部分整理成表遇到问题可以先在这里对一遍现象大概率原因排查动作只打印了第一个分支条件链顺序错误宽松条件在前按“最严格优先”重排代码块原本该跳过的逻辑被执行了switch漏写break检查每个 case 末尾String switch报 NPE传入nullhashCode调用失败switch 前判空循环结果比预期少一轮循环体里手动改了循环变量只用continue或break循环永远不结束浮点比较用!或变量未更新改范围比较、检查更新逻辑方法里改了数组外面没变方法内new了新数组并赋值给参数通过返回值传出Arrays.toString打印出地址对二维数组用了单层打印换Arrays.deepToString遍历数组抛越界循环条件写成 length改为或用增强 for两个数组互相影响赋值只是引用复制未拷贝内容用Arrays.copyOf调用方法找不到参数类型不匹配重载规则显式强转或按签名对齐递归直接栈溢出终止条件缺失或未收敛打印每层参数确认收敛性再补充几个表格里塞不进去的经验。第一个是关于char的char c a 1;是合法的结果是b因为char参与运算时会提升为int赋值时再窄化回去但char c a 1L;就编译不过因为结果是long窄化转换需要强转。这个规则在处理字符编解码时经常遇到。第二个是关于整数除法的5 / 2结果是2不是2.5而且-5 / 2结果是-2是向零取整而不是向下取整。涉及金额计算时任何时候都不要用double或float用BigDecimal并配合setScale指定精度这是金融类需求的硬规矩。第三个是关于取中间值的溢出问题。二分查找里写int mid (low high) / 2;当low和high都接近Integer.MAX_VALUE时会整数溢出变成负数。正确写法有两种int mid low (high - low) / 2;或者int mid (low high) 1;后者利用无符号右移把溢出位当成正常位处理。这个 bug 在 JDK 的Arrays.binarySearch里真实存在过很多年知道的人不多但确实值得记住。7. 写完这些语法之后我给新人的几条建议语法层面的内容到这儿基本闭环了。分支、循环、数组、方法这四样东西组合起来已经能写出完整的算法逻辑也能应付绝大多数课后练习和面试手撕题。在这个阶段我的建议是不急着上框架先把这几类问题的“肌肉记忆”练出来。第一是多写不带自动补全的代码。IDE 很聪明会自动补全break、会提示数组长度、会在你写了错误重载时报黄但它也会掩盖你对语法的真实掌握程度。我带的几个新人里凡是能在记事本里手写完整循环和方法的后面出语法 bug 的概率明显低很多。第二是养成“写下标先想边界”的习惯。for循环条件、数组访问、字符串截取凡是涉及下标的地方先在心里过一遍“最小是多少、最大是多少、空的情况怎么办”。这个习惯养成之后越界类异常基本可以杜绝。第三是别怕写重复的代码来验证理解。比如不确定参数传递的行为就写个二十行的main方法跑一遍看到输出比看十遍文章都管用。Java 的语法规则不算多但每一条的边界情况都值得亲手验证一次。第四是读源码的时候留意基础语法的用法。JDK 里Arrays.sort、Objects.equals、String.valueOf这些方法内部大量使用我们今天讨论的东西读起来不费力但对“什么叫规范写法”的感受会越来越清晰。这种感受没法速成只能靠积累。最后说一句关于“基础”的看法。我在实际项目里见过太多人框架玩得飞起但一个简单的循环逻辑写出来要多跑一轮或者数组边界判断错位导致数据错位一位。这类问题不会写在简历上但会体现在每一次代码审查的意见里。基础语法扎实不扎实平时看不出来关键时刻全暴露。今天聊的这些东西代码不超过几百行但真正在业务里做到不出错需要的是反复练和反复踩坑之后的警觉。8. 数组与方法的组合实战一个小工具类的完整实现光讲语法点难免碎片化我把前面那些零散的知识串到一个具体的小场景里写一个数组工具类实现“查找最大值、区间求和、数组反转、二分查找”四个功能。别看功能简单每个都藏着前面提到的坑正好当一次综合演练。8.1 需求拆解与接口设计先明确接口。我打算提供四个静态方法都放在一个工具类里外部直接通过类名调用不需要实例化。设计上遵循几个原则参数校验放在方法入口、返回值语义明确、不修改传入的原始数组除非方法名明确表示会修改。public class ArrayTool { public static int max(int[] arr) { ... } public static int sumRange(int[] arr, int from, int to) { ... } public static void reverse(int[] arr) { ... } public static int binarySearch(int[] sortedArr, int target) { ... } }为什么用静态方法而不是普通方法因为这类工具方法没有状态全靠参数驱动做成静态省去了实例化的开销调用方也更直观。这也是 JDK 里Arrays、Collections这些工具类的通用做法。不过要注意静态方法没法被多态覆盖如果你的场景里需要子类替换行为那就得改成实例方法或者接口默认方法。参数校验这一块我倾向于在方法入口直接抛IllegalArgumentException而不是返回一个特殊值比如-1或者0表示失败。原因是特殊返回值容易和正常结果混淆比如求和结果是0到底是数组为空还是数据本身就是 0调用方根本分不清。抛异常虽然显得“重”但语义明确能强制调用方处理异常情况。当然如果是对性能极其敏感的路径也可以用assert或者干脆约定由调用方保证参数合法这属于设计取舍没有绝对对错。8.2 四个方法的实现细节与选型理由先看max。最直觉的写法是排序后取最后一个但排序是O(n log n)而找最大值只需要遍历一遍O(n)就够了。这种“不用排序也能解决”的判断是写代码时该有的基本意识public static int max(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; }初始值取arr[0]而不是0这个细节很关键。如果数组全是负数初始值设成0就会返回错误结果。这是我在代码审查里纠正过最多的一个点尤其是从其他语言转过来的人容易顺手写int max 0;。循环从i 1开始因为第 0 个元素已经作为初始值了少一次比较。再看sumRange区间求和坑在于“闭区间还是半开区间”以及边界校验。我选择闭区间[from, to]因为业务代码里调用方更习惯说“从第 3 个到第 5 个”包含两端更符合直觉public static int sumRange(int[] arr, int from, int to) { if (arr null) { throw new IllegalArgumentException(数组不能为空); } if (from 0 || to arr.length || from to) { throw new IllegalArgumentException(区间非法: [ from , to ]); } int sum 0; for (int i from; i to; i) { sum arr[i]; } return sum; }这里from to的判断容易被省略但如果不判断循环一次都不执行返回0调用方会误以为区间内元素之和真的是 0。抛出异常虽然有成本但比静默返回错误结果要好得多。另外注意累加用int可能溢出如果是金额或者长度较大的场景应该改用long这一点在接口设计时就要想清楚。reverse是唯一一个会修改原数组的方法所以命名上不加get前缀明确告诉调用方“我有副作用”。实现用双指针从两端往中间交换时间复杂度O(n)空间O(1)public static void reverse(int[] arr) { if (arr null || arr.length 1) { return; } int left 0; int right arr.length - 1; while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } }循环条件必须写left right而不是left right。如果写等号长度为奇数时中间那个元素会被自己和自己交换一次结果虽然正确但多做了一次无用功更麻烦的是某些场景下会出问题。这个边界我在早期的笔试里错过一次后来就记住了。binarySearch是四个方法里坑最多的因为它要求输入有序还涉及中间值计算。写成返回下标找不到返回-1public static int binarySearch(int[] sortedArr, int target) { if (sortedArr null) { throw new IllegalArgumentException(数组不能为空); } int low 0; int high sortedArr.length - 1; while (low high) { int mid low (high - low) / 2; if (sortedArr[mid] target) { return mid; } else if (sortedArr[mid] target) { low mid 1; } else { high mid - 1; } } return -1; }三个地方值得单独说。第一high初始值是length - 1配合while (low high)使用这是闭区间写法如果high初始值写length循环条件就得改成low high两种写法不能混。第二中间值用low (high - low) / 2而不是(low high) / 2防止两个大数相加溢出前面提到过。第三low mid 1和high mid - 1里的加减一不能少少了就是死循环因为mid可能一直等于low或者high。8.3 边界测试清单与踩坑复盘方法写完不等于完工得有一份边界测试清单跑一遍。我习惯按“空、单元素、两元素、有重复、全相同、找不到”这几个维度设计用例覆盖住了基本就能放心用例类型输入预期空数组new int[0]抛IllegalArgumentException单元素[5]max 返回 5reverse 后仍是[5]全负数[-3, -7, -1]max 返回 -1含重复[2, 3, 3, 5]二分查找返回任意一个 3 的下标首尾元素查找第一个和最后一个都能命中不存在查找比最小值还小的值返回 -1区间越界sumRange(arr, -1, 2)抛IllegalArgumentException这套用例看着基础但我用它在实际项目里抓到过不少问题。有一次是sumRange在from to时返回了 0原因是我写成了for (int i from; i to; i)少了个等号。还有一次是reverse传入空数组时抛了异常因为我在空判断前就访问了arr.length顺序放反了。这类问题单看代码不容易发现跑一遍用例立刻现形。我个人在实际操作中的体会是这几个小方法写十遍不如带着“边界清单”写一遍。写的时候每敲完一个循环条件就停下来问一句“这个等号该不该有”比事后调试半小时划算得多。工具类的价值不在于功能多复杂而在于它在各种奇怪输入下都能给出明确、可预期的行为这才是它能被放心复用的前提。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →