前言
众所周知,Java中有多种针对文件的操作类,以面向字节流和字符流可分为两大类,这里以写入为例:
面向字节流的:FileOutputStream 和 BufferedOutputStream
面向字符流的:FileWriter 和 BufferedWriter
近年来发展出New I/O ,也叫NIO,里面又包装了两个类:NewOutputStream 和 NewBufferedWriter
现在,我们建立测试程序,比较这些类写入文件的性能。
机器配置
- Processor Name: Intel Core i7
- Processor Speed: 2.2 GHz
- Number of Processors: 1
- Total Number of Cores: 4
- L2 Cache (per Core): 256 KB
- L3 Cache: 6 MB
- Memory: 16 GB
测试程序
纵向比较:几种文件操作类向文件中写入相同行数的内容(每行内容均为“写入文件Data\n”),比较其耗费时间
横向比较:对于同一个文件操作类,比较写入不同行数内容情况下所耗费时间;本文以2的次方指数级增长行数
import java.io.File;
import java.io.FileOutputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths; public class testFileIO { public static void testDriver () throws IOException {
int maxlineNum = 100000001;//写入文件的最大行数
int startlineNum = 1;//写入文件的行数
int Multiplying = 2;//行数增长倍率 long begin = 0L;
long end = 0L; //将时间统计写入文件Result.txt中
FileWriter fileWriter = new FileWriter("./Result.txt", true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); System.out.println("Test FileOutputStream begin.");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileOutputStream(lineNum);
end = System.currentTimeMillis();
long timeElapse_FileOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileOutputStream)+"\t");
}
System.out.println("Test FileOutputStream end.\n"); System.out.println("Test BufferedOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedOutputStream(lineNum);
end = System.currentTimeMillis();
long timeElapse_BufferedOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedOutputStream)+"\t");
}
System.out.println("Test BufferedOutputStream end.\n"); System.out.println("Test FileWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileWriter(lineNum);
end = System.currentTimeMillis();
long timeElapse_FileWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileWriter)+"\t");
}
System.out.println("Test FileWriter end.\n"); System.out.println("Test BufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedWriter(lineNum);
end = System.currentTimeMillis();
long timeElapse_BufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedWriter)+"\t");
}
System.out.println("Test BufferedWriter end.\n"); System.out.println("Test NewOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewOutputStream(lineNum);
end = System.currentTimeMillis();
long timeElapse_NewOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewOutputStream)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); System.out.println("Test NewBufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewBufferedWriter(lineNum);
end = System.currentTimeMillis();
long timeElapse_NewBufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewBufferedWriter)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); bufferedWriter.close();
} /************************** I/O *****************************/
//面向字节
public static void testFileOutputStream (int lineNum) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testFileOutputStream.txt"));
while (--lineNum > 0) {
fileOutputStream.write("写入文件Data\n".getBytes());
}
fileOutputStream.close();
} public static void testBufferedOutputStream (int lineNum) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testBufferedOutputStream.txt"));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
while (--lineNum > 0) {
bufferedOutputStream.write("写入文件Data\n".getBytes());
}
bufferedOutputStream.close();
} //面向字符
public static void testFileWriter (int lineNum) throws IOException {
FileWriter fileWriter = new FileWriter("./testFileWriter.txt");
while (--lineNum > 0) {
fileWriter.write("写入文件Data\n");
}
fileWriter.close();
} public static void testBufferedWriter (int lineNum) throws IOException {
FileWriter fileWriter = new FileWriter("./testBufferedWriter.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
while (--lineNum > 0) {
bufferedWriter.write("写入文件Data\n");
}
bufferedWriter.close();
} /************************** NIO ****************************/
public static void testNewOutputStream (int lineNum) throws IOException {
OutputStream outputStream = Files.newOutputStream(Paths.get("./testNewOutputStream.txt"));
while (--lineNum > 0) {
outputStream.write("写入文件Data\n".getBytes());
}
outputStream.close();
} public static void testNewBufferedWriter (int lineNum) throws IOException {
BufferedWriter newBufferedReader = Files.newBufferedWriter(Paths.get("./testNewBufferedWriter.txt"));
while (--lineNum > 0) {
newBufferedReader.write("写入文件Data\n");
}
newBufferedReader.close();
} public static void main (String[] args) throws IOException {
//多次测试时可清空result.txt文件
FileWriter fileWriter = new FileWriter("./Result.txt");
testDriver();
}
}
测试结果
从上图可以看出,写入行数超过20W以上时,FileOutputStream和NewOutputStream耗费时间远远超出其他4个类。为了清晰,让我们放大其他4个类的图:
可以看出,这4个类中,BufferWriter和NewBufferedWriter所耗费时间更少,但总体差别不是很大。
让我们再来看看,写入26W行数据以下时的情况:
可以看出,在数据量较小的情况下,这4个类所耗费时间的差异并不是很大,在更小的数据量下,它们的效率几乎没有差别。
后记
从以上分析可知(注意横坐标写入行数是指数级增加的),各个类的时间复杂度大致为O(k),其中不同的类的k不同,导致了最终巨大的差异。
这里只给出了测试结果,并未很深入地分析其底层实现原理,欢迎评论区留言。
另外,我没有在其他机器测试,有兴趣的小伙伴可以将自己的测试结果发出来,共同进步^_^
附件
本次测试数据结果(若看不清,可以将浏览器字体放大,或下载到本地看)
~~~~~~~~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~~~~~
评论区小伙伴“ andorxor”提出:
XXXOutputStream是用来写二进制的,你把字符串转换成字节数组再写自然就慢了,主要慢在转换的过程。
因此,将程序修改,提前把字符和字节内容都准备好,再次验证。新程序如下:
import java.io.File;
import java.io.FileOutputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths; public class testFileIO { public static void testDriver () throws IOException {
int maxlineNum = 100000001;//写入文件的最大行数
int startlineNum = 1;//写入文件的行数
int Multiplying = 2;//行数增长倍率 String contentChars = "写入文件Data\n";//每行的内容(字符流)
byte[] contentBytes = "写入文件Data\n".getBytes();//每行的内容(字节流) long begin = 0L;
long end = 0L; //将时间统计写入文件Result.txt中
FileWriter fileWriter = new FileWriter("./Result.txt", true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); System.out.println("Test FileOutputStream begin.");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_FileOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileOutputStream)+"\t");
}
System.out.println("Test FileOutputStream end.\n"); System.out.println("Test BufferedOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_BufferedOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedOutputStream)+"\t");
}
System.out.println("Test BufferedOutputStream end.\n"); System.out.println("Test FileWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_FileWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileWriter)+"\t");
}
System.out.println("Test FileWriter end.\n"); System.out.println("Test BufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_BufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedWriter)+"\t");
}
System.out.println("Test BufferedWriter end.\n"); System.out.println("Test NewOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_NewOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewOutputStream)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); System.out.println("Test NewBufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewBufferedWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_NewBufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewBufferedWriter)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); bufferedWriter.close();
} /************************** I/O *****************************/
//面向字节
public static void testFileOutputStream (int lineNum, byte[] content) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testFileOutputStream.txt"));
while (--lineNum > 0) {
fileOutputStream.write(content);
}
fileOutputStream.close();
} public static void testBufferedOutputStream (int lineNum, byte[] content) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testBufferedOutputStream.txt"));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
while (--lineNum > 0) {
bufferedOutputStream.write(content);
}
bufferedOutputStream.close();
} //面向字符
public static void testFileWriter (int lineNum, String content) throws IOException {
FileWriter fileWriter = new FileWriter("./testFileWriter.txt");
while (--lineNum > 0) {
fileWriter.write(content);
}
fileWriter.close();
} public static void testBufferedWriter (int lineNum, String content) throws IOException {
FileWriter fileWriter = new FileWriter("./testBufferedWriter.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
while (--lineNum > 0) {
bufferedWriter.write(content);
}
bufferedWriter.close();
} /************************** NIO ****************************/
public static void testNewOutputStream (int lineNum, byte[] content) throws IOException {
OutputStream outputStream = Files.newOutputStream(Paths.get("./testNewOutputStream.txt"));
while (--lineNum > 0) {
outputStream.write(content);
}
outputStream.close();
} public static void testNewBufferedWriter (int lineNum,String content) throws IOException {
BufferedWriter newBufferedReader = Files.newBufferedWriter(Paths.get("./testNewBufferedWriter.txt"));
while (--lineNum > 0) {
newBufferedReader.write(content);
}
newBufferedReader.close();
} public static void main (String[] args) throws IOException {
//多次测试时可清空result.txt文件
FileWriter fileWriter = new FileWriter("./Result.txt");
testDriver();
}
}
结果为:
可以看出和前面的案例几乎没有差异(图就不画了)。
所以XXXOutputStream效率低的原因并不是字符串转换成字节数组,而是其本身的实现方式所致。
~~~~~~~~~~~~~~~~~~~~~分割线:底层实现原理浅谈~~~~~~~~~~~~~~~~~~~~~~~
其实,计算机中都是针对字节操作的(即字符都要经过编码转换为字节),那么问题来了,FileOutputStream为什么比FileWriter(FileWriter内部还有FileOutputStream转换操作,具体看源码)还要慢呢?且慢,让我们把写入文件的数据改一下:
import java.io.File;
import java.io.FileOutputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths; public class testFileIO { public static void testDriver () throws IOException {
int maxlineNum = 500001;//写入文件的最大行数
int startlineNum = 1;//写入文件的行数
int Multiplying = 2;//行数增长倍率 String baseContent = "背景\n" +
"考虑以下场景:\n" +
"\n" +
"InfoTable(信息表):\n" +
"\n" +
"Name\tGender\tAge\tScore\n" +
"张三\t男\t21\t90\n" +
"李四\t女\t20\t87\n" +
"王五\t男\t22\t92\n" +
"赵六\t女\t19\t94\n" +
"孙七\t女\t23\t88\n" +
"周八\t男\t20\t91\n" +
"StatusTable(状态表,指是否有在考试之前复习):\n" +
"\n" +
"Name\thasReview\n" +
"张三\t是\n" +
"李四\t否\n" +
"王五\t是\n" +
"赵六\t是\n" +
"孙七\t否\n" +
"周八\t是\n" +
"现在,我想知道所有复习过的学生的成绩,可以利用mysql中的子查询来实现:\n" +
"\n" +
"SELECT Score \n" +
"FROM InfoTable \n" +
"WHERE Name in (SELECT Name \n" +
" FROM StatusTable \n" +
" WHERE hasReview = '是');\n" +
"这种方式非常方便,我们只要把查询条件写出来,剩下的操作都由mysql来处理。而在实际场景中,为了减少底层耦合,我们一般不通过mysql中的子查询方式联表查询,而是先执行子查询得到结果集,再以结果集作为条件执行外层查询。通常情况下,子查询和外层查询由上层的不同服务执行,这样就在一定程度上达到了底层数据库解耦的目的。注意这种实现方式将mysql内部的一部分复杂操作抛给了我们。这时,Mybatis中的foreach标签就有了用武之地。\n" +
"\n" +
"Mybatis 中foreach标签的用法\n" +
"还以刚才的例子来说,先执行子查询\n" +
"\n" +
"SELECT Name FROM StatusTable WHERE hasReview = '是'\n" +
"再执行外层查询,就是\n" +
"\n" +
"SELECT Score \n" +
"FROM InfoTable \n" +
"WHERE Name in ('张三' , '王五', '赵六', '周八');\n" +
"也就是一个批量查询操作,将其抽象一下(假设有三个条件):\n" +
"\n" +
"SELECT * \n" +
"FROM <tableName> \n" +
"WHERE <ColumnName> IN (<case1>,<case2>,<case3>)\n" +
"实际情况中,case可能远不止3个,这时可以在XXXMapper.xml文件中利用Mybatis中的foreach编写sql语句:\n" +
"\n" +
"SELECT * \n" +
"FROM <tableName> \n" +
"WHERE <ColumnName> IN \n" +
"<foreach collection=\"list\" index=\"index\" item=\"item\" open=\"(\" separator=\",\" close=\")\">\n" +
" #{item}\n" +
"</foreach>\n" +
"就可以实现相同的效果了。\n" +
"\n" +
"那么问题来了,foreach标签中各种参数是什么含义呢?\n" +
"\n" +
"collection\n" +
"如果传入的是单参数且参数类型是一个List的时候,collection属性值为list\n" +
"如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array\n" +
"如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在breast里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key\n" +
"index 集合迭代位置\n" +
"item 集合中的每一个元素别名\n" +
"open 开始符号,例如这里的(,就对应于IN (<case1>,<case2>,<case3>)中IN后面的第一个(\n" +
"separator 分隔符,例如这里的,,就对应于IN (<case1>,<case2>,<case3>)中的,\n" +
"close 结束符号,例如这里的),就对应于IN (<case1>,<case2>,<case3>)中<case3>后面的)\n" +
"参考\n"; String contentChars = baseContent;//每行的内容(字符流)
byte[] contentBytes = baseContent.getBytes();//每行的内容(字节流) long begin = 0L;
long end = 0L; //将时间统计写入文件Result.txt中
FileWriter fileWriter = new FileWriter("./Result.txt", true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); System.out.println("Test FileOutputStream begin.");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_FileOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileOutputStream)+"\t");
}
System.out.println("Test FileOutputStream end.\n"); System.out.println("Test BufferedOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_BufferedOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedOutputStream)+"\t");
}
System.out.println("Test BufferedOutputStream end.\n"); System.out.println("Test FileWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testFileWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_FileWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_FileWriter)+"\t");
}
System.out.println("Test FileWriter end.\n"); System.out.println("Test BufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testBufferedWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_BufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_BufferedWriter)+"\t");
}
System.out.println("Test BufferedWriter end.\n"); System.out.println("Test NewOutputStream begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewOutputStream(lineNum,contentBytes);
end = System.currentTimeMillis();
long timeElapse_NewOutputStream = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewOutputStream)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); System.out.println("Test NewBufferedWriter begin.");
bufferedWriter.write("\n");
for (int lineNum = startlineNum; lineNum < maxlineNum; lineNum *= Multiplying) {
begin = System.currentTimeMillis();
testNewBufferedWriter(lineNum,contentChars);
end = System.currentTimeMillis();
long timeElapse_NewBufferedWriter = end - begin;
bufferedWriter.write(String.valueOf(timeElapse_NewBufferedWriter)+"\t");
}
System.out.println("Test NewOutputStream end.\n"); bufferedWriter.close();
} /************************** I/O *****************************/
//面向字节
public static void testFileOutputStream (int lineNum, byte[] content) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testFileOutputStream.txt"));
while (--lineNum > 0) {
fileOutputStream.write(content);
}
fileOutputStream.close();
} public static void testBufferedOutputStream (int lineNum, byte[] content) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(new File("./testBufferedOutputStream.txt"));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
while (--lineNum > 0) {
bufferedOutputStream.write(content);
}
bufferedOutputStream.close();
} //面向字符
public static void testFileWriter (int lineNum, String content) throws IOException {
FileWriter fileWriter = new FileWriter("./testFileWriter.txt");
while (--lineNum > 0) {
fileWriter.write(content);
}
fileWriter.close();
} public static void testBufferedWriter (int lineNum, String content) throws IOException {
FileWriter fileWriter = new FileWriter("./testBufferedWriter.txt");
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
while (--lineNum > 0) {
bufferedWriter.write(content);
}
bufferedWriter.close();
} /************************** NIO ****************************/
public static void testNewOutputStream (int lineNum, byte[] content) throws IOException {
OutputStream outputStream = Files.newOutputStream(Paths.get("./testNewOutputStream.txt"));
while (--lineNum > 0) {
outputStream.write(content);
}
outputStream.close();
} public static void testNewBufferedWriter (int lineNum,String content) throws IOException {
BufferedWriter newBufferedReader = Files.newBufferedWriter(Paths.get("./testNewBufferedWriter.txt"));
while (--lineNum > 0) {
newBufferedReader.write(content);
}
newBufferedReader.close();
} public static void main (String[] args) throws IOException {
//多次测试时可清空result.txt文件
FileWriter fileWriter = new FileWriter("./Result.txt");
testDriver();
}
}
这次数据量就很大了,结果也就变了:
所以,数据量很小的情况下,字符到字节的编码操作带来的性能降低几乎忽略不计;而数据量很大的时候,编码耗费的时间就很可观了。至于为什么在小数据量的情况下FileWriter快很多,目前我认为是一次操作两个字节所致(有了缓存之后就差不多了)。