Java基础(九) IO流 —— 字符流

导航

字符流

字符流与字节流的关系

FileReader

异常与close()

read()

改进

FileWriter

异常和追加写入

write()

缓冲区

自定义缓冲区

用字符流读取非纯文本文件

文件拷贝

LineNumberReader

readLine()

lineNumber

装饰设计模式

InputStreamReader&OutputStreamWriter

指定编码操作文件

char与汉字

POI


上一篇文章中,我们学习了字节流(建议先阅读上一篇文章:Java基础(八) IO流 —— 字节流)。

在文章末尾留下了这样一个问题:使用字节流读取含有中文的文件中的数据后,在控制台上打印这些数据会出现乱码问题。那么应该怎么解决这个问题呢?下面就带着问题来开始字符流的学习吧。

 

字符流

和字节流相同,字符流也有两个顶层抽象类——Reader和Writer。不过和字节流不同的是,字符流的文件输入输出流——FileReader和FileWriter并不是Reader和Writer的直接子类而是间接子类。本篇文章依然讲解文件输入输出流。

字符流与字节流的关系

在开始文件输入输出流的讲解之前,我们先要明白字符流和字节流之间是一种怎样的关系。

上一篇文章中给字符流下了这样的定义:

字符流 = 字节流 + 编码表

从上面的等式中,我们可以发现字符流的本质还是字节流,是对字节流深层次的加工。

字符流就是依照对应的编码表需要的字节(个数不确定,可能是一个、两个、三个、四个)翻译成字符,或者将指定的字符依照对应的编码表转换成字节。

FileReader

FileReader类中只有三个构造方法,该类的其他方法继承自父类InputStreamReader:

public class FileReader extends InputStreamReader { ... }

所以InputStreamReader才是Reader的直接子类,至于设计成这样原因,在后文将会详细解释。

FileReader的基本操作和FileInputStream类似,不同的是底层实现,这里着重讲解二者不同的地方。见下面一个例子:

public class FileReader1 {
	
	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		FileReader fr = new FileReader("testReader1.txt");  // FileNotFoundException
		int a = fr.read();     	   // IOException
		System.out.println(a);     // 97
		int b = fr.read();
		System.out.println(b);     // 25105
		int c = fr.read(); 
		System.out.println(c);     // 67
		int d = fr.read();
		System.out.println(d);     // -1
		fr.close(); 
	}

}

异常与close()

这部分知识和上一篇文章重合,不再赘述。

read()

下面是文件testReader1.txt中的数据:

a我C

read()方法是一个重载方法,这里介绍无参的read()方法。每次调用read()方法会读取字符流中的一个字符并返回一个int类型的值,再次调用该方法时会读取字符流中的下一个字符。若字符流中已经不存在下一个字符,则返回-1。

既然read()方法读取的是一个字符,为什么返回值不是char类型呢?char类型取值范围是0~65535,也就是说将char类型作为返回值是无法返回-1的,因此这里将读取到的字符类型提升为int类型作为返回值。如果想要得到char类型值,只需要对返回值进行强转即可。

改进

下面对例子加以改进:

public class FileReader2 {
	
	public static void main(String[] args) throws IOException {
		
		FileReader fr = new FileReader("testReader1.txt");
		int ch;
		while((ch = fr.read()) != -1) {
			System.out.print((char)ch);
		}
		fr.close();
	}

}

FileWriter

和FileReader相同,FileWriter类中也只存在构造方法。见下面一个例子:

public class FileWriter1 {
	
	public static void main(String[] args) throws FileNotFoundException, IOException {
		
		FileWriter fw = new FileWriter("testWriter1.txt");  // FileNotFoundException
		fw.write(97);   	   // IOException
		fw.write("bc");
		fw.close(); 
	}

}

异常和追加写入

这部分知识和上一篇文章重合,不再赘述。

write()

相较于FileOutputStream,FileReader的write()方法不仅可以向输出流中写出字节,还可以直接写出字符串。打印结果如下所示:

其实FileOutputStream也可以直接向输出流中写出字符串,只不过写出的过程中还需要多进行一步操作——将字符串转换成字节数组。如下所示:

FileOutputStream fos  = new FileOutputStream("xxx.txt");
fos.write("abc".getBytes());

缓冲区

将上面例子中的fw.close()注释掉,再次运行程序,刷新并查看testWriter1.txt文件,你看到了什么呢?一片空白。这个结果并没有错,那么出现这种情况就只有一种可能了——存在缓冲区。

这个缓冲区并没有定义在FileWriter中,而是定义在顶层抽象类Writer中,缓冲区的大小是一个字节:

public abstract class Writer implements Appendable, Closeable, Flushable {
    private char[] writeBuffer;
    private static final int WRITE_BUFFER_SIZE = 1024;
    ...
}

这个缓冲区很小,一般情况下我们会自定义一个更大的缓冲区。

自定义缓冲区

缓冲区这一块在上一篇文章讲的很详细,这里就不在赘述了。

用字符流读取非纯文本文件

字符流只能操作纯文本文件,假如我们非要用字符流来操作非纯文本文件呢?比如图片。口说无凭,实际操作才能知道结果是怎么样。见下面一个例子:

public class FileReader3 {

	public static void main(String[] args) throws IOException {
		
		FileReader fr = new FileReader("picture.jpg");
		int ch;
		while((ch = fr.read()) != -1) {
			System.out.print((char) ch);
		}
		fr.close();
	}

}

打印结果的一部分:

4�U�N��@nYt
$�k�M4�c<�QM��1�5�Vn�L�:+�X�����"A�3*
�s2o{��3BT

打印结果看上去全是乱码,但其实这些乱码也是有迹可循的。不难发现,乱码是由正常的字符和�组成。

使用字符流读取文件时,会将读取到的字节依照编码表转化成字符。然而对于图片文件而言,读取到的字节能否成功转换成字符是一个未知数。对于碰巧能够成功转换的字节,返回编码表中对应的字符;反之转换失败的字节,则返回�。这就是我们所认为的乱码的来源。

所以只有在操作纯文本文件的时候才可以使用字符流,操作其他类型的文件时应该使用字节流。

文件拷贝

学会了文件输入输出流,就可以进行文件拷贝。但是文件拷贝的工作并不推荐使用字符流来做,因为使用字符流读取和写入文件时,会有一个转换的过程。如下图所示:

而拷贝这件事情所需要的就是字节->字节就可以了,所以仅仅是为了拷贝文件的话不要使用字符流

除此之外,如果使用字符流来拷贝非纯文本文件的话还会出错。例如运行下面的程序就会出现错误:

public class FileCopy {
	
	public static void main(String[] args) throws IOException {
		
		FileReader fr = new FileReader("picture.jpg");
		FileWriter fw = new FileWriter("copy3.jpg");
		char[] arr = new char[8192];
		int len;
		while((len = fr.read(arr)) != -1) {
			fw.write(arr, 0, len);
		}
		fr.close();
		fw.close();
	}

}

拷贝完成后,你就会发现copy3.jpg文件根本无法打开,究其原因正是字符流所做的转换而导致的。如果这里原因你不是很清楚,请仔细阅读上面的用字符流读取非纯文本文件

LineNumberReader

使用LineNumberReader可以设置读取文本文件的每行数据的行号,其构造函数接收一个Reader作为参数。见下面一个例子:

public class LineNumberReader1 {
	
    public static void main(String[] args) throws IOException{
    	
    	LineNumberReader lnr = new LineNumberReader(new FileReader("chinese.txt"));
    	String line;
    	while((line = lnr.readLine()) != null) {
    		System.out.println(lnr.getLineNumber() + ":" +  line);
    	}
    	lnr.close();
    }
    
}

打印结果如下:

1:你好你好

readLine()

每次调用readLine()方法可以读取字符流中的一行数据,若字符流中已经不存在下一行数据,则返回null。

可能你会觉得奇怪——之前调用read()方法时,若流中(字节流、字符流)不存在可读取的数据都是返回-1,怎么到readLine()方法返回值就变成了null呢?

这是因为readLine()方法的返回值是String类型,无法返回-1这种数值类型。所以readLine()对返回值进行了一次转换,将-1转换成null返回来作为文件的结束标志。

lineNumber

LineNumberReader中存在成员变量:lineNumber,通过getLineNumber()和setLineNumber()方法可以读取和设置该成员变量,若不调用setLineNumber()方法设置lineNumber,则lineNumber默认为0。

public class LineNumberReader extends BufferedReader {
    private int lineNumber = 0;
    ...
}

而打印结果的行号为1,是因为调用readLine()方法时会让lineNumber自增。

public String readLine() throws IOException {
    ...
    lineNumber++;
    ...
}

装饰设计模式

其实我们不难发现LineNumberReader的功能就仅仅是为读取文本文件的每行数据设置行号,除此之外就没有其他作用了。我们将LineNumberReader称之为装饰类,LineNumberReader使用的设计模式就是装饰设计模式。

装饰类是对既有类(FileReader)的一种扩展,让既有类变的更加强大。

可能你会说为什么不将LineNumberReader设计成FileReader的子类呢?当然这也是可以的。但是如果将LineNumberReader设计为FileReader的子类,二者之间的的耦合性会很高,随着扩展功能的增加,继承体系会变得越来越臃肿,扩展性变差。

而既有类衍生出的装饰类和既有类之间的耦合性很低,如果需要追加新的扩展功能,只需要增加装饰类即可;反之若不再需要某个扩展功能,只要删除装饰类就行了。

InputStreamReader&OutputStreamWriter

截止目前,我们workspace使用的编码是utf-8,而我们读取文本文件的编码也都是utf-8编码:

   

那么如果workspace使用的编码和文本文件的编码不一致,使用字符流读取文件会怎么样呢?见下面一个例子:

public class FileReader4 {

	public static void main(String[] args) throws IOException {
		
		FileReader fr = new FileReader("utf-16.txt");
		int ch;
		while((ch = fr.read()) != -1) {
			System.out.print((char) ch);
		}
		fr.close();
	}

}

文件utf-16.txt编码和内容如下:

     

控制台打印结果:

��`O}Y`O}Y

很明显,当workspace使用的编码和文本文件的编码不一致时,使用字符流读取数据就会出现问题。因此出现这种情况,就不能再使用FileReader读取文件,而应该使用FileReader的父类——InputStreamReader。

指定编码操作文件

InputStreamReader的构造函数接收两个参数——InputStream对象和指定的编码,编码的大小写不用在意。见下面一个例子:

public class InputStreamReader1 {
	
    public static void main(String[] args) throws IOException{
    	
    	InputStreamReader isr = new InputStreamReader(new FileInputStream("utf-16.txt"), "uTf-16");
    	int ch;
    	while((ch = isr.read()) != -1) {
    		System.out.print((char) ch);
    	}
    	isr.close();
    }
    
}

控制台打印结果如下:

你好你好

需要注意的是InputStreamReader接收的对象是InputStream类型而不是Reader类型。这里可以佐证之前给出的等式:

字符流 = 字节流 + 编码表

而OuputStreamWriter则可以指定编码写出到文件中,如下所示:

public class InputStreamReader2 {
	
    public static void main(String[] args) throws IOException{
    	
    	InputStreamReader isr = new InputStreamReader(new FileInputStream("utf-16.txt"), "utf-16");
    	OutputStreamWriter osr = new OutputStreamWriter(new FileOutputStream("utf-8.txt"), "utf-8");
    	int ch;
    	while((ch = isr.read()) != -1) {
    		osr.write(ch);
    	}
    	isr.close();
    	osr.close();
    }
    
}

 

char与汉字

char类型的取值范围是0~65535,也就是说我们最多可以在控制台上打印65536种字符,这其中包括了符号、中文、英文、日文、韩文等各种字符。下面是Unicode码表部分码截图(这里用到了一个小工具:UniToy):

然而光中文汉字有10万多个,很明显在Java程序中无法显示所有的汉字,只能显示常见的汉字(3500个左右)。那么码值超出char取值范围的汉字在Java程序中会怎么显示呢?

首先在Unicode码表中选取一个码值超出char取值范围的汉字,这里选取的汉字码值为0x020068,见下图:

接着打印一些相关的信息:

public class CharAndChinese {

	public static void main(String[] args) {
		
		System.out.println(0x020068);
		System.out.println((char) 131176);
		System.out.println((int) 'h');
		System.out.println(131176 % 65536);
		
	}
	
}

打印结果如下:

131176
h
104
104

0x020068对应的十进制是131176,很明显这个值已经超出了char的取值范围,将131176强转为char类型,其结果是‘h’。然而打印‘h’所对应的十进制,其结果却是104。将131176模65536,其结果也是104。

通过这个例子可以看出来,对于超出char取值范围的汉字,Java采用的策略是取模——取这个汉字的码值模65535之后的结果。

 

POI

至此文件输入输出流的部分就算全部讲完了,然而你可能已经发现——目前为止我们用字符流操作的文本文件全都是TXT格式的,也就是我们一直所说的纯文本文件,或者称之为无格式文本文件

假如你尝试着用字符流去操作Word或Excel这类带格式的文本文件(非纯文本文件),就会发现在控制台上打印出来的是一片乱码。操作这两类文件,我们就需要引入新的jar包——apache.poi包,当然这已经不在本文的讨论范围之内。

文件的类型肯定远远不止我提到的这些,而学习脚步也不应该止于此。下次再会啦。

 

参考:

https://my.oschina.net/u/914655/blog/318664

https://www.zhihu.com/question/52346583

字符编解码的故事(ASCII,ANSI,Unicode,Utf-8区别)

https://blog.csdn.net/qq_27093465/article/details/53323187

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值