Java中常见的IO流及其使用

Java中IO流分成两大类,一种是输入流。全部的输入流都直接或间接继承自InputStream抽象类,输入流作为数据的来源。我们能够通过输入流的read方法读取字节数据。还有一种是输出流,全部的输出流都直接或间接继承自OutputStream抽象类,输出流接收数据。能够通过write方法写入字节数据。在Java的IO流类中,大部分的输入流和输出流都是成对存在的。即假设存在XXXInputStream。那么就存在XXXOutputStream。反之亦然。(SequenceInputStream和StringBufferInputStream是特例,没有相应的SequenceOutputStream类和StringBufferOutputStream类,稍后会解释)。很多IO操作都可能会抛出IOException异常,比方read、write、close操作。
下面是Java的IO流中常见的输入流,由于每一个输入流都有其相应的输出流,所以此处就不再列出输出流的继承结构图。

Java中常见IO流

下面依次对这些类进行介绍以及怎样使用。

 


ByteArrayInputStream & ByteArrayOutputStream

ByteArrayInputStream构造函数中须要传入一个byte数组作为数据源,当运行read操作时,就会从该数组中读取数据,正如其名,是一种基于字节数组实现的一种简单输入流,显而易见的是,假设在构造函数中传入了null作为字节数据。那么在运行read操作时就会出现NullPointerException异常。可是在构造函数初始化阶段不会抛出异常。与之相相应的是ByteArrayOutputStream,其内部也有一个字节数组用于存储write操作时写入的数据,在构造函数中能够传入一个size指定其内部的byte数组的大小。假设不指定,那么默认它会将byte数组初始化为32字节,当持续通过write向ByteArrayOutputStream中写入数据时,假设其内部的byte数组的剩余空间不能够存储须要写入的数据,那么那么它会通过调用内部的ensureCapacity
方法对其内部维护的byte数组进行扩容以存储全部要写入的数据,所以不必操心其内部的byte数组太小导致的IndexOutOfBoundsException之类的异常。

下面是ByteArrayInputStream 和 ByteArrayOutputStream的代码片段演示样例:

private static void testByteArrayInputOutStream(){
        byte[] bytes = "I am iSpring".getBytes();
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte[] buf = new byte[1024];

        int length = 0;

        try{
            while((length = bais.read(buf)) > 0){
                baos.write(buf, 0, length);
            }
            System.out.println(baos.toString("UTF-8"));
            bais.close();
            baos.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

在上面的样例中,我们通过字符串获取字节数组将其作为ByteArrayInputStream的数据流来源,然后通过读取ByteArrayInputStream的数据,将读到的数据写入到ByteArrayOutputStream中。


FileInputStream & FileOutputStream

FileInputStream 能够将文件作为数据源,读取文件里的流,通过File对象或文件路径等初始化。在其构造函数中。假设传入的File对象(或与其相相应的文件路径所表示的File对象)不存在或是一个文件夹而不是文件或者由于其它原因无法打开读取数据,都会导致在初始化阶段导致抛出FileNotFoundException异常。与FileInputStream 相相应的是FileOutputStream,能够通过FileOutputStream向文件里写入数据,也须要通过File对象或文件路径对其初始化,如同FileInputStream ,假设传入的File对象(或与其相相应的文件路径所表示的File对象)是一个文件夹而不是文件或者由于其它原因无法创建该文件写入数据,都会导致在初始化阶段抛出FileNotFoundException异常。

下面是FileInputStream 和 FileOutputStream的代码演示样例片段:

private static void testFileInputOutStream(){
        try{
            String inputFileName = "D:\iWork\file1.txt";
            String outputFileName = "D:\iWork\file2.txt";
            FileInputStream fis = new FileInputStream(inputFileName);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            byte[] buf = new byte[1024];
            int length = 0;
            while ((length = fis.read(buf)) > 0){
                fos.write(buf, 0, length);
            }
            fis.close();
            fos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

在上面的样例中。我们通过FileInputStream的read方法读取file1.txt中的数据,然后将获得的字节数据通过FileOutputStream的write方法将其写入到还有一个文件file2.txt中,这样就实现了文件的拷贝,即将file1.txt复制到file2.txt。

假设file2.txt已经存在,那么在初始FileOutputStream时,能够传入一边boolean变量append表示是向已有文件里追加写入数据还是覆盖已有数据。


PipedInputStream & PipedOutputStream

PipedInputStream和PipedOutputStream通常是结合使用的。这两个类用于在两个线程间进行管道通信,一般在一个线程中运行PipedOutputStream 的write操作。而在还有一个线程中运行PipedInputStream的read操作。能够在构造函数中传入相关的流将PipedInputStream 和PipedOutputStream 绑定起来,也能够通过二者的connect方法将二者绑定起来,一旦二者进进行了绑定,那么PipedInputStream的read方法就会自己主动读取PipedOutputStream写入的数据。PipedInputStream的read操作是堵塞式的,当运行PipedOutputStream的write操作时,PipedInputStream会在还有一个线程中自己主动读取PipedOutputStream写入的内容,假设PipedOutputStream一直没有运行write操作写入数据,那么PipedInputStream的read方法会一直堵塞PipedInputStream的read方法所运行的线程直至读到数据。

单独使用PipedInputStream或单独使用PipedOutputStream时没有不论什么意义的。必须将二者通过connect方法(或在构造函数中传入相应的流)进行连接绑定。假设单独使用当中的某一个类,就会触发IOException: Pipe Not Connected.
下面是PipedInputStream和PipedOutputStream的代码演示样例片段:

WriterThread类

import java.io.*;

public class WriterThread extends Thread {

    PipedOutputStream pos = null;

    public WriterThread(PipedOutputStream pos){
        this.pos = pos;
    }

    @Override
    public void run() {
        String message =  "这条信息来自于WriterThread.";

        try{
            byte[] bytes = message.getBytes("UTF-8");
            System.out.println("WriterThread发送信息");
            this.pos.write(bytes);
            this.pos.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

ReaderThread类
import java.io.*;

public class ReaderThread extends Thread {
    private PipedInputStream pis = null;

    public ReaderThread(PipedInputStream pis){
        this.pis = pis;
    }

    @Override
    public void run() {
        byte[] buf = new byte[1024 * 8];

        try{
            System.out.println("ReaderThread堵塞式的等待接收数据...");
            int length = pis.read(buf);
            System.out.println("ReaderThread接收到例如以下信息:");
            String message = new String(buf, 0, length, "UTF-8");
            System.out.println(message);
            pis.close();
        }catch(IOException e){
            e.printStackTrace();
        }
    }
}

測��代码

private  static void testPipedInputOutputStream(){
        try{
            PipedInputStream pis = new PipedInputStream();
            PipedOutputStream pos = new PipedOutputStream();
            pos.connect(pis);
            WriterThread writerThread = new WriterThread(pos);
            ReaderThread readerThread = new ReaderThread(pis);
            readerThread.start();
            writerThread.start();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

在上面的实例中,我们创建了两个线程类WriterThread和ReaderThread,在WriterThread的构造函数中我们传入了一个PipedOutputStream,并在线程运行run方法时向WriterThread中写入数据。在ReaderThread的构造函数中我们传入了一个PipedInputStream。在其线程运行run方法时堵塞式的运行read操作。等待获取数据。

我们通过pos.connect(pis)将这两种流绑定在一起,最后分别运行线程ReaderThread和WriterThread。

输出结果例如以下:
输出结果

我们能够看到即使我们先运行了ReaderThread线程,ReaderThread中的PipedInputStream还是一直在堵塞式的等待数据的到来。

 


ObjectInputStream & ObjectOutputStream

ObjectOutputStream具有一系列writeXXX方法,在其构造函数中能够掺入一个OutputStream,能够方便的向指定的输出流中写入基本类型数据以及String。比方writeBoolean、writeChar、writeInt、writeLong、writeFloat、writeDouble、writeCharts、writeUTF等,除此之外。ObjectOutputStream还具有writeObject方法。writeObject方法中传入的类型必须实现了Serializable接口,从而在运行writeObject操作时将对象进行序列化成流。并将其写入指定的输出流中。与ObjectOutputStream相相应的是ObjectInputStream,ObjectInputStream有与OutputStream中的writeXXX系列方法全然相应的readXXX系列方法,专门用于读取OutputStream通过writeXXX写入的数据。

下面是ObjectInputStream 和 ObjectOutputStream的演示样例代码:

Person类

import  java.io.Serializable;

public class Person implements Serializable {
    private String name = "";
    private int age = 0;

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

測试代码

private static void testObjectInputOutputStream(){
        try{
            String fileName = "D:\iWork\file.tmp";
            //将内存中的对象序列化到物理文件里
            FileOutputStream fos = new FileOutputStream(fileName);
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            String description = "下面是人员数组";

            Person[] persons = new Person[]{
                    new Person("iSpring", 26),
                    new Person("Mr.Sun", 27),
                    new Person("Miss.Zhou", 27)
            };

            oos.writeObject(description);

            oos.writeInt(persons.length);

            for(Person person : persons){
                oos.writeObject(person);
            }

            oos.close();

            //从物理文件里反序列化读取对象信息
            FileInputStream fis = new FileInputStream(fileName);
            ObjectInputStream ois = new ObjectInputStream(fis);
            String str = (String)ois.readObject();
            System.out.println(str);
            int personCount = ois.readInt();
            for(int i = 0; i < personCount; i++){
                Person person = (Person)ois.readObject();
                StringBuilder sb = new StringBuilder();
                sb.append("姓名: ").append(person.getName()).append(", 年龄: ").append(person.getAge());
                System.out.println(sb);
            }
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }

输出结果例如以下:
測试结果
Person实现了Serializable接口,须要注意的是,Serializable接口是一个标识接口,并不须要实现不论什么方法。我们首先通过ObjectOutputStream。将Person等数组信息序列化成流。然后通过调用writeObject等方法将其写入到FileOutputStream中,从而实现了将内存中的基本类型和对象序列化保存到硬盘的物理文件里。

然后通过FileInputStream读取文件,将文件的输入流传入到到ObjectInputStream的构造函数中。这样ObjectInputStream就能够通过运行相应的readXXX操作读取基本类型或对象。当运行readObject操作时。返回的是Object类型,须要强制转换为相应的实际类型。须要注意的是,ObjectInputStream运行readXXX操作的方法顺序须要与ObjectOutputStream运行writeXXX操作的方法顺序一致。否则就会读到错误的数据或抛出异常。比方一開始向FileOutputStream中运行writeFloat。而在FileInputStream中首先运行了readInt操作,不会报错,由于writeFloat写入了4个字节的数据。readInt读入了4个字节的数据,尽管能够将该Float转换为相应的int,可是事实上已经不是我们想要的数据了,所以要注意readXXX操作与writeXXX操作运行顺序的相应。


SequenceInputStream

SequenceInputStream 主要是将两个(或多个)InputStream在逻辑上合并为一个InputStream,比方在构造函数中传入两个InputStream。分别为in1和in2,那么SequenceInputStream在读取操作时会先读取in1,假设in1读取完成。就会接着读取in2。在我们理解了SequenceInputStream 的作用是将两个输入流合并为一个输入流之后,我们就能理解为什么不存在相应的SequenceOutputStream 类了。由于将一个输出流拆分为多个输出流是没有意义的。
下面是关于SequenceInputStream的演示样例代码:

private static void testSequenceInputOutputStream(){
        String inputFileName1 = "D:\iWork\file1.txt";
        String inputFileName2 = "D:\iWork\file2.txt";
        String outputFileName = "D:\iWork\file3.txt";

        try{
            FileInputStream fis1 = new FileInputStream(inputFileName1);
            FileInputStream fis2 = new FileInputStream(inputFileName2);
            SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            byte[] buf = new byte[1024];
            int length = 0;
            while((length = sis.read(buf)) > 0){
                fos.write(buf, 0, length);
            }
            sis.close();
            fos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

我们通过FileInputStream分别获取了file1.txt和file2.txt的输入流,然后将这两个输入流作为构造函数的參数创建了SequenceInputStream 的实例,所以该SequenceInputStream 中已经在逻辑上将file1.txt和file2.txt的内容合并为了一个输入流,然后我们读取该SequenceInputStream 中的数据,并将读到的数据写入到一个新的FileOutputStream中,这样我们就实现了将file1.txt和file2.txt合并为一个新的文件file3.txt,原有的file1.txt和file2.txt文件不受不论什么影响。

 


StringBufferInputStream

StringBufferInputStream同意通过在构造函数中传入字符串以读取字节,在读取时内部主要调用了String的charAt方法。与SequenceInputStream相似。StringBufferInputStream也没有相应的OutputStream。即不存在StringBufferOutputStream类。

Java没有设计StringBufferOutputStream类的理由也非常easy。我们假设StringBufferOutputStream存在,那么StringBufferOutputStream应该是内部通过运行write操作写入数据更新其内部的String对象,比方有可能是通过StringBuilder来实现,可是这样做毫无意义,由于一旦我们String的构造函数中能够直接传入字节数组构建字符串。简单明了,所以设计StringBufferOutputStream就没有太大的必要了。StringBufferInputStream这个类本身存在一点问题,它不能非常好地将字符数组转换为字节数组,所以该类被Java标记为废弃的(Deprecated),其官方推荐使用StringReader作为取代。

下面是关于StringBufferInputStream的演示样例代码:

private static void testStringBufferInputStream(){
        String message = "I am iSpirng.";
        StringBufferInputStream sbis = new StringBufferInputStream(message);
        byte[] buf = new byte[1024];
        try{
            int length = sbis.read(buf);
            if(length > 0){
                System.out.println(new String(buf, 0, length, "UTF-8"));
            }
            sbis.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

输出结果例如以下:

输出结果


FilterInputStream & FilterOutputStream

FilterInputStream包括了其它的输入流,说具体点就是在其构造函数中须要传入一个InputStream并将其保存在其名为in的字段中,FilterInputStream仅仅是简单的覆盖了全部的方法。之所说是简单覆盖是由于在每一个覆盖函数中。它仅仅是调用内部的保存在in字段中的InputStream所相应的方法,比方在其覆盖read方法时,内部仅仅是简单调用了in.read()方法。FilterInputStream的子类能够进一步覆盖某些方法以保持接口不变的情况下实现某一特性(比方其子类有的能够通过使用缓存优化读取的效率)或者提供一些其它额外的实用方法。所以在使用时FilterInputStream能够让传入的InputStream具有一些额外的特性。即对构造函数传入的InputStream进行了一层包裹,使用了典型的装饰着模式,假设仅仅看FilterInputStream本身这一个类的话。则该类自己本身意义不大。由于其仅仅是通过内部的字段in简单覆写某些方法。

可是假设将FilterInputStream 和其子类结合起来使用话,那么就非常实用了。

比方FilterInputStream 有两个子类BufferedInputStream和DataInputStream,这两个类在下面还会具体介绍。

BufferedInputStream对read操作做了优化,每次读操作时都读取一大块数据。然后将其放入内部维护的一个字节数组缓冲区中。当外面调用BufferedInputStream的read方法时,首先去该缓冲区中读取数据,这样就避免了频繁的实际的读操作。BufferedInputStream对外没有暴露额外的其它方法,可是其内部的read方法已经经过优化了。所以在运行读操作的时候效率更高。

DataInputStream与ObjectInputStream有点相似,能够通过一些readXXX方法读取基本类型的数据,这是非常实用的一些方法。假设我们即想使用BufferedInputStream读取效率高的特性。又想是想DataInputStream中的readXXX方法怎么办呢?非常easy,例如以下代码所看到的:

InputStream is = getInputStreamBySomeway();
BufferedInputStream bis = new BufferedInputStream(is);
DataInputStream dis = new DataInputStream(bis);

然后我们就能够调用dis.readXXX()等方法,即快又方便,这就是FilterInputStream子类通过构造函数层层传递结合在一起使用多种特性的魅力。与之相相应的是BufferedOutputStream和DataOutputStream,BufferedOutputStream优化了write方法。提高了写的效率。DataOutputStream具有非常多writeXXX方法。能够方便的写入基本类型数据。假设想使用writeXXX方法,还想提高写入到效率,能够例如以下代码使用,与上面的代码差点儿相同:

OutputStream os = getOutputStreamBySomeway();
BufferedOutputStream bos = new BufferedOutputStream();
DataOutputStream dos = new DataOutputStream(bos);

然后在调用dos.writeXXX方法时效率就已经提高了。


BufferedInputStream & BufferedOutputStream

如上面所介绍的那样,在BufferedInputStream的构造函数中须要传入一个InputStream, BufferedInputStream内部有一个字节数组缓冲区,每次运行read操作的时候就从这buf中读取数据,从buf中读取数据没有多大的开销。假设buf中已经没有了要读取的数据,那么就去运行其内部绑定的InputStream的read方法,并且是一次性读取非常大一块数据,以便填充满buf缓冲区。缓冲区buf的默认大小是8192字节,也就是8K,在构造函数中我们也能够自己传入一个size指定缓冲区的大小。由于我们在运行BufferedInputStream的read操作的时候。非常多时候都是从缓冲区中读取的数据,这样就大大降低了实际运行其指定的InputStream的read操作的次数,也就提高了读取的效率。与BufferedInputStream 相对的是BufferedOutputStream。在BufferedOutputStream的构造函数中我们须要传入一个OutputStream。这样就将BufferedOutputStream与该OutputStream绑定在了一起。

BufferedOutputStream内部有一个字节缓冲区buf,在运行write操作时。将要写入的数据先一起缓存在一起,将其存入字节缓冲区buf中,buf是有限定大小的,默认的大小是8192字节,即8KB,当然也能够在构造函数中传入size指定buf的大小。该buf仅仅要被指定了大小之后就不会自己主动扩容,所以其是有限定大小的,既然有限定大小,就会有被填充完的时刻,当buf被填充完成的时候会调用BufferedOutputStream的flushBuffer方法,该方法会通过调用其绑定的OutputStream的write方法将buf中的数据进行实际的写入操作并将buf的指向归零(能够看做是将buf中的数据清空)。假设想让缓存区buf中的数据理解真的被写入OutputStream中。能够调用flush方法。flush方法内部会调用flushBuffer方法。由于buf的存在。会大大降低实际运行OutputStream的write操作的次数,优化了写的效率。
下面是BufferedInputStream 和 BufferedOutputStream的演示样例代码片段:

private static void testBufferedInputOutputStream(){
        try{
            String inputFileName = "D:\iWork\file1.txt";
            String outputFileName = "D:\iWork\file2.txt";
            FileInputStream fis = new FileInputStream(inputFileName);
            BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 10);
            FileOutputStream fos = new FileOutputStream(outputFileName);
            BufferedOutputStream bos = new BufferedOutputStream(fos, 1024 * 10);
            byte[] buf = new byte[1024];
            int length = 0;
            while ((length = bis.read(buf)) > 0){
                bos.write(buf, 0, length);
            }
            bis.close();
            bos.close();
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

上面的代码将从file1.txt读取文件输入流。然后将读到的数据写入到file2.txt中。即实现了将file1.txt复制到file2.txt中。事实上不通过BufferedInputStream 和 BufferedOutputStream也能够完成这种工作,使用这个两个类的优点是,能够对file1.txt的读取以及file2.txt的写入提高效率,从而提升文件拷贝的效率。