在 Java 算法竞赛和大数据处理场景中,一个常见的痛点就是输入速度慢。很多初学者使用 JDK 自带的Scanner类处理输入,在数据量超过 1 万行时就容易出现超时问题。今天我们就来深入分析一个被广泛使用的自定义快速输入工具类,从逐行代码解析到底层原理,再到实际应用,彻底搞懂它为什么能比Scanner快 10 倍以上。

一、完整代码展示

这是我们今天要分析的Read类,也是几乎所有 Java 算法竞赛选手都会使用的标准输入模板:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Read {
    StringTokenizer st = new StringTokenizer("");
    BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
    
    String next() throws IOException {
        while (!st.hasMoreElements()) {
            st = new StringTokenizer(bf.readLine());
        }
        return st.nextToken();
    }
    
    String nextLine() throws IOException {
        return bf.readLine();
    }
    
    int nextInt() throws IOException {
        return Integer.parseInt(next());
    }
    
    long nextLong() throws IOException {
        return Long.parseLong(next());
    }
    
    double nextDouble() throws IOException {
        return Double.parseDouble(next());
    }
}

二、逐行代码详细分析

让我们一行一行拆解这个看似简单却设计精妙的工具类。

1. 包声明与导入

  • 标准的 Java 包声明,将类组织到指定的包结构中,避免命名冲突。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
  • 导入四个核心类:
    • BufferedReader:缓冲字符输入流,提供高效的字符读取能力
    • IOException:输入输出异常,所有 IO 操作都可能抛出
    • InputStreamReader:字节流到字符流的转换器
    • StringTokenizer:轻量级字符串分词器,用于分割字符串

2. 成员变量初始化

StringTokenizer st = new StringTokenizer("");
  • 初始化一个空的StringTokenizer对象
  • 为什么初始化为空字符串?因为我们需要在第一次调用next()方法时才真正读取输入
  • 这是一种懒加载思想,避免在类初始化时就进行 IO 操作
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
  • 这是整个类中最关键的一行,构建了一个高效的输入流链:
    1. System.in:标准输入流,是一个字节流(InputStream类型)
    2. InputStreamReader:将字节流转换为字符流,处理字符编码问题
    3. BufferedReader:为字符流添加缓冲功能,默认缓冲区大小为 8192 字节(8KB)

3. 核心方法:next ()

String next() throws IOException {
    while (!st.hasMoreElements()) {
        st = new StringTokenizer(bf.readLine());
    }
    return st.nextToken();
}
  • 这是整个工具类的心脏,所有其他读取方法都基于它实现
  • 方法逻辑:
    1. 检查当前StringTokenizer是否还有未读取的标记(token)
    2. 如果没有,就从控制台读取一整行字符串,并用这行字符串重新初始化StringTokenizer
    3. 返回下一个标记(即下一个 "单词")
  • 设计巧妙之处:一次读一行,多次用。通过减少 IO 调用次数来大幅提升性能。

4. 整行读取方法:nextLine ()

String nextLine() throws IOException {
    return bf.readLine();
}
  • 直接调用BufferedReaderreadLine()方法读取一整行
  • 注意:这个方法会读取从当前位置到行尾的所有字符,包括空格,但不包括换行符
  • next()的区别:next()会跳过前导空白符,只返回下一个非空白符序列

5. 基本数据类型读取方法

int nextInt() throws IOException {
    return Integer.parseInt(next());
}

long nextLong() throws IOException {
    return Long.parseLong(next());
}

double nextDouble() throws IOException {
    return Double.parseDouble(next());
}
  • 这三个方法都是对next()方法的简单封装
  • 先调用next()读取字符串形式的数字,再用对应包装类的parseXxx()方法解析成基本数据类型
  • 这种设计实现了代码复用,避免了重复编写 IO 逻辑

三、核心原理深度解析

1. 为什么 BufferedReader 比 Scanner 快?

这是很多人都会问的问题,我们从两个层面来对比:

对比维度 Scanner BufferedReader + StringTokenizer
IO 机制 无缓冲 IO,每次读取都直接与操作系统交互 有缓冲 IO,先将数据读入内存缓冲区,再从缓冲区读取
解析方式 使用正则表达式匹配输入,逻辑复杂 简单的字符分割,逻辑极轻量
异常处理 内部捕获 IO 异常,返回布尔值 直接抛出 IOException,由调用者处理
性能 慢,适合小数据量 快,适合大数据量

性能差距有多大? 在处理 10 万个整数时,Scanner大约需要 1000ms,而BufferedReader + StringTokenizer只需要不到 100ms,差距超过 10 倍。

2. StringTokenizer 的工作原理

StringTokenizer是一个非常古老的类,从 JDK 1.0 就存在了。它的工作原理非常简单:

  • 按默认分隔符(空格、制表符\t、换行符\n、回车符\r、换页符\f)将字符串分割成多个标记
  • 内部维护一个指针,指向当前读取的位置
  • 每次调用nextToken()就返回当前指针位置的标记,并将指针移动到下一个标记

相比String.split()方法,StringTokenizer不需要创建数组来存储所有标记,内存占用更小,速度也更快。

3. 异常处理设计

这个工具类的所有方法都声明抛出IOException,而不是在内部捕获。这是一个非常好的设计:

  • 让调用者决定如何处理异常(在算法竞赛中通常直接抛出即可)
  • 避免了异常被吞掉导致的调试困难
  • 符合 Java IO 的标准设计模式

四、实际应用示例

场景:算法题 - 整数求和

这是一个典型的算法竞赛输入场景,需要读取 n 个整数并计算它们的和。

import java.io.IOException;

public class Main {
    // 声明为静态变量,全局复用
    static Read in = new Read();

    public static void main(String[] args) throws IOException {
        // 读取数据量n
        int n = in.nextInt();
        
        long sum = 0;
        // 循环读取n个整数并累加
        for (int i = 0; i < n; i++) {
            sum += in.nextInt();
        }
        
        // 输出结果
        System.out.println(sum);
    }
}

输入示例:

5
10 20 30 40 50

输出示例:

150

场景:读取混合类型数据

这个工具类也可以轻松处理混合类型的输入:

public class StudentInfo {
    static Read in = new Read();

    public static void main(String[] args) throws IOException {
        // 读取学生姓名
        String name = in.next();
        // 读取年龄
        int age = in.nextInt();
        // 读取成绩
        double score = in.nextDouble();
        
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
        System.out.println("成绩:" + score);
    }
}

输入示例:

张三 20 95.5

输出示例:

姓名:张三
年龄:20
成绩:95.5

五、常见问题与优化建议

1. 常见问题

问题 1:next () 和 nextLine () 混用导致的空行问题

  • 原因:nextInt()等方法只会读取数字,不会读取后面的换行符
  • 解决:在调用nextLine()之前先调用一次in.nextLine()吃掉换行符

问题 2:读取空行时抛出 NullPointerException

  • 原因:当bf.readLine()读取到流的末尾时会返回 null,而new StringTokenizer(null)会抛出空指针异常
  • 解决:在实际应用中可以添加空值检查

2. 优化建议

优化 1:添加空值检查

String next() throws IOException {
    while (!st.hasMoreElements()) {
        String line = bf.readLine();
        if (line == null) {
            return null; // 或者抛出EOFException
        }
        st = new StringTokenizer(line);
    }
    return st.nextToken();
}

优化 2:支持自定义分隔符

// 添加一个重载的构造方法
public Read(String delimiter) {
    this.delimiter = delimiter;
}

// 在next()方法中使用自定义分隔符
st = new StringTokenizer(bf.readLine(), delimiter);

优化 3:添加 close () 方法

public void close() throws IOException {
    bf.close();
}

六、总结

这个只有 30 多行代码的Read类,是 Java 算法竞赛选手的必备工具。它的核心设计思想就是减少 IO 次数简化解析逻辑

  • 使用BufferedReader提供的缓冲功能,将 IO 次数从 "每次读一个单词" 减少到 "每次读一行"
  • 使用StringTokenizer进行轻量级字符串分割,避免了Scanner正则解析的开销
  • 通过方法封装,提供了与Scanner类似的 API,使用起来非常方便

虽然现在 JDK 也提供了Scanner这样更强大的输入类,但在性能要求极高的场景下,这个简单的自定义工具类仍然是最佳选择。理解它的实现原理,不仅能帮助你在算法竞赛中避免超时问题,还能让你对 Java IO 体系有更深入的理解。

更多推荐