ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java流程控制与方法实战:从if-switch到重载、递归与值传递

Java流程控制与方法实战:从if-switch到重载、递归与值传递 很多刚接触Java的朋友学到“流程控制与方法”这一块时会觉得自己都会了——不就是if、for、while、定义方法嘛太简单了。等到真正去写一个完整的小项目或者去面试做笔试题才发现自己其实对细节一知半解switch的case穿透、循环里的标签跳转、值传递和引用传递、重载的规则……随便拎一个出来都能把人问懵。这篇博文我想用实战的视角把JavaSE里流程控制和方法的关键知识点拆开揉碎讲清楚每个语法背后的逻辑和容易踩的坑。不管你是零基础刚学完变量和数据类型还是学了一遍想巩固基础都可以参考这份总结。1. 顺序、选择、循环先想清楚程序为什么需要“流程控制”1.1 从一段生活化的代码说起如果你写过几行Java代码应该知道程序默认是从上往下、一行一行执行的。这种执行方式叫“顺序结构”。但真实世界的问题很少是线性的明天天气好就出门下雨就待在家账户余额够就支付不够就提示充值。这些“根据条件决定下一步”的逻辑在程序里就需要选择结构而“重复做同一件事直到某个条件满足”的逻辑就需要循环结构。我见过不少初学者直接上手去背语法背完if和for就觉得自己会了。其实更重要的是建立一种“流程思维”拿到需求先别急着敲代码在纸上把步骤理清楚。比如写一个“给成绩打分”的小程序你脑子里应该先有这样的流程输入成绩 - 判断成绩是否合法 - 根据分数区间输出对应等级 - 结束这个流程里判断和重复就是流程控制最核心的两个动作。Java提供了if、switch来处理选择提供了for、while、do-while来处理循环再加上break、continue、return这些跳转语句基本能覆盖所有业务逻辑。1.2 代码块作用域流程控制里最容易被忽略的“隐形规则”很多新手学流程控制时不太关注大括号{}的问题。其实大括号除了组织代码块还决定了变量的作用域。在Java里在一个代码块内部声明的变量只能在这个代码块及其子代码块中使用出了大括号就不能访问了。我举个特别典型的场景if (score 60) { String result 及格; System.out.println(result); } // 这里如果写 System.out.println(result); 编译会直接报错这个代码块里的result变量作用域仅限于if的大括号内部。如果你在if外面想用这个变量必须在外面先声明。类似的还有for循环的循环变量for (int i 0; i 10; i) { // 使用i } // i在这里已经不存在了这个规则看起来简单但实际写代码时经常引发“找不到符号”的编译错误。尤其是一个变量需要在多个分支中赋值然后在分支外面使用的情况。正确的做法是把变量声明在块外面String result; if (score 60) { result 及格; } else { result 不及格; } System.out.println(result);所以我的建议是初学流程控制时刻意去留意每一个变量“出生”和“死亡”的位置。写得多了一眼就能看出来但刚开始多踩几次编译报错的坑比死记规则更有效。2. 选择结构实战if-else的短路逻辑与switch的边界2.1 短路求值为什么你的第二个条件可能根本没执行if-else里最常见的一个隐藏细节就是“短路求值”。Java的和||运算符会从左到右计算一旦结果已经确定后面的表达式就不会再执行。这个特性用得好能避免很多异常用不好就会让程序行为变得诡异。举个例子判断一个字符串是否以“Java”开头你可能会写if (str.startsWith(Java) str.length() 5) { // ... }如果str是null这里会怎样str.startsWith(Java)会抛NullPointerException因为null没有startsWith方法。但是如果你换个顺序if (str ! null str.startsWith(Java)) { // ... }当str为null时第一个条件str ! null为false整个表达式已经确定为false后面的str.startsWith(Java)根本不会执行于是异常就不会发生。这就是短路求值的价值——我们可以利用它安全地做前置判断。同样的道理||也有短路效应。比如判断一个数字是否为非法输入if (num 0 || num 100) { // 非法 }只要num 0为true后面的num 100就不用算了。这个特性在日常编码中非常实用但也要注意不要在短路条件里写带有副作用的代码比如a 0 || (b) 0因为b不一定会执行程序的可读性会变得很差。2.2 switch的变迁从int到String再到枚举case穿透怎么防很多教材讲到switch时还是会从int类型讲起。其实Java 7之后switch支持了StringJava 5之后支持枚举再加上现代Java里还支持var和箭头语法switch的写法已经比老古董版本灵活很多。先回顾最传统的写法int day 3; switch (day) { case 1: System.out.println(星期一); break; case 2: System.out.println(星期二); break; case 3: System.out.println(星期三); break; default: System.out.println(非法输入); break; }这里一定要记住每个case里如果不写break程序会继续往下执行下一个case这叫“case穿透”。我见过很多笔试面试题专门考这个。比如int x 2; switch (x) { case 1: System.out.print(A); case 2: System.out.print(B); case 3: System.out.print(C); }输出是什么答案是BC。因为case 2匹配后没有break就会一直执行到case 3。这个坑在早期的switch里特别容易踩尤其是从其他语言转过来的同学习惯性地以为每个case执行完会自动跳出。现代JavaJava 14引入了箭头语法可以这样写switch (day) { case 1 - System.out.println(星期一); case 2 - System.out.println(星期二); default - System.out.println(非法输入); }箭头语法不会穿透每个分支执行完就自动结束还支持yield返回值写起来简洁很多。不过在很多企业项目里为了兼容老版本JDK传统写法还是大量存在所以两种都得看得懂。2.3 什么场景用if链什么场景用switch新手经常纠结多个条件判断到底用if-else if还是switch我的经验是看两点判断的依据是不是某个变量的多个取值以及每个分支的逻辑是否只是等值比较。如果是区间判断比如成绩90分以上、80到90分之间这种必须用if-else if因为switch只能做等值匹配没法直接判断范围。如果是离散的固定取值比如星期几、月份、状态码用switch更清晰可读性更高。如果分支数量很少两三支用if更简洁分支数量多且取值固定用switch更直观。另外要注意if-else if的顺序条件范围越窄的放前面越宽的放后面否则后面的分支永远执行不到。比如if (score 60) { // 及格 } else if (score 90) { // 优秀 }这个代码永远走不到score 90因为既然90分也满足60早就进入第一个分支了。正确的顺序应该是先判断高分区间再逐级往下。3. 循环结构for、while、do-while的区别不止语法3.1 先判断还是先执行do-while的适用场景Java里常用循环有三种for、while、do-while。很多初学者只关注语法忽略了它们的本质区别——判断条件执行的时机。while先判断条件条件为true才执行循环体。如果一开始条件就是false循环体一次都不执行。do-while先执行一次循环体再判断条件所以循环体至少会执行一次。for本质上是while的紧凑写法把初始化、条件判断、迭代更新写在一起。举个实际例子从键盘读取用户输入直到用户输入“q”才退出。这个场景就非常适合do-while因为至少要先读一次输入才能判断要不要继续Scanner sc new Scanner(System.in); String input; do { System.out.print(请输入内容输入q退出); input sc.nextLine(); System.out.println(你输入了 input); } while (!input.equals(q));如果用while写就得在循环外先读一次再把读入操作在循环体末尾重复一遍代码就不够优雅了。所以记住只要“至少要执行一次”的场景优先考虑do-while。3.2 break、continue与标签跳转到底该不该用break用来终止当前循环continue用来跳过本次循环的剩余语句、进入下一次迭代。这两个是循环里最常见的控制语句。还有一个比较冷门的“带标签的break”可以跳出外层循环。比如有个二维数组要在里面找某个数字找到后立刻结束所有循环int[][] arr {{1, 2}, {3, 4}}; int target 4; boolean found false; for (int i 0; i arr.length; i) { for (int j 0; j arr[i].length; j) { if (arr[i][j] target) { found true; break; // 这个break只能跳出内层循环 } } if (found) break; // 外层还需要再判断一次 }传统写法需要用一个标记变量内层break后再检查标记才能最终跳出外层循环。Java也支持标签写法outer: for (int i 0; i arr.length; i) { for (int j 0; j arr[i].length; j) { if (arr[i][j] target) { break outer; // 直接跳出外层循环 } } }这种写法简洁但很多公司的代码规范是禁用标签跳转的因为会破坏代码的可读性。我个人的建议是小范围使用问题不大但在团队项目里尽量用单独的方法加return来替代可读性更好。比如把查找逻辑封装成一个方法找到就返回下标或true调用方直接返回即可不需要标签。3.3 增强for循环背后的迭代器为什么不能一边遍历一边删除Java 5开始提供了增强for循环也叫foreach写起来特别爽ListString list new ArrayList(); for (String s : list) { System.out.println(s); }但这里有个大坑如果你在增强for循环里直接调用list.remove()几乎百分百会抛ConcurrentModificationException。原因很简单增强for循环的底层其实是迭代器Iterator迭代器内部会维护一个modCount修改次数期望值每次遍历时会检查集合的实际修改次数是否和期望值一致。不一致就抛出异常目的是防止多线程环境下出现不一致的数据。那如果想在遍历中删除满足条件的元素该怎么办有三种靠谱方案使用Iterator的remove方法IteratorString it list.iterator(); while (it.hasNext()) { String s it.next(); if (s.equals(bad)) { it.remove(); // 这是迭代器自己的方法会同步更新期望值 } }收集要删除的元素循环结束后统一删除。Java 8之后使用removeIflist.removeIf(s - s.equals(bad));removeIf是Java 8给Collection接口加的默认方法内部也是基于迭代器实现的所以可以直接用。很多老项目还是Java 7所以前两种方案也必须掌握。4. 方法把代码块变成可复用的逻辑单元4.1 方法签名与重载为什么返回类型不能作为重载依据方法是Java里组织代码的核心。定义方法时要注意“方法签名”这个概念方法签名 方法名 参数列表参数类型、个数、顺序。注意返回类型不属于方法签名。这一点最直接的影响就是“重载”。重载指的是在同一个类里方法名相同、参数列表不同。比如public int add(int a, int b) { return a b; } public double add(double a, double b) { return a b; } public int add(int a, int b, int c) { return a b c; }这三个add方法就是重载调用时编译器根据你传的参数个数和类型来决定调用哪一个。那为什么返回类型不能作为重载依据因为有时候调用方法时不接收返回值add(1, 2);如果两个方法只有返回类型不同参数列表相同编译器根本无法分辨你要调用哪个。这样的重载没有意义Java直接规定不允许。所以你在写重载时千万不要只改返回类型必须让参数列表有区别。4.2 参数传递的本质Java到底是值传递还是引用传递这个问题几乎每个Java面试都会问到。结论很明确Java只有值传递没有引用传递。这里的“值”有两种情况基本类型int、double等传的是值本身方法内修改不会影响外部变量。引用类型数组、对象传的是引用的拷贝也就是“对象地址的副本”。方法内通过这个副本去修改对象属性外部对象会跟着变但如果把副本重新指向一个新对象外部引用不会变。我画个最经典的例子public class Test { public static void main(String[] args) { String s hello; change(s); System.out.println(s); // 输出 hello } public static void change(String str) { str world; } }这里为什么输出还是hello因为String是不可变对象str world只是把方法内的形参副本指向了一个新字符串main中的s指向的还是原来的hello。再比如public class Test { public static void main(String[] args) { StringBuilder sb new StringBuilder(hello); change(sb); System.out.println(sb.toString()); // 输出 hello world } public static void change(StringBuilder builder) { builder.append( world); } }这里输出hello world因为builder和sb指向同一个对象builder.append修改的是对象内容所以外部能看到变化。所以别再说“对象是引用传递”了。准确的理解是Java把对象的地址这个“值”传了一份拷贝给形参。这个细节搞明白了很多关于参数修改的笔试题根本不会错。4.3 递归的终止条件与栈溢出一个斐波那契引发的血案方法调用自身叫递归。递归写起来很优雅但一定要有终止条件否则会无限调用下去最终抛出StackOverflowError——栈溢出。为什么呢因为每次方法调用JVM都会在栈中分配一个“栈帧”用于存储局部变量、方法状态等信息。递归一层层调用栈帧就一层层压入栈中。栈的内存是有限的当深度过大时栈就装不下了。经典的反面教材是朴素递归求斐波那契数列public static long fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }这段代码虽然能算fib(10)、fib(20)但当你算fib(50)时会卡得怀疑人生。因为它做了大量重复计算——算fib(50)需要先算fib(49)和fib(48)而fib(49)又去算fib(48)和fib(47)……重复次数呈指数增长更别提栈深度也可能撑不住。所以递归不是不能用而是要聪明地用。很多场景可以改成循环或者使用记忆化缓存中间结果。比如用数组保存已经算过的值public static long fib(int n) { if (n 1) return n; long[] dp new long[n 1]; dp[0] 0; dp[1] 1; for (int i 2; i n; i) { dp[i] dp[i - 1] dp[i - 2]; } return dp[n]; }这样时间复杂度和空间复杂度都很好。使用递归时一定要先想清楚两件事终止条件是什么每一层递归如何向终止条件逼近如果这两个问题答不清楚你的递归大概率是错的。5. 综合实战用流程控制和方法实现一个菜单式计算器5.1 需求拆解与结构设计知识学了一堆如果不串起来很容易忘。我建议你把这个“菜单式计算器”亲手写一遍。需求很简单程序启动后显示菜单用户输入1到4分别执行加、减、乘、除输入0退出。每次计算完成后需要重新显示菜单直到用户选择退出。这个需求非常适合练习流程控制和方法设计。我先把功能拆一下一个主流程负责循环显示菜单、读取用户选择。一个读取两个操作数的方法因为加减乘除都需要输入两个数。四个计算方法add、subtract、multiply、divide。一个处理无效输入和除零异常的逻辑。这里的关键设计是主流程要尽量“瘦”具体的计算逻辑放到独立的方法里这样后续想扩展幂运算、取模运算只需要加一个方法和一个case不需要改动主流程的结构。5.2 完整代码实现与运行验证直接上代码我写的版本适合Java 8以上环境import java.util.Scanner; public class Calculator { public static void main(String[] args) { Scanner sc new Scanner(System.in); while (true) { // 显示菜单 System.out.println( 简易计算器 ); System.out.println(1. 加法); System.out.println(2. 减法); System.out.println(3. 乘法); System.out.println(4. 除法); System.out.println(0. 退出); System.out.print(请选择); int choice sc.nextInt(); if (choice 0) { System.out.println(感谢使用再见); break; } if (choice 1 || choice 4) { System.out.println(无效输入请重新选择); continue; } double num1 readNumber(sc, 请输入第一个数); double num2 readNumber(sc, 请输入第二个数); switch (choice) { case 1: System.out.println(结果 add(num1, num2)); break; case 2: System.out.println(结果 subtract(num1, num2)); break; case 3: System.out.println(结果 multiply(num1, num2)); break; case 4: if (num2 0) { System.out.println(错误除数不能为0); } else { System.out.println(结果 divide(num1, num2)); } break; default: break; } System.out.println(); } sc.close(); } public static double readNumber(Scanner sc, String tip) { System.out.print(tip); return sc.nextDouble(); } public static double add(double a, double b) { return a b; } public static double subtract(double a, double b) { return a - b; } public static double multiply(double a, double b) { return a * b; } public static double divide(double a, double b) { return a / b; } }运行起来大概是这样的 简易计算器 1. 加法 2. 减法 3. 乘法 4. 除法 0. 退出 请选择1 请输入第一个数10 请输入第二个数5 结果15.0 简易计算器 ...这个示例看起来简单但涵盖了本篇文章用到的大部分知识while循环驱动菜单、if判断输入合法性、switch选择分支、方法封装计算逻辑、break退出循环、continue跳过非法选择。如果你能不看我的代码自己写出来并且能做到逻辑清晰那么流程控制和方法这一关基本就过了。5.3 从这段代码里反馈出的几个隐藏问题很多初学者把代码跑通就以为完事了其实这个计算器还有几个值得思考的扩展点第一个是sc.nextDouble()读取后如果用户输入的不是数字程序会直接抛InputMismatchException崩溃。怎么处理可以用sc.hasNextDouble()先判断或者用try-catch捕获异常。这是流程控制跟异常处理结合的地方后面学到异常时可以回头来优化。第二个是switch的case穿透问题。这里每个case后面都有break所以没问题。但如果你把break漏掉程序就会把下一个分支也执行了。你可以故意删掉一个break试验一下看看控制台输出会变成什么这样对case穿透的理解会更深刻。第三个是除法的精度问题。double在计算小数时不是精确的比如1.0 / 3.0结果会是0.3333333333333333这是因为计算机用二进制无法精确表示某些十进制小数。实际项目中涉及金额计算要使用BigDecimal但现在初学阶段能意识到这个问题就够了。第四个是重复代码问题。读取两个操作数时用了两次readNumber调用代码挺简洁如果不用方法封装每个case里都要写一遍sc.nextDouble()代码会非常冗余。这就是方法的价值——把重复逻辑收敛到一处修改时只需要改一处。5.4 更多进阶玩法用方法重构让代码更健壮如果你学有余力可以试着把上面的计算器优化成下面这样把选择菜单和计算逻辑分离。比如用一个方法返回计算结果public static double calculate(int choice, double a, double b) { switch (choice) { case 1: return add(a, b); case 2: return subtract(a, b); case 3: return multiply(a, b); case 4: return divide(a, b); default: throw new IllegalArgumentException(无效操作符); } }主循环里就可以把整个选择判断简化为调用这个方法代码看起来会清爽很多。这就是“方法让主流程瘦身”的直观体验。另外也可以尝试用Scanner读取一行字符串再解析而不是直接nextInt()这样能避免输入缓冲区的换行符问题。比如在读取完数字后你可能想再读取一个字符串如果不额外调用一次sc.nextLine()就会直接收到一个空字符串。这种“输入缓冲区坑”在初学者中非常常见值得记下来。我在实际教学和带新人时最明显的一个感受是很多人不是不会语法而是不会“拆分问题”。面对一个稍微复杂一点的需求脑子里一团浆糊不知从何下手。流程控制是整个程序执行的骨架方法则是把骨架上的血肉组织成模块的关键。你可以试着不看我写的代码自己从零实现一遍这个计算器然后把你写的版本和我的版本对比看看差在哪里。不要觉得这个例子简单能把简单例子写得干净、可靠、易扩展本身就是程序员的基本功。
返回列表