Java BIO体系详解

前言

本文讲解IO体系中的流式部分,即InputStream、OutputStream、Reader、Writer大家族。即BIO。

输入流和输出流——明确流向

总是说输入流和输出流,但是有的人可能很迷糊这流向到底是从哪里到哪里。首先要明确,输入和输出都是相对于内存而言的。
在这里插入图片描述

  • 输入流是将数据从文件中流向内存;输出流则是将数据从内存中流向文件。
  • 输入流能做的事情无非是:将文件中的数据放入java的数据类型中(比如byte、char、short、int)。
  • 输出流能做的事情无非是:将java数据类型中的数据放入文件中
  • 对于程序来说:输入流是有能力产出数据的流,输出流是有能力接受数据的流。

流的特点

  • 使用流的好处就是,当你获得了一个流对象后,你就不用关心与流交互的具体节点是什么了。
  • 读出时,只能顺序读出;写入时,只能顺序写入。
  • 一个流只能要么是输入流、要么是输出流,不可能同时具有读和写的功能。(当然这里要除了RandomAccessFile)

基类的划分

Java的BIO体系从基类来分析还是很清晰的,因为可以从数据的流向处理的数据类型来进行分类。

处理字节 处理字符
输入流 InputStream Reader
输出流 OutputStream Writer
  • InputStream和OutputStream:处理的数据类型为byte。
  • Reader和Writer:处理的数据类型为char。
  • 总的来说,Reader/Writer相比InputStream/OutputStream算是一种升级,将当初设计得不好的地方进行了优化。

而字符流本质上来讲,是对字节流的功能拓展,因为字符流本质上也是在处理各个字节。比如FileReader的构造器,本质上也是先构造出一个FileInputStream。

    public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
    }

子类的划分——实体类和装饰类

其实子类可以分为两种:实体类和装饰类和其他。因为整个IO体系中使用到了装饰器模式,所以你可以动态组装这些流以构成你想要的功能,而装饰器就类似俄罗斯套娃(禁止套娃!)。现将可以进行装饰的流称为装饰类,真正拥有交互节点(文件、网络)的类称为实体类。

实体类必须有一个真正的节点(比如文件),用来产出数据或接受数据。
而装饰类的构造器一般将一个流作为参数,所以它不用关心是否有节点,反正构造器接受的流肯定会直接或间接地包含到节点。装饰类,主要用来为流增加新功能。

InputStream的子类

在这里插入图片描述
还有一些被遗弃的类(就没有体现在上图中了),以及推荐使用的类:

Deprecated Class Alternative
StringBufferInputStream StringReader
LineNumberInputStream LineNumberReader

从上图可以看出:

  1. InputStream有若干儿子类和孙子类。
  2. ByteArrayInputStream、FileInputStream都属于实体类。
  3. FilterInputStream及其子类和ObjectInputStream都属于装饰类。
  4. PipedInputStream是为了与其他线程公用管道来读取数据。
  5. SequenceInputStream更像是一个工具类,组合多个InputStream并依次读取。

再分别讲解:

  • 从FileInputStream的成员以及构造器中可以看出,这确实是一个实体类,构造完成会持有一个file作为节点。
public class FileInputStream extends InputStream
{
    private final FileDescriptor fd;
    private final String path;

    public FileInputStream(String name) throws FileNotFoundException {...}
    public FileInputStream(File file) throws FileNotFoundException {...}
    public FileInputStream(FileDescriptor fdObj) {...}
}
  • 从FileInputStream的成员以及构造器中可以看出,这确实是一个实体类,构造完成会持有一个byte[]字节数组作为节点。
public class ByteArrayInputStream extends InputStream {
    protected byte buf[];
    
    public ByteArrayInputStream(byte buf[]) {...}
    public ByteArrayInputStream(byte buf[], int offset, int length) {...}
}
  • FilterInputStream以及它的三个子类都是属于装饰类,它们的构造器都会接受一个InputStream,接受的这个流就会被装饰掉,装饰后的效果取决你使用了哪个装饰类。
  • 它们都是InputStream的子类,且构造器接受一个InputStream。这便是装饰器模式的要领:装饰者和被装饰者必须是同一个类型,且装饰者可以拓展功能。
  • FilterInputStream从本质上来讲,只能算代理模式,因为它完全没有拓展任何功能。
  • BufferedInputStream和PushbackInputStream为了拓展相应的功能,自己新加了一个字节数组成员。
  • BufferedInputStream则是为了让读写更快,具体做法则是通过让程序不再直接与文件打交道(也就不必每次程序执行读写时,就真的执行一次物理读写动作),而是与缓冲区打交道。效果如下图,读取数据时,都是从缓冲区里读取,而缓冲区里的数据则都是提前读取一部分了的;写入数据时,都是往缓冲区里写入,而真正的物理写动作则是在缓冲区的数据满到一定程度后才执行的。
    在这里插入图片描述
//FilterInputStream
public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
}
//DataInputStream构造器
public DataInputStream(InputStream in) {
        super(in);
    }
//BufferedInputStream构造器
public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }
public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}
//PushbackInputStream构造器
public PushbackInputStream(InputStream in) {
        this(in, 1);
    }
public PushbackInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("size <= 0");
    }
    this.buf = new byte[size];
    this.pos = size;
}
  • ObjectInputStream也是一个装饰类,但它没有继承FilterInputStream。
public ObjectInputStream(InputStream in) throws IOException {...}

OutputStream的子类

在这里插入图片描述

  1. ByteArrayOutputStream、FileOutputStream是实体类。
  2. FilterOutputStream及其子类们和ObjectOutputStream都是装饰类。
  3. PipedOutputStream是线程用来输出数据的。

Reader的子类

在这里插入图片描述

  1. CharArrayReader、StringReader是实体类。
  2. BufferedReader及其子类是装饰类。(注意BufferedReader并没有继承自FilterReader)
  3. FilterReader及其子类也是装饰类。注意FilterReader算是jdk提供给你自定义装饰类的标准父类,而且它一个抽象类,让你来继承用的。
  4. InputStreamReader及其子类也是装饰类,但它是用来装饰InputStream的,所以说它是用来将InputStream转换为Reader的。
  5. PipedReader是线程用来读取数据的。

Writer的子类

在这里插入图片描述

  1. CharArrayWriter、StringWriter是实体类。
  2. BufferedWriter是装饰类。(注意它也没有继承FilterWriter)
  3. FilterWriter是装饰类,注意它是抽象类,用来继承实现自定义装饰类的。
  4. PrintWriter也是装饰类,它功能强大,一般作为装饰器模式的最外层。
  5. OutputStreamWriter及其子类也是装饰类,但它是用来装饰OutputStream的,所以说它是用来将OutputStream转换为Writer的。
  6. PipedWriter是线程用来输出数据的。

典型使用方法

缓冲读取File(使用BufferedReader)

import java.io.*;

public class BufferedInputFile {
    // Throw exceptions to console:
    public static String read(String filename) throws IOException {
        // Reading input by lines:
        BufferedReader in = new BufferedReader(
                new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while((s = in.readLine())!= null)
            sb.append(s + "\n");
        in.close();
        return sb.toString();
    }

    public static String read_Deprecated(String filename) throws IOException {
        // Reading input by lines:
        DataInputStream in = new DataInputStream(
                new FileInputStream(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while((s = in.readLine())!= null)//readLine是Deprecated的
            sb.append(s + "\n");
        in.close();
        return sb.toString();
    }

    public static void main(String[] args)
            throws IOException {
        // 注意如果报错文件找不到,那么根据根目录看自己文件的位置
        System.out.println("此工程的根目录:"+System.getProperty("user.dir"));
        System.out.print(read("src/BufferedInputFile.java"));
        System.out.print(read_Deprecated("src/BufferedInputFile.java"));
    }
} /* (Execute to see output) *///:~
  • 使用了BufferedReader来装饰FileReader
  • 如果报错FileNotFoundException,那么注意是你给的文件路径不正确,请根据工程根目录修正路径。
  • 对比两个静态函数,read函数能够正常读取;read_Deprecated函数在遇到中文字符时会出错(java文件一般是用utf-8来存代码的,而中文则一个汉字三个字节,DataInputStream遇到多字节存储的字符时就会出错,因为它认为字符都是ASCII字符即都是单字节的)。
  • 对于readLine方法,要使用BufferedReader的,而不是DataInputStream的。
Deprecated Method Alternative
String DataInputStream.readLine() String BufferedReader.readLine()

装饰InputStream类型的System.in

结合上例例子,首先是错误示例:

import java.io.*;

public class test2 {
    public static void main(String[] args) throws IOException {
        DataInputStream in = new DataInputStream(System.in);
        String s;
        while((s=in.readLine()) !=null) {
            System.out.println(s);
        }
    }
}

可见打印结果不对。
在这里插入图片描述
然后是正确示例:使用了BufferedReader来装饰InputStreamReader

import java.io.*;

public class test2 {
    public static void main(String[] args) throws IOException {
    	// 通过InputStreamReader将一个InputStream转换为Reader
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s=in.readLine()) !=null) {
            System.out.println(s);
        }
    }
}

在这里插入图片描述

从内存中的String读取(必然使用StringReader)

使用StringReader

import java.io.*;

public class MemoryInput {
    public static void main(String[] args)
            throws IOException {
        //注意StringReader构造器只是需要一个String
        StringReader in = new StringReader(
                BufferedInputFile.read("src/MemoryInput.java"));
        int c;
        while((c = in.read()) != -1)
            System.out.print((char)c);
    }
} /* (Execute to see output) *///:~

上面程序能够将文本一个字符一个字符地打印出来。

使用StringReader打印两遍

import java.io.*;

public class MemoryInput {
    public static void main(String[] args)
            throws IOException {
        StringReader in = new StringReader(
                BufferedInputFile.read("src/MemoryInput.java"));
        int c;
        while((c = in.read()) != -1)
            System.out.print((char)c);
        in.reset();

        while((c = in.read()) != -1)
            System.out.print((char)c);
    }
} /* (Execute to see output) *///:~
  • 上面程序能够打印两遍代码。
  • 如果没有这句in.reset();,那么只能打印一遍,因为流已经用完了。
  • 在最开始读取之前,不用调用in.mark(),也能起到作用。

使用BufferedReader打印两遍

import java.io.*;

public class MemoryInput {
    public static void main(String[] args)
            throws IOException {
        //注意StringReader构造器只是需要一个String
        BufferedReader in = new BufferedReader(new StringReader(
                BufferedInputFile.read("src/MemoryInput.java")));
        in.mark(BufferedInputFile.read("src/MemoryInput.java").length()+1);
        int c;
        while((c = in.read()) != -1)
            System.out.print((char)c);
        System.out.println("========================我是分割线==========================");
        in.reset();
        String s;
        while((s = in.readLine()) != null) {
            System.out.println(s);
        }
    }
} /* (Execute to see output) *///:~
  • 程序使用了BufferedReader来装饰StringReader。此版本可以将Reader流用两遍,打印MemoryInput.java两次。
  • 如果没有in.mark(BufferedInputFile.read("src/MemoryInput.java").length()+1);in.reset();这两句,第二遍就无法打印出东西,因为流已经用完了。
  • mark函数的参数使用了String.length(),这很自然,因为Reader就是处理字符的流,length返回的也是字符个数。mark函数的参数,必须加1,不然reset时就会报错。
  • 由于使用了BufferedReader,所以也可以使用readLine方法了。(相比笨拙的read方法)

从内存中的byte[ ]读取(必然使用ByteArrayInputStream)

import java.io.*;

public class FormattedMemoryInput {
    public static void main(String[] args) throws IOException {
        try {
            DataInputStream in = new DataInputStream(
                    new ByteArrayInputStream("test测试\n".getBytes()));
            while(true)
                System.out.print((char)in.readByte());
        } catch(EOFException e) {
            System.err.println("End of stream");
        }
    }
} /* (Execute to see output) *///:~

在这里插入图片描述

  • 此程序使用了DataInputStream来装饰ByteArrayInputStream。打印时使用了readByte来读取每一个字节,再通过强转char来打印。
  • 由于中文字符在getBytes后一个字符肯定对应多个字节,且getBytes默认使用UTF-8来encode,所以在读取到中文字符的对应若干字节时,肯定会打印乱码。
  • 一直读取下去,如果读了最后一个字节后再次读取,就会抛出EOFException。(这看起来很烦人)
import java.io.*;

public class FormattedMemoryInput {
    public static void main(String[] args) throws IOException {
        try {
            DataInputStream in = new DataInputStream(
                    new ByteArrayInputStream("test测试\n".getBytes("UTF-16BE")));
            while(true)
                System.out.print(in.readChar());
        } catch(EOFException e) {
            System.err.println("End of stream");
        }
    }
} /* (Execute to see output) *///:~

在这里插入图片描述

  • 此程序克服了读取中文字符乱码的情况。
  • readChar在读取时,是以Unicode来读取的,即每两个字节对应到一个char。
  • getBytes("UTF-16BE")使得encode时,使用UTF-16来编码,BE代表大端模式。
  • 当Unicode<=\uFFFF时,Unicode码就是UTF-16的存储,所以行得通。
import java.io.*;

public class FormattedMemoryInput {
    public static void main(String[] args) throws IOException {
        DataInputStream in = new DataInputStream(
                new ByteArrayInputStream("test测试\n".getBytes("UTF-16BE")));
        while(in.available() != 0)
            System.out.print(in.readChar());
    }
} /* (Execute to see output) *///:~

此程序终于摆脱了EOFException,通过提前判断available。available返回为0代表没有字节可以读取了。但注意当available遇到网络IO时,就不好用了。

将字符串写入File(使用PrintWriter)

import java.io.*;

public class test2 {
    public static void main(String[] args) throws IOException {
        PrintWriter out = new PrintWriter(
                new BufferedWriter(new FileWriter("BasicFileOutput.out")));
        out.println("我是第一行");
        out.println(1111);
        out.println(3.14159);
        out.close();
        // Show the stored file:
        BufferedReader in = new BufferedReader(new FileReader("BasicFileOutput.out"));
        System.out.println(in.readLine());
        System.out.println(in.readLine());
        System.out.println(in.readLine());
    }
}
  • 使用了装饰类BufferedWriter来装饰FileWriter,这就使得写入时先写入到缓冲区里。
  • PrintWriter装饰在最外面,因为它有很多方便的方法,比如print、println方法有很多重载版本。
  • PrintWriter也有很多重载版本的构造器:
    public PrintWriter(String fileName) throws FileNotFoundException {
        this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
             false);
    }

	public PrintWriter(OutputStream out) {
	        this(out, false);//调用下面的构造器
	}
    public PrintWriter(OutputStream out, boolean autoFlush) {
        this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);

        // save print stream for error propagation
        if (out instanceof java.io.PrintStream) {
            psOut = (PrintStream) out;
        }
    }	    
  • 不管是String版的还是OutputStream版的构造器,它都自动为我们加上了缓冲功能(BufferedWriter)。

将Java数据类型写入File(使用DataOutputStream)

import java.io.*;

public class StoringAndRecoveringData {
    public static void main(String[] args) throws IOException {
        DataOutputStream out = new DataOutputStream(
                new BufferedOutputStream(
                        new FileOutputStream("Data.txt")));
        out.writeDouble(3.14159);//各种重载版本
        out.writeUTF("That was pi");
        out.writeDouble(1.41413);
        out.writeUTF("Square root of 2");
        out.close();
        DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream("Data.txt")));
        System.out.println(in.readDouble());
        // Only readUTF() will recover the
        // Java-UTF String properly:
        System.out.println(in.readUTF());
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
    }
} /* Output:
3.14159
That was pi
1.41413
Square root of 2
*///:~
  • 这句new DataOutputStream( new BufferedOutputStream( new FileOutputStream("Data.txt")))中,装饰类BufferedOutputStream起到了缓冲作用,DataOutputStream负责把各种数据类型转换为对应的内存存储字节。
  • 注意writeUTF可能不大一样,因为字符串本质是char数组,而java的char的底层存储是Unicode码。但writeUTF的实现并不是直接存入Unicode码,而是通过字符集UTF-8来映射到字节,再存入文件。
  • writeUTF在写入UTF-8的映射字节之前,会先写入字符数量。这很合理,对于这种不定长的数据类型,首先给出长度是必须的。
posted @ 2020-02-14 19:51  allMayMight  阅读(1046)  评论(0编辑  收藏  举报