本章导读 :输入与输出(I/O)是程序与外部世界交互的桥梁。没有输入,程序便无法接收数据;没有输出,程序运行的结果便无法被感知。本章将深入剖析 Java 的标准输入输出机制,从控制台输出
System.out的基础用法,到Scanner类的各种输入技巧,再到命令行参数、缓冲流和生产环境中的输入输出最佳实践,帮助你构建起完整的 Java I/O 知识体系。
一、Java 标准输入输出概述
1.1 什么是标准 I/O
在操作系统中,每个进程启动时都会默认打开三个标准流:
| 标准流 | Java 中的常量 | 设备方向 | 描述 |
|---|---|---|---|
| 标准输入 | System.in |
输入 | 默认连接键盘,用于接收用户输入 |
| 标准输出 | System.out |
输出 | 默认连接显示器/终端,用于输出程序结果 |
| 标准错误 | System.err |
输出 | 默认连接显示器/终端,用于输出错误信息 |
这三者构成了 Java 程序中最基础、最常用的输入输出方式。下图展示了它们的流向关系:
#mermaid-svg-zJd6AyDPsDX7r1qU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zJd6AyDPsDX7r1qU .error-icon{fill:#552222;}#mermaid-svg-zJd6AyDPsDX7r1qU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zJd6AyDPsDX7r1qU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zJd6AyDPsDX7r1qU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zJd6AyDPsDX7r1qU .marker.cross{stroke:#333333;}#mermaid-svg-zJd6AyDPsDX7r1qU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zJd6AyDPsDX7r1qU p{margin:0;}#mermaid-svg-zJd6AyDPsDX7r1qU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster-label text{fill:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster-label span{color:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster-label span p{background-color:transparent;}#mermaid-svg-zJd6AyDPsDX7r1qU .label text,#mermaid-svg-zJd6AyDPsDX7r1qU span{fill:#333;color:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU .node rect,#mermaid-svg-zJd6AyDPsDX7r1qU .node circle,#mermaid-svg-zJd6AyDPsDX7r1qU .node ellipse,#mermaid-svg-zJd6AyDPsDX7r1qU .node polygon,#mermaid-svg-zJd6AyDPsDX7r1qU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zJd6AyDPsDX7r1qU .rough-node .label text,#mermaid-svg-zJd6AyDPsDX7r1qU .node .label text,#mermaid-svg-zJd6AyDPsDX7r1qU .image-shape .label,#mermaid-svg-zJd6AyDPsDX7r1qU .icon-shape .label{text-anchor:middle;}#mermaid-svg-zJd6AyDPsDX7r1qU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zJd6AyDPsDX7r1qU .rough-node .label,#mermaid-svg-zJd6AyDPsDX7r1qU .node .label,#mermaid-svg-zJd6AyDPsDX7r1qU .image-shape .label,#mermaid-svg-zJd6AyDPsDX7r1qU .icon-shape .label{text-align:center;}#mermaid-svg-zJd6AyDPsDX7r1qU .node.clickable{cursor:pointer;}#mermaid-svg-zJd6AyDPsDX7r1qU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zJd6AyDPsDX7r1qU .arrowheadPath{fill:#333333;}#mermaid-svg-zJd6AyDPsDX7r1qU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zJd6AyDPsDX7r1qU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zJd6AyDPsDX7r1qU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zJd6AyDPsDX7r1qU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zJd6AyDPsDX7r1qU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zJd6AyDPsDX7r1qU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster text{fill:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU .cluster span{color:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zJd6AyDPsDX7r1qU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zJd6AyDPsDX7r1qU rect.text{fill:none;stroke-width:0;}#mermaid-svg-zJd6AyDPsDX7r1qU .icon-shape,#mermaid-svg-zJd6AyDPsDX7r1qU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zJd6AyDPsDX7r1qU .icon-shape p,#mermaid-svg-zJd6AyDPsDX7r1qU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zJd6AyDPsDX7r1qU .icon-shape .label rect,#mermaid-svg-zJd6AyDPsDX7r1qU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zJd6AyDPsDX7r1qU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zJd6AyDPsDX7r1qU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zJd6AyDPsDX7r1qU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 标准输入流
原始字节
输出数据
错误信息
标准输出流
标准错误流
键盘
System.in
(InputStream)
程序逻辑
System.out
(PrintStream)
System.err
(PrintStream)
终端/控制台
终端/控制台
1.2 System.in、System.out 和 System.err 的类型体系
理解这三个字段的具体类型,对于后续深入使用至关重要:
java
public final class System {
public static final InputStream in; // java.io.InputStream
public static final PrintStream out; // java.io.PrintStream
public static final PrintStream err; // java.io.PrintStream
}
System.in是一个InputStream(字节流),这意味着它只能直接读取原始字节。要方便地读取字符或字符串,我们需要用Scanner或BufferedReader对其进行包装。System.out是一个PrintStream,它提供了丰富的输出方法(print、println、printf),可以方便地输出各种数据类型。System.err也是一个PrintStream,它和System.out具有完全相同的 API,唯一区别是它输出到标准错误流而非标准输出流。
1.3 重定向标准输入输出
Java 允许程序在运行时重定向标准流,这在测试或自动化场景中非常有用:
java
// 将标准输出重定向到文件
System.setOut(new PrintStream(new FileOutputStream("output.txt")));
// 将标准输入重定向为文件读取
System.setIn(new FileInputStream("input.txt"));
// 将标准错误重定向到文件
System.setErr(new PrintStream(new FileOutputStream("error.log")));
二、System.out 输出详解
2.1 println 与 print 的区别
System.out 提供了三种基础输出方法,它们的区别集中体现在换行行为上:
| 方法 | 是否自动换行 | 参数类型 | 使用场景 |
|---|---|---|---|
System.out.print() |
否 | 可接受任意类型(调用 toString) | 需要连续输出、不换行的场景 |
System.out.println() |
是 | 可接受任意类型。无参时只输出一个换行 | 最常见的输出方法 |
System.out.printf() |
否 | (String format, Object... args) | 需要格式化输出的场景 |
java
public class PrintDemo {
public static void main(String[] args) {
// print 不会自动换行
System.out.print("张三");
System.out.print("李四");
System.out.print("王五");
// 输出:张三李四王五(全部连在一起)
System.out.println(); // 输出一个空行
// println 会自动换行
System.out.println("苹果");
System.out.println("香蕉");
System.out.println("橘子");
// 输出:
// 苹果
// 香蕉
// 橘子
// println 的链式输出技巧
System.out.println("姓名:" + "张三" +
",年龄:" + 18;
}
}
2.2 println 的底层原理
println 之所以能接受各种类型的参数,是因为 PrintStream 对所有基本类型和 Object 类型都进行了方法重载。当传入一个对象时,Java 会隐式调用该对象的 toString() 方法:
java
public class ToStringDemo {
static class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 重写 toString,控制输出格式
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Student s = new Student("张三", 20);
System.out.println(s);
// 输出:Student{name='张三', age=20}
// 如果没有重写 toString,会输出类名@哈希码,例如:
// Student@15db9742
}
}
}
2.3 printf 格式化输出详解
printf 是 PrintStream 提供的格式化输出方法,它使用类似 C 语言的格式说明符来控制输出格式:
java
System.out.printf(String format, Object... args);
常用格式说明符一览表:
| 格式说明符 | 描述 | 示例 | 输出结果 |
|---|---|---|---|
%d |
十进制整数 | printf("%d", 42) |
42 |
%f |
浮点数 | printf("%f", 3.14) |
3.140000 |
%.2f |
保留两位小数的浮点数 | printf("%.2f", 3.14159) |
3.14 |
%s |
字符串 | printf("%s", "Hello") |
Hello |
%c |
字符 | printf("%c", 'A') |
A |
%b |
布尔值 | printf("%b", true) |
true |
%n |
平台无关的换行符 | printf("Line1%nLine2") |
Line1\nLine2 |
%% |
输出百分号本身 | printf("90%%") |
90% |
%e |
科学计数法 | printf("%e", 1234.5) |
1.234500e+03 |
%x |
十六进制整数 | printf("%x", 255) |
ff |
%o |
八进制整数 | printf("%o", 8) |
10 |
2.4 printf 高级格式控制
通过宽度、精度和标志,可以对输出做精细控制:
java
public class PrintfAdvancedDemo {
public static void main(String[] args) {
// ---- 宽度控制 ----
// %10d:最少占 10 个字符宽度,右对齐
System.out.printf("|%10d|%n", 42);
// 输出:| 42|
// %-10d:最少占 10 个字符宽度,左对齐
System.out.printf("|%-10d|%n", 42);
// 输出:|42 |
// ---- 补零 ----
// %010d:最少占 10 位,不足用 0 补齐
System.out.printf("%010d%n", 42);
// 输出:0000000042
// ---- 千位分隔符 ----
System.out.printf("%,d%n", 1234567);
// 输出:1,234,567
// ---- 浮点数精度 ----
System.out.printf("%.3f%n", Math.PI);
// 输出:3.142(四舍五入到三位小数)
// ---- 组合使用 ----
// 宽度 10,右对齐,保留 2 位小数,带千位分隔符
System.out.printf("|%,10.2f|%n", 1234567.891);
// 输出:|1,234,567.89|
// ---- 正负号控制 ----
System.out.printf("%+d, %+d%n", 42, -42);
// 输出:+42, -42
// ---- 多个参数按顺序替换 ----
String name = "张三";
int age = 25;
double salary = 12500.50;
System.out.printf("姓名:%-6s 年龄:%3d 工资:%,10.2f%n", name, age, salary);
// 输出:姓名:张三 年龄: 25 工资: 12,500.50
// ---- 索引参数(1$ 表示使用第一个参数) ----
System.out.printf("%2$s 今年 %1$d 岁,%1$d 岁正是奋斗的年纪%n", 25, "张三");
// 输出:张三 今年 25 岁,25 岁正是奋斗的年纪
}
}
2.5 利用 printf 打印格式化表格
printf 在打印对齐表格时非常实用:
java
public class TablePrintDemo {
public static void main(String[] args) {
// 表头
System.out.println("┌──────┬──────────┬────┬──────────┐");
System.out.printf("│ %-4s │ %-8s │ %-2s │ %8s │%n", "编号", "姓名", "年龄", "成绩");
System.out.println("├──────┼──────────┼────┼──────────┤");
// 数据行
String[][] data = {
{"1001", "张三", "20", "95.50"},
{"1002", "李小萌", "19", "88.00"},
{"1003", "王文远", "21", "76.25"},
{"1004", "赵思琪", "20", "92.00"},
};
for (String[] row : data) {
System.out.printf("│ %-4s │ %-8s │ %2s │ %8.2f │%n",
row[0], row[1], row[2], Double.parseDouble(row[3]));
}
System.out.println("└──────┴──────────┴────┴──────────┘");
// 输出效果:
// ┌──────┬──────────┬────┬──────────┐
// │ 编号 │ 姓名 │ 年龄 │ 成绩 │
// ├──────┼──────────┼────┼──────────┤
// │ 1001 │ 张三 │ 20 │ 95.50 │
// │ 1002 │ 李小萌 │ 19 │ 88.00 │
// │ 1003 │ 王文远 │ 21 │ 76.25 │
// │ 1004 │ 赵思琪 │ 20 │ 92.00 │
// └──────┴──────────┴────┴──────────┘
}
}
三、String.format 方法
3.1 String.format 与 printf 的关系
String.format() 与 System.out.printf() 使用完全相同的格式说明符规则,区别在于前者返回格式化后的字符串,后者直接输出到控制台。这一差异赋予了 String.format 更广泛的应用场景:
java
public class StringFormatDemo {
public static void main(String[] args) {
// printf:直接打印到控制台
System.out.printf("价格:%.2f 元%n", 9.9);
// String.format:返回字符串,可以赋值给变量
String message = String.format("价格:%.2f 元", 9.9);
// message = "价格:9.90 元"
// 后续可以:
// 写入文件
// 写入数据库
// 发送到网络
// 存储在日志中
// ...
// 典型的日志场景
String userName = "admin";
String operation = "登录";
String timestamp = "2026-08-12 10:30:00";
String logEntry = String.format("[%s] 用户 %s 执行操作:%s", timestamp, userName, operation);
System.out.println(logEntry);
// 输出:[2026-08-12 10:30:00] 用户 admin 执行操作:登录
}
}
3.2 String.format 实战示例
java
public class StringFormatUseCase {
public static void main(String[] args) {
// 场景 1:生成编号(补零对齐)
String orderNo = String.format("ORD%06d", 125);
System.out.println(orderNo); // ORD000125
// 场景 2:金额格式化
double amount = 123456.78;
String amountStr = String.format("¥%,.2f", amount);
System.out.println(amountStr); // ¥123,456.78
// 场景 3:进度百分比
int completed = 37;
int total = 50;
double percent = (double) completed / total * 100;
String progress = String.format("完成进度:%.1f%% (%d/%d)", percent, completed, total);
System.out.println(progress); // 完成进度:74.0% (37/50)
// 场景 4:SQL 语句拼接(仅用于演示,生产环境应使用 PreparedStatement)
String tableName = "students";
long id = 1001L;
String sql = String.format("SELECT * FROM %s WHERE id = %d", tableName, id);
System.out.println(sql); // SELECT * FROM students WHERE id = 1001
// 场景 5:格式化日期时间
int year = 2026, month = 8, day = 12;
int hour = 14, minute = 30, second = 5;
String dateTime = String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
System.out.println(dateTime); // 2026-08-12 14:30:05
}
}
四、Scanner 类从入门到精通
4.1 为什么需要 Scanner
System.in 是一个原始的 InputStream,它只能逐字节读取。直接用它读取键盘输入需要繁琐的字节级处理。java.util.Scanner 类正是为了解决这个痛点而诞生的------它将原始的输入流包装起来,提供了类型安全的、基于分隔符的解析能力。
#mermaid-svg-Yz3Ua1E7Ps14aQ1e{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .error-icon{fill:#552222;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .marker.cross{stroke:#333333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e p{margin:0;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster-label text{fill:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster-label span{color:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster-label span p{background-color:transparent;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .label text,#mermaid-svg-Yz3Ua1E7Ps14aQ1e span{fill:#333;color:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node rect,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node circle,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node ellipse,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node polygon,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .rough-node .label text,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node .label text,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .image-shape .label,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .icon-shape .label{text-anchor:middle;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .rough-node .label,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node .label,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .image-shape .label,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .icon-shape .label{text-align:center;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node.clickable{cursor:pointer;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .arrowheadPath{fill:#333333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster text{fill:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .cluster span{color:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e rect.text{fill:none;stroke-width:0;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .icon-shape,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .icon-shape p,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .icon-shape .label rect,#mermaid-svg-Yz3Ua1E7Ps14aQ1e .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Yz3Ua1E7Ps14aQ1e :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 使用 Scanner
System.in
(InputStream)
Scanner 包装
new Scanner(System.in)
自动解析
nextInt()
自动解析
nextLine()
自动解析
nextDouble()
不使用 Scanner
System.in
(InputStream)
逐字节读取
read()
手动组装字符串
StringBuilder
手动解析类型
Integer.parseInt()
4.2 创建 Scanner 对象
java
import java.util.Scanner;
public class ScannerCreation {
public static void main(String[] args) {
// 从标准输入(键盘)读取
Scanner scanner = new Scanner(System.in);
// 从字符串读取(常用于测试或字符串解析)
Scanner stringScanner = new Scanner("张三 25 95.5");
// 从文件读取
// Scanner fileScanner = new Scanner(new File("data.txt"));
// 使用完毕后关闭 Scanner(释放资源)
scanner.close();
}
}
重要提示 :Scanner 持有了系统资源,使用完毕后应调用 close() 方法关闭。但在实际开发中,不要关闭包装了 System.in 的 Scanner ,因为关闭 Scanner 会同时关闭底层的 System.in 流,导致程序中的其他代码再也无法从标准输入读取数据。
4.3 Scanner 核心读取方法
Scanner 提供了丰富的读取方法,覆盖了各种数据类型:
| 方法 | 返回值 | 描述 | 分隔符行为 |
|---|---|---|---|
next() |
String |
读取下一个完整的"token"(单词) | 遇到空白字符结束(空格、Tab、换行) |
nextLine() |
String |
读取当前行剩余的全部文本(包括空格) | 遇到 \n 结束,读取后光标移到下一行 |
nextInt() |
int |
读取下一个 token 并转为 int | 同上 |
nextDouble() |
double |
读取下一个 token 并转为 double | 同上 |
nextBoolean() |
boolean |
读取下一个 token 并转为 boolean | 同上 |
nextLong() |
long |
读取下一个 token 并转为 long | 同上 |
nextFloat() |
float |
读取下一个 token 并转为 float | 同上 |
nextShort() |
short |
读取下一个 token 并转为 short | 同上 |
nextByte() |
byte |
读取下一个 token 并转为 byte | 同上 |
nextBigInteger() |
BigInteger |
读取下一个 token 并转为 BigInteger | 同上 |
nextBigDecimal() |
BigDecimal |
读取下一个 token 并转为 BigDecimal | 同上 |
4.4 next() 与 nextLine() 的区别
这是初学 Scanner 时最容易混淆的两个方法:
java
public class NextVsNextLine {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 假如用户输入:Hello World 2026
// 👆 注意三个 token 之间只有空格
String word = sc.next(); // 只读到 "Hello",遇到空格就停止
String line = sc.nextLine(); // 读缓冲区中剩余的内容:" World 2026"
System.out.println("word = [" + word + "]"); // word = [Hello]
System.out.println("line = [" + line + "]"); // line = [ World 2026]
sc.close();
}
}
java
public class NextLineFullLine {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 假如用户输入:Hello World 2026
String fullLine = sc.nextLine();
System.out.println("fullLine = [" + fullLine + "]");
// 输出:fullLine = [Hello World 2026]
// nextLine 读取了整行,包括空格
sc.close();
}
}
关键记忆口诀:
next()以空白字符(空格、Tab、换行)为分隔,只读取连续非空白文本。nextLine()以换行符 为分隔,读取该行剩余的全部内容(可以包含空格)。
4.5 hasNext* 系列方法:安全的输入检测
在读数据之前,使用 hasNext*() 方法进行预检测,可以避免程序因输入类型不匹配而崩溃:
java
public class HasNextDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入一个整数:");
if (sc.hasNextInt()) {
int number = sc.nextInt();
System.out.println("你输入的数字是:" + number);
} else {
System.out.println("输入的不是有效的整数!当前内容:" + sc.next());
}
System.out.print("请输入一个浮点数:");
if (sc.hasNextDouble()) {
double value = sc.nextDouble();
System.out.printf("你输入的浮点数是:%.2f%n", value);
} else {
System.out.println("输入的不是有效的浮点数!");
}
System.out.print("请输入你的姓名:");
if (sc.hasNextLine()) {
// 注意:hasNextLine 总是返回 true,只要有换行符就可以读到空行
// 这里的 isBlank 判断帮助我们区分 "真的没输入" 和 "输入了空格"
String name = sc.nextLine();
if (!name.isBlank()) {
System.out.println("你好," + name + "!");
} else {
System.out.println("姓名为空,程序退出。");
}
}
sc.close();
}
}
hasNext 系列方法一览:
| 方法 | 返回值 | 描述 |
|---|---|---|
hasNext() |
boolean |
是否还有下一个 token |
hasNextInt() |
boolean |
下一个 token 是否可以解析为 int |
hasNextDouble() |
boolean |
下一个 token 是否可以解析为 double |
hasNextLine() |
boolean |
是否还有下一行(即使空行也返回 true) |
hasNextBoolean() |
boolean |
下一个 token 是否可以解析为 boolean |
hasNext(Pattern) |
boolean |
下一个 token 是否匹配指定的正则表达式 |
4.6 设置分隔符
默认情况下,Scanner 使用空白字符作为分隔符。你可以通过 useDelimiter() 方法自定义分隔符:
java
public class DelimiterDemo {
public static void main(String[] args) {
// 以逗号作为分隔符,解析 CSV 格式数据
String csvData = "张三,25,北京,工程师";
Scanner scanner = new Scanner(csvData);
scanner.useDelimiter(",");
while (scanner.hasNext()) {
System.out.println(scanner.next());
}
// 输出:
// 张三
// 25
// 北京
// 工程师
scanner.close();
// 以正则表达式作为分隔符
String complexData = "苹果:5.5元;香蕉:3.2元;橘子:4.8元";
Scanner scanner2 = new Scanner(complexData);
scanner2.useDelimiter("[;:]"); // 分号或中文冒号
while (scanner2.hasNext()) {
System.out.println(scanner2.next());
}
scanner2.close();
}
}
五、Scanner 的经典陷阱与解决方案
5.1 nextLine() 跳过输入问题(最重要的陷阱)
这是 Java 初学者遇到的最常见问题之一。当 nextInt() 或 nextDouble() 之后调用 nextLine() 时,nextLine() 似乎会"跳过"输入:
java
public class ScannerPitfall {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入年龄:");
int age = sc.nextInt();
// 用户输入:25 并按回车
// 此时缓冲区中:"25\n"
// nextInt() 读取了 "25",但 "\n" 留在了缓冲区
System.out.print("请输入姓名:");
String name = sc.nextLine();
// nextLine() 立刻读到了缓冲区中残留的 "\n"
// 于是它认为"这一行已经结束了",返回空字符串
// 用户根本没有机会输入姓名!
System.out.println("年龄:" + age);
System.out.println("姓名:[" + name + "]"); // 输出:姓名:[]
sc.close();
}
}
问题根因图解:
程序 Scanner 输入缓冲区 用户 程序 Scanner 输入缓冲区 用户 #mermaid-svg-BGy6tU4K8oEZEOx0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BGy6tU4K8oEZEOx0 .error-icon{fill:#552222;}#mermaid-svg-BGy6tU4K8oEZEOx0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BGy6tU4K8oEZEOx0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BGy6tU4K8oEZEOx0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BGy6tU4K8oEZEOx0 .marker.cross{stroke:#333333;}#mermaid-svg-BGy6tU4K8oEZEOx0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BGy6tU4K8oEZEOx0 p{margin:0;}#mermaid-svg-BGy6tU4K8oEZEOx0 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BGy6tU4K8oEZEOx0 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BGy6tU4K8oEZEOx0 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-BGy6tU4K8oEZEOx0 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-BGy6tU4K8oEZEOx0 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-BGy6tU4K8oEZEOx0 .sequenceNumber{fill:white;}#mermaid-svg-BGy6tU4K8oEZEOx0 #sequencenumber{fill:#333;}#mermaid-svg-BGy6tU4K8oEZEOx0 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-BGy6tU4K8oEZEOx0 .messageText{fill:#333;stroke:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BGy6tU4K8oEZEOx0 .labelText,#mermaid-svg-BGy6tU4K8oEZEOx0 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .loopText,#mermaid-svg-BGy6tU4K8oEZEOx0 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BGy6tU4K8oEZEOx0 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BGy6tU4K8oEZEOx0 .noteText,#mermaid-svg-BGy6tU4K8oEZEOx0 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-BGy6tU4K8oEZEOx0 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BGy6tU4K8oEZEOx0 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BGy6tU4K8oEZEOx0 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BGy6tU4K8oEZEOx0 .actorPopupMenu{position:absolute;}#mermaid-svg-BGy6tU4K8oEZEOx0 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-BGy6tU4K8oEZEOx0 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BGy6tU4K8oEZEOx0 .actor-man circle,#mermaid-svg-BGy6tU4K8oEZEOx0 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-BGy6tU4K8oEZEOx0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 缓冲区剩余:\n 用户还没来得及输入姓名! 输入 "25\n" 调用 nextInt() 读取 "25" 返回 25 调用 nextLine() 立即读到残留的 \n 返回空字符串 ""
解决方案(三种方法):
java
public class ScannerPitfallSolution {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入年龄:");
int age = sc.nextInt();
// ===== 解决方案 1:额外调用一次 nextLine() 吃掉换行符 =====
sc.nextLine(); // 消费掉缓冲区中残留的 "\n"
System.out.print("请输入姓名:");
String name = sc.nextLine();
System.out.println(name + "," + age + "岁");
// ===== 解决方案 2:统一使用 nextLine() + 手动转换 =====
// 这是最推荐的方式,bug 最少
System.out.print("请输入年龄:");
String ageInput = sc.nextLine();
int age2 = Integer.parseInt(ageInput.trim());
System.out.print("请输入姓名:");
String name2 = sc.nextLine();
System.out.println(name2 + "," + age2 + "岁");
// ===== 解决方案 3:使用 skip 方法跳过换行符 =====
System.out.print("请输入年龄:");
int age3 = sc.nextInt();
sc.skip("\n"); // 跳过换行符
System.out.print("请输入姓名:");
String name3 = sc.nextLine();
System.out.println(name3 + "," + age3 + "岁");
sc.close();
}
}
5.2 InputMismatchException 异常
当用户输入的数据类型与期望的类型不匹配时,Scanner 会抛出 InputMismatchException:
java
public class InputMismatchDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
try {
System.out.print("请输入一个整数:");
int number = sc.nextInt();
System.out.println("你输入的是:" + number);
} catch (InputMismatchException e) {
System.out.println("错误:请输入一个有效的整数!");
// 注意:异常发生后,导致异常的 token 仍在缓冲区中
// 需要先清掉它,才能继续读取
String badInput = sc.nextLine(); // 清掉错误输入
System.out.println("被忽略的输入:" + badInput);
}
sc.close();
}
}
问题输入何时导致哪种异常:
| 用户输入 | Scanner 调用 | 异常类型 | 说明 |
|---|---|---|---|
abc |
nextInt() |
InputMismatchException |
无法将 abc 解析为整数 |
3.14 |
nextInt() |
InputMismatchException |
浮点数不能直接转为整数 |
| (输入结束 Ctrl+Z/D) | next() / nextInt() 等 |
NoSuchElementException |
已经没有 token 可读 |
| 直接回车(空行) | nextInt() / nextDouble() |
NoSuchElementException |
空行中没有数字 |
999999999999 |
nextInt() |
InputMismatchException |
超过 int 范围 |
5.3 NoSuchElementException 异常
当 Scanner 尝试读取但输入源已经耗尽时触发:
java
public class NoSuchElementDemo {
public static void main(String[] args) {
Scanner sc = new Scanner("hello");
System.out.println(sc.next()); // 输出 hello
System.out.println(sc.next()); // 抛出 NoSuchElementException!
// 字符串中只有一个 token,第二次读取时已无内容
sc.close();
}
}
5.4 预防异常的健壮输入模式
java
public class RobustInput {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int age = readIntSafely(sc, "请输入年龄:", 1, 150);
double salary = readDoubleSafely(sc, "请输入月薪:", 0, Double.MAX_VALUE);
sc.nextLine(); // 消费残留换行符
String name = sc.nextLine();
System.out.println("姓名:" + name);
System.out.println("年龄:" + age);
System.out.printf("月薪:%.2f%n", salary);
}
// 安全读取整数(带范围校验)
private static int readIntSafely(Scanner sc, String prompt, int min, int max) {
while (true) {
System.out.print(prompt);
if (sc.hasNextInt()) {
int value = sc.nextInt();
if (value >= min && value <= max) {
return value;
}
System.out.printf("输入不合法,请输入 %d 到 %d 之间的值%n", min, max);
} else {
System.out.println("请输入一个有效的整数!");
sc.next(); // 清掉非法输入
}
}
}
// 安全读取浮点数(带范围校验)
private static double readDoubleSafely(Scanner sc, String prompt, double min, double max) {
while (true) {
System.out.print(prompt);
if (sc.hasNextDouble()) {
double value = sc.nextDouble();
if (value >= min && value <= max) {
return value;
}
System.out.printf("输入不合法,请输入 %.0f 到 %.0f 之间的值%n", min, max);
} else {
System.out.println("请输入一个有效的数字!");
sc.next(); // 清掉非法输入
}
}
}
}
六、从键盘读取 vs 从文件读取
Scanner 的统一 API 使得从不同数据源读取数据的代码几乎一样:
java
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class MultiSourceScanner {
public static void main(String[] args) throws FileNotFoundException {
// ===== 从键盘读取 =====
Scanner keyboardScanner = new Scanner(System.in);
System.out.print("请输入:");
String keyboardInput = keyboardScanner.nextLine();
System.out.println("键盘输入:" + keyboardInput);
// 注意:不要关闭 keyboardScanner,它会关闭 System.in
// ===== 从文件读取(逐词) =====
File file = new File("students.txt"); // 假设文件内容:张三 20 95.5
Scanner fileScanner = new Scanner(file);
while (fileScanner.hasNext()) {
String name = fileScanner.next();
int age = fileScanner.nextInt();
double score = fileScanner.nextDouble();
System.out.printf("姓名:%s,年龄:%d,成绩:%.1f%n", name, age, score);
}
fileScanner.close(); // 文件 Scanner 用完应该关闭
// ===== 从文件读取(逐行) =====
Scanner fileLineScanner = new Scanner(new File("students.txt"));
while (fileLineScanner.hasNextLine()) {
String line = fileLineScanner.nextLine();
System.out.println("行:" + line);
}
fileLineScanner.close();
}
}
Scanner 数据源对比表:
| 数据源 | 构造方法 | 是否需要关闭 | 使用场景 |
|---|---|---|---|
| 键盘 | new Scanner(System.in) |
不要关闭 | 交互式命令行程序 |
| 文件 | new Scanner(new File(path)) |
用完必须关闭 | 读取配置文件、数据文件 |
| 字符串 | new Scanner(str) |
不影响外部资源 | 解析已有字符串、单元测试 |
| 网络流 | new Scanner(url.openStream()) |
用完必须关闭 | 读取网络 API 响应 |
| 进程输出 | new Scanner(process.getInputStream()) |
用完必须关闭 | 读取外部程序的输出 |
七、BufferedReader:更高效的文本输入
对于大量文本输入场景,BufferedReader 比 Scanner 更高效:
7.1 BufferedReader 基础用法
java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 把 System.in 逐层包装:
// System.in (字节流) -> InputStreamReader (字节->字符) -> BufferedReader (缓冲)
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入一行文本:");
String line = reader.readLine();
System.out.println("你输入了:" + line);
// 连续读取多行,直到输入 "exit"
System.out.println("请输入文本(输入 exit 结束):");
String input;
while (!(input = reader.readLine()).equals("exit")) {
System.out.println("> " + input);
}
reader.close(); // 也会关闭 System.in
}
}
7.2 Scanner vs BufferedReader 对比
| 特性 | Scanner | BufferedReader |
|---|---|---|
| 解析能力 | 内置类型解析(nextInt 等) | 只能读取字符串,需手动解析 |
| 性能 | 一般(有解析开销) | 高(只做缓冲+字符处理) |
| 分隔符 | 默认空白字符,可自定义 | 只按行读取 |
| 使用便捷性 | 高(开箱即用) | 需配合包装流 |
| 正则匹配 | 内置 hasNext(Pattern) | 不支持 |
| 适用场景 | 交互式输入、小文件、需要类型解析 | 大文件读取、HTTP 响应流、逐行处理 |
| 线程安全 | 否 | 否 |
java
// 性能对比示例:读取大文件
public class ScannerVsBufferedReader {
public static void main(String[] args) throws IOException {
String filePath = "large_data.txt";
// 使用 Scanner 逐行读取
long start1 = System.currentTimeMillis();
Scanner sc = new Scanner(new File(filePath));
int lines1 = 0;
while (sc.hasNextLine()) {
sc.nextLine();
lines1++;
}
sc.close();
long time1 = System.currentTimeMillis() - start1;
System.out.printf("Scanner 读取 %d 行,耗时:%d ms%n", lines1, time1);
// 使用 BufferedReader 逐行读取
long start2 = System.currentTimeMillis();
BufferedReader br = new BufferedReader(new FileReader(filePath));
int lines2 = 0;
while (br.readLine() != null) {
lines2++;
}
br.close();
long time2 = System.currentTimeMillis() - start2;
System.out.printf("BufferedReader 读取 %d 行,耗时:%d ms%n", lines2, time2);
// 典型结果:BufferedReader 比 Scanner 快 2~5 倍
}
}
八、Console 类:安全的密码输入
8.1 为什么需要 Console 类
Scanner 读取密码时,密码会明文显示在屏幕上,存在安全隐患。Java 提供了 java.io.Console 类,支持密码回显屏蔽(输入时屏幕不显示字符):
java
public class ConsolePasswordDemo {
public static void main(String[] args) {
java.io.Console console = System.console();
// 注意:在 IDE 中(Eclipse、IntelliJ IDEA)直接运行时,
// System.console() 可能返回 null!
// 需要在命令行终端中运行 java ConsolePasswordDemo
if (console == null) {
System.out.println("错误:无法获取控制台对象,请在命令行中运行本程序。");
return;
}
System.out.println("===== 用户登录 =====");
// 用户名(普通显示)
String username = console.readLine("用户名:");
// 密码(输入时不显示字符)
char[] password = console.readPassword("密 码:");
// 密码验证(这里演示,实际应比对加密后的值)
String passwordStr = new String(password);
if ("admin".equals(username) && "123456".equals(passwordStr)) {
System.out.println("登录成功!");
} else {
System.out.println("用户名或密码错误!");
System.out.printf("你输入的是:%s / %s%n", username, passwordStr);
}
// 安全建议:使用完后立即清除密码字符数组
java.util.Arrays.fill(password, ' ');
}
}
8.2 Console 类读取流程
渲染错误: Mermaid 渲染失败: Parse error on line 2: ...--> B{System.console()} B -->|返回 nul -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
重要提示 :readPassword() 方法返回 char[] 而非 String。这是因为 char[] 在使用完后可以被手动清空(用 Arrays.fill 填充为空字符),而 String 是不可变对象,它会一直驻留在 JVM 的字符串常量池中,直到 GC 回收。使用 char[] 并手动清空能最大程度降低密码泄露风险。
九、JOptionPane:图形化输入输出
9.1 基础用法
javax.swing.JOptionPane 提供了弹出式对话框,适合简单的 GUI 输入输出场景:
java
import javax.swing.JOptionPane;
public class JOptionPaneDemo {
public static void main(String[] args) {
// ---- 输入对话框 ----
// 显示一个带输入框的弹窗
String name = JOptionPane.showInputDialog(null, "请输入你的姓名:", "信息输入", JOptionPane.QUESTION_MESSAGE);
if (name != null && !name.trim().isEmpty()) {
// ---- 消息对话框 ----
JOptionPane.showMessageDialog(null, name + ",欢迎你!", "欢迎", JOptionPane.INFORMATION_MESSAGE);
// ---- 确认对话框 ----
int choice = JOptionPane.showConfirmDialog(null, "是否继续?", "请确认", JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION) {
JOptionPane.showMessageDialog(null, "你选择了继续");
} else {
JOptionPane.showMessageDialog(null, "你选择了取消");
}
}
}
}
9.2 JOptionPane 消息类型
| 常量 | 图标 | 用途 |
|---|---|---|
JOptionPane.ERROR_MESSAGE |
错误图标 | 显示错误信息 |
JOptionPane.INFORMATION_MESSAGE |
信息图标 | 显示一般提示信息 |
JOptionPane.WARNING_MESSAGE |
警告图标 | 显示警告信息 |
JOptionPane.QUESTION_MESSAGE |
问号图标 | 提出疑问 |
JOptionPane.PLAIN_MESSAGE |
无图标 | 纯文本信息 |
9.3 Scanner 与 GUI 输入对比
| 特性 | Scanner(控制台) | JOptionPane(GUI) |
|---|---|---|
| 运行环境 | 终端/命令行 | 桌面 GUI 环境 |
| 用户友好度 | 低(纯文本) | 高(可视化弹窗) |
| 自动化测试 | 容易 | 较难 |
| 数据类型处理 | 内置类型解析 | 返回 String,需手动转换 |
| 适用场景 | 命令行工具、后台服务 | 桌面应用、GUI 工具 |
十、命令行参数:程序启动时传递数据
10.1 main 方法的 String\[\] args
每个 Java 程序的入口 main 方法都接收一个 String[] args 参数,它包含了程序启动时从命令行传入的所有参数:
java
public class CommandLineArgsDemo {
public static void main(String[] args) {
// 查看所有命令行参数
System.out.println("参数数量:" + args.length);
System.out.println("所有参数:");
for (int i = 0; i < args.length; i++) {
System.out.printf("args[%d] = %s%n", i, args[i]);
}
// 编译:javac CommandLineArgsDemo.java
// 运行:java CommandLineArgsDemo 张三 25 北京
// 输出:
// 参数数量:3
// 所有参数:
// args[0] = 张三
// args[1] = 25
// args[2] = 北京
}
}
10.2 命令行参数实战
java
public class CalculatorWithArgs {
public static void main(String[] args) {
// 用法:java CalculatorWithArgs <数字1> <操作符> <数字2>
// 示例:java CalculatorWithArgs 10 + 5
if (args.length != 3) {
System.out.println("用法:java CalculatorWithArgs <数字1> <操作符> <数字2>");
System.out.println("操作符:+ - * /");
return;
}
try {
double num1 = Double.parseDouble(args[0]);
double num2 = Double.parseDouble(args[2]);
String operator = args[1];
double result = 0;
switch (operator) {
case "+": result = num1 + num2; break;
case "-": result = num1 - num2; break;
case "*": result = num1 * num2; break;
case "/":
if (num2 == 0) {
System.out.println("错误:除数不能为零!");
return;
}
result = num1 / num2;
break;
default:
System.out.println("不支持的操作符:" + operator);
return;
}
System.out.printf("%.2f %s %.2f = %.2f%n", num1, operator, num2, result);
} catch (NumberFormatException e) {
System.out.println("错误:请输入有效的数字!");
}
}
}
10.3 命令行参数 vs Scanner 输入
| 比较维度 | 命令行参数 (args) | Scanner 交互输入 |
|---|---|---|
| 输入时机 | 程序启动时一次性传递 | 程序运行中实时交互 |
| 适用场景 | 批处理、脚本、配置参数 | 交互式应用、向导式程序 |
| 自动化 | 容易(脚本直接传参) | 需要模拟键盘输入 |
| 可修改性 | 程序运行中不可修改 | 可在运行时动态接收 |
| 参数可见性 | 直接可见(命令行历史) | 不直接暴露 |
十一、实战项目
11.1 项目一:交互式计算器
java
import java.util.Scanner;
public class InteractiveCalculator {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("╔══════════════════════╗");
System.out.println("║ 交互式计算器 V1.0 ║");
System.out.println("╚══════════════════════╝");
System.out.println("支持运算:+ - * / % (输入 quit 退出)");
System.out.println();
while (true) {
try {
// ---- 读取第一个数字 ----
System.out.print("请输入第一个数字:");
String input = sc.nextLine();
if ("quit".equalsIgnoreCase(input)) {
System.out.println("感谢使用,再见!");
break;
}
double num1 = Double.parseDouble(input);
// ---- 读取运算符 ----
System.out.print("请输入运算符(+ - * / %):");
String operator = sc.nextLine();
if ("quit".equalsIgnoreCase(operator)) break;
// ---- 读取第二个数字 ----
System.out.print("请输入第二个数字:");
String input2 = sc.nextLine();
if ("quit".equalsIgnoreCase(input2)) break;
double num2 = Double.parseDouble(input2);
// ---- 执行运算 ----
double result;
switch (operator) {
case "+": result = num1 + num2; break;
case "-": result = num1 - num2; break;
case "*": result = num1 * num2; break;
case "/":
if (num2 == 0) {
System.out.println("错误:除数不能为零!");
continue;
}
result = num1 / num2;
break;
case "%":
if (num2 == 0) {
System.out.println("错误:取模运算的除数不能为零!");
continue;
}
result = num1 % num2;
break;
default:
System.out.println("不支持的运算符!");
continue;
}
// ---- 美化输出结果 ----
System.out.println("────────────────────────");
System.out.printf(" 结果:%.2f %s %.2f = %.4f%n", num1, operator, num2, result);
System.out.println("────────────────────────");
System.out.println();
} catch (NumberFormatException e) {
System.out.println("输入格式错误,请输入有效的数字或 quit 退出。");
}
}
sc.close();
}
}
11.2 项目二:信息登记表单
java
import java.util.Scanner;
public class RegistrationForm {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("╔════════════════════════════╗");
System.out.println("║ 用户信息登记表 ║");
System.out.println("╚════════════════════════════╝");
System.out.println();
// 基本信息
System.out.println("----- 基本信息 -----");
System.out.print("姓名:");
String name = sc.nextLine();
int age = 0;
while (true) {
System.out.print("年龄:");
if (sc.hasNextInt()) {
age = sc.nextInt();
if (age > 0 && age < 150) {
break;
}
System.out.println("年龄应在 1-150 之间!");
} else {
System.out.println("请输入有效的数字!");
sc.next(); // 清掉非法输入
}
}
sc.nextLine(); // 消费残留换行符
System.out.print("性别(男/女):");
String gender = sc.nextLine();
// 联系方式
System.out.println("\n----- 联系方式 -----");
System.out.print("手机号码:");
String phone = sc.nextLine();
System.out.print("电子邮箱:");
String email = sc.nextLine();
System.out.print("所在城市:");
String city = sc.nextLine();
// 教育/职业
System.out.println("\n----- 教育/职业 -----");
System.out.print("最高学历(高中/大专/本科/硕士/博士):");
String education = sc.nextLine();
double salary = 0;
while (true) {
System.out.print("期望月薪(元):");
if (sc.hasNextDouble()) {
salary = sc.nextDouble();
if (salary >= 0) break;
System.out.println("月薪不能为负数!");
} else {
System.out.println("请输入有效的数字!");
sc.next();
}
}
sc.nextLine();
// 输出确认信息
System.out.println("\n╔════════════════════════════╗");
System.out.println("║ 登记信息确认 ║");
System.out.println("╚════════════════════════════╝");
System.out.printf(" 姓名: %-12s%n", name);
System.out.printf(" 年龄: %-12d%n", age);
System.out.printf(" 性别: %-12s%n", gender);
System.out.printf(" 手机号: %-12s%n", phone);
System.out.printf(" 邮箱: %-12s%n", email);
System.out.printf(" 城市: %-12s%n", city);
System.out.printf(" 学历: %-12s%n", education);
System.out.printf(" 期望月薪:¥%,.2f%n", salary);
System.out.println("──────────────────────────────");
System.out.print("确认无误?(Y/N):");
String confirm = sc.nextLine();
if ("Y".equalsIgnoreCase(confirm) || "Yes".equalsIgnoreCase(confirm)) {
System.out.println("登记成功!感谢你的提交。");
} else {
System.out.println("登记已取消。");
}
sc.close();
}
}
11.3 项目三:交互式功能菜单
java
import java.util.Scanner;
public class InteractiveMenu {
// 模拟数据存储
private static String[] students = new String[100];
private static int count = 0;
private static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
while (true) {
printMenu();
System.out.print("请选择操作(1-5):");
String choice = sc.nextLine();
switch (choice) {
case "1": addStudent(); break;
case "2": listStudents(); break;
case "3": searchStudent(); break;
case "4": System.out.println("统计信息:共 " + count + " 名学生"); break;
case "5":
System.out.println("感谢使用,再见!");
sc.close();
return;
default:
System.out.println("无效选项,请重新选择!");
}
}
}
private static void printMenu() {
System.out.println("\n╔══════════════════════╗");
System.out.println("║ 学生信息管理系统 ║");
System.out.println("╠══════════════════════╣");
System.out.println("║ 1. 添加学生 ║");
System.out.println("║ 2. 查看全部 ║");
System.out.println("║ 3. 搜索学生 ║");
System.out.println("║ 4. 统计信息 ║");
System.out.println("║ 5. 退出系统 ║");
System.out.println("╚══════════════════════╝");
}
private static void addStudent() {
if (count >= students.length) {
System.out.println("存储已满,无法添加!");
return;
}
System.out.print("请输入学生姓名:");
String name = sc.nextLine();
students[count++] = name;
System.out.println("添加成功!(当前共 " + count + " 名学生)");
}
private static void listStudents() {
if (count == 0) {
System.out.println("暂无学生数据!");
return;
}
System.out.println("\n----- 学生列表 -----");
System.out.printf("%-6s %-20s%n", "编号", "姓名");
System.out.println("───────────────────────");
for (int i = 0; i < count; i++) {
System.out.printf("%04d %-20s%n", (i + 1), students[i]);
}
}
private static void searchStudent() {
System.out.print("请输入搜索关键词:");
String keyword = sc.nextLine().toLowerCase();
boolean found = false;
System.out.println("\n----- 搜索结果 -----");
for (int i = 0; i < count; i++) {
if (students[i].toLowerCase().contains(keyword)) {
System.out.printf("%04d %s%n", (i + 1), students[i]);
found = true;
}
}
if (!found) {
System.out.println("未找到匹配的学生。");
}
}
}
十二、输入输出方法选型指南
#mermaid-svg-51LtMuaKj9adJb9I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-51LtMuaKj9adJb9I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-51LtMuaKj9adJb9I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-51LtMuaKj9adJb9I .error-icon{fill:#552222;}#mermaid-svg-51LtMuaKj9adJb9I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-51LtMuaKj9adJb9I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-51LtMuaKj9adJb9I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-51LtMuaKj9adJb9I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-51LtMuaKj9adJb9I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-51LtMuaKj9adJb9I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-51LtMuaKj9adJb9I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-51LtMuaKj9adJb9I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-51LtMuaKj9adJb9I .marker.cross{stroke:#333333;}#mermaid-svg-51LtMuaKj9adJb9I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-51LtMuaKj9adJb9I p{margin:0;}#mermaid-svg-51LtMuaKj9adJb9I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-51LtMuaKj9adJb9I .cluster-label text{fill:#333;}#mermaid-svg-51LtMuaKj9adJb9I .cluster-label span{color:#333;}#mermaid-svg-51LtMuaKj9adJb9I .cluster-label span p{background-color:transparent;}#mermaid-svg-51LtMuaKj9adJb9I .label text,#mermaid-svg-51LtMuaKj9adJb9I span{fill:#333;color:#333;}#mermaid-svg-51LtMuaKj9adJb9I .node rect,#mermaid-svg-51LtMuaKj9adJb9I .node circle,#mermaid-svg-51LtMuaKj9adJb9I .node ellipse,#mermaid-svg-51LtMuaKj9adJb9I .node polygon,#mermaid-svg-51LtMuaKj9adJb9I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-51LtMuaKj9adJb9I .rough-node .label text,#mermaid-svg-51LtMuaKj9adJb9I .node .label text,#mermaid-svg-51LtMuaKj9adJb9I .image-shape .label,#mermaid-svg-51LtMuaKj9adJb9I .icon-shape .label{text-anchor:middle;}#mermaid-svg-51LtMuaKj9adJb9I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-51LtMuaKj9adJb9I .rough-node .label,#mermaid-svg-51LtMuaKj9adJb9I .node .label,#mermaid-svg-51LtMuaKj9adJb9I .image-shape .label,#mermaid-svg-51LtMuaKj9adJb9I .icon-shape .label{text-align:center;}#mermaid-svg-51LtMuaKj9adJb9I .node.clickable{cursor:pointer;}#mermaid-svg-51LtMuaKj9adJb9I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-51LtMuaKj9adJb9I .arrowheadPath{fill:#333333;}#mermaid-svg-51LtMuaKj9adJb9I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-51LtMuaKj9adJb9I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-51LtMuaKj9adJb9I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-51LtMuaKj9adJb9I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-51LtMuaKj9adJb9I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-51LtMuaKj9adJb9I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-51LtMuaKj9adJb9I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-51LtMuaKj9adJb9I .cluster text{fill:#333;}#mermaid-svg-51LtMuaKj9adJb9I .cluster span{color:#333;}#mermaid-svg-51LtMuaKj9adJb9I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-51LtMuaKj9adJb9I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-51LtMuaKj9adJb9I rect.text{fill:none;stroke-width:0;}#mermaid-svg-51LtMuaKj9adJb9I .icon-shape,#mermaid-svg-51LtMuaKj9adJb9I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-51LtMuaKj9adJb9I .icon-shape p,#mermaid-svg-51LtMuaKj9adJb9I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-51LtMuaKj9adJb9I .icon-shape .label rect,#mermaid-svg-51LtMuaKj9adJb9I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-51LtMuaKj9adJb9I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-51LtMuaKj9adJb9I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-51LtMuaKj9adJb9I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
是
否
否
是
是
否
否
是
否
是
否
是
否
需求:如何实现输入输出?
需要图形化界面?
简单的弹窗即可?
JOptionPane
showInputDialog + showMessageDialog
Swing / JavaFX
完整 GUI 框架
需要密码输入?
在命令行终端中运行?
System.console()
readPassword()
Scanner 或自定义
回显屏蔽方案
需要类型解析能力?
Scanner
nextInt / nextDouble 等
读取大量数据?
BufferedReader
- 手动解析
一次性传参即可?
命令行参数
String\[\] args
速查表:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单命令行交互 | Scanner |
开箱即用,类型解析方便 |
| 逐行处理大文件 | BufferedReader |
性能优异,内存友好 |
| 密码输入 | System.console().readPassword() |
安全,输入不回显 |
| 格式化输出 | System.out.printf() / String.format() |
精确控制格式 |
| 单次传参/批处理 | String[] args 命令行参数 |
自动化友好 |
| 桌面应用输入 | JOptionPane |
用户友好,可视化 |
| 需要正则匹配 token | Scanner.hasNext(Pattern) |
Scanner 内置正则支持 |
| 严格类型校验输入 | Scanner.hasNextInt() 等 + 循环 |
容错性强 |
本章关键词回顾
| 关键词 | 英文 | 说明 |
|---|---|---|
| 标准输入 | standard input (stdin) | 程序读取数据的默认输入流,对应 System.in |
| 标准输出 | standard output (stdout) | 程序输出信息的默认输出流,对应 System.out |
| 标准错误 | standard error (stderr) | 程序输出错误信息的专用流,对应 System.err |
| 输入流 | InputStream | 字节级别的输入抽象基类 |
| 打印流 | PrintStream | 提供 print/println/printf 方法的输出流 |
| 扫描器 | Scanner | 用于解析原始输入文本的便利工具类 |
| 格式说明符 | format specifier | printf 中 %d、%s 等控制输出格式的占位符 |
| 缓冲读取器 | BufferedReader | 带缓冲的高效字符输入流 |
| 控制台 | Console | 支持密码回显屏蔽的系统控制台对象 |
| 分隔符 | delimiter | Scanner 解析 token 时的分隔依据 |
| 令牌 | token | Scanner 根据分隔符拆分出的一个个子字符串 |
| 输入类型不匹配异常 | InputMismatchException | 输入内容无法按期望类型解析时抛出 |
| 无此元素异常 | NoSuchElementException | 输入已耗尽但程序仍尝试读取时抛出 |
| 命令行参数 | command-line arguments | main 方法的 String[] args,程序启动时传入 |
| 字符串格式化 | String.format() | 返回格式化字符串而不是直接输出 |
| 确认对话框 | ShowConfirmDialog | JOptionPane 中带确认按钮的对话弹窗 |
思考与练习
基础练习
- 编写一个程序,使用
printf打印九九乘法表,要求每列对齐,输出格式美观。 - 使用
Scanner编写程序,让用户输入圆的半径,计算并输出圆的周长和面积(保留三位小数)。 - 编写程序,读取用户输入的三个整数,使用
printf按从大到小的顺序输出。 - 验证
nextLine()陷阱:先调用nextInt(),再调用nextLine(),观察输出结果并解释原因。
进阶练习
- 编写一个"猜数字"游戏:程序随机生成一个 1-100 之间的整数,给用户最多 7 次猜测机会。每次猜测后提示"大了"或"小了",使用
hasNextInt()做输入校验,并用printf显示剩余的猜测次数。 - 实现一个"温度转换器":支持摄氏度(C)与华氏度(F)互转。程序接收命令行参数,例如
java TempConvert 100 C F表示将 100 摄氏度转为华氏度。需要处理好参数不足时的错误提示。 - 编写程序,使用
BufferedReader读取一个文本文件,统计文件中的总行数、总字符数(含空格)、总单词数。 - 设计一个控制台版的"ATM 取款机"程序,包含登录(使用
Console.readPassword()读密码)、查询余额、存款、取款和退出功能。使用菜单驱动,所有输入需要异常处理。
综合练习
- 实现"学生成绩管理系统":支持命令行参数模式(
--add 张三 95)和交互模式(无参数时进入菜单)。系统能录入、查询、统计(平均分、最高/最低分),并使用printf打印格式化成绩单。 - 编写一个"文件编码检测器":读取文件的前几行内容,尝试判断文件的编码类型(UTF-8、GBK 等),输出文件基本信息(大小、行数、推测编码)。使用
BufferedReader进行高效读取。