深入解析 Java 算法竞赛必备:自定义快速输入工具类
在 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));
- 这是整个类中最关键的一行,构建了一个高效的输入流链:
System.in:标准输入流,是一个字节流(InputStream类型)InputStreamReader:将字节流转换为字符流,处理字符编码问题BufferedReader:为字符流添加缓冲功能,默认缓冲区大小为 8192 字节(8KB)
3. 核心方法:next ()
String next() throws IOException {
while (!st.hasMoreElements()) {
st = new StringTokenizer(bf.readLine());
}
return st.nextToken();
}
- 这是整个工具类的心脏,所有其他读取方法都基于它实现
- 方法逻辑:
- 检查当前
StringTokenizer是否还有未读取的标记(token) - 如果没有,就从控制台读取一整行字符串,并用这行字符串重新初始化
StringTokenizer - 返回下一个标记(即下一个 "单词")
- 检查当前
- 设计巧妙之处:一次读一行,多次用。通过减少 IO 调用次数来大幅提升性能。
4. 整行读取方法:nextLine ()
String nextLine() throws IOException {
return bf.readLine();
}
- 直接调用
BufferedReader的readLine()方法读取一整行 - 注意:这个方法会读取从当前位置到行尾的所有字符,包括空格,但不包括换行符
- 与
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 体系有更深入的理解。
更多推荐



所有评论(0)