Java IO之Reader与Writer对象常用操作(包含了编码问题的处理)
涉及到文件(非文件夹)内容的操作,如果是纯文本的情况下,除了要用到File(见之前文章),另外就必须用到字符输入流或字符输出流。
字符输入流:该流处理时,数据由外部流向程序(内存),一般指代“读取字符”,更清晰点地说:从外部读取字符数据到内存中。
字符输出流:该流处理时,数据由程序(内存)流向外部,一般指代“写入字符”,更清晰点地说:将字符数据从内存写入到外部。
在Java中,可使用:Reader 与 Writer 及其子类。
对字符的操作,采用 Reader与Writer。它们的为声明分别为:
public class FileReader extends InputStreamReader
public abstract class Writer implements Appendable, Closeable, Flushable
它们都是抽象类,需要由具体子类进行实例化。
Reader主要子类有:
- BufferedReader:缓存区字符输入流。从缓存区中读取字符数据。类似于BufferedInputStream。
- CharArrayReader:从字符数组中读取字符数据。类似于ByteArrayInputStream。
- FileReader:文件字符输入流。从文件中读取字符数据。类似于FileInputStream。
- FilterReader:过滤器字符输入流。用于装饰。其子类有:PushbackReader, BufferedReader。类似于FilterInputStream。
- InputStreamReader:字节字符转化流。从字节中读取字符数据。
- PipedReader:管道字符流。用于从管道中读取字符数据。类似于PipedInputStream。
- StringReader :字符读取器。从字符中读取数据。
常用的有FileIReader,我们将以它为例。对比InputStream,Reader少了一些从字节中读取数据的类:AudioInputStream,ObjectInputStream,SequenceInputStream,但多了一些从字符中读取数据的类:InputStreamReader, StringReader。
Writer主要子类有:
- BufferedWriter:缓冲字符输出流。将字符数据写入缓冲区。类似于BufferedOutputStream。
- CharArrayWriter:字符数组输出流。将字符数据写入字符数组。类似于ByteArrayInputStream。
- FileWriter:文件输出流。将字符数据写入文件。类似于FileOutputStream。
- FilterWriter:过滤输出流。用于装饰。类似于FilterOutputStream。
- OutputStreamWriter:字节字符转化流。
- PipedWriter:管道字符流。用于将字符数据写入管道。类似于PipedOutputStream。
- PrintWriter:打印输出流。
- StringWriter :字符输出流。
常用的有FileWriter,我们将以它为例。对比OutputStream,Writer少了一些将字节数据写入的类:ObjectOutputStream,但多了一些将字符数据写入的类:OutputStreamWriter,StrngWriter,PrintWriter
Reader与Writer的方法
Reader字符输入流,其主要方法有:
abstract void close() :释放流对象。用于关闭资源。close()后无法进行read及skip等操作。所以一般在流操作结束后进行close()调用。
void mark(int readAheadLimit) :标记流的位置。系统不一定支持。不推荐使用。
boolean markSupported() :检测是否支持流位置标记。(mark方法与reset方法在其支持下才可用。一般能不用则不用此3个方法)
int read() :从流中读取一个字符(一个或两个或三个字节,依据编码决定),如果没有数据,返回-1。
int read(char[] cbuf) :从流中读取字符,并将数据存入到指定的字符数组中,读取的字符数为指定数组的长度。如果没有数据,返回-1。
abstract int read(char[] cbuf, int off, int len) :从流中读取字符,并将数据存入到指定的字符数组中,读取的字符数为len,存入时从数组off开始存。如果没有数据,返回-1。
int read(CharBuffer target) :从流中读取字符,并将数据存入到指定的字符缓冲上,读取的字符数为指定字符缓冲的大小。如果没有数据,返回-1。
boolean ready() :判断流是否可以被读取。
void reset() :从新设置流的开始。系统不一定支持。不推荐使用。
long skip(long n):跳过指定数目字符。可以是正数负数或0(对于文件流来说,该方法虽然已被重载,但不可以用来解决系统不支持reset()问题。负数或0是不支持的,会抛出异常)。
输入字符流Reader用于“读取”,其中最常用的方法是:read(), read(char[] b), read(char[] b, int off, int len)
代码:(源代码文件编码为:GBK)
1 import java.io.File; 2 import java.io.FileReader; 3 import java.io.IOException; 4 import java.io.Reader; 5 6 public class Reader001 { 7 public static void main(String[] args) throws IOException { 8 Reader r = new FileReader(new File("g:/java2019/file.txt"));//gbk编码的文件,内容为:123abc我爱你 9 int c = 0; 10 while((c=r.read())!=-1){ 11 System.out.println(c+"(0x"+Integer.toHexString(c).toUpperCase()+"):"+(char)c); 12 } 13 r.close(); 14 } 15 }
输出:
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
改变file.txt的编码为UTF-8,再次运行,输出:
38168(0x9518):锘
65533(0xFFFD):?
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
37812(0x93B4):鎴
25120(0x6220):戠
22477(0x57CD):埍
28003(0x6D63):浣
65533(0xFFFD):?
产生乱码,可见,使用FileReader读取数据时,当源文件(程序)与被读取的文件编码不一致的时候,会产生乱码。
将源文件编码也修改为UTF-8,执行输出:
65279(0xFEFF):
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
没有产生乱码,但是头部多了一些东西:65279(0xFEFF)。这个是utf-8的BOM
为什么会多这个标记呢?这个与WINDOWS记事本保存时的编码设置有关。
要去除这个BOM标记,可以用IDE或高级文本编码器重新保存一下为UTF-8(去BOM)则可以
如果还是不行,则可以先用WINDOWS记事本保存为GBK(保存为GBK一般会去除BOM),再次在IDE或高级文本编码器中保存为UTF-8。
对于字符流,往往涉及到编码问题。所以,在谈及字符操作之前,先来说明一下编码的问题。
WINDOW记事本保存文件时对编码的处理:
1。可以保存为ANSI(系统默认编码,简体中文中一般指GBK,繁体中文中可能是BIG5)或者UTF-8等编码。
2。如果保存为ANSI内置编码,会去除文件中包含的BOM标记。
3。如果保存为UTF-8编码,会自动添加BOM标记。
4。当保存为UTF-8编码时,如果文件内容当中没有任何中文(ANSI)相关的内容出现,经常会自动保存为ANSI编码并去除BOM(不是一定,有些会有些不会。并且这个不仅仅是记事本会产生的问题,而是WINDOWS系统的问题,在WINDOWS下,其它文本编辑工具也会产生这样的问题)。很多时候明明保存为UTF-8保存,却变成了GBK就是这个原因,包括很多用代码生成和写入的纯文本文件。
UTF-8对于Java而言最大的问题是:Java编译器不支持带BOM的UTF-8,只支持无BOM的UTF-8源文件的编译。所以如果要采用UTF-8编写源文件的话,必须去除BOM。
去除BOM的方法:
- 1。用WINDOWS记事本先将编码设置为ANSI,再用支持UTF-8的高级文本编辑器(这种类型的编码器在从ANSI转码为UTF-8时不会主动加BOM,但从有BOM的UTF-8无法转ANSI无法去BOM。比如:Eclipse)保存为UTF-8无BOM编码。
- 2。直接用支持去BOM的高级文本编码器保存为UTF-8无BOM编码(比如:Editplus,Notepad++)。
WINDOWS中查看文件编码的方法:
用记事本打开该文件,然后点击”文件“,再点击”另存为“,这时有”编码“项,当前显示的编码就是该文件的编码。
不要太确信高级文本编辑器(或IDE)显示的编码,因为有时候是错误的。
有时候是因为纯英文无ANSI文字造成了显示UTF-8而真实却是ANSI,有时候是因为UTF-8带BOM设置成ANSI(GBK)无效造成了显示ANSI真实却是UTF-8带BOM。在Eclipse,Notepad++都遇到过这种情形。
另外某些IDE进行编码指定时,并不会改变文件的编码,而是指定读取文件或者运行该文件生成的字节码时的附加编码。所以特别注意:IDE设置的编码真的不一定可靠!
所以要确认文件的编码还是要通过记事本的方法。不要太轻信高级编辑器或IDE。包括上边说到的“去除BOM的方法”还需要最终再用记事本打开的方法再确认一下。
随着上边的方案,我们可以做到无BOM的UTF-8。
为了避免BOM或编码问题,编辑器的选择原则是:通用一种编辑器进行文件编辑和编码,不采用另外的编辑器对文件进行修改或二次保存,并且不采用WINDOWS记事本进行编辑(它无法解决带BOM的UTF-8问题),只用WINDOWS记事本来确认编码。
java源文件的编码:是指.java文件的编码,在保存的时候可以选择ANSI(系统默认编码,简体中文中一般指GBK,繁体中文中可能是BIG5),UTF-8等。一般都会在ansi和utf-8之间选择一个。由上边的问题可知:如果是UTF-8编码的源文件,必须是无BOM的UTF-8源文件,否则编译时会出现:错误: 需要class, interface或enum
java类文件的编码:是指.class文件的编码。由于.class文件由.java文件产生的,所以源文件什么编码会决定生成的类文件什么编码。但是类文件的编码,并不一定代表程序执行时的最终编码。
程序执行时的最终编码:由Java执行器当中的参数:java -Dfile.encoding=编码 决定。并且,一般情况下,如果没有指定(即省略-Dfile.encoding=编码), 则为类文件的编码。也就是说,默认情况(无指定)下,file.encoding的值由类文件编码决定。
在程序中输出 System.getProperty("file.encoding")或Charset.defaultCharset()可以获取file.encoding的指定值。这个值或是默认的(类文件编码),或是外部设置的(高级编辑器(或IDE)通过设置编码指定的,或是某些WEB运行环境下配置指定的)所以输出的值如果在命令行或IDE或WEB环境下,都可能会不相同。由于外部可以设置,所以说程序执行时的编码不一定是类文件的编码。另一方面证明了:虽然高级编辑器(或IDE)保存纯英文化的无BOM的UTF-8源文件不成功导致生成了GBK编码的类文件,但执行时仍然是UTF-8形式,就是因为设置UTF-8同时指定了file.encoding。所以在该高级编辑器(或IDE)下进行执行是没问题的,但要保证移植到其它编辑或运行环境下仍然想要正确的结果,那么就要对其它编辑或运行环境设置相同编码约定(有时不需要设定是因为已经一致)。
纯英文代码的GBK编码的类文件与UTF编码无BOM的类文件对于执行器执行来说是兼容的,但file.encoding,它会对字符流之类以及对与编码相关的代码产生影响。所以说来说去最重要的还是file.encoding。这个是我们代码无法保证的,因环境变化而变化,我们要确保的是所有环境保持相同的编码,以避免开发与运行的环境不同导致错误的结果。但是另一个问题是,很多时候环境并不是我们能确保的(比如很多第三方环境无法进行配置),这时候涉及到与字符流或编码相关的代码时,我们应当使用支持编码设定的类并配合Charset类进行编码处理,这个是更推荐的办法。还是要强调:不要太依赖于源文件的编码。
有了以上编码基础现在开始分析运行FileReader程序读取file.txt的情况。
1。头部输出多余的FEFF :这个属于UTF-8带BOM,先按照“去除BOM的方法”去除掉UTF-8中的BOM。再次输出的时候就不会有多余的BOM标记。
2。IDE与file.txt设置的编码不同的情况下,会出现乱码,设置的编码的情况下,不会出现乱码。
3。构造三个编码不同的文件:GBK编码文件(file-gbk.txt),有BOM的UTF-8编码文件(file-bom.txt),无BOM的UTF-8编码文件(file-nobom.txt)进行测试。以验证上边结论。
(1) 假定IDE设置的源文件编码为:GBK。
分别读取上边3个文件进行输出验证(文件内容都为:123abc我爱你)
测试代码:
1 import java.io.File; 2 import java.io.FileReader; 3 import java.io.IOException; 4 import java.io.Reader; 5 import java.nio.charset.Charset; 6 7 public class Reader002 { 8 public static void main(String[] args) throws IOException { 9 System.out.println("Charset.defaultCharset():" + Charset.defaultCharset()); 10 System.out.println("System.getProperty(\"file.encoding\"):" + System.getProperty("file.encoding")); 11 12 String[] paths = new String[] { 13 "g:/java2019/file-gbk.txt", 14 "g:/java2019/file-nobom.txt", 15 "g:/java2019/file-bom.txt" }; 16 17 for (String path : paths) { 18 System.out.println("---------"+path+"----------"); 19 Reader r = new FileReader(new File(path)); 20 int c = 0; 21 while ((c = r.read()) != -1) { 22 System.out.println(c + "(0x" + Integer.toHexString(c).toUpperCase() + "):" + (char) c); 23 } 24 r.close(); 25 } 26 27 } 28 }
输出:
Charset.defaultCharset():GBK
System.getProperty("file.encoding"):GBK
---------g:/java2019/file-gbk.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
---------g:/java2019/file-nobom.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
37812(0x93B4):鎴
25120(0x6220):戠
22477(0x57CD):埍
28003(0x6D63):浣
65533(0xFFFD):?
---------g:/java2019/file-bom.txt----------
38168(0x9518):锘
65533(0xFFFD):?
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
37812(0x93B4):鎴
25120(0x6220):戠
22477(0x57CD):埍
28003(0x6D63):浣
65533(0xFFFD):?
(2) 假定IDE设置的源文件编码为:UTF-8。
测试代码不变,仍然执行,输出:
Charset.defaultCharset():UTF-8
System.getProperty("file.encoding"):UTF-8
---------g:/java2019/file-gbk.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
65533(0xFFFD):�
1200(0x4B0):Ұ
65533(0xFFFD):�
65533(0xFFFD):�
65533(0xFFFD):�
---------g:/java2019/file-nobom.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
---------g:/java2019/file-bom.txt----------
65279(0xFEFF):
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
另外,通过命令行也可以分别进行测试:
> java Reader002 回车输出:
Charset.defaultCharset():GBK
System.getProperty("file.encoding"):GBK
---------g:/java2019/file-gbk.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
---------g:/java2019/file-nobom.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
37812(0x93B4):鎴
25120(0x6220):戠
22477(0x57CD):埍
28003(0x6D63):浣
65533(0xFFFD):?
---------g:/java2019/file-bom.txt----------
38168(0x9518):锘
65533(0xFFFD):?
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
37812(0x93B4):鎴
25120(0x6220):戠
22477(0x57CD):埍
28003(0x6D63):浣
65533(0xFFFD):?
> java -Dfile.encoding=utf-8 Reader002 回车输出:
Charset.defaultCharset():UTF-8
System.getProperty("file.encoding"):utf-8
---------g:/java2019/file-gbk.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
65533(0xFFFD):?
1200(0x4B0):?
65533(0xFFFD):?
65533(0xFFFD):?
65533(0xFFFD):?
---------g:/java2019/file-nobom.txt----------
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
---------g:/java2019/file-bom.txt----------
65279(0xFEFF):?
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
以上测试验证了上边的结论:
(1).执行程序的编码/Charset.defaultCharset()/System.getProperty("file.encoding") 是由运行环境的-Dfile-encoding=XXX决定的(通常可以由外部环境指定。外部环境一般指:命令行参数或IDE编码设置或WEB环境文件配置)。如果没有指定时,-Dfile-encoding默认等于类文件的编码(这时才由类文件编码决定)
(2).执行程序的编码与要处理的文本文件的编码不一致时,在没有任何代码相关的编码处理时(上边的测试程序没有应用编码相关的代码处理,而FileReader默认使用Charset.defaultCharset()),会出现乱码。另外,程序中使用System.setProperty("file.encoding","utf-8");是没有作用的。
如果要处理的文件资源编码不可变(比如现在要读取file-nobom.txt这个UTF-8无BOM文件)同时file.encoding也无法设置(假定当前为GBK)。那么显示编码是冲突了,使用FileReader使用的是默认的无法改变的编码(与file.encoding相同:GBK),如果要正常输出,这是不可能的,不过Reader的子类:InputStreamReader可以处理编码冲突的情况,它通过InputStream并指定编码,可以用来处理字符并解决编码问题。测试代码:
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.io.Reader; 6 import java.nio.charset.Charset; 7 8 public class Reader003 { 9 public static void main(String[] args) throws IOException { 10 System.out.println("Charset.defaultCharset():"+Charset.defaultCharset()); 11 System.out.println("System.getProperty(\"file.encoding\"):"+System.getProperty("file.encoding")); 12 File file = new File("g:/java2019/file-nobom.txt"); 13 Reader r = new InputStreamReader(new FileInputStream(file),"utf-8");//内容为:123abc我爱你 14 int c = 0; 15 while((c=r.read())!=-1){ 16 System.out.println(c+"(0x"+Integer.toHexString(c).toUpperCase()+"):"+(char)c); 17 } 18 r.close(); 19 } 20 }
输出:
Charset.defaultCharset():GBK
System.getProperty("file.encoding"):GBK
49(0x31):1
50(0x32):2
51(0x33):3
97(0x61):a
98(0x62):b
99(0x63):c
25105(0x6211):我
29233(0x7231):爱
20320(0x4F60):你
说明:通过InputStreamReader,可以在编码不匹配的情况下解决冲突问题,解决了FileReader默认编码的问题。在不能进行环境设置的情况下特别有用!!!
InputStreamReader的类声明如下:
public class InputStreamReader extends Reader
InputStreamReader主要的构造方法有:
public InputStreamReader(InputStream in):采用默认编码利用InputStream进行构造。相同于:InputStreamReader(InputStream in, System.getProperty("file.encoding")) 或 InputStreamReader(InputStream in, Charset.defaultCharset())
public InputStreamReader(InputStream in, String charsetName):通过字符串指明编码利用InputStream进行构造。常用。
public InputStreamReader(InputStream in, Charset cs):通过Charset对象指明编码利用InputStream进行构造。
public InputStreamReader(InputStream in, CharsetDecoder dec):较为复杂,少用。
现在继续说Reader的读取操作,可以更高效的处理的方法,是采用:read(char[] c, int off, int len) 来减少读取次数,毕竟每次读取会阻塞,减少了读取次数,效率就提高了。
一次性读取不同编码的文件内容,代码 :
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.io.Reader; 6 7 public class Reader004 { 8 public static void main(String[] args) throws IOException { 9 File file = new File("g:/java2019/file-nobom.txt"); 10 Reader r = new InputStreamReader(new FileInputStream(file),"utf-8");//内容为:123abc我爱你 11 12 char[] c = new char[(int)file.length()]; 13 while((r.read(c))!=-1){ 14 System.out.println(c); 15 } 16 r.close(); 17 } 18 }
输出:123abc我爱你
非一次性读取代码:
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.io.Reader; 6 import java.util.Arrays; 7 8 public class Reader005 { 9 public static void main(String[] args) throws IOException { 10 File file = new File("g:/java2019/file-nobom.txt"); 11 Reader r = new InputStreamReader(new FileInputStream(file),"utf-8");//内容为:123abc我爱你 12 13 int len=0; 14 char[] c = new char[3]; 15 while((len=r.read(c))!=-1){ 16 System.out.println(Arrays.copyOfRange(c, 0, len)); 17 } 18 r.close(); 19 } 20 }
输出:
123
abc
我爱你
Writer字符输出流,其主要方法有:
Writer append(char c) :以追加的方式写入一个字符。可链式调用。
Writer append(CharSequence csq) :以追加的方式写入一个字符序列。可链式调用。
Writer append(CharSequence csq, int start, int end) :以追加的方式写入一个指定开始与结束位置的字符序列。可链式调用。
abstract void close() :释放流对象。用于关闭资源。close()后无法进行read及skip等操作。所以一般在流操作结束后进行close()调用。
abstract void flush() :刷新输出流。特别在使用缓冲区时,如果在进行write和append方法当缓冲区超过输出数据量时,会自动刷新并写入,如果没有超过则不会进行刷新与写入。如果没有close()方法时末端未超过的可能不会进行写入。
不过如果采用JDK1.7的try-resource异常处理,会进行自动close()关闭,倒可以放心。
void write(char[] cbuf) :写入一个字符数组数据。
abstract void write(char[] cbuf, int off, int len) :写入一个指定开始与结束的字符数组数据。
void write(int c) :写入一个字符数据。
void write(String str) :写入一个字符串的数据。
void write(String str, int off, int len):写入一个指定开始与结束的字符串的数据。
输出字符流Writer用于“写入”,其中最常用的方法是:write(int c), write(char[] b), write(char[] b, int off, int len), writer(String str) , Write(String str, int off, int len)
测试代码:
1 import java.io.FileWriter; 2 import java.io.IOException; 3 import java.io.Writer; 4 import java.nio.charset.Charset; 5 6 public class Write001 { 7 public static void main(String[] args) throws IOException { 8 System.out.println(Charset.defaultCharset()); 9 Writer w = new FileWriter("g:/java2019/file-001.txt"); 10 w.write("我爱你"); 11 w.close(); 12 } 13 }
输出:UTF-8
生成或改变的文件格式为:UTF-8
内容为:我爱你
说明:FileWriter按照默认的file.encoding生成了utf-8格式的文件,并且写入了相应的字符串“我爱你”
改下程序:将写入的字符串“我爱你”改成“123abc"
1 import java.io.FileWriter; 2 import java.io.IOException; 3 import java.io.Writer; 4 import java.nio.charset.Charset; 5 6 public class Write001 { 7 public static void main(String[] args) throws IOException { 8 System.out.println(Charset.defaultCharset()); 9 Writer w = new FileWriter("g:/java2019/file-001.txt"); 10 w.write("123abc"); 11 w.close(); 12 } 13 }
输出:UTF-8
文件格式变为:ANSI(GBK)
文件内容:123abc
可见:纯英文下的UTF-8在生成的时候,会自动变成GBK。
如果需要在纯英文下也能生成UTF-8,则可以采取添加BOM头,一旦有BOM头并指明UTF-8,则必须是UTF-8格式。代码如:
1 import java.io.File; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.OutputStreamWriter; 5 import java.nio.charset.Charset; 6 7 public class Write001_2 { 8 public static void main(String[] args) throws IOException { 9 System.out.println(Charset.defaultCharset()); 10 File file = new File("g:/java2019/file-001.txt"); 11 FileOutputStream fos = new FileOutputStream(file); 12 //fos.write BOM header 13 fos.write(0xEF); 14 fos.write(0xBB); 15 fos.write(0xBF); 16 fos.close(); 17 fos = new FileOutputStream(file,true); 18 OutputStreamWriter w = new OutputStreamWriter(fos,"utf-8"); 19 w.write("123abc"); 20 w.close(); 21 } 22 }
一旦头部添加了BOM标记,即使new OutputStreamWriter(fos,"utf-8") 设置编码为gbk,仍然会生成UTF-8(带BOM)。不过生成的带BOM的UTF-8文件如果要程序中要再次进行读取操作的话,就会出现头部有BOM的问题。除非一定要生成UTF-8编码文本或者生成之后不进行后续的被代码程序读取,否则指定UTF-8而被生成GBK格式的文件也是无所谓的(后续遇到程序要处理,也可以配合转换流进行操作)
不考虑复制后是否UTF-8格式,现在进行纯文本文件的复制,代码为:
1 import java.io.FileReader; 2 import java.io.FileWriter; 3 import java.io.IOException; 4 import java.io.Reader; 5 import java.io.Writer; 6 import java.nio.charset.Charset; 7 8 public class Write002 { 9 public static void main(String[] args) { 10 System.out.println(Charset.defaultCharset()); 11 try (Reader r = new FileReader("g:/java2019/file.txt"); 12 Writer w = new FileWriter("g:/java2019/file-001.txt");) { 13 int len = 0; 14 char[] c = new char[1024]; 15 while ((len = r.read(c)) != -1) { 16 w.write(c, 0, len); 17 } 18 } catch (IOException e) { 19 e.printStackTrace(); 20 } 21 } 22 }
输出:utf-8
生成的UTF-8格式文件file-001.txt内容为:123abc�Ұ���
显然乱码了,原因在于读取的资源文件编码为GBK与file-encoding冲突。解决办法上边已经使用过多次。要么将file.txt编码更换为utf-8,要么采用读取转换流。这里用后者,代码更改为:
1 import java.io.FileInputStream; 2 import java.io.FileWriter; 3 import java.io.IOException; 4 import java.io.InputStreamReader; 5 import java.io.Writer; 6 import java.nio.charset.Charset; 7 8 public class Write003 { 9 public static void main(String[] args) { 10 System.out.println(Charset.defaultCharset()); 11 try (InputStreamReader r = new InputStreamReader(new FileInputStream("g:/java2019/file.txt"),Charset.forName("GBK")); 12 Writer w = new FileWriter("g:/java2019/file-001.txt");) { 13 int len = 0; 14 char[] c = new char[1024]; 15 while ((len = r.read(c)) != -1) { 16 w.write(c, 0, len); 17 } 18 } catch (IOException e) { 19 e.printStackTrace(); 20 } 21 } 22 }
现在不会出现乱码了!!
另外,Java还为高效的操作字符流提供了缓冲字符输入流BufferedReader与缓冲字符输出流BufferedWriter,操作代码:
1 import java.io.BufferedReader; 2 import java.io.BufferedWriter; 3 import java.io.FileInputStream; 4 import java.io.FileWriter; 5 import java.io.IOException; 6 import java.io.InputStreamReader; 7 import java.nio.charset.Charset; 8 9 public class Write004 { 10 public static void main(String[] args) { 11 System.out.println(Charset.defaultCharset()); 12 try (BufferedReader r = new BufferedReader( 13 new InputStreamReader(new FileInputStream("g:/java2019/file.txt"), Charset.forName("GBK"))); 14 BufferedWriter w = new BufferedWriter(new FileWriter("g:/java2019/file-001.txt"));) { 15 int len = 0; 16 char[] c = new char[1024]; 17 while ((len = r.read(c)) != -1) { 18 w.write(c, 0, len); 19 } 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 } 24 }
可见,完全可以兼容我们自己写的复制代码,只需要替换特定的输入流与输出流的构造部分,其它部分可以不需要变化。另外,缓冲的字符流还提供了比较好用的一行一行的读取与写入的方法,以及写入换行的方法,见代码:
1 import java.io.BufferedReader; 2 import java.io.BufferedWriter; 3 import java.io.FileInputStream; 4 import java.io.FileWriter; 5 import java.io.IOException; 6 import java.io.InputStreamReader; 7 import java.nio.charset.Charset; 8 9 public class Write005 { 10 public static void main(String[] args) { 11 System.out.println(Charset.defaultCharset()); 12 try (BufferedReader r = new BufferedReader( 13 new InputStreamReader(new FileInputStream("g:/java2019/file.txt"), Charset.forName("GBK"))); 14 BufferedWriter w = new BufferedWriter(new FileWriter("g:/java2019/file-001.txt"));) { 15 String s = null; 16 while ((s = r.readLine())!=null) { 17 w.write(s); 18 w.newLine(); 19 } 20 } catch (IOException e) { 21 e.printStackTrace(); 22 } 23 } 24 }
另外还可以使用打印流PrintWriter进行文件复制:
1 import java.io.BufferedReader; 2 import java.io.FileInputStream; 3 import java.io.FileWriter; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.io.PrintWriter; 7 import java.nio.charset.Charset; 8 9 public class Write006 { 10 public static void main(String[] args) { 11 System.out.println(Charset.defaultCharset()); 12 try (BufferedReader r = new BufferedReader( 13 new InputStreamReader(new FileInputStream("g:/java2019/file.txt"), Charset.forName("GBK"))); 14 PrintWriter w = new PrintWriter(new FileWriter("g:/java2019/file-001.txt"));) { 15 String s = null; 16 while ((s = r.readLine())!=null) { 17 w.println(s); 18 } 19 } catch (IOException e) { 20 e.printStackTrace(); 21 } 22 } 23 }
说明:打印流有println可以输出并换行(内部调用了newLine())。另外,打印流的write(int c)与print(int c)区别:前者会转化为字符进行写入,后者直接原样写入。
其它的字符处理类:暂不介绍。
总结:
- FileReader与FileWriter可以以字符流形式处理文件,进行读取或写入操作。
- 要进行高效的字符流处理,可以使用内置的BufferedReader及BufferedWriter缓冲字符流,它可以处理各种Reader及Writer(不仅仅是FileReader及FileWriter)
- 使用read(char[] b, int off, int len)及write(char[], int off, int len) 可以更高效的进行字符复制(可以替代缓冲字符流)
- 对于文件编码不一致时,可以采用转换输入流InputStreamReader或转换输出流OutputStreamWriter进行转码操作