Loading

Scanner 类的使用

定义

public final class Scanner implements Iterator<String>, Closeable{}

构造方法

Scanner 只是"中间层",构造时把数据源交给它:

构造方法 数据源 典型场景
new Scanner(System.in) 键盘 控制台交互(最常见)
new Scanner("1,2,3") 字符串 拆分/解析一段文本
new Scanner(new File("a.txt")) 文件 简易读文件(抛 FileNotFoundException)
new Scanner(Path, Charset) 文件 JDK 10+,可指定编码

两种读取模式

Token 模式(next() / nextInt() / nextDouble() …):

  • 统一规则:跳过前导空白 → 取一段连续非空白 → 停
  • 空格、制表符、换行都视为前导空白,因此可以跨行读(例:上一行末尾的 25 后面直接跨行读下一行的数字)
  • 末尾换行符不消耗——这是设计出来的:分隔符留给下一次调用处理

行模式(nextLine()):

  • 规则:从当前位置读到第一个换行(含空格),连换行一起消耗,返回不含换行的整行

方法

hasNextXxx = 一一对应的"先探测"保险栓,不消耗输入:

读取 先探测 说明
next() hasNext() 读一个词,遇空白停
nextInt() hasNextInt() ⭐ 使用频率最高
nextLong() hasNextLong() 大整数(超 int 范围)
nextDouble() hasNextDouble() 小数(受 Locale 影响,见 §八)
nextBoolean() hasNextBoolean() 只认 "true" / "false"
nextBigInteger() hasNextBigInteger() 任意大整数
nextLine() hasNextLine() 读整行,返回 String
nextFloat() nextShort() nextByte() 同理 hasNextXxx() 用得少,知道即可

细节分析:nextInt() + nextLine()

缓冲区三步推演

键盘输入两行 25 和 Tom,缓冲区里的字符(\n = 看不见的换行符):

    [2][5][\n][T][o][m][\n]

① nextInt() 执行完
    [2][5][\n][T][o][m][\n]
    ^^^^^^
          ^ 读位置:换行只是分隔符,没被消耗,留在缓冲区
    → 返回 25

② 紧接着第 1 次 nextLine()
    [2][5][\n][T][o][m][\n]
    ^^^^^^^^^^
              ^ 读位置:当前位置到第一个换行 = 空!
    → 返回 ""(空串,只吞掉了那个换行)

③ 再来第 2 次 nextLine()
    [2][5][\n][T][o][m][\n]
    ^^^^^^^^^^^^^^^^^^^^^^
                          ^ 读位置:全部消耗完
    → 返回 "Tom"(连同行尾换行一起消耗)

根因一句话:token 方法"拿数不拿行尾",行方法"见换行就收"——两边规则各自都对,混用时对不上才出坑。

修复(二选一)

// 方式一:补一句 nextLine() 吞掉残留换行(最常用)
int age = sc.nextInt();
sc.nextLine();                 // 吞换行
String name = sc.nextLine();   // 正常读到 "Tom Smith"

// 方式二:数字也用 nextLine() 读,自己转型
int age = Integer.parseInt(sc.nextLine().trim());
String name = sc.nextLine();

为什么 Java 要这样设计?

token 方法"只拿属于自己的,分隔符留给下一家"——这样 "1 2 3" 能一个一个读、"12\n34" 跨行也能连续读,不丢数据。
C 的 scanf("%d") 也是同样的规则,所以 scanf 后接 fgets() 读到空行是 C 里的同款经典坑,根因一模一样,可以直接用 C 的心智模型。

close() 为什么要放到最后

使用完Scanner后,我们一定要记得将它关闭!

因为使用Scanner本质上是打开了一个IO流,如果不关闭的话,它将会一直占用系统资源。

但是注意一旦你关闭后,就算在sc.close()这行代码后你再重新new Scanner(System.in),那也不能重新再打开一个扫描器了

所以大家一定要在用不到扫描器之后再关闭,即把sc.close()代码放到最后。

next() 会自动调用 hasNext() 吗?

结论:不会。 只时共用底层 readInput() 作为读缓冲。

源码证据(JDK 24,java.util.Scanner 真实源码)

// next(): 取走一个 token;没数据 → 抛异常,绝不返回 null
public String next() {
    ensureOpen();
    clearCaches();                                  // 一上来就清掉 hasNext 的缓存
    modCount++;
    while (true) {
        String token = getCompleteTokenInBuffer(null);  // 注意:不是 hasNext()
        if (token != null) {
            matchValid = true;
            skipped = false;
            return token;
        }
        if (needInput)
            readInput();                            // 也会阻塞等键盘
        else
            throwFor();                             // → NoSuchElementException
    }
}

// hasNext(): 偷看一眼再恢复现场,不消耗
public boolean hasNext() {
    ensureOpen();
    saveState();                                    // 存现场
    modCount++;
    while (!sourceClosed) {
        if (hasTokenInBuffer()) {
            return revertState(true);               // 恢复现场 → 不消耗
        }
        readInput();
    }
    boolean result = hasTokenInBuffer();
    return revertState(result);
}

关键差异就是 saveState() / revertState():hasNext 是"看一眼再还原",next 是"取走并把 position 往前推"。

性能实测:Scanner 到底多慢

ScannerBench.java,同一份内存数据(100 万个整数,排除 I/O 噪音,第 2 轮成绩):

Scanner       : 238 ms
BufferedReader:  48 ms     → 约慢 4~5 倍

原因:Scanner 每读一个 token 都要走一遍正则匹配 + 类型判断,开销是"每个数字"级别的;BufferedReader 只负责把字符搬进缓冲区,解析交给 parseInt 一步到位。

结论:

  • 几千几万个输入随便用 Scanner,代码干净值这个价
  • OJ 上数据量到 10⁵ 以上(一次读几十万个数)再换 BufferedReader,现在不用急
  • LeetCode 核心代码模式不写 main,用不到 Scanner;只有 ACM 模式(如牛客部分题)和自己写本地测试才需要
posted @ 2026-09-14 12:37  wsttask  阅读(16)  评论(0)    收藏  举报