键盘输入与输出:Scanner与System.out详解

本章导读 :输入与输出(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 中带确认按钮的对话弹窗

思考与练习

基础练习

  1. 编写一个程序,使用 printf 打印九九乘法表,要求每列对齐,输出格式美观。
  2. 使用 Scanner 编写程序,让用户输入圆的半径,计算并输出圆的周长和面积(保留三位小数)。
  3. 编写程序,读取用户输入的三个整数,使用 printf 按从大到小的顺序输出。
  4. 验证 nextLine() 陷阱:先调用 nextInt(),再调用 nextLine(),观察输出结果并解释原因。

进阶练习

  1. 编写一个"猜数字"游戏:程序随机生成一个 1-100 之间的整数,给用户最多 7 次猜测机会。每次猜测后提示"大了"或"小了",使用 hasNextInt() 做输入校验,并用 printf 显示剩余的猜测次数。
  2. 实现一个"温度转换器":支持摄氏度(C)与华氏度(F)互转。程序接收命令行参数,例如 java TempConvert 100 C F 表示将 100 摄氏度转为华氏度。需要处理好参数不足时的错误提示。
  3. 编写程序,使用 BufferedReader 读取一个文本文件,统计文件中的总行数、总字符数(含空格)、总单词数。
  4. 设计一个控制台版的"ATM 取款机"程序,包含登录(使用 Console.readPassword() 读密码)、查询余额、存款、取款和退出功能。使用菜单驱动,所有输入需要异常处理。

综合练习

  1. 实现"学生成绩管理系统":支持命令行参数模式(--add 张三 95)和交互模式(无参数时进入菜单)。系统能录入、查询、统计(平均分、最高/最低分),并使用 printf 打印格式化成绩单。
  2. 编写一个"文件编码检测器":读取文件的前几行内容,尝试判断文件的编码类型(UTF-8、GBK 等),输出文件基本信息(大小、行数、推测编码)。使用 BufferedReader 进行高效读取。

下一篇文章 :第二十三章:异常处理------try-catch-finally 完全指南

相关推荐
qq_436962181 小时前
金蝶云星空数据分析怎么做?
java·数据库·数据分析
瓶瓶持续学习中1 小时前
数字孪生到空间智能,5家厂商的技术路线有何不同?
大数据·人工智能·信息可视化·架构
一楼的猫1 小时前
活动会议照片直播云相册:服务形式与适用场景
学习
碧海银沙音频科技研究院1 小时前
基于炬芯ATS308X的BLE 无线方式离线日志传输导出功能实现
java·linux·服务器
愤怒火龙果1 小时前
AIGC 安全学习笔记02 后门攻击・LLM 安全靶场・提示词注入
学习·安全·aigc
2601_954811821 小时前
人工智能实验室集群选型:32节点PB级存储架构实践
人工智能·架构
kkkkkkkkkk_Z1 小时前
新手自学嵌入式 | 学习日记: Linux驱动进阶 —— Platform总线与I2C子系统实战
linux·运维·学习
外收内放1 小时前
FastAPI猜字谜实战(3):把每一局存成JSON
开发语言·python·学习·json·fastapi
denlaku1 小时前
JDK 20 新特性详解
java