流是一个抽象的概念,它表示任何有能力产出数据的数据源对象或者是有能力接收数据的接收对象,它屏蔽了实际的I/O设备处理数据的细节。Java中InputStream是对有能力产出数据的数据源对象的抽象称为输入流,OutputStream则是对有能力接收数据的接收端的抽象称为输出流。InputStream和OutpuStream都是抽象类,所有的流都继承自它们。

装饰器流和非装饰器流

流从实现的角度分为装饰器流和非装饰器流。非装饰器流可以独立工作,它封装了对输入输出设备的读写操,比如FileInputStream和FileOutputStream属于非装饰器流,它们封装了对文件的读写操作。 下面使用FileInputStream和FileOutputStream来说明非装饰器流对象的创建:

//创建对文件file的写流
FileOutputStream of = new FileOutputStream("file");
//创建对文件file的读流
FileInputStream fi = new FileInputStream("file");

装饰器流则需要依赖其它流才能工作,具体表现为装饰器流的构造函数需要传入一个流对象,装饰器流不操作底层的输入输出设备,它只是对已有流的封装或者装饰。装饰器流使用设计模式中的装饰器设计模式,输入流装饰器的基类为FilterInputStream,输出流装饰器的基类为FilterOutputStream,所有的装饰器流都继承自FilterInputStream和FilterOutputStream.下面是装饰器流对象的创建示例:

//创建对文件file的写流
FileOutputStream of = new FileOutputStream("file");      
//创建装饰器流BufferedOutputStream,它用来装饰文件流对象of
BufferedOutputStream bufferedStream = new BufferedOutputStream(of);

上例中BufferedOutputStream是装饰器流,关于BufferedOutputStream会在后面介绍。可以看到创建BufferedOutputStream对象时传入了另外一个流of,bufferedStream对象用来装饰of对象,装饰器流的构造方法总有一个参数表示将被装饰的流,装饰器流无法独立使用。

流的基本读写

InputStream定义了抽象方法read表示从输入流读取数据,OutputStream定义了抽象方法write表示把数据写到输出流,每次可以读写一个字节(byte)或一组字节(byte[])。下面以FileInputStream和FileOutputStream为例说明read和write方法的使用:

public void readTest() throws IOException
{
    FileInputStream fi = new FileInputStream("file");
    int oneByte = fi.read();
    byte[] moreByte = new byte[1024];
    fi.read(moreByte);
}

public void writeTest() throws IOException
{
    FileOutputStream fo = new FileOutputStream("file");
    fo.write(10);
    byte[] moreByte = new byte[1024];
    fo.write(moreByte);
}

从上面的例子中可以看到,通过read和write方法可以读写一个字节或者一组字节,如果我们想读写一个int类型的数据该怎么办呢?把一个int型数据转换为字节数组然后调用write方法就可以往输出流写一个int型数据,反之通过read方法读取一个字节数组然后把它转换为int型数据就可以实现从输入流读取一个int型数据。读取其它类型的数据思路也是一样。从使用角度来看读写一个非byte型数据需要进行各种转换,使用起来很是麻烦。Java提供了很多装饰器流来帮忙处理这些转换,简化我们的使用。

输入流装饰器

输入流的装饰器基类为FilterInputStream,常用的FilterInputStream装饰器子类有:

  • DataInputStream
  • BufferedInputStream

DataInputStream用来从输入流读取基本数据类型及字符串,读取示例代码如下:

public void readTest(FileInputStream fi) throws IOException
{
    DataInputStream di = new DataInputStream(fi);
    int intValue = di.readInt();
    float floatVal = di.readFloat();
    String str = di.readUTF();
}

可以看到使用DataInputStream的readInt、readFloat、readUTF等方法可以直接从输入流得到整型、浮点型、字符串等数据,我们不用考虑数据类型的转换问题。DataInputStream和DataOutputStream需要配合使用,后面会介绍DataOutputStream的使用。

BufferedInputStream实现了缓存机制,BufferedInputStream从InputStream读取很多数据并缓存起来,当我们调用BufferedInputStream的read方法时直接从缓存里面取数据返回,这种缓存机制用来提高读数据的效率,BufferedInputStream使用示例如下:

public void bufferdReadTest(FileInputStream fi) throws IOException
{
    BufferedInputStream di = new BufferedInputStream(fi);
    int oneByte = di.read();
    byte[] byteBuf = new byte[1024];
    di.read(byteBuf);
}

BufferedInputStream并没有什么新的方法,它和InputStream使用一模一样,只是在内部增加了缓存机制。

输出流装饰器

输出流的装饰器基类为FilterOutputStream, 常用的FilterOutputStream装饰器子类有:

  • DataOutputStream
  • BufferedOutputStream
  • PrintStream

DataOutputStream和DataInputStream相互配合使用,只有被DataOutputStream写入的流才可以被DataInputStream读取,并且写入的顺序和读取的顺序要一致,写入的类型和读取的类型也必须一致,下面是写入输出流和读取输入流示例代码:

public void dataOutputTest(FileOutputStream fo) throws IOException
{
    DataOutputStream di = new DataOutputStream(fo);
    int ival = 10;
    di.writeInt(ival);
    float fval = 0.01f;
    di.writeFloat(fval);
    String str = "hello";
    di.writeUTF(str);
}
public void dataInputTest(FileInputStream fi) throws IOException
{
    DataInputStream di = new DataInputStream(fi);
    int ival = di.readInt(); //ival = 10
    float fval = di.readFloat(); //fval = 0.01f;
    String str = di.readUTF(); //str = "hello";
}
public void test() throws IOException
{
    FileOutputStream fo = new FileOutputStream("file");
    dataOutputTest(fo);
    FileInputStream fi = new FileInputStream("file");
    dataInputTest(fi);
}

上例代码中先调用dataOutputTest向文件file写入整数10,浮点数0.01,字符串“hello”,然后调用dataInputTest读取整数10,浮点数0.01,字符串“hello” 。可以看到读取的顺序和写入的顺序一致且读取的类型一样。

BufferedOutputStream用来对输出进行缓存,当缓冲区满了以后才会把数据真正写到OutputStream,缓存的目的是提高写数据的效率,使用示例如下:

public void buffereddOutputTest(FileOutputStream fo) throws IOException
{
    BufferedOutputStream bo = new BufferedOutputStream(fo);
    bo.write(10);
    bo.close();
}

可以看到代码的最后调用了close函数,调用该函数的目的在于确保缓冲区的残留数据写入到输出流。

PrintStream用来格式输出,把基本数据类型转换为字符串写入到输出流,System.out就是一个PrintStream对象,使用示例如下:

public void printStreamTest(FileOutputStream fo) throws IOException
{
    PrintStream ps = new PrintStream(fo);
    ps.print(10);
    ps.print(0.0f);
    ps.print("hello");
}

DataOutputStream和PrintStream都可以往输出流写基本数据类型,它们之间的区别是什么呢?看下面代码:

//使用DataOutputStream写整数97
FileOutputStream fo1 = new FileOutputStream("dataOut");
DataOutputStream datao = new DataOutputStream(fo1);
datao.write(97);

//使用PrintStream写整数97
FileOutputStream fo2 = new FileOutputStream("psOut");
PrintStream ps = new PrintStream(fo2);
ps.print(97);

上面代码执行完成以后使用记事本打开dataOut文件将看到字母“a”,打开psOut文件将看到“97”,DataOutputStream把整数97当做二进制存储,97是字符“a”的ASCII码,所以看到“a”。PrintStream则是把整数97转换为字符串“97”进行存储,所以看到的是“97”。

装饰器流嵌套调用

装饰器流可以嵌套调用,像洋葱一样一层套一层,但前提是嵌套是有意义的。就目前学习的装饰器流来说,只有嵌套BufferedInputStream和BufferedOutputStream是有意义的,其它的嵌套没有意义。嵌套使用示例如下:

//PrintStream嵌套BufferedOutputStream
PrintStream ps = new PrintStream(new BufferedOutputStream(new FileOutputStream("psOut")));
ps.print(97);
ps.close();

上例中为PrintStream嵌套一个BufferedOutputStream流来提升写效率,由于装饰器的限制,嵌套的最里面一层始终是非装饰器流。怎么判断嵌套是否有意义?如果嵌套前和嵌套后输入输出没有变化且性能也没有提升则认为嵌套没有意义,反之嵌套有意义。

close

输出装饰器流使用完成以后需要调用close函数用来清理装饰器流,像上面使用BufferedOutputStream的示例中,如果最后不调用close函数可能缓冲的数据不会写到文件里面,这样会造成数据的丢失,所以使用装饰器输出流后应该调用close函数。

类图

下面是常用的装饰器和非装饰器流的类图:

java有sprintf吗 java有input吗_FileInputStream

java有sprintf吗 java有input吗_FileOutputStream_02

绿色的类表示非装饰器流,蓝色的类表示装饰器流。

最后

理解装饰器流和非装饰器流对正确使用流来说非常重要,非装饰器流是对底层输入输出设备的读写操作的封装。装饰器流不操作底层的输入输出设备,它只是对已有流进行封装或装饰。流只支持字节(byte)读写,虽然装饰器流可以读写其它的基本数据类型,但它只是做了类型转换,底层还是使用字节进行读写。