4.3 字节流和字符流

  在正式学习字节流及字符流以前,有必要先来了解一下I/O流。

4.3.1 I/O流

  在Java中,文件的输入和输出是通过流(Stream)来实现的,流的概念源于UNIX中管道(pipe)的概念。在 UNIX 系统中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备、外部文件等。

  一个流,必有源端和目的端,它们可以是计算机内存的某些区域,也可以是磁盘文件,甚至可以是Internet上的某个URL。对于流而言,不用关心数据是如何传输的,只需要向源端输入数据,从目的端获取数据即可。

  输入流和输出流的示意图分别如图4.5和图4.6所示。

4.3 字节流和字符流 - 图1


图4.5 输入流示意图

4.3 字节流和字符流 - 图2


图4.6 输出流示意图

  如何理解输入和输出呢?简单地说,你听别人唠叨就是输入,你向别人发牢骚就是输出。在计算机的世界中,输入Input和输出Output都是针对计算机的内存而言的。比如读取一个硬盘上的文件,对于内存就是输入;向控制台打印输出一句话,就是输出。Java中对于此类输入/输出的操作统称为I/O,即Input/Output。

  流是对I/O操作的形象描述,水从一个地方转移到另一个地方就形成了水流,而信息从一处转移到另一处就叫做I/O流。

  输入流的抽象表示形式是接口InputStream;输出流的抽象表示形式是接口OutputStream。

  JDK中InputStream和OutputStream的实现就抽象了各种方式向内存读取信息和向外部输出信息的过程。我们之前常用的System.out.println();就是一个典型的输出流,目的是向控制台输出信息。而new Scanner(System.in);就是一个典型的输入流,读取控制台输入的信息。System.in和System.out两个变量就是InputStream和OutputStream的实例对象。

  流按照处理数据的单位,可以分为字节流和字符流。字节流的处理单位是字节,通常用来处理二进制文件,例如音乐、图片文件等。而字符流的处理单位是字符,因为Java采用Unicode编码,Java字符流处理的即为Unicode字符,所以在操作汉字、国际化等方面,字符流具有优势。

4.3.2 字节流

  所有的字节流类都继承自InputStream或OutputStream两个抽象类,这两个抽象类拥有的方法可以通过查阅Java API获得。JDK提供了不少字节流,下面列举了5个输入字节流类,输出字节流类和输入字节流类存在对应关系,这里不再一一列举。

  • FileInputStream:把一个文件作为输入源,从本地文件系统中读取数据字节,实现对文件的读取操作。

  • ByteArrayInputStream:把内存中的一个缓冲区作为输入源,从内存数组中读取数据字节。

  • ObjectInputStream:对以前使用ObjectOutputStream写入的基本数据和对象进行反序列化,用于恢复那些以前序列化的对象,注意这个对象所属的类必须实现 Serializable 接口。

  • PipedInputStream:实现了管道的概念,从线程管道中读取数据字节。主要在线程中使用,用于两个线程间通信。

  • SequenceInputStream:表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直至到达文件末尾,接着从第二个输入流读取,依次类推,直至到达包含的最后一个输入流的文件末尾为止。

  • System.in:从用户控制台读取数据字节,在System类中,in是InputStream类的静态对象。

  接下来通过一个案例来说明如何使用FileInputStream和FileOutputStream两个字节流类,实现复制文件内容的目的。

  1. import java.io.*;
  2. public class TestByteStream{
  3. public static void main(String[] args) throws IOException {
  4. FileInputStream in = null;
  5. FileOutputStream out = null;
  6. try{
  7. File f = new File("C:\\com\bd\\zuche\\Vehicle1.java");
  8. f.createNewFile();
  9. //通过构造方法之一:String构造输入流
  10. in = new FileInputStream("C:\\com\\bd\\zuche\\Vehicle.java");
  11. //通过构造方法之一:File类构造输出流
  12. out = new FileOutputStream(f);
  13. //通过逐个读取、存入字节,实现文件复制
  14. int c;
  15. while ((c = in.read()) != -1) {
  16. out.write(c);
  17. }
  18. }catch(IOException e){
  19. System.out.println(e.getMessage());
  20. }finally{
  21. if(in != null){
  22. in.close();
  23. }
  24. if(out != null){
  25. out.close();
  26. }
  27. }
  28. }
  29. }

  上面的代码分别通过传入字符串和File类,创建了文件输入流和输出流,然后调用输入流类的read()方法从输入流读取字节,再调用输出流的write()方法写入字节,从而实现了复制文件内容的目的。

  代码中有两个细节需要注意:一是read()方法碰到数据流末尾,返回的是-1;二是在输入、输出流用完之后,要在异常处理的finally块中关闭输入、输出流,以节省资源。

  编译、运行程序,C:\com\bd\zuche目录下新建了一个Vehicle1.java文件,打开该文件和Vehicle.java对比,内容一致。再次运行程序,并再次打开Vehicle1.java文件,Vehicle1.java里面的原内容没有再重复增加一遍,这说明输出流的write()方法是覆盖文件内容,而不是在文件内容后面添加内容。如果想采用添加的方式,则在使用构造方法创建字节输出流时,增加第二个值为true的参数即可,例如new FileOutputStream(f,true)。

  程序中,通过f.createNewFile();代码创建了Vehicle1.java这个文件,然后从Vehicle.java向Vehicle1.java实施内容复制。如果注释掉创建文件的这行代码(删除之前创建的Vehicle1.java文件),编译、运行程序,会自动创建出这个文件吗?请大家自己尝试!

  接下来列举InputStream输入流的可用方法。

  • int read()

  从输入流中读取数据的下一个字节,返回0~255范围内的int型字节值。

  • int read(byte[] b)

  从输入流中读取一定数量的字节,并将其存储在字节数组b中,以整数形式返回实际读取的字节数。

  • int read(byte[] b, int off, int len)

  将输入流中最多len个数据字节读入字节数组b中,以整数形式返回实际读取的字节数,off指数组b中将写入数据的初始偏移量。

  • void close()

  关闭此输入流,并释放与该流关联的所有系统资源。

  • int available()

  返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数。

  • void mark(int readlimit)

  在此输入流中标记当前的位置。

  • void reset()

  将此输入流重新定位到最后一次对此输入流调用mark()方法时的位置。

  • boolean markSupported()

  判断此输入流是否支持mark()和reset()方法。

  • long skip(long n)

  跳过和丢弃此输入流中数据的n个字节。

4.3.3 字符流

  所有的字符流类都继承自Reader和Writer这两个抽象类,其中Reader是用于读取字符流的抽象类,子类必须实现的方法只有read(char[], int, int)和close()。但是,多数子类重写了此处定义的一些方法,以提供更高的效率或完成其他功能。Writer是用于写入字符流的抽象类,和Reader类对应。

  Reader和Writer要解决的最主要问题是国际化。原先的I/O类库只支持8位的字节流,因此不能很好地处理16位的Unicode字符。Unicode是国际化的字符集,这样在增加了Reader和Writer之后,就可以自动在本地字符集和Unicode国际化字符集之间进行转换,程序员在应对国际化时不需要做过多额外的处理。

  JDK提供了一些字符流实现类,下面列举了部分输入字符流类,同样,输出字符流类和输入字符流类存在对应关系,这里不再一一列举。

  • FileReader:与FileInputStream对应,从文件系统中读取字符序列。

  • CharArrayReader:与ByteArrayInputStream对应,从字符数组中读取数据。

  • PipedReader:与PipedInputStream对应,从线程管道中读取字符序列。

  • StringReader:从字符串中读取字符序列。

  在前面的案例中通过字节流实现了复制文件内容的目的,接下来再使用FileReader和FileWriter这两个字符流类实现相同的效果。和上一个程序不同的是,这个程序,源文件名及目标文件名不是写死在程序里面,也不是在程序运行过程中让用户输入的,而是在执行程序时,作为参数传递给程序源文件名及目标文件名。具体代码如下:

  1. import java.io.*;
  2. public class TestCharStream{
  3. public static void main(String[] args) throws IOException {
  4. FileReader in = null;
  5. FileWriter out = null;
  6. try{
  7. //其中args[0]代表程序执行时输入的第一个参数
  8. in = new FileReader(args[0]);
  9. out = new FileWriter(args[1]);
  10. //通过逐个读取、存入字符,实现文件复制
  11. int c;
  12. while ((c = in.read()) != -1) {
  13. out.write(c);
  14. }
  15. }catch(IOException e){
  16. System.out.println(e.getMessage());
  17. }finally{
  18. if(in != null){
  19. in.close();
  20. }
  21. if(out != null){
  22. out.close();
  23. }
  24. }
  25. }
  26. }

  上面的代码和TestByteStream的代码类似,只是分别使用了字符流类或字节流类,逐个读取和写入的分别是字符或字节。

  编译、运行程序,运行时在命令行输入java TestCharStream C:\com\bd\zuche\Vehicle.java C:\com\bd\zuche\Vehicle2.java,其中C:\com\bd\zuche\Vehicle.java是第一个参数,C:\com\bd\ zuche\Vehicle2.java是第二个参数,运行结束后在C:\com\bd\zuche 目录下新建了一个Vehicle2.java文件,内容和Vehicle.java文件内容一致。

  在程序里,main()方法中有args这个字符串数组参数,通过这个参数,可以获取用户执行程序时输入的多个参数,其中args[0]代表程序执行时用户输入的第一个参数,args[1]代表程序执行时用户输入的第二个参数,依次类推。

  接下来列举Writer输出字符流的可用方法,希望大家有所了解。注意,这些方法操作的数据是char类型,不是byte类型。

  • Writer append(char c)

  将指定字符添加到此Writer,此处是添加,不是覆盖。

  • Writer append(CharSequence csq)

  将指定字符序列添加到此Writer。

  • Writer append(CharSequence csq, int start, int end)

  将指定字符序列的子序列添加到此Writer。

  • void write(char[] cbuf)

  写入字符数组。

  • void write (char[] cbuf, int off, int len)

  写入字符数组的某一部分。

  • void write(int c)

  写入单个字符。

  • void write(String str)

  写入字符串。

  • void write(String str, int off, int len)

  写入字符串的某一部分。

  • void close()

  关闭此流。