Java 快速输入输出

Java 快速输入输出

前言

最近刷算法题又遇到Java超时的问题了,重新研究一下 Java 的输入与输出。总结了下Java的输入输出。

直接说总结吧:Java 的输入输出,只要套上缓冲流(BufferdXxx开头的)速度就会变得高效(更快)。

如果你不愿意看全文具体的分析,你可以直接跳转到 ==一、工具类== 中,我封装了快速输入的工具类,同时举例的快速输出的使用,这样以后会用起来更加简单一些。

如果你愿意整篇文章看一遍,并且IDE打开源码跟着看,我相信你应该也会有收获的。

一、工具类

1.快速输入

StreamTokenizer只能接收数字或字母,如果输入除空格和回车以外的字符(如:~!@#$%^&*()_+{}:<>?)无法识别,会显示null。同时如果要求输出的是字符,你的输入却是数字开头,那么字符会变成null。要求是输入数字你却输出了字母,那么数字会变成0。

        // 创建分词器输入流
        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        // 转到下一个标记
        in.nextToken();
        // 输入字符
        String str = in.sval;
        // 转到下一个标记
        in.nextToken();
        // 输入数字
        double num = in.nval;

静态工具类,直接调用方法就可以了。

    /** 快速输入类 */
    static class Reader {
        static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        /** 获取下一段文本 */
        static String next() throws IOException {
            in.nextToken();
            return in.sval;
        }
        /** 获取数字 */
        static int nextInt() throws IOException {
            in.nextToken();
            return (int)in.nval;
        }
        static double nextDouble() throws IOException {
            in.nextToken();
            return in.nval;
        }
    }

2.快速输入

使用这个不会出现上面,字符为null的情况。

写题的时候推荐这样,读入一行后使用String的split()方法按空格分割。如果需要是数字就用Integer.parseInt()方法转换一下。

        // 创建输入流
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        // 输入一行
        String line = in.readLine();
        // 按空格分割
        String[] split = line.split(" ");

你也可以使用下面这个静态类,直接放到你的类中做内部类,然后类名调用方法就可以了。

备注:StringTokenizer是由于兼容性原因而保留的遗留类,在新代码中不鼓励使用它。

    /** 快速输入类 */
    static class Reader {
        static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        static StringTokenizer tokenizer = new StringTokenizer("");
        /** 获取下一段文本 */
        static String next() throws IOException {
            while ( ! tokenizer.hasMoreTokens() ) {
                tokenizer = new StringTokenizer(reader.readLine());
            }
            return tokenizer.nextToken();
        }
        /** 获取数字 */
        static int nextInt() throws IOException {
            return Integer.parseInt(next());
        }
        static double nextDouble() throws IOException {
            return Double.parseDouble(next());
        }
    }

3.快速输出示例

输出比较大的时候可以用下面这种方式。

// 创建一个输出
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
// 输入
out.println("hello");
// 刷新(需要刷新才能显示)
out.flush();
// 关闭流(不用了就关闭,不然占内存,这是一个好习惯,个人电脑没关系,开发工作时候记得关)
out.close();

二、Java快速输入

1.Scanner 输入

这个类在util包下,属于一个工具。

优点:输入是最简单与方便的(有很多方法可以用)

缺点:输入的效率很低。在数据量大的时候,容易出现超时(Time Limit Exceeded)问题。

使用:

Scanner sc = new Scanner(System.in);
String next = sc.next();
int i = sc.nextInt();

扩展:System.in

“标准”输入流。 该流已经打开,准备提供输入数据。 通常,该流对应于键盘输入或由主机环境或用户指定的另一个输入源。

System.in 该流是对应键盘输入(或另外输入源)。这个常常作为参数,传入到类里。

2.BufferedReader 输入(主要)

从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取。

这个类在io包下。主要带有缓冲区,可以提高输入效率。查看源码可以看到默认字符缓冲大小(defaultCharBufferSize)为8192字节(8KB),默认值足够大,可用于大多数用途。你也可以单独进行设置。

优点:输入效率高。

缺点:方法比较少,常用的就一个readLine()方法用于读入,处理的时候常用String类的split()方法进行分割字符。用着比较麻烦。

使用:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line = b.readLine();

扩展:InputStreamReader

InputStreamReader是从字节流到字符流的桥:它读取字节,并使用指定的charset将其解码为字符 。

传入的InputStreamReader类用于读入字节,然后解码成我们可以理解的字符。

3.StreamTokenizer 分词器

StreamTokenizer类接收输入流并将其解析为“令牌”,允许一次读取一个令牌。 解析过程由表和多个可以设置为各种状态的标志来控制。 流标记器可以识别标识符,数字,引用的字符串和各种注释样式。

创建一个解析给定字符流的tokenizer(分词器)。主要用来分隔字符串。(我们上面用BufferedReader 不是比较麻烦,可以用这个解决)

优点:输入字符和数字比BufferedReader要方便。

缺点:特殊字符会显示为null

使用:

        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        // 下一个分词
        in.nextToken();
        // 读入一个字符串和一个数字并打印
        System.out.println(in.sval);
        in.nextToken();
        System.out.println(in.nval);
        // 循环读入字符串(下一个分词不是不是行末尾时)
        while (in.nextToken() != in.TT_EOL) {
            System.out.println(in.nval);
        }

注意:如果使用这个进行输入时in.sval,如果输入的是不是英文字母或中文,则会显示为null(输入数字或"~!@#$%^&*()_+{}:<>?"是null)。

参考:https://blog.csdn.net/qq_40693171/article/details/81433637

三、Java 快速输出

1.System.out.println()

我们经常使用的是以下这句打印输出,
System.out返回的是一个PrintStream类型,该类型的对象调用方法时,比如print()会直接进行显示,我们看源码可以知道它内部调用了flush() 方法,所以我们不需要手动的去调用flush()方法。

备注:PrintStream对象中,调用的打印方法是BufferedWriter对象的write(String str)方法(BufferedWriter对象的默认字符缓冲大小defaultCharBufferSize为8192)。

System.out.println("hello");

2.PrintWriter 不带缓冲流

我翻看了很多别人写的快速输出,我发现他们常用的是这个,用作输出。但是下面这种声明,经过测试(数据比较大),我发现效率和第一种差别不大。

备注:跟源码可以看到,PrintWriter类如果传入的是OutputStreamWriter类型,那么使用的输出类为默认Writer抽象类的write(String str)方法。而Writer类写入缓冲大小WRITE_BUFFER_SIZE仅为1024。

PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
out.print("hello");
out.flush();

3.PrintWriter 加上缓冲流。

后来我查看了JDK API 文档看到了这段话。发现,发现加上缓冲流之后才能输出的更快一些。

备注:跟源码可以看到,PrintWriter类如果传入的是BufferedWriter类型,那么使用的为传入的BufferedWriter类(该类继承Writer类)。调用的是BufferedWriter类的write(String s, int off, int len)方法。

一般来说,Writer将其输出立即发送到底层字符或字节流。 除非需要提示输出,否则建议将BufferedWriter包装在其write()操作可能很昂贵的Writer上,例如FileWriters和OutputStreamWriters。

例如, PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out"))); >

将缓冲PrintWriter的输出到文件。 没有缓冲,每次调用print()方法都会使字符转换为字节,然后立即写入文件,这可能非常低效。

        // 输出缓冲流(输出字节流转输出字符流(输出字节流))
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
        out.print("hello");
        out.flush();

4.测试

4.1 测试思路

给定一个文本,包含 ==一百万个"0123456789"== 字符串。在依次使用上面的3中方式,每个循环打印 ==100== 次,求耗时平均值。

4.2 测试结果
测试方法消耗时间(平均毫秒)
System.out.println()方法704
PrintWriter 不带缓冲流725
PrintWriter 加上缓冲流665
4.3 测试代码

测试代码:

import java.io.*;

/**
 * @author ChangSheng
 * @date 2020-04-07
 */
public class TestPrint {
    public static void main(String[] args) throws IOException {
        // 文本数据
        String text = getText();

        // 1. 测试 System.out.print
         test01(text);
        // 2. 测试 PrintWriter 不带缓冲流
        // test02(text);
        // 3. 测试 PrintWriter 加上缓冲流
        // test03(text);

    }

    /** 测试 System.out.print */
    static void test01(String text) {
        int n = 10;
        long sum = 0;
        for (int i = 0; i < n; i++) {
            long start = getTime();
            System.out.println(text);
            long end = getTime();
            sum += end - start;
        }
        System.out.print("平均所用毫秒数:" + (sum / n));
    }

    /** 测试 PrintWriter 不带缓冲流 */
    static void test02(String text) {
        int n = 10;
        long sum = 0;
        for (int i = 0; i < n; i++) {
            PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
            long start = getTime();
            out.print(text);
            out.flush();
            long end = getTime();
            sum += end - start;
        }
        System.out.print("平均所用毫秒数:" + (sum / n));
    }

    /** 测试 PrintWriter 加上缓冲流 */
    static void test03(String text) {
        int n = 10;
        long sum = 0;
        for (int i = 0; i < n; i++) {
            PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
            long start = getTime();
            out.print(text);
            out.flush();
            long end = getTime();
            sum += end - start;
        }
        System.out.print("平均所用毫秒数:" + (sum / n));
    }

    static long getTime() {
        return System.currentTimeMillis();
    }

    /** 获取10_000_000次的"0123456789"字符文本 */
    static String getText() {
        int n = 10_000_000;
        StringBuffer text = new StringBuffer();
        for (int i = 0; i < n; i++) {
            text.append("0123456789");
        }
        return text.toString();
    }
}

5.总结

5.1 测试总结

PrintWriter 不带缓冲流(最慢) < System.out.println()方法(其次) < PrintWriter 加上缓冲流(最快)

5.2 原因

参照我上面三种使用时的备注,可以知道:

  • 第一种System.out.println()方法,使用的是BufferedWriter(字符缓存大小8192defaultCharBufferSize),但每次都会刷新(耗时)
  • 第二种PrintWriter 不带缓冲流方法,使用的是抽象类Writer的方法(写入缓存大小仅WRITE_BUFFER_SIZE1024),虽然不会每次刷新,但是缓冲比较小。
  • 第三种PrintWriter 加上缓冲流方法,使用的也是BufferedWriter,但是不用每次都刷新,所以是最快的。

四、扩展

附上IO流导图。

图片参考自:https://www.cnblogs.com/l2rf/p/5320010.html