前言
最近刷算法题又遇到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