不少人问我Java到底该怎么练,我一般会先反问一句:你最近有没有完完整整写过一百条不重样的条件判断?对面经常愣住。Java语句练习这件事,看起来只属于入门阶段,可实际面试和工作中,翻车最多的恰恰是语句层面的基本功——switch穿透、循环里改集合、异常被吞、SQL语句拼接注入,哪一个不是语句组织能力出了问题。所以这篇内容不打算讲语法百科,而是想把“Java语句练习”当成一套可以照着做的能力训练方案,从控制流、输入输出、集合遍历、Stream、JDBC里的SQL语句一路练到笔试面试题,每一段都给出具体写法和避坑经验。
1. 先把“语句练习”从入门任务升级成能力指标
1.1 语句才是程序里真正“可运行”的最小单位
很多初学者会把Java学习拆成变量、运算符、流程控制、数组、面向对象这样一条条知识点去背,结果背会了却写不出程序。问题出在,知识的最小单位是“概念”,可程序的最小可运行单位是“语句”。变量声明是语句,赋值是语句,if判断是语句,循环是语句,方法调用是语句,异常处理也是语句。只有当你能连续写出几十条逻辑正确的语句,程序才可能跑起来。
我见过不少人学面向对象时很投入,类、继承、多态背得滚瓜烂熟,一让他写一个从文件中读取数字并求平均值的程序,他会在main方法里卡住半小时。原因很简单:对象的语法会了,但语句的组合不会。Java语句练习的真正目的不是背语法,而是建立一种条件反射——看到“读取输入”就知道要准备Scanner或BufferedReader,看到“需要重复处理”就知道该上for还是while,看到“可能出错”就知道要考虑try-catch或throws。
这种条件反射只能靠大量手写语句来训练。我的建议是,不要只看教程里的示例,要把示例改成自己的话重写一遍。比如教程写了一个for循环打印1到10,你就改成打印1到100之间的偶数,再改成倒序打印,再改成断点观察每一步i的值。改的过程就是在练语句边界。
1.2 我判断语句熟练度的三个标准
参加过不少面试,也带过新人,我慢慢总结出判断一个人语句熟练度的三个标准,你可以拿来自测。
第一条,拿到一个需求,能不能在10秒内说出“核心语句长什么样”。比如“统计一个字符串里每个字符出现次数”,核心语句应该是Map<char,Integer>配合循环,再配合getOrDefault或merge,而不是一上来先想类怎么设计。第二条,知不知道每条常见语句的“副作用”。i++和++i的区别、for循环里i<list.size()会反复调用方法、switch不写break会穿透、finally里写return会覆盖try里的return,这些都属于语句级的坑。第三条,能不能把输入、处理、输出三段组织得干净清晰。哪怕只是几十行的练习程序,也应该像一个小型流水线,而不是所有逻辑堆在一起。
这三条标准对应三种练习方向:需求到语句的映射、语句本身的边界条件、语句之间的组合顺序。后面几章的内容就是围绕这三条展开的,你可以对照自己的薄弱环节跳着看。
2. 分支与循环:从语法正确到边界正确
2.1 别小看if-else:语句覆盖不是玄学
Java语句练习里,最容易被忽视的就是if-else。很多人在公司里写代码,一个if后面跟了四五个else if,看起来功能正常,实际上分支覆盖严重不足。面试题里考“语句覆盖”这个词,说白了就是:你得保证每条语句都至少被执行过一次。
举个例子,你写了一个根据分数返回等级的方法:
public String getGrade(int score) { String grade; if (score >= 90) { grade = "A"; } else if (score >= 60) { grade = "B"; } else { grade = "C"; } return grade; }如果只测score=95,你只覆盖了第一个if分支,后面两行else if和else里的语句根本没执行。语句覆盖要求的是“每条语句都跑到”,看起来很容易,实际上一旦分支变多,漏测的情况非常常见。
练习的时候我建议你做一件事:把每个if条件里的边界值、区间中点、非法值全部列出来。比如上面的分数,至少要测-1、0、59、60、89、90、100、101。不要觉得这些值太极端,很多线上Bug就是边界值没判断。另一个容易出问题的是判空语句,操作字符串或集合之前先if (str != null && !str.isEmpty()),这种“防御性语句”练得多了,空指针自然就少了。
2.2 switch、for、while的边界条件练习清单
switch语句是另一个值得刻意练习的点。老式switch的case穿透问题,几乎每届面试都会问到。你写一个switch,case里没有break,执行完一个分支会继续往下跑,这到底是Bug还是特性?如果是故意利用穿透做统一处理,那叫特性;如果忘了写break,那纯属事故。Java 14之后有了箭头语法,不再需要break,可很多老项目还是老写法,所以两种都得会。
switch (day) { case 1 -> System.out.println("周一"); case 2, 3, 4, 5 -> System.out.println("工作日"); default -> System.out.println("周末"); }循环语句的边界条件更是重灾区。for (int i = 0; i < 10; i++)和for (int i = 0; i <= 9; i++)虽然结果一样,但第二个写法容易被后续修改带偏。练习时建议把区间端点、步长、反向遍历都写一遍。比如把一个数组倒序输出,你有几种写法?for循环从length-1到0,Collections.reverse后再遍历,或者用IntStream.rangeClosed配合mapToObj,每一种写法的边界都不一样。
循环里还有一个经典坑:在增强for循环里直接删除集合元素会抛ConcurrentModificationException。正确做法是用Iterator的remove,或者收集要删的元素最后统一removeAll。这个坑我在分享里提过很多次,因为新手几乎必踩,踩过一次之后就会记住“遍历时不要改结构”这条语句级铁律。
3. 输入输出与异常处理的语句组合拳
3.1 Scanner的换行陷阱与格式化输出
Java语句练习到一定阶段,你会发现输入输出语句才是真正的拦路虎。最典型的就是Scanner的nextInt和nextLine混用问题。假设你写了这样一段练习代码:
Scanner sc = new Scanner(System.in); System.out.print("请输入数字:"); int num = sc.nextInt(); System.out.print("请输入字符串:"); String line = sc.nextLine();运行时会发现,第二行的nextLine直接返回空字符串,根本没等你输入。原因很简单:nextInt只读取了数字,数字后面的换行符还留在缓冲区里,nextLine一上来就吃掉了那个残留的换行符。解决办法有两个,要么在nextInt之后额外调一次sc.nextLine()把这个换行消费掉,要么统一用nextLine读取后再用Integer.parseInt转换。
格式化输出也是练习重点。System.out.println适合调试,真正到报表、日志场景,更多用printf或String.format。比如输出一个表格,对齐方式、保留小数位,组合起来就是一组非常实用的语句练习:
System.out.printf("%-10s %8.2f%n", "苹果", 3.14159); System.out.printf("%-10s %8.2f%n", "香蕉", 2.0);练习时可以给自己出一个综合任务:读取一个CSV文件,每行按逗号拆分成字段,过滤掉空行,统计每一列的最大值和平均值,最后格式化输出结果。这个任务会用到循环、分支、字符串拆分、类型转换、格式化输出,几乎把基础语句全练遍了。
3.2 try-with-resources和异常恢复的写法
异常处理语句是Java里最容易被写错的“语句组合”。很多初学者写try-catch,catch里就一句e.printStackTrace(),看起来程序不崩了,实际上异常被吞得干干净净,线上排查时一头雾水。
正确的异常处理至少要分三步:第一步,想清楚这个异常当前能不能处理。能处理,比如用户输入了非法数字,就catch住并给出提示,让程序继续往后走;不能处理,就往上抛,用throws或throw。第二步,catch之后要记录足够的信息,只printStackTrace在本地练习可以,在真实项目里要把日志打全。第三步,资源释放必须用try-with-resources,不要再手动在finally里close,代码不仅短,还能自动处理关闭顺序。
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { String line; while ((line = reader.readLine()) != null) { // 处理每一行 } } catch (IOException e) { System.err.println("读取文件失败:" + e.getMessage()); }这里有一个面试常考的坑:finally里的return会覆盖try或catch里的return。你只要记住一句话,finally是无论如何都会执行的,所以不要在finally里写return或者改变返回值。练习的时候我建议你故意写一段“finally里改值”的代码,然后单步调试,亲眼看看返回值是怎么被覆盖的,比背十遍理论都有用。
异常语句的进阶练习可以围绕“自定义异常”展开。比如写一个账户取款程序,余额不足时抛一个InsufficientBalanceException,然后在调用处捕获并提示。这个练习能帮你理解throw和throws的使用边界,也能体会“用异常控制流程”和“用if判断控制流程”的区别。
4. 集合遍历与Lambda:一句话把逻辑说清楚
4.1 从for-each到Stream的思维切换
集合遍历是Java语句练习里最有意思的部分。早期版本写循环遍历,代码长且重复;Java 8之后有了Lambda和Stream,可以用一条语句表达完整的数据处理逻辑。但很多人从传统写法切到Stream时很不适应,因为思维模式变了。传统写法是“怎么一步一步做”,Stream写法是“我想得到什么结果”。
练习时可以拿同一个需求对比两种写法。比如把一个字符串列表里的空字符串去掉,转成大写,按长度排序,取前三个:
传统写法:
List<String> result = new ArrayList<>(); for (String s : list) { if (s != null && !s.trim().isEmpty()) { result.add(s.toUpperCase()); } } result.sort((a, b) -> Integer.compare(a.length(), b.length())); List<String> top3 = result.subList(0, Math.min(3, result.size()));Stream写法:
List<String> top3 = list.stream() .filter(s -> s != null && !s.trim().isEmpty()) .map(String::toUpperCase) .sorted(Comparator.comparingInt(String::length)) .limit(3) .collect(Collectors.toList());第二种写法读起来像一句自然语言,每个中间操作就是一条“子语句”。练习时不要满足于能跑,要能解释每一步的类型。比如filter之后还是Stream,map把Stream 转换成了Stream ,sorted如果没有传比较器就按自然顺序排,limit一旦截断就不再往后取数据。这些细节在面试里经常被追着问。
4.2 常见Stream语句调试与性能误区
Stream虽好,写起来也会踩坑。第一个坑是Stream只能消费一次。Stream对象是一个管道,你用了一次终止操作之后,再对同一个stream调用collect就会报IllegalStateException。第二个坑是惰性求值。中间操作不会立刻执行,只有遇到终止操作才开始流水线计算,所以如果你只是调用stream.filter()却没写collect或forEach,那过滤逻辑根本不会发生。第三条是关于外部变量。Stream的lambda里如果要修改外部的局部变量,变量必须是final或 effectively final,否则编译都过不去。
我建议调试Stream时多用peek这一招:
list.stream() .peek(x -> System.out.println("原始值:" + x)) .filter(x -> x > 10) .peek(x -> System.out.println("过滤后:" + x)) .toList();看到每一步的输入输出,语句的问题就非常清楚了。但是注意,peek是中间操作,如果没有终止操作,它依然不会执行。
不要为了秀技术而滥用Stream。如果一个数据处理逻辑超过三个中间操作,或者涉及异常处理,传统循环反而更清晰。比如在map里做解析数字,可能抛出NumberFormatException,Stream里处理受检异常很别扭,写成for循环加try-catch就简单多了。语句练习的最终目标是“用最合适的语句解决问题”,不是“用最高级的语句炫技”。
5. 在Java里练SQL语句:从crud到防注入
5.1 JDBC连接中一条SQL语句的完整生命周期
Java语句练习绕不开SQL,因为业务系统里最常见的语句其实就是Java代码里拼出来的SQL语句。很多人在练习阶段不重视这块,到了工作里写个简单的增删改查都要折腾半天。你得把一条SQL语句放进Java程序里,让它真正连上MySQL执行,才算完成了闭环。
最基本的顺序是:加载驱动、建立连接、创建语句、执行SQL、处理结果集、关闭资源。用Java语句写出来大概是:
String url = "jdbc:mysql://localhost:3306/school"; String user = "root"; String password = "123456"; try (Connection conn = DriverManager.getConnection(url, user, password); PreparedStatement ps = conn.prepareStatement("SELECT * FROM student WHERE age > ?"); ResultSet rs = ps.executeQuery()) { ps.setInt(1, 18); while (rs.next()) { System.out.println(rs.getString("name") + " - " + rs.getInt("age")); } } catch (SQLException e) { e.printStackTrace(); }这里有几个语句级别的细节。Connection、PreparedStatement、ResultSet都实现了AutoCloseable,所以用try-with-resources可以省去手动关闭的麻烦,而且关闭顺序是自动的:先关ResultSet,再关PreparedStatement,最后关Connection。另一个细节是占位符的索引从1开始,不是0,容易和数组搞混。
5.2 预编译语句、事务边界和in查询报错
SQL语句练习最容易踩的坑就是拼接字符串。很多人一开始会写成:
String sql = "SELECT * FROM student WHERE age > " + age;这种写法在练习阶段确实能跑,但有两个问题:第一,SQL注入风险,如果age来自用户输入,用户输入“20 OR 1=1”就会把整张表查出来;第二,每次执行都要让数据库重新编译SQL,性能差。正确的做法是使用PreparedStatement占位符,这属于“语句安全”的基本功。
还有in查询报错的问题在热搜里很常见。你写“SELECT * FROM student WHERE id IN (?)”,然后想用ps.setString(1, "1,2,3")把一组ID传进去,执行时会发现查不出数据,因为数据库拿到的不是三个值,而是一个字符串“1,2,3”。正确做法是动态生成占位符:
List<Integer> ids = List.of(1, 2, 3); String placeholder = ids.stream().map(id -> "?").collect(Collectors.joining(",")); String sql = "SELECT * FROM student WHERE id IN (" + placeholder + ")"; try (PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < ids.size(); i++) { ps.setInt(i + 1, ids.get(i)); } try (ResultSet rs = ps.executeQuery()) { // 处理结果 } }这个例子把Java的Stream语句和SQL语句结合起来了,是很经典的综合练习。
事务控制的语句顺序也值得专门练。JDBC默认是自动提交,也就是每一条SQL执行完就立刻生效。如果你需要多条SQL作为一个整体,比如转账时扣款和加款必须同时成功或同时失败,就要先设置setAutoCommit(false),然后执行多条SQL,最后再commit;任何一步出错就rollback。练习时要注意一个隐蔽问题:事务提交后Connection会被放回连接池,如果中间某条SQL抛了异常,一定要在finally里做回滚,而不是只输出日志。
6. 面试和笔试中的语句题:八股文背后的真实考点
6.1 语句覆盖、分支覆盖和典型笔试陷阱
Java语句练习不只是为了写业务代码,更是为了应付面试和笔试。近年来的Java面试题越来越卷,“八股文”满天飞,但剥开包装之后,你会发现核心还是在考语句级理解。
语句覆盖这个概念我在第2章提过,面试时你会遇到类似问题:有一段包含if、while、return的代码,让你至少设计几条测试用例才能达到100%语句覆盖。这个问题的本质是考察你有没有“每条语句都会被走到”的意识。但要注意,语句覆盖并不等于分支覆盖,更不等于条件覆盖。有些Bug藏得很深,比如if (a || b)这样的复合条件,语句覆盖只需要让这条if语句执行一次,但分支覆盖要求a为真和b为真两种情况都测到,条件覆盖则要求a和b的每一个取值组合都被覆盖到。这类题目在测试岗位面试中出现频率很高,开发岗位也喜欢用来考“你写的代码自己测过没有”。
另一个高频考点是程序输出题。看起来在考语法,其实考的是语句执行顺序。比如:
int i = 0; System.out.println(i++ + ++i);很多人以为是1或2,实际结果是2:先取i=0参与加法,i变成1,然后++i变成2,0+2=2,最终i变成2。再比如switch的穿透输出、try-catch-finally的返回顺序、String用+拼接时到底创建了多少个对象,这些都是语句级题目。我的经验是,遇到这种题不要凭感觉,拿IDE跑一遍,再用JShell单步执行,看变量的实时变化,多跑几次自然就形成条件反射了。
6.2 两周练习计划与自查清单
最后给出一份可以直接照做的两周Java语句练习计划,适合正在备考或想系统查漏补缺的人。第一周以基础语句为主:周一到周三,每天手写20个分支判断和循环练习,重点是边界值和异常输入;周四周五,集中练字符串和集合相关的循环遍历,把“遍历时删除”“字符串拼接性能”这些坑都亲手踩一遍;周六周日,练习输入输出和异常处理,写一个完整的文件读取、解析、统计、输出程序。
第二周进阶:周一周二,把第4章的集合遍历和Stream练习过一遍,每种传统写法都改成Stream写法,再把Stream写法改回传统写法;周三周四,集中练JDBC和SQL语句,从单表CRUD到多表查询,再到预编译防注入、事务回滚;周五周六,刷20道程序输出题和语句覆盖题,每道题都用IDE验证;周日,把两周踩过的坑整理成一份自己的“语句避坑清单”,以后写代码之前先扫一眼。
自查清单可以很具体:能不能在5分钟内写出一个带PreparedStatement的增删改查类?能不能解释try-with-resources为什么比手动close强?能不能用一条Stream表达式完成过滤、排序、截取、收集?如果这三件事都能做到,说明你的Java语句练习已经过关了。
最后再分享一个我自己的习惯:每天抽10分钟,找一段别人写的Java代码,用更精简的语句重写一遍。不一定非要比原版好,重要的是在改写中理解每条语句的作用。练得多了你会发现,Java语句练习练的从来不是语法,而是把脑子里想做的事,准确、安全、高效地翻译给计算机听。