目录
一、比特、字节、字符概念铺垫
二、输入输出流的演变(字节流)
三、 InputStream/OutStream、FilterInputStream/FilterOutputStream、OutputStreamWriterInputStreamReader、BufferedWriter/BufferedReader关系图
四、文件读写BufferedWriter/BufferedReader(重要)
五、FileWriter/Reader的默认编码
六、文件读写BufferedWriter/BufferedReader指定编码(重要)
七、OutputStreamWriter写文件
八、OutputStreamWriter和BufferedWriter的区别
九、不用写finally的文件流try..catch(重要)
十、读写文件实战
(1)文件重命名失败
(2)多线程边读边写文件如何设计?
声明:优越G和喷子请自行关闭网页。
比特、字节、字符、编码的概念
位/比特(bit)。是计算机数据存储的最小单位。每个二进制数字0或者1就是1个位;
字节。1个字节=8比特。它也是二进制0和1组成的.
字符。包括各国家文字(一个汉字、韩国字、日本字)、标点符号、图形符号、数字等。
A ---用 0100 0001 表示
字符集。被收入某些标准的字符集合,比如Unicode字符集,GB2312字符集、ASCii字符集等。
编码。规定每个“字符”分别用一个字节还是多个字节存储,用哪些字节来存储,这个规定就叫做“编码”。GBK就是汉字编码,GB2312中一个汉字用2个字节表示。
介绍了概念,那么回到输入输出流 InputStream OutStream,实际上就是一个字节流,输入输出字节流,并不能对文件进行交互。
因此有了FilterInputStream/FilterOutputStream,可以把文件转为文件流。
而文件流也不能直接变成字符串。因此需要桥梁Reader和Writer。
那么Java怎么完成文件读写的呢?
文字是一个字符,早期,Java只提供了字节流处理。为了文件读写,引入抽象类Reader和Writer两个类。
也就是字节与字符的转换桥梁!!
InputStreamReader/OutputStreamReader
FileReader,继承InputStreamReader,读取速度慢。/FileWriter
BufferedReader 行读取提高效率 -BufferedWriter
文字写文件的过程
字符串-BufferedWriter(字符缓冲区,内存)-OutputStreamWriter(字符转字节)-FilterOutputStream(文件输出流)-文件
注意:写文件为了提高速度,会先放入缓冲区。
读文件则相反
//写 (推荐)
public void bufferedWriterWrite(String path, String content) {
try (/*这是新写法,不用写finally关闭流,关闭流的动作由JVM自己管理,推荐*/
FileWriter fileWritter = new FileWriter(path, true);//追加
BufferedWriter bufferedWriter = new BufferedWriter(fileWritter);
) {
bufferedWriter.write(content);
bufferedWriter.newLine();
} catch (IOException e) {
e.printStackTrace();
}
}
//读 (推荐)
public void bufferedWriterRead(String path, String content) {
File file = new File(path);
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
String s;
while ((s = bufferedReader.readLine()) != null && !s.isEmpty()) {
System.out.println(s);//逐行读出,建议放到list里
}
}catch (IOException e){
e.printStackTrace();
}
}
//感受下写finally的恐惧
public void bufferedWriterWrite2(String path, String content) {
FileWriter fileWritter = null;
BufferedWriter bufferedWriter = null;
try {
fileWritter = new FileWriter(path, true);
bufferedWriter = new BufferedWriter(fileWritter);
bufferedWriter.write(content);
bufferedWriter.newLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileWritter != null) {
try {
fileWritter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BufferWriter/Reader总结
支持文件追加或者覆盖
构造参数是Writer/Reader接口,本身不能指定
默认取运行IDE项目编码,除非启动参数配置了 -Dfile.encoding="GBK"
如果默认运行环境编码是utf-8,但是又需要读写GBK编码,就不适用这个FileWriter/Reader
/**
* 推荐 指定编码
* @param path
* @param content
*/
public static void bufferedWriterRead2(String path, String content) {
File file = new File(path);
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "GBK"));) {
String s;
while ((s = bufferedReader.readLine()) != null) {
System.out.println(s);//逐行读出,建议放到list里
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void bufferedWriterWrite3(String path, String content) {
File file = new File(path);
try (
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "GBK"));
) {
bufferedWriter.write(content);
bufferedWriter.newLine();
} catch (IOException e) {
e.printStackTrace();
}
}
这里提供读写文件的写法之一(针对字符串):
public static void writeFile(File file, String content) {
// FileOutputStream会出现中文乱码,用 OutputStreamWriter
OutputStreamWriter osw = null;
try {
osw = new OutputStreamWriter(new FileOutputStream(file), "gbk");
osw.write(content);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void writeFile(String path, String content) {
// FileOutputStream会出现中文乱码,用 OutputStreamWriter
File file = new File(path);
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "gbk");) {
osw.write(content);
//osw.flush(); 调用close会自动flush,而这种写法JVM会自动close
} catch (IOException e) {
e.printStackTrace();
}
}
OutputStreamWriter一个个字符写入磁盘,而BufferedWriter是先写入缓冲区,再一批次写入磁盘。
BufferedWriter本身无法控制编码,OutputStreamWriter可以指定编码。
遇到杠精同学、杠精领导、杠精同事,跟你说finally里面close失败了你是怎么处理的,试试这种写法:
try(..定义区){
write..
}catch(IOException e){
..
}
以前都要写finally自己close,一不小心就忘了写了,遇到杠精还要考虑万一close失败的问题,现在这种写法,finally部分由JVM自己管理,该关闭的流JVM会自己关闭,省心省事。
原因1:重命名的路径文件夹不存在
原因2:如下例子,文件流还没关闭就执行重命名,就会失败。
public static void bufferedWriterWrite(String path, String content) {
File file = new File(path);
File file2 = new File("e:/a/tmp/test.txt");
try (
FileWriter fileWritter = new FileWriter(file, true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWritter);
) {
bufferedWriter.write(content);
bufferedWriter.newLine();
file.renameTo(file2);//文件流还没关闭,不能重命名
} catch (IOException e) {
e.printStackTrace();
}
//file.renameTo(file2); 上面的那行放到这里就可以了
}
public static void main(String[] args) {
bufferedWriterWrite("e:/a/test.txt", "wo我们ww");
}
最近做一个项目,有两个线程,一个线程写文件,另一个线程去读取落地的文件,设计不好的话,可能出现写一半就被读走的情况。
1.避免文件读写一半的情况
首先,写文件也是多个线程去写的,不然会很慢,设计思路就是每个线程共享一个计数器,然后写到批次文件里面(从1开始递增),写的时候先放到tmp文件夹。
然后,写完文件后,重命名的方式从tmp文件夹搬出来。(至于这个重命名的方式会不会出现被读走一半的情况,我不好验证,其他项目的同事是这么做的,前人经验)
读文件就从正式文件夹读,不要从tmp读。
2.如果程序停了怎么办
bufferreader可以从文件末尾追加内容,所以,计数器从1开始也没关系,内容追加进去就好了。
如果不想重新开始写,写的时候再记录一个文件,里面记录当前计数器编号,这样,就可以判断是否已经存在文件,如果存在就去编号记录文件去读取最新的编号,reset一下就可以从断点编号开始了。
3.文件计数器编号,要支持隔天刷新。 文件名格式参考:file-日期-批次号.txt
4.关于第1点还有更极致的办法
如果重命名搬文件,也会出现搬一半的情况,造成读文件的线程只读取到一半内容。那还可以这样设计,文件写完再去另一个文件夹finish去生成一个空文件,文件名为 xx.finish。
然后读文件的线程去遍历正式文件夹的时候,同时去检查finish文件下有没有存在同名文件,有才处理。处理完后可以删掉finish文件也可以不删。
5.读文件也是要分多线程的
写的时候是按照多个文件写的,读文件根据文件夹下的文件来分线程(循环扫描这个文件夹),开个线程池,每个文件分配一个线程去读这个文件。