qinghuan
← Java SE / 全部笔记

单元 04 / Java SE

P0 · 必会主线L0 · 零基础

控制台输入、条件与循环:写一个可交互程序

读取一行输入,检查并转换成数据,再通过条件和循环完成一个能反复交互的小程序。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能编写接受输入并有明确停止条件的命令行程序,覆盖非法输入与边界。

学习理由、准备要求与知识点

为什么学它

输入校验、分支和循环组成几乎所有程序的基本执行结构。

先读这些内容

知识点清单

  • 标准输入与输出
  • Scanner token 与整行
  • 输入类型和范围验证
  • if/else 分支
  • for 执行顺序
  • while 与 do-while
  • break/continue/return
  • 零次与末次循环边界

运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。

01先弄懂它是什么

用户在控制台输入 80,程序首先读到的是字符,需要转换后才能把它当成整数计算。新手可以先读取整行,再判断它是数字、命令还是结束信号。Scanner 的 nextInt 按标记读,nextLine 按行读,混用时剩下的换行可能让下一次得到空行;统一按行读取后再解析,更容易看清处理顺序。

if 和 else 根据条件选择一条路径,多个独立 if 则可能分别执行。需要重复处理时再用循环:for 常用于已知次数,while 常用于达到某个状态才停止,do-while 至少执行一次。写循环前先说清从哪里开始、什么时候继续、每一步怎样靠近结束,再填写语法,能减少一直循环或少处理一项的问题。

一个可操作练习是读取多行分数,输入 q 结束;每一行先去除首尾空白,再检查是否为结束命令,再尝试解析,最后校验分数范围。格式错误与范围错误应给出不同反馈。总分与人数只在输入完全通过后更新,避免把无效记录算进平均分。读到输入流末尾也应退出,不应假设用户永远会输入 q。

文中这几个词,先认一认

分支
根据条件选择执行哪段代码,例如及格走一条路径、不及格走另一条。
循环不变量
每轮循环的某个固定位置都应成立的规则,用来检查结果是否一直正确。
哨兵值
事先约定的特殊输入,例如输入 q 表示结束,而不是把它当普通数据处理。
输入边界
程序接收外部输入时做转换和校验的位置,通过后再交给后面的业务代码使用。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

一次循环应尽量遵守先读、再判、最后更新的顺序。结束信号不参与业务统计;解析失败不推进计数;合法值才更新累计状态。这三个分支若混在一起,很容易出现一个输入被处理两次、错误值影响结果或结束命令触发解析异常。图把这些节点分开,帮助检查每条路径是否最终返回读取或明确退出。

循环终止需要可说明的进展条件。遍历数组时下标逐次增加且上界固定;交互输入时每轮消耗一行,结束依赖命令或输入结束。break 结束最近一层循环,continue 跳过本轮剩余部分;如果更新下标写在 continue 之后,就可能永远停在同一项。嵌套循环还需明确每一层的控制变量,避免内层意外修改外层边界。

检查数字输入时,要先说清接受什么:能不能带符号、能不能留空、允许哪些数字字符、数值范围是多少。不能只让输入通过一个宽松的正则,就认为它一定合法。Integer.parseInt 会拒绝格式不符或超出 int 范围的输入;它接受的字符比下面仅接受 ASCII 的教学模型更广。读取输入时可以先处理首尾空白,解析时明确是否允许正号,最后再检查业务范围,例如零到一百分。

正在加载流程图…

结束、解析失败和有效输入走不同路径,统计状态只在验证完成后变化。
查看流程图源码(Mermaid)
flowchart TD
 A["读取一行"] --> B{"输入结束或为 q"}
 B -->|"是"| C["结束并显示统计"]
 B -->|"否"| D["解析十进制整数"]
 D --> E{"格式和范围合法"}
 E -->|"否"| F["显示错误原因"]
 F --> A
 E -->|"是"| G["更新计数与累计值"]
 G --> A

按顺序看,每一步做了什么

  1. 确定输入单位是整行还是标记。
  2. 定义结束命令和输入结束行为。
  3. 区分格式校验与业务范围。
  4. 只在合法分支更新状态。
  5. 核对每条继续路径都能消耗输入或推进下标。

03对照实现代码

十进制解析为什么需要边累积边检查 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Integer.java · java.lang.Integer#parseInt(String,int)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
public final class SourceModel {
    public static int parseAscii(String text) {
        if (text == null || text.isEmpty())
            throw new NumberFormatException("empty");
        int i = 0, sign = 1;
        if (text.charAt(0) == '-' || text.charAt(0) == '+') {
            sign = text.charAt(0) == '-' ? -1 : 1;
            i++;
        }
        if (i == text.length()) throw new NumberFormatException("sign only");
        long value = 0;
        long limit = sign < 0 ? 2147483648L : 2147483647L;
        for (; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c < '0' || c > '9') throw new NumberFormatException("digit");
            // 先确认下一次乘十加位不会超过约定上界。
            int digit = c - '0';
            if (value > (limit - digit) / 10)
                throw new NumberFormatException("overflow");
            value = value * 10 + digit;
        }
        return (int) (sign * value);
    }
}

真实 parseInt(String,int) 先校验输入与进制,再处理可选符号和数字。源码采用负数累积,以应对 int 最小值的绝对值不能放入正 int 的不对称范围。模型选择 long 保存非负幅度,目的相同但实现路线不同;同时只支持 ASCII 十进制,不能用它替代真实 API 的全部字符和进制语义。

每读一位,计算就是旧值乘以进制,再加新数字。范围必须提前检查,等有限位数的整数已经溢出再看结果,就丢失了原来的数学值。模型先用“上界减当前位,再除以十”判断旧值是否还能安全推进,所以即使输入很长,也不会先让中间 long 溢出再报错。

只有符号而没有数字不是合法整数。空串、null 和非法字符也都应走明确失败路径,不能默认返回零。零是业务上常见的合法输入,若错误也返回零,调用者就无法知道用户真的输入了零还是解析失败。交互层可以捕获格式异常后继续读取,但不应修改成功计数。

实际 parseInt 的异常信息、进制支持和字符转换比模型更多。阅读源码时要关注校验顺序和范围推导,并通过公开 API 文档确认契约;不要根据教学代码推断 Unicode 数字是否全部被支持。把格式解析从业务范围校验分开,也能让单元测试分别覆盖两类错误。

代码先调用谁,再调用谁

  1. parseInt 验证输入与 radix
  2. 读取可选正负号
  3. 转换每个数字并检查累积边界
  4. 返回有符号结果或抛格式异常

04动手写一个例子

单次扫描统计有效分数并在哨兵处停止

输入 int 数组,-1 表示停止;零到一百为有效分数,其余值计入无效数。返回有效数量、总分和无效数量,哨兵后数据不再处理。

输入 1
[80,101,60,-1,100]
输出 1
[2,140,1]
输入 2
[]
输出 2
[0,0,0]
输入 3
[-1,80]
输出 3
[0,0,0]

状态只有有效数量、总分与无效数量。遍历到一项时先判断是否为哨兵,再区分合法范围和无效范围。哨兵检查放在范围检查之前,避免把结束标记计为错误。数组只代表已经完成解析的输入,字符格式错误属于上一层,不混进本算法。

在每轮开始时,三个计数恰好描述已处理前缀,且前缀中不包含哨兵。遇到有效值只更新有效数和总分,遇到无效值只更新错误数;因此每个普通元素恰好贡献到一种类别。空数组与哨兵在开头都返回全零,不需要特判平均分。

总分使用 long,是因为数组元素数量增长时 int 总分可能不够;即使单个分数最大只有一百,也不能只根据单个值的范围选择累计类型。平均值可以在外层判断有效数非零后计算。本题返回原始统计,避免把没有数据和平均零分混为一谈。

为什么这样写是对的每轮开始时,统计结果准确覆盖当前下标之前、首个哨兵之前的全部元素。

时间 O(k),k 为首个哨兵之前的元素数,最坏 O(n)。额外空间 O(1) 额外空间。

每个已读元素仅做常数次判断与加法,固定三个 long 状态和长度为三的结果数组。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 33 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.Arrays;
public class Main {
    static long[] summarize(int[] scores) {
        if (scores == null) throw new IllegalArgumentException("scores");
        long valid = 0, sum = 0, invalid = 0;
        for (int score : scores) {
            // 哨兵必须在范围判断前处理,不计入任何统计。
            if (score == -1) break;
            if (score >= 0 && score <= 100) {
                // 只有范围合法的分数进入有效统计。
                valid++;
                sum += score;
            } else invalid++;
        }
        return new long[]{valid, sum, invalid};
    }
    static void check(long[] wanted, int... input) {
        if (!Arrays.equals(wanted, summarize(input)))
            throw new AssertionError(Arrays.toString(input));
    }
    public static void main(String[] args) {
        // 正常、空、立即停止、边界分数与无效负值。
        check(new long[]{2,140,1}, 80,101,60,-1,100);
        check(new long[]{0,0,0});
        check(new long[]{0,0,0}, -1,80);
        check(new long[]{2,100,0}, 0,100);
        check(new long[]{0,0,2}, -2,101);
        boolean rejected = false;
        try { summarize(null); } catch (IllegalArgumentException e) { rejected = true; }
        if (!rejected) throw new AssertionError("null");
        System.out.println("PASS 6");
    }
}

这些特殊情况也要试一试

  • 结束标记不计入无效数量。
  • 哨兵后所有数据忽略。
  • 零与一百都是合法分数。
  • null 参数拒绝,空数组合法。
进阶选读 · 可选深入:流式输入与背压

场景输入记录持续产生,不能一次全部装进数组。

做法保留同样累计状态,按记录逐步处理,并给队列设置容量和取消信号;如果统计窗口有时限,再加入窗口淘汰规则。

复杂度累计每条 O(1);滚动窗口可能额外保存 O(w) 记录。

好处与代价流式处理节省整批内存,但需要明确中断、重试、重复记录和部分结果语义。

05项目里怎么用

命令行工具需要把正常输出与错误诊断分开,方便调用者重定向和自动解析。对用户输入的错误可说明哪一行、哪种格式不对,但不要把整个内部异常栈作为日常交互提示。对程序缺陷则保留足够诊断;两类失败的处理目标不同,不能统一吞掉后继续输出一个貌似正确的结果。

无限等待输入有时是正常交互,有时是批处理挂起。应定义输入结束、取消和超大记录的处理方式,对来自文件或管道的数据尤其如此。关闭 Scanner 会关闭其底层输入源,封装方法若接收调用者提供的 System.in 包装对象,应明确由谁负责关闭,避免一个小功能结束了整个进程的后续输入。

统计逻辑与控制台读写分离后,可以用固定数组测试业务状态,不需要在自动测试中人工键盘输入。实际整行解析还应额外测试空白、符号、超长文本和换行差异。算法自测验证的是数组统计契约,不能替代交互层的输入解析与退出行为检查。

容易踩的坑

  • 混用 nextInt 与 nextLine 却忽略剩余换行。
  • continue 跳过了推进下标的语句。
  • 结束标记参与计数或求和。
  • 无有效记录时直接计算平均分。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
整行读取后解析格式边界清楚且便于反馈需要显式处理解析异常交互命令与文本记录
按标记读取简单空白分隔数据方便混用行读取容易出错规则稳定的小型输入
纯函数统计加外部 IO逻辑容易测试与复用需要拆分接口可维护的批处理和命令行工具

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01多个 if 与 if-else 有什么差别?

展开参考答案与推导

多个独立 if 会逐个判断,可能执行多个分支;if-else 链选择其中一条路径。判断有效、无效与结束时应明确互斥关系,避免一个输入同时更新多个统计。

02循环为什么需要不变量?

展开参考答案与推导

它把复杂执行过程压缩成每轮都成立的陈述。只要初始成立、每次更新保持它,并在结束时推出目标,就能解释结果正确;测试只能检查选定样本,不能替代这个推理。

03为什么解析失败不直接返回零?

展开参考答案与推导

零是合法数值。错误也返回零会丢失失败信息,使调用者无法决定重试还是统计;应以异常或明确结果类型表示失败,再由输入层决定提示和继续方式。

换个情况,还会处理吗

01用户输入 q 后总人数增加了一,哪里可能错?

展开参考答案与推导

检查计数更新是否发生在结束命令判断之前。结束标记应直接退出读取循环,不参与任何业务状态;用哨兵在首项和中间两种测试固定这一行为。

02循环遇到无效值后一直打印同一行,怎么定位?

展开参考答案与推导

检查 continue 是否跳过了读取下一行或下标递增。每条继续路径都必须推进输入状态;可以在读取处与 continue 前观察当前行标识,确认是否消费了新输入。

03分数都合法但总分溢出,为什么?

展开参考答案与推导

累计值范围取决于记录数量乘以单项上限,不只是单项范围。先推导最大记录数与总分,再选择 long 或精确累积;对无限输入仍需考虑长期增长边界。

写代码试一试

01增加平均分结果

输入 / 条件
[0,100]
输出 / 预期
50.0

提示 先判断有效数量。

展开参考答案与推导

在 valid 非零时以 (double)sum / valid 计算平均,零记录时返回单独的缺失状态。不要把无记录返回零,否则会与确有记录且全部零分混淆。

02让无效输入立即失败

输入 / 条件
[80,101,60]
输出 / 预期
在位置 2 报错

提示 为遍历保存下标。

展开参考答案与推导

把无效分支改为抛带下标的参数异常,或返回包含错误位置的结果。该变更改变原先“累计无效数并继续”的契约,必须同步修改测试和调用者预期。

03验证重复哨兵

输入 / 条件
[80,-1,-1]
输出 / 预期
[1,80,0]

提示 首个哨兵已经终止。

展开参考答案与推导

加入 check(new long[]{1,80,0},80,-1,-1)。后续哨兵根本不会被读取,不能增加错误数;这能检查循环使用的是 break 而非仅跳过当前项的 continue。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能编写接受输入并有明确停止条件的命令行程序,覆盖非法输入与边界。

本篇重点加练

重点加练输入校验与循环首尾边界,检查程序面对未按预期结束的输入是否仍可控。

  • 输入类型和范围验证
  • 零次与末次循环边界

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01排错实验

    读取“年龄+一整行昵称”的控制台输入,样本包含 18 后换行再输入“清 欢”、非法年龄文本及空昵称。记录每次读取实际消费的内容并制定重试合同。

    提交内容
    Java 小实验、自写输入样本与运行记录。
    怎样算完成
    • token 与整行边界明确
    • 非法输入不造成无限循环

    练到的知识点标准输入与输出 · Scanner token 与整行 · 输入类型和范围验证

  2. 作业 02代码推演

    写出三档成绩分支以及从 1 到 n 的 for 循环,对 n=0、1、4 逐轮列出初始化、条件、循环体、更新;自行核对等号边界。

    提交内容
    分支覆盖表和循环轨迹。
    怎样算完成
    • 每个边界分支被覆盖
    • 循环执行顺序完整

    练到的知识点if/else 分支 · for 执行顺序

  3. 作业 03原理问答

    为“至少询问一次密码”和“输入可能立即结束”分别选择循环合同,说明 break、continue、return 在嵌套方法与循环中结束的范围。

    提交内容
    两份伪代码与控制流图。
    怎样算完成
    • 停止条件可达
    • 三种控制语句作用范围准确

    练到的知识点while 与 do-while · break/continue/return

  4. 作业 04代码推演

    为 while 与 do-while 各设计“初始条件不满足”和“最后一轮恰好达到边界”的输入,逐轮写出条件与状态,不使用旧题的分数统计。

    提交内容
    两类循环轨迹和边界表。
    怎样算完成
    • 零轮与至少一轮分别观察
    • 末轮状态和停止理由明确

    练到的知识点零次与末次循环边界

综合应用

  1. 作业 05编程实践

    实现最多接收五个有效分数的统计程序,分数范围 0 到 100,可输入 quit 结束;非法输入不计入有效数量,未输入有效值时单独处理。

    提交内容
    完整 Java 代码、自写零项/五项/非法输入/提前结束测试和运行证据。
    怎样算完成
    • 有效计数与读取次数分开
    • 退出条件均可测试
    • 空数据无未定义计算

    练到的知识点标准输入与输出 · 输入类型和范围验证 · if/else 分支 · 零次与末次循环边界

  2. 作业 06面试场景

    程序要求处理十条记录却只处理九条,加入 continue 后又可能不结束。设计两个最小失败循环并逐轮定位,修复后保留证明边界的测试。

    提交内容
    失败与修复代码、轨迹表及测试记录。
    怎样算完成
    • 推进变量在各路径得到审查
    • 首轮末轮与零轮均考虑

    练到的知识点for 执行顺序 · while 与 do-while · break/continue/return · 零次与末次循环边界

  3. 作业 07编程实践

    实现最多三次机会输入 1 到 9 之间整数的程序,合法即结束;输入流可能提前结束,非法文本与越界数字分别记录,不使用数组或集合。

    提交内容
    Java 程序、自写首次成功/最后成功/提前结束测试和运行证据。
    怎样算完成
    • 尝试次数有独立计数
    • 所有退出入口可检查

    练到的知识点输入类型和范围验证 · 零次与末次循环边界

  4. 作业 08设计评审

    命令行容量输入允许前后空白,但不允许负数、小数或超范围值。制定读取、解析与范围检查的合同,以及每类失败后是否继续。

    提交内容
    输入语法与交互失败表。
    怎样算完成
    • 文本有效与数值合法分开
    • 输入失败不产生未定义状态

    练到的知识点输入类型和范围验证

进阶挑战 · 掌握前置后选做

  1. 作业 09编程实践

    完成方法后:编写菜单式学习记录器,只支持新增次数、显示次数和退出,非法选项最多连续三次后终止;明确成功操作是否重置错误计数。

    提交内容
    Java 程序、交互规则、自写状态序列测试与运行证据。
    怎样算完成
    • 正常与错误输入状态可追踪
    • 无无限等待或无界输入存储

    练到的知识点输入类型和范围验证 · if/else 分支 · while 与 do-while · 零次与末次循环边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    交互程序改为从预先准备的有限文本读取,不能再次询问用户。重新审查输入失败、提前结束与最大次数的合同,比较两种模式。

    提交内容
    两模式状态图与相同样本对照。
    怎样算完成
    • 交互前提变化清楚
    • 终止性与结果完整性均有判据

    练到的知识点输入类型和范围验证 · 零次与末次循环边界

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗