1. 这一节到底在讲什么
打开某培训机构的Java教程,第7章无非是流程控制。很多自学Android的人看到这个标题就直接跳过了——“if-else谁不会啊?”说实话,我在学Java4Android系列之前也是这么想的。但等到自己真正动手写一个小Demo的时候,才发现分支语句远不是“会写”那么简单。
这一节包含的内容其实很基础:if语句、if-else、多重if,以及Java里的switch。看上去就是“条件满足就执行这段代码,不满足就执行另一段”。但作为面向Android开发的自学路线,这节内容的定位不是让你“认识”分支语句,而是让你站在Android开发的角度,搞清楚三个问题:分支判断在什么时候用、怎么写才不容易出错、写出来的代码别人能不能一眼看懂。
这一节的适用对象,适合两类人。一类是完全零基础、刚开始学Java的读者,把这节当作语法入门的必啃章节;另一类是有点Java基础但没写过Android的人,看完这节可以结合Android里的真实场景,重新理解分支的取舍。
我想提醒的是,我写这篇并不是课程笔记的复述,而是结合我实际敲代码、调试、回头看录播视频的经验,把分支语句里那些“老师没细讲但实际经常踩”的点拆开聊一遍。看完之后,你再去写if-else和switch,至少不会写出那种一眼看过去就让人皱眉的代码。
另外还要提前说一句:这一节的内容和你后面的Android生命周期、事件处理、列表适配都强相关。也就是说,分支语句不是“学完就扔”的语法,而是你后面每天都要用的基础工具。所以别急着往后赶进度,把这块吃透,后面会省很多事。
2. if语句的三种常用写法与执行套路
2.1 从“如果…就…”开始理解执行过程
if的中文意思就是“如果”。在Java里,if后面跟一个小括号,括号里放一个布尔表达式——也就是结果为true或false的表达式。如果结果是true,就执行大括号里的代码;如果结果是false,就跳过去。
我回想自己刚开始学的时候,总觉得这个结构简单到不需要思考。但后来在Android里写逻辑时,才意识到真正难的并不是“if怎么写”,而是“条件怎么定”。
举个例子。假设你现在要判断用户是否登录,你可能很自然地写成:
if (user != null) { // 显示用户信息 }这段代码看起来没问题。但在真实的Android项目里,user可能是一个从网络接口返回的对象,服务器返回的数据里可能包含一个字段叫“isLogin”。这时候判断条件往往是“isLogin为true”和“user不为null”同时满足,才能显示用户信息。那代码就变成:
if (user != null && user.isLogin()) { // 显示用户信息 }这就是分支语句的第一个核心点:写if之前,先想清楚“条件到底是什么”。条件不是一个变量名,而是一个经过逻辑分析后得到的结论。
我在自学过程中总结了一个笨办法:每次写if之前,先在注释里用中文写清楚“什么情况下做什么事”,然后再翻译成代码。比如:
// 如果用户已登录并且不是游客身份,显示个人中心 if (user != null && !user.isGuest()) { // 显示个人中心入口 }这个习惯看起来有点傻,但对踩坑预防特别有效。因为很多时候我们不是不会写if,而是条件本身就分析错了。
2.2 if-else的配对问题:else到底跟谁
if-else是最常见的分支形式,语义是“如果条件为true做A,否则做B”。代码写起来也很顺:
if (score >= 60) { System.out.println("及格"); } else { System.out.println("不及格"); }但这里有一个经典的坑叫做“悬空else”。什么叫悬空else?当一个else前面有多个if嵌套的时候,这个else到底跟哪个if配对?Java语言的规则是:else总是和最近的、尚未配对的if结合。看下面这段代码:
if (a > 0) if (b > 0) System.out.println("A"); else System.out.println("B");这个else表面上看像是和第一个if配对,但实际情况是它和第二个if配对。原因是Java编译器不会根据缩进来判断配对关系,而是按照就近原则。所以如果你想让上面的else跟着第一个if走,就必须用大括号明确地包裹起来:
if (a > 0) { if (b > 0) { System.out.println("A"); } } else { System.out.println("B"); }我第一次踩这个坑是在做一个数字比较的小程序时,缩进排版看着很整齐,但输出结果总是和预期不一致。后来查了一圈资料才明白是“悬空else”的问题。这里我的建议很简单:不管条件体只有一行还是多行,一律加上大括号。这样做不仅消除了配对歧义,还能避免后续往代码里加逻辑的时候漏加括号。
2.3 多重if与else if的执行顺序
在实际开发中,判断往往不是二选一,而是多选一。这时候就要用else if,比如:
if (score >= 90) { grade = "优秀"; } else if (score >= 80) { grade = "良好"; } else if (score >= 60) { grade = "及格"; } else { grade = "不及格"; }这段代码的逐步判断逻辑是:先看是否大于等于90,是就执行并结束整个判断,否则再看是否大于等于80,以此类推。也就是说,一旦某个条件满足,执行完对应的代码之后,整个if-else链就结束了,后续的else if不会再被判断。
这里就必须注意一个顺序问题:条件的顺序决定了结果的正确性。还是拿成绩等级来举例,如果我把条件反过来写:
if (score >= 60) { grade = "及格"; } else if (score >= 90) { grade = "优秀"; }那么一个95分的同学会被判定为“及格”,因为第一个条件就满足了,后面的判断根本轮不到。这是一个极其低级的错误,但我在给某同学的代码做评审时就遇到过,他自己还找不出原因。
所以,写多重if的时候,应当先看范围大的条件,还是先看窄的条件,取决于你的具体业务逻辑。但无论如何,都必须测试每个分支的边界值——比如60分、61分、59分、90分、89分这种情况,确保没有逻辑漏洞。
3. switch语句:适合多值等值判断的分支结构
3.1 语法基础与case穿透
if-else适合范围判断,也就是“大于”“小于”“区间内”这类比较。但如果要判断的是一个变量的值是否等于某一个固定值,而且候选值有很多个,再用if-else写就会显得很啰嗦。这时候就该switch出场了。
标准语法格式如下:
switch (变量) { case 值1: // 代码块1 break; case 值2: // 代码块2 break; default: // 默认代码块 break; }switch的执行过程是:先拿变量和case后面的值逐一比较,找到相等的那个case,然后从这个位置往下执行,直到遇到break或者switch结束。如果所有case都不匹配,就执行default块。
“从匹配位置往下执行”这一点特别重要,它引出了一个初学必踩的大坑——case穿透。看这个例子:
int day = 3; switch (day) { case 1: System.out.println("周一"); case 2: System.out.println("周二"); case 3: System.out.println("周三"); default: System.out.println("未知"); }这里的输出结果是“周三”和“未知”。因为case 3匹配之后,没有遇到break,代码会一路向下执行,连default也顺带执行了。这就是case穿透。解决方法是每个case结尾都写上break。
我在自学时还有一个额外的体会:有些初学者会认为switch的case是按顺序从上到下匹配的,只有顺序匹配才会命中。这其实是误解。switch的比较不分先后顺序,只要值相等,不管case写在第几行,都能命中。所以case的顺序在逻辑上并不会影响匹配结果,前提是每个case都有break且逻辑上互相独立。
3.2 Java的switch能判断什么类型
在我看的这套Java4Android系列视频里,用的Java版本相对较早。在这类版本中,switch支持的判断类型有:int、short、byte、char,以及它们的包装类型。从Java 7开始,switch支持了String类型。到了Java 14以后,switch表达式又有更简洁的写法。但这些版本差异在不同的教程里体现得很不一致。
这就有必要提醒一下自学的读者:看书或看视频的时候,先搞清这个教程是基于哪个Java版本讲的。如果你在较新的JDK环境下练习,会发现编译器允许你写出更简洁的switch箭头语法,例如:
switch (day) { case 1 -> System.out.println("周一"); case 2 -> System.out.println("周二"); default -> System.out.println("未知"); }有些教程视频里不会讲这种写法,但你自己编译却能通过。等到网上搜到答案后反而容易搞混。我的建议是:如果目标是Android开发,优先把传统的switch写法掌握好,因为Android项目中用到的Java语法版本通常不会太激进,传统switch写法的兼容性更好。等Java基础稳定了,再回头补充新语法的知识并不晚。
3.3 switch在Android开发中的“隐形规则”
Android开发中switch的使用场景很多,典型的就是根据id判断用户点了哪个按钮。但这里有个细节,是视频课程里不会讲、只有做项目才会发现的:Android里的R.id,也就是资源id,本质上是一个int值,所以在case里可以直接写R.id.xxx。
但是,如果是通过动态生成控件的方式而不是用XML布局,控件的id可能不是常量,这时就不能写成case的取值了。因为Java要求switch的case值必须是编译期常量。有些初学者在写代码时会遇到“case表达式必须是常量表达式”的编译错误,就是这个原因。
我在做某个模拟项目时也遇到过类似情况。当时想用switch去判断多个EditText的输入类型,但case对象是变量,直接报编译错误,最后只能老老实实改成if-else。后来我总结出一个经验:switch适合分支数量多、判断条件固定为字面量或常量的场景;一旦判断的“候选值”是动态的、来自运行时计算的变量,就别硬套switch。
4. 分支语句的代码组织与优化思路
4.1 多层嵌套怎么拆:用卫语句减缩进
写分支语句最怕的事情,就是把代码写成“箭头形”嵌套。比如:
if (user != null) { if (user.isLogin()) { if (user.getType() == 1) { // 做管理员操作 } } }这种代码在功能上没错,但一旦嵌套超过三层,阅读起来就会很吃力。像我这种自学者,隔两周再回头看自己写的代码,都认不出当时的逻辑。所以后来我学着用“卫语句”来改这种结构。
卫语句的思想是:把不符合条件的情况提前返回或跳过,让主干逻辑保持平铺。改写之后,上面的代码就变成:
if (user == null) { return; } if (!user.isLogin()) { return; } if (user.getType() == 1) { // 做管理员操作 }这样每一层判断都是独立的,代码的缩进从三层变成一层,阅读顺序也变成了从上到下的线性流程,大脑负担小很多。我在后来的Android网络请求处理中大量使用了这种写法,尤其是处理接口返回的code字段时,先判空再判断业务码,整个过程干净直接。
4.2 if-else和switch怎么选:向业务场景要答案
很多初学者纠结“什么时候用if,什么时候用switch”。我的实践经验可以简单归纳为三条:
- 判断条件是范围型比较,比如“大于60”“小于90”,用if-else。
- 判断条件是等值比较,候选值在三个以上且是固定常量,用switch。
- 判断条件是多个条件组合起来,比如“是否登录且是否VIP”,用if-else组合逻辑运算符。
之所以这样选,不是因为switch不能做范围判断,而是switch的case定位在常量上,它的设计初衷就是“等值分支”。如果硬把范围判断塞进switch,代码反而又绕又长。
此外还要考虑一个因素:可读性和扩展性。if-else链在分支较多时依然可读,但会显得线性很长;switch在分支较多时更紧凑,但所有分支都在一个代码块里摆着,编辑器折叠后看起来也很清晰。所以在Android里,如果是根据菜单选项、按钮id、消息类型这类固定枚举值做分支,我的习惯是优先用switch,否则用if-else。
4.3 分支条件别写太长:提取布尔变量
还有一种常见问题是条件表达式太长。比如下面这个:
if (user != null && user.getAge() > 18 && user.getVipLevel() > 3 && user.getLastLoginTime() > timestamp) { // ... }这种条件链一眼扫过去根本读不出含义。我自己的做法是:把复杂条件提取成一个布尔变量,赋予它一个有意义的名字:
boolean isActiveVip = user != null && user.getAge() > 18 && user.getVipLevel() > 3 && user.getLastLoginTime() > timestamp; if (isActiveVip) { // ... }这样写的好处是:第一,if这一行的语义变得很清楚;第二,如果条件后续要调整,只需要改布尔变量的定义,不需要动if的主体逻辑;第三,这个布尔变量可以在同一个方法里复用,避免重复写下品条件表达式。
我在做某个模拟项目的个人中心页面时,就用这个思路处理了“是否展示VIP专属入口”的判断。逻辑条件有四个,提取成布尔变量之后,整个方法立刻清爽了许多。这也是我认为分支语句进阶的第一个标志——不再纠结语法,而是开始想代码读起来舒不舒服。
5. 分支语句在Android场景中的实战视角
5.1 模拟场景:处理登录状态的伪代码
这一部分我尽量不展开Android完整项目,因为我们还在Java基础阶段。但是把分支语句和Android的常见业务场景结合起来,有助于理解“为什么学这个”。下面是一个典型需求:在MainActivity中,根据登录状态决定跳转到哪个页面。
代码可以写成这样:
boolean isLogin = getLoginStateFromSp(); if (isLogin) { // 跳转到主界面 startActivity(new Intent(this, HomeActivity.class)); } else { // 跳转到登录页 startActivity(new Intent(this, LoginActivity.class)); }这里就涉及一个重要的分支语句知识点:在Android中,分支语句经常承担“路由”的工作。所谓路由,就是根据不同的条件决定程序下一步去哪里。这种逻辑遍布App的各个角落,比如:
- 根据网络状态决定展示加载界面还是错误界面;
- 根据用户权限决定显示哪些功能入口;
- 根据消息类型决定点击通知后跳转到哪个页面。
我在自学过程中的体会是:不要等到学完整套Java课程再开始想Android,而是每个Java语法知识点学完后,都试着去搜一下这个语法在Android里怎么用。分支语句连接到Android最直接的场景,就是页面跳转和状态切换。
5.2 空指针预防:分支语句的前置条件
写分支语句还有一个在Android里尤其重要的话题——空指针。Java中,如果对一个null对象调用方法,会抛出空指针异常(NullPointerException),应用可能因此闪退。而分支语句经常被用来预判这种情况。
常见的写法是:
if (data != null && data.getList().size() > 0) { // 显示列表数据 }这里用了短路运算符“&&”。它的特点是:如果左侧条件为false,右侧就不会再执行。也就是说,当data为null时,data.getList()根本不会被调用,因此不会出现空指针。这一点在Android处理网络返回数据时极其常用。
我自己踩过一次很惨的坑:当时写一个列表页,直接用接口返回的数据去更新UI,没有判断返回值为空的情况,结果测试时一进入页面就崩溃。后来加上分支判断,逻辑就稳定了很多。所以这里我强烈建议初学者养成一个习惯:凡是拿到从网络、数据库、用户输入等外部来源获取的数据,使用前都要先做判空分支,再进入业务逻辑。
5.3 分支语句的测试意识:边界值不能偷懒
最后再说一个跟分支语句强相关的话题——测试。很多自学Java的人写分支语句时,只测“正常情况”,比如写一个成绩等级判断,就只输入一个85分,看到输出“良好”就觉得完事了。其实分支语句最大的风险,恰恰在于边界处。
以成绩等级为例:60分和59分、90分和89分,这些边界值决定了分支是否正确。我在练习时给自己定了一个规矩:每个分支语句都至少要测四类输入——临界值、临界值左右、中间值、完全不匹配的值。比如判断成绩等级,至少要测60、61、59、90、91、89、150这样几组数据。
练习方法看起来有点机械,但对培养程序思维很有帮助。分支语句的逻辑正确性不是“感觉”出来的,而是靠数据“试”出来的。Android开发的测试虽然不止这一种,但边界值意识贯穿着整个职业生涯。
6. 自学过程中的三个常见误区
6.1 误区一:只抄看视频的示例代码
我看Java4Android系列时,有一段时间的坏习惯是跟着视频敲示例代码。视频里老师敲一行,我敲一行,敲完运行出结果就很满足。但后来我发现,这种学习方式有个致命问题:代码是敲进去了,但敲完就忘了。整个过程更像是打字练习,而不是编程训练。
后来我开始改变策略:看完视频讲解后,先合上视频,自己根据需求写一遍代码,写不出来的地方再回去看视频是怎么实现的。遇到分支语句,我给自己规定至少写三个不同的练习题,其中至少一个要用到嵌套if,一个要用到switch,一个要混合使用if和switch。手写不出代码,才算真正学会。
这个方法听起来很死板,但确实是我用下来的最有效方式。特别是分支语句这种内容,语法点就那几个,但组合方式可以千变万化。只有自己亲手把它从零写出来,才算真正掌握了。
6.2 误区二:忽略JDK版本带来的语法差异
刚才已经提到过Java 7支持String类型的switch,Java 14引入了switch表达式。很多自学者用的教材或视频是基于某一种固定JDK版本录制的,但自己电脑上装的却是另一个版本。结果就是,你照着视频写的代码,在自己机器上编译不通过,你又不知道为什么。
我遇到的实际情况是:教程里还在用传统冒号加break的写法,但我的编译环境已经支持箭头写法了。自己尝试用箭头写法后,代码更简洁,但回到旧教程示例中又会觉得别扭。后来我才想明白:语法知识不必贪多,关键是先掌握一种当前环境下能跑通的写法,再了解版本演进方向即可。
对于目标是Android开发的读者,我的建议是:优先掌握传统switch写法,它兼容所有环境;同时了解新写法的存在,出去玩的时候知道有这么回事,但不急着全面切换。毕竟Android项目的Gradle配置和JDK版本,通常由项目环境决定,不是自己想换就能换的。
6.3 误区三:试图把所有的判断都写成分支
还有一种常见误区是“为了用而用”,明明可以用多态或者查表法解决的问题,非要写一大堆if-else。比如某种对象的类型分成三种,有人会这样写:
if (type == 1) { // 处理第一种 } else if (type == 2) { // 处理第二种 } else if (type == 3) { // 处理第三种 }这段代码没有错,但当“处理逻辑”本身很复杂时,这种做法会把一个方法撑得非常臃肿。更合理的方案可能是把每一种类型的处理都封装成独立的方法,然后用switch去做分发:
switch (type) { case 1: handleTypeOne(); break; case 2: handleTypeTwo(); break; case 3: handleTypeThree(); break; }甚至更进一步,可以用一个Map或者枚举来关联类型和处理逻辑,这样连分支语句都可以不用写。但这一步需要更多经验和知识积累,不是初学阶段必须掌握的。我想表达的意思是:分支语句是基础工具,但不是万能工具。随着代码越写越多,你会慢慢体会到什么场景适合用分支,什么场景适合用其他方案替代。
7. 我在练习分支语句时用得顺手的几个方法
7.1 用“三遍法”对抗遗忘
我的记忆力不算好,学了就忘是常事。为了对抗这东西,我自己发明了一个“三遍法”:
第一遍,看视频,跟着老师的代码敲一遍,体会语法的用法和逻辑。第二遍,关掉视频,只看题目要求,自己默写代码,写不出来的地方做标记。第三遍,隔天再写一遍,重点是上次标记的地方。三遍写完之后,分支语句的语法基本就刻进脑子里了。
这个东西的本质是:把一个知识点在短时间间隔内反复激活,强化记忆。第一遍是输入,第二遍是输出,第三遍巩固薄弱点。三个环节走完,覆盖率其实很高了。我学switch的case穿透时就是这样练的,至今都记得“break忘了写”是个多大的坑。
7.2 日常练习题的三个设计方向
如果你不知道练什么,我建议你可以围绕三个方向自己出题:
第一个方向是数值判断。比如输入月份输出天数,输入分数输出等级,这类题目能练到多重if和边界值的把握。第二个方向是菜单选择。模拟一个命令行菜单,输入数字执行不同功能,这类题目能练到switch和break配合。第三个方向是条件组合。模拟一个登录场景,要求用户名不为空且密码长度大于6且验证码匹配,这类题目能练到复合条件和短路运算。
这三个方向渐次递进,从单层判断到多层选择再到复杂条件组合,恰好覆盖了分支语句的主要应用形态。我在自学前期,几乎就是靠这类题目撑过来的。虽然这些题看起来“很学生气”,但真的能把基础夯实。
7.3 代码注释:写给未来的自己看
最后一个经验是写注释。我刚开始写分支语句的时候,总认为代码那么短,一目了然,写注释是多余的。但后来回头看自己两周前的代码,竟然会忘记当时为什么要设置某个判断条件。尤其是那些处理异常情况的分支,如果没有注释,完全想不起来是防什么用的。
现在的习惯是:凡是非显而易见的条件判断,都会在if那一行上方写一行中文注释,说明这里为什么要做这个判断。比如:
// 服务器返回的code为-1时,表示token过期,需要重新登录 if (code == -1) { // 跳转到登录页面 }这段注释不写,代码也能运行。但写了之后,两周后回来维护代码的自己会感谢当时的自己。尤其是面试时如果被问到项目里某个分支逻辑的来龙去脉,注释就是最清晰的记忆线索。
我在实际练习中还将注释与git提交记录互相配合:每次修改分支逻辑后,都会在commit message里写清楚“为什么改”。这样一来,即使注释写漏了,版本历史也会告诉你当时发生了什么。这个习惯用久了以后,会发现自己写代码的思路越来越清晰,分支语句里的每一个分支都变得有据可依,而不是拍脑袋拍出来的。