快速的Java ACM IO模板

背景

对于大多数ACMer来说,Java速度慢一直是个让人头疼的问题。实际上,Java的性能并不差,而是由于自带的ScannerPrintStream包装太过复杂(例如Scanner对数据做了缓存,PrintStream对输出操作都加了锁等),而拖慢了I/O速度。使用它们会比C语言的scanf/printf慢好几倍,并且内存开销相当大。

目前网上最常见的快速输入模板是使用BufferedReader,它带缓冲并按行读入测试数据,然后结合StringTokenizer将每一行数据分片。但我找到的几个模板太过简单,虽然代码量少,却无法支持一些常用操作,比如:丢弃当前行、检查是否还有数据等,有一定局限性。也有部分实现了这两个功能的模板,但比较啰嗦,于是自己写了一个。

题外话: 还有很多对Java一知半解的人给出new Scanner(new BufferedInputStream())这种”减速”方法,Scanner的实现中有缓冲,慢并不是慢在这里。

介绍

Reader使用BufferedReadernextLine方法按行读取数据,由内部方法innerNextLine包装,并使用StringTokenizer对每一行数据分片。hasNext方法可以判断是否还有数据,它首先检查当前tokenizer是否有数据,若没有,则会循环读入新行并创建tokenizer,直到找到可用数据或读到尾部为止。nextLine方法会舍弃当前行并读入一行新数据。next方法会返回下一个数据。nextXXX方法可以将next的数据解析成具体数据类型。

Writer则是对BufferedWriter的简单包装,提供了接受Object的printprintln方法。注意,程序结束前一定要调用Writer的close方法来刷新缓冲区并关闭输出流

性能测试

测试题目:给出未知行,每行第一个数N,随后给出N个数,求N个数之和。

测试输入:1000w行,均为5 1 10 100 1000 10000,结果为11111

测试环境:

Windows10 i7-7700HQ

Java: OpenJDK 12

C++: MinGW GCC

方法执行时间峰值内存平均内存
Scanner91.2s88.7MB81.5MB
Scanner(缓冲输入流包装 错误用法)106.9s96.2MB81.5MB
Java快速IO模板9.8s84.1MB79.1MB
cin / cout107.5s0.4MB0.4MB
scanf / printf10.1s0.4MB0.4MB
C / C++输入输出挂2.4s0.4MB0.4MB

模板

class AReader {
    private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    private StringTokenizer tokenizer = new StringTokenizer("");

    private String innerNextLine() {
        try {
            return reader.readLine();
        } catch (IOException ex) {
            return null;
        }
    }

    public boolean hasNext() {
        while (!tokenizer.hasMoreTokens()) {
            String nextLine = innerNextLine();
            if (nextLine == null) {
                return false;
            }
            tokenizer = new StringTokenizer(nextLine);
        }
        return true;
    }

    public String nextLine() {
        tokenizer = new StringTokenizer("");
        return innerNextLine();
    }

    public String next() {
        hasNext();
        return tokenizer.nextToken();
    }

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

    public BigInteger nextBigInt() {
        return new BigInteger(next());
    }

    // 若需要nextDouble等方法,请自行调用Double.parseDouble包装
}

// 如果输出数据量不大,可以忽略Writer,直接使用System.out输出
class AWriter implements Closeable {
    private BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));

    public void print(Object object) throws IOException {
        writer.write(object.toString());
    }

    public void println(Object object) throws IOException {
        writer.write(object.toString());
        writer.write(System.lineSeparator());
    }

    @Override
    public void close() throws IOException {
        writer.close();
    }
}

使用示例:计算大数A+B

public class Main {
    public static void main(String[] args) throws IOException {
        AReader reader = new AReader();
        AWriter writer = new AWriter();

        writer.print(reader.nextBigInt().add(reader.nextBigInt()));

        writer.close();
    }
}

Azure99

底层码农,休闲音游玩家,偶尔写写代码

看看这些?

3 条评论

  1. lin说道:

    博主请问你的读入类的String读入测试通过了吗,我测试好像不能用,如果你那边能用的话,能不能给个实例。

  2. wangzitiansky说道:

    请问一下这个输出类是不是应该这么用呢
    AWriter writer = new AWriter(System.out);
    writer.println();

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注