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类的理由也很简单,我们假设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方法怎么办呢?很简单,如下代码所示:

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的写入提高效率,从而提升文件拷贝的效率。

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值