Java基础语法避坑:分支、循环、数组与方法实战

发布时间:2026/10/31 6:19:52
Java基础语法避坑:分支、循环、数组与方法实战 评审新人代码时最能看出基础扎不扎实的地方往往不是用了什么框架、调了什么库而是流程控制写得清不清楚。有人一个方法里套了四层if每层再包一个for读起来像走迷宫有人写了个while循环循环体里顺手改了循环变量程序一跑直接卡死还有人对着一个数组arr[arr.length]取元素然后盯着ArrayIndexOutOfBoundsException发呆。这些问题追到根上全落在Java 基础语法里“让程序动起来”的那一块分支、循环、数组和方法。上一篇聊变量、数据类型、运算符那属于“认字”这一篇进入“造句”把一条条孤立的语句组织成会判断、能重复、可复用的逻辑单元。这篇内容面向的是刚过完语法第一关、准备动手写点小工具的人也适合写了两三年但一直靠 IDE 补全、没认真梳理过细节的开发者回头补课。下面这些内容不是语法手册的搬运而是我在带人、看代码、改 bug 的过程中反复撞见的真实问题为什么switch少写一个break会静悄悄出错为什么增强for改了元素却没生效为什么方法里改了数组、外面也跟着变了为什么明明逻辑没错编译器却报“可能尚未初始化”。每一个坑我都会给出复现路径、根因解释和可直接套用的写法看完能直接落到自己的代码里。1. 分支语句让代码从一条道走到黑变成有判断力程序默认是顺序执行的一行接一行往下走。真正让它“有脑子”的是分支。Java 里就两个关键词if和switch。听起来简单但这两个东西的使用密度太高了写歪一次后面维护的人就要多花半小时。1.1 if 的布尔条件与短路求值Java 的if条件必须是boolean类型这一点和 C/C 有本质区别。在 C 里写if (x 1)是合法语法赋值表达式的结果是 1非零即真在 Java 里这行代码连编译都过不去因为int不能隐式转换成boolean。这个设计当年被不少人吐槽“啰嗦”但实际写久了会发现它挡掉了大量手滑if (count 0)这种本想比较却写成赋值的错误在 Java 里根本活不到运行阶段。真正需要留意的是和||的短路特性。左边为false时右边不执行||左边为true时右边不执行。这个特性最实用的场景是把可能抛异常的判断放在前面// 安全写法先判空再取长度 if (name ! null name.length() 3) { System.out.println(名称合法); }顺序反过来写name.length() 3 name ! null当name为null时第一段就抛NullPointerException后面那个判空压根轮不到执行。我见过不止一次有人把判空写在后面然后困惑“我明明判空了怎么还空指针”。不是判断没写是它排在了雷区后面。另一个容易忽略的点是短路带来的副作用差异。如果右侧表达式带有副作用比如方法调用会改状态短路与否会直接影响结果。写业务代码时尽量别在条件里塞副作用条件就该是纯粹的判断这个约定能省掉很多“为什么这个方法有时候没被调用”的排查时间。1.2 嵌套 if 的降维写法卫语句分支本身没问题问题出在嵌套层数。三层以上的if嵌套阅读成本会指数级上升因为读代码的人要在脑子里同时维护好几个前提条件。我见过最夸张的一段是六层嵌套最里面那行return距离方法开头有四十多行中间还夹着两个for。处理办法是把“异常路径”提前踢出去业内叫卫语句guard clause。思路很简单不满足前置条件的直接return或抛异常让主流程始终保持在最外层缩进。对比一下两种写法// 嵌套写法 public String getLevel(int score) { if (score 0 score 100) { if (score 90) { return 优秀; } else { if (score 60) { return 及格; } else { return 不及格; } } } else { return 分数非法; } } // 卫语句写法 public String getLevel(int score) { if (score 0 || score 100) { return 分数非法; } if (score 90) { return 优秀; } if (score 60) { return 及格; } return 不及格; }第二种写法每一行都在同一缩进层级从上往下读就是一条直线。这里还有一个隐藏收益卫语句把参数校验集中到了方法头部后面无论加多少业务分支校验逻辑都不会被埋进深处。我在团队里推这个写法的时候有个同事说他改完发现自己一个两百行的方法能拆成三个因为原来那些嵌套其实对应着不同的职责。1.3 switch 的穿透特性与适用场景switch适合处理“一个值对应多个固定分支”的场景。它支持的表达式类型有byte、short、char、int及其包装类枚举以及String从 Java 7 开始。注意long、float、double和boolean都是不支持的这一点新手经常踩。switch最著名的坑是穿透fall-through某个case匹配成功后如果没有break会继续执行后面所有case的代码直到遇到break或者整个switch结束。这个特性偶尔有用多个值共享同一段逻辑时可以故意省略break但绝大多数情况下是 bug 来源switch (day) { case 1: System.out.println(周一); case 2: System.out.println(周二); break; default: System.out.println(其他); } // 当 day 1 时会同时打印周一和周二如果确实需要多个值走同一段逻辑更稳妥的写法是连续写多个case标签中间不写任何语句switch (day) { case 1: case 2: case 3: case 4: case 5: System.out.println(工作日); break; default: System.out.println(休息日); }较新的 JDK 提供了箭头语法case 1 - ...箭头标签天然不穿透而且可以当作表达式直接返回值写起来干净很多。如果你的项目还在用较老的 JDK 版本那就老老实实每个分支写break并且把default放在最后面——虽然语法上default可以放任意位置但放在最后符合大多数人的阅读习惯。还有一个隐蔽的空指针问题switch的表达式如果求值结果为null会直接抛NullPointerException。用String或枚举做分支时前面务必先判空。这个错误在日志里看起来莫名其妙因为堆栈指向的是switch那一行看不出是谁是null。对比维度if-elseswitch判断依据任意布尔表达式单个值的等值匹配支持类型布尔表达式整型、字符、字符串、枚举区间判断天然支持只能列举区间判断很啰嗦分支很多时的可读性长链 if-else 较差表格化较清晰穿透风险无有需注意 break选择逻辑很直接判断条件是范围、组合条件、布尔表达式用if判断条件是“等于某几个固定值”且分支数量超过三个用switch。2. 循环结构次数已知和未知时的两种兵器循环的本质是重复执行一段代码直到条件不再满足。Java 提供了for、while、do-while三种外加增强for专门遍历容器。选哪个不是风格问题而是取决于你“知不知道要循环多少次”。2.1 for 的三个表达式与执行顺序标准for的结构是for (初始化; 条件; 更新)执行顺序很多人背过但没真正内化初始化只执行一次然后判断条件条件为真执行循环体循环体结束后执行更新表达式再回到条件判断。这个顺序决定了几个常见误区的成因。第一个误区是在循环体里修改循环变量for (int i 0; i 10; i) { System.out.println(i); i; // 手动又加了一次 }这段代码只会打印 0、2、4、6、8因为每次循环i被加了两次。更危险的是减法方向写反直接导致死循环。循环变量应该由for的更新表达式独占管理循环体里不要碰它。如果确实需要基于i算别的值用一个临时变量。第二个误区是循环变量的作用域。for (int i 0; ...)里声明的i只在循环内可见循环结束后就没了。有人习惯在循环外先声明int i然后在循环里用循环结束后还想拿i的值做后续处理——这种写法能跑但会让i的生命周期变长增加误用风险。除非确实需要在循环外读取否则一律写在for的初始化部分。第三个误区藏在浮点数上for (double d 0.0; d ! 1.0; d 0.1) { System.out.println(d); }这段代码永远不会结束。因为0.1在二进制浮点里无法精确表示累加十次得到的是0.9999999999999999而不是1.0条件永远为真。浮点数不能直接用!或做循环边界判断正确做法是控制循环次数或者用d 1.0配合容差比较。2.2 while 与 do-while 的边界差异while适合“循环次数事先不知道靠条件决定何时停”的场景比如读取输入直到遇到特定值、不断重试直到成功。do-while和while的唯一区别是do-while至少执行一次循环体因为条件判断放在末尾。这个差异在实际业务里很关键。比如“让用户输入一个合法数字”这类交互int num; do { System.out.print(请输入 1-100 之间的整数); num scanner.nextInt(); } while (num 1 || num 100);用do-while是因为提示语必须先出现一次用户才有机会输入。如果换成while就得先把num初始化成一个非法值比如-1逻辑上多绕一道弯可读性反而下降。使用while时最常见的坑是忘记更新条件变量int i 0; while (i 10) { System.out.println(i); // 忘了写 i }这种死循环在控制台里会疯狂刷屏在有 I/O 或数据库操作的场景里则是灾难——几秒钟能打满日志磁盘。我个人的习惯是写while的时候先把“改变条件的那行代码”敲进去再补循环体从顺序上杜绝遗漏。2.3 break、continue 和标签跳出break跳出当前所在的最近一层循环continue跳过本次循环剩余部分直接进入下一次。这两个语义清楚麻烦的是多层嵌套时想跳出外层循环。Java 提供了带标签的breakouter: for (int i 0; i 5; i) { for (int j 0; j 5; j) { if (i * j 6) { System.out.println(找到 i * j); break outer; } } }标签写在循环前面冒号结尾break outer会直接跳出标签所标记的那层循环。这套语法在老代码里很常见但现代 Java 里其实有更清晰的替代方案把嵌套循环抽成一个方法找到目标直接return。方法的return天然跳出所有层级而且抽出去之后那段逻辑还能单独测试。带标签的continue同理能跳到外层循环的下一次迭代但在实际项目中极少使用可读性代价太高。我审代码时看到标签第一反应是“这段逻辑能不能抽方法”十次里有八次答案是能。另外提醒一句break在switch里表示跳出switch块如果switch外面还包着循环break不会跳出循环。这个嵌套场景下语义容易混淆写的时候多看一眼缩进层级。3. 数组Java 里第一个真正意义上的容器变量一次只能存一个值数组一次能存一批同类型的值。它是 Java 里最基础的容器也是后面学集合框架的地基。数组这部分看着简单但越界、空指针、引用语义三个问题几乎每个初学者都要栽一遍。3.1 声明的两种写法和初始化三件套数组声明有两种写法int[] arr和int arr[]。前者把“数组”这个属性挂在类型上后者挂在变量名上。两种都能编译但推荐第一种因为int[]读起来就是“整型数组”这一个完整类型而且int[] a, b声明的两个变量都是数组而int a[], b里只有a是数组、b是普通int。这个差异小但阴险统一用int[]就绕过去了。初始化分三种// 1. 声明并静态初始化长度由元素个数决定 int[] a {1, 2, 3}; // 2. 声明并动态初始化元素取默认值 int[] b new int[5]; // 3. 先声明后赋值 int[] c; c new int[]{1, 2, 3}; // 注意这里不能写成 {1, 2, 3}第三种写法有个细节c {1, 2, 3}是编译错误静态初始化的简写形式只能用在声明语句里一旦分开写就必须补上new int[]。这个报错信息不太直观新手经常卡住。默认值规则要记牢整型是0浮点是0.0boolean是falsechar是\u0000不可见字符引用类型是null。动态初始化之后数组里不是“什么都没有”而是一批默认值。基于这一点用new int[n]做累加是安全的但用new String[n]之后直接调元素的方法就会空指针。3.2 length 属性、越界访问与默认值数组的长度通过length获取注意它是属性不是方法写arr.length而不是arr.length()。字符串的length()是方法两者容易混IDE 补全时多看一眼括号。合法索引范围是0到length - 1。访问arr[length]会抛ArrayIndexOutOfBoundsException。这个异常的名字有点长但排查思路很固定看异常信息里的索引值再看数组实际长度基本立刻定位。我见过最典型的一个错误是循环边界写错int[] arr new int[5]; for (int i 0; i arr.length; i) { // 应该是 arr[i] i; }让i取到了5而最大合法索引是4最后那次赋值直接抛异常。这类差一错误off-by-one在数组和字符串处理里出现频率极高写边界条件时默念一遍“长度是 n最大下标是 n-1”。还有一个容易忽略的点数组本身是对象如果数组变量是null访问arr.length或者arr[0]都会抛NullPointerException而不是数组越界异常。两种异常要分清楚前者是“数组根本不存在”后者是“数组存在但你摸到了外面”。3.3 增强 for 的便利与局限增强forfor-each的写法是for (元素类型 变量名 : 数组或集合)专门用于遍历代码短、可读性好int[] scores {78, 92, 55}; for (int s : scores) { System.out.println(s); }但它有明确的能力边界用之前必须清楚第一拿不到索引。如果需要知道当前是第几个元素只能用普通for。有些人为了拿索引用增强for再手动维护一个计数器变量这属于自找麻烦直接换普通for更清楚。第二修改循环变量不影响原数组。对基本类型数组增强for里的循环变量是元素值的副本int[] arr {1, 2, 3}; for (int x : arr) { x x * 2; // 只改了副本arr 依然是 {1, 2, 3} }想原地修改元素必须用带索引的普通for通过arr[i] ...赋值。对引用类型数组情况略有不同如果修改的是对象内部状态比如person.setAge(20)会生效因为副本里存的是同一个对象的引用但如果把循环变量重新指向一个新对象就不生效。这个区分是理解后面方法传参的前提值得多琢磨两遍。第三遍历过程中不能增删元素。增强for底层用的是迭代器遍历集合时修改结构会抛ConcurrentModificationException。数组没有这个问题但换成ArrayList就会踩到这个坑留到集合那部分再展开先记住“增强 for 只适合纯读取遍历”。二维数组也顺带说一下。int[][] matrix new int[3][4]表示 3 个长度为 4 的一维数组。matrix.length是 3matrix[0].length是 4。Java 的二维数组本质是“数组的数组”所以支持不规则形状int[][] jagged {{1}, {2, 3}, {4, 5, 6}};完全合法每一行的长度可以不同。遍历二维数组时内层循环的边界要用matrix[i].length而不是matrix.length写错就会越界。4. 方法把一段逻辑打包成可复用的黑盒方法解决的是重复代码问题。同一段逻辑写三遍改需求时就要改三处漏一处就是 bug。把逻辑抽成方法之后调用方只需要知道“传什么进去、拿什么出来”内部的实现细节被封装起来。4.1 方法签名的组成与重载判定一个方法的完整定义包括修饰符、返回值类型、方法名、参数列表、方法体。其中方法名 参数列表构成方法签名这是判断重载的唯一依据。重载overload指的是同一个类里方法名相同但参数列表不同的多个方法。参数列表不同包括类型不同、个数不同、顺序不同public void print(int x) { } public void print(String s) { } public void print(int x, int y) { } public void print(String s, int x) { }以上四个都是合法重载。但下面这种不行public int getValue(int a) { return a; } public double getValue(int a) { return a; } // 编译错误方法已定义返回值类型不参与签名判定两个方法只要名字和参数列表一样哪怕返回值不同也算重复定义。这个规则背后的道理其实很实在调用方写getValue(1)的时候编译器没法从调用语句本身推断你想要int还是double所以语言层面直接禁止。实际调用时编译器选哪个重载方法遵循“最精确匹配”原则。传入int而同时存在print(int)和print(long)会选print(int)因为不需要类型提升。如果只存在print(long)int会自动宽化过去。这套规则大部分时候符合直觉但混入装箱和可变参数之后会变得微妙。我的经验是不要设计需要靠类型提升或装箱才能确定的重载比如同时提供f(int)和f(Integer)调用时容易出意外也没人愿意去翻规范确认。方法命名用驼峰式动词开头见名知意。calculateAverage比calc好isValid比check好。方法体控制在能一屏看完的长度超了就考虑拆。这不是洁癖是维护成本问题——三十行以内的方法出了问题扫一眼就能定位两百行的方法光找相关代码就要滚半天。4.2 值传递Java 里没有引用传递这是 Java 基础里被误解最多的一个知识点。明确结论Java 只有值传递没有引用传递。所有参数传递都是把实参的值复制一份给形参。对基本类型复制的是数值本身所以方法内修改形参完全不影响外部public static void change(int x) { x 100; } int a 1; change(a); System.out.println(a); // 输出 1对引用类型复制的是引用的值可以理解成对象地址的副本。这带来两个结果通过副本可以修改对象内部状态因为副本和原引用指向同一个对象但把副本重新指向新对象外部引用不受影响public static void modify(int[] arr) { arr[0] 100; // 生效改的是同一个数组对象 } public static void reassign(int[] arr) { arr new int[]{9, 9}; // 不生效只是让形参指向了新对象 } int[] data {1, 2, 3}; modify(data); System.out.println(data[0]); // 输出 100 reassign(data); System.out.println(data.length); // 依然输出 3很多人把“能通过参数改数组内容”误解成“Java 有引用传递”其实只是引用的副本指向同一个对象而已。区分方法很简单看方法内部能不能让外部的变量改指向。答案是不能那就说明传的是值。这个知识点在实际开发中的价值在于如果你不希望方法修改传入的数组就在方法内部拷贝一份再操作或者干脆改成返回新数组的函数式写法。我见过一个工具方法偷偷把调用方传进来的数组排序了调用方后面又依赖原始顺序排查了两小时才发现是这个方法干的。4.3 可变参数和递归的使用边界可变参数varargs的写法是类型... 参数名让方法能接收任意个同类型参数public static int sum(int... nums) { int total 0; for (int n : nums) { total n; } return total; } sum(); // 合法nums 是长度为 0 的数组 sum(1, 2, 3); // 合法 sum(new int[]{1, 2, 3}); // 也合法直接传数组三条规则必须记住可变参数必须是参数列表的最后一个一个方法最多只能有一个可变参数可变参数在方法内部就是个普通数组nums可以为null显式传null时所以方法体里最好加个判空。还有一个语法细节sum(int... nums)和sum(int[] nums)不能同时存在编译器认为它们签名冲突因为可变参数本质就是数组。可变参数适合“参数个数不确定但类型一致”的场景比如日志格式化、批量求和。但它会降低方法签名的明确性参数超过三个且含义不同时老老实实写固定参数更清楚。递归是方法自己调用自己必须满足两个条件有终止条件base case且每次递归都向终止条件靠近。缺了任何一个就是无限递归结果是StackOverflowError——注意是Error不是Exceptioncatch块抓不住程序直接崩。public static long factorial(int n) { if (n 0) { throw new IllegalArgumentException(n 不能为负); } if (n 1) { return 1; // 终止条件 } return n * factorial(n - 1); // 向终止条件靠近 }递归写出来的代码确实简洁阶乘、树形结构遍历、分治算法都适合。但代价是每次调用都要在栈上压一帧递归深度大了就爆栈。计算斐波那契数列如果用朴素递归时间复杂度是指数级的O(2^n)算到第 40 项就要等好几秒而且中间大量重复计算。这种场景要么改写成循环要么加缓存记忆化。我的判断标准是递归深度可能超过几千层、或者存在大量重复子问题就别用朴素递归。5. 作用域与初始化那些编译期就拦下你的错误有些错误运行起来很难查有些错误编译器直接拦在门口。作用域和初始化相关的报错属于后者看着吓人但理解规则之后非常好定位。5.1 局部变量与成员变量的默认值差异规则只有一条但极其重要局部变量必须显式初始化后才能使用成员变量有默认值。public class Demo { private int member; // 成员变量默认 0 public void test() { int local; // 局部变量未初始化 System.out.println(member); // 正常输出 0 System.out.println(local); // 编译错误可能尚未初始化 } }为什么设计成这样成员变量跟着对象走对象创建时 JVM 会把整块内存清零所以有确定的默认值。局部变量在栈上分配JVM 不会主动清零为了性能考虑语言层面强制要求程序员先赋值再用。这个设计避免了读到内存里的随机残留值属于安全兜底。实际写代码时最好的习惯是声明时就初始化int count 0;、String name ;、ListString list new ArrayList();。这样既满足编译要求也让人一眼看出这个变量的初始状态。只有在初始化依赖前面逻辑的情况下才采用“先声明后赋值”的写法而且声明和首次赋值之间不要隔太远。数组的情况稍有不同int[] arr new int[5];里的元素有默认值0但数组变量arr本身属于局部变量必须先完成初始化才能使用。这个区分别搞混。5.2 变量遮蔽与代码块作用域Java 用花括号{}划分作用域内层代码块可以访问外层变量但内层声明的变量出了块就失效int x 10; { int y 20; System.out.println(x y); // 30 } System.out.println(y); // 编译错误找不到符号 y变量遮蔽shadowing指的是内层代码块声明了和外层同名的变量。局部变量遮蔽成员变量是合法的但局部变量之间在嵌套作用域里同名不行——for循环变量尤其容易撞车int i 5; for (int i 0; i 3; i) { // 编译错误变量 i 已定义 // ... }同一个方法里外层已经有ifor里再声明i会直接报错。这个规则和某些语言不同写惯了其他语言的人容易在这里卡一下。解决办法很简单换个变量名或者把外层那个i的作用范围收窄。遮蔽带来的最大问题是可读性下降。局部变量把成员变量遮住之后方法体里看到的name到底是哪一个得往上翻代码才能确认。团队协作里我一般建议给成员变量加明确前缀或者用this.显式访问避免歧义。5.3 一份常见编译错误速查表下面这些报错信息基本上每个学 Java 的人都会遇到至少一次。看懂它们在说什么排查效率能提高一大截。报错信息节选常见原因处理方向cannot find symbol变量名拼错、作用域外访问、缺少 import核对拼写确认变量在当前位置可见variable might not have been initialized局部变量未赋值就使用声明时给初始值或确保所有分支都赋值missing return statement有返回值的方法存在未覆盖的分支补全 return或末尾加兜底 returnunreachable statementreturn/break 之后还有代码删除或调整控制流incompatible types类型不匹配赋值或传参时出现检查是否需要强制转换或换类型method is already defined重载时参数列表重复修改参数类型或个数array required, but int found对非数组变量用了[]检查变量声明non-static variable cannot be referenced静态方法里访问了实例成员改为实例方法或把成员也改为静态missing return statement这个坑值得单独说一句。它的典型触发场景是方法声明返回int但if-else里只覆盖了部分情况编译器检测到某条路径没有返回值public int getLevel(int score) { if (score 60) { return 1; } // 缺少 score 60 时的返回值 }编译器不会去推理“score 不是大于等于 60 就是小于 60”这种逻辑它只看控制流。补一个兜底的return就行。反过来说如果方法所有分支都有return末尾就不用再写写了反而触发unreachable statement。6. 串起来做一个成绩统计小工具语法点单独看都很零碎串成一个完整的小工具才能真正消化。下面这个例子用到了前面讲的全部分支、循环、数组、方法知识代码不长但结构完整可以直接拿去改。6.1 需求拆解与方法划分需求是这样的给定一组学生成绩整数0 到 100输出平均分、最高分、最低分、及格率以及各个分数段的人数分布优秀 90 以上、良好 80 到 89、及格 60 到 79、不及格 60 以下。拆方法的思路是“一个方法只做一件事”average(int[] scores)算平均分返回doublemax(int[] scores)/min(int[] scores)算最高最低返回intpassRate(int[] scores)算及格率返回doublecountByRange(int[] scores)返回长度为 4 的数组分别存四个分数段的人数每个方法都独立处理空数组和null的情况这样main方法里就不用反复判空逻辑清爽。6.2 完整实现与逐段说明import java.util.Arrays; public class ScoreStats { public static void main(String[] args) { int[] scores {78, 92, 55, 61, 88, 100, 43, 76, 69, 90}; if (scores null || scores.length 0) { System.out.println(没有成绩数据); return; } System.out.printf(平均分%.2f%n, average(scores)); System.out.println(最高分 max(scores)); System.out.println(最低分 min(scores)); System.out.printf(及格率%.1f%%%n, passRate(scores) * 100); int[] ranges countByRange(scores); System.out.println(优秀 ranges[0]); System.out.println(良好 ranges[1]); System.out.println(及格 ranges[2]); System.out.println(不及格 ranges[3]); int[] sorted Arrays.copyOf(scores, scores.length); Arrays.sort(sorted); System.out.println(排序后 Arrays.toString(sorted)); } public static double average(int[] scores) { if (scores null || scores.length 0) { return 0.0; } long sum 0; // 用 long 防溢出 for (int s : scores) { sum s; } return (double) sum / scores.length; } public static int max(int[] scores) { if (scores null || scores.length 0) { throw new IllegalArgumentException(成绩数组不能为空); } int result scores[0]; for (int i 1; i scores.length; i) { if (scores[i] result) { result scores[i]; } } return result; } public static int min(int[] scores) { if (scores null || scores.length 0) { throw new IllegalArgumentException(成绩数组不能为空); } int result scores[0]; for (int i 1; i scores.length; i) { if (scores[i] result) { result scores[i]; } } return result; } public static double passRate(int[] scores) { if (scores null || scores.length 0) { return 0.0; } int passCount 0; for (int s : scores) { if (s 60) { passCount; } } return (double) passCount / scores.length; } public static int[] countByRange(int[] scores) { int[] result new int[4]; // 默认值全是 0 if (scores null) { return result; } for (int s : scores) { if (s 90) { result[0]; } else if (s 80) { result[1]; } else if (s 60) { result[2]; } else { result[3]; } } return result; } }几处细节值得单独说明。average里求和用long而不是int因为如果成绩数组有几千万条int累加会溢出变成负数平均分算出来荒唐得离谱。这种溢出问题在小数据量测试时完全看不出来上线之后才发现属于典型的“测试环境永远复现不了”的坑。max和min的初始值取scores[0]而不是0或Integer.MAX_VALUE是因为成绩全是负数时虽然这个场景下不会从0开始会得到错误结果。从第一个元素开始然后从第二个元素往后比较是更稳妥的写法。同理循环从i 1开始。countByRange的分支顺序有讲究。s 90判断在前后面的else if就不用再写 90的限定因为能走到第二个分支就说明不满足第一个条件。这种“阶梯式”写法比每个条件写全区间更简洁也不容易写漏。Arrays.copyOf那句是为了在排序前保留原数组顺序。如果直接Arrays.sort(scores)传入的数组会被原地排序main里后面再用scores就是排好序的了。这个副作用前面提过这里用实际代码演示一次。6.3 三处可以继续优化的地方第一处参数校验重复了五次。scores null || scores.length 0这个判断在每个方法开头都写了一遍。可以抽一个checkScores私有方法统一处理抛异常或返回布尔值。重复的校验逻辑一旦规则变化比如允许空数组返回 0就要改五个地方漏一个就是隐患。第二处countByRange返回int[]靠下标约定含义调用方必须知道“0 是优秀、1 是良好”这种隐式约定容易记错。换成枚举或者一个小的结果类会更清晰虽然代码量多一点但可读性和可维护性提升明显。这个改法涉及面向对象的内容等学完类和对象再回来重构一次效果很好。第三处max和min的循环结构几乎一样只有比较符号不同。可以合并成一个方法加个布尔参数控制方向也可以利用Math.max简写。不过我不太建议为了消除几行重复就引入参数开关那会让方法语义变模糊。重复的简单代码有时候比抽象过的复杂代码更好维护这个度要自己把握。最后分享一个我踩过的实际坑。早期写这类统计代码时喜欢在循环里用scores.length作为边界某次重构把参数从数组换成了Listlength变成size()结果一个文件里改了一半漏了一半编译报错倒是好事怕的是只改了一处、另一边用了别的容器还能编译通过运行起来结果全错。从那之后涉及容器边界的地方我都尽量用增强for把“遍历所有元素”这个意图写清楚让编译器帮我盯着类型。基础语法的这些细节平时看着琐碎真到出问题的时候往往就是它们决定了你是五分钟定位还是排查一下午。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询