3

流的原理:

clipboard.png

一连串有顺序的数据系列可以看成是一个流。

输入输出流:

数据从IO输入到程序的流是输入流,数据从程序输出到IO的流是输出流。

下面我们使用字节输入输出流来说明这个问题:

  • 输入流 InputStream一般是由javaio对象(如File)建立的,当新建一个InputStream时,io对象建立了一个包含有数据的管道(其实就是我们所说的这个“流”)并将io对象存储的数据输入(input)到管道中,因此管道里的数据流就是io对象内的数据。当调用InputStream数据流的read方法时,管道里的数据流读出到内存对象中(比如数组或者字符串),注意,读出的比特流将会被移除,具体能可读的数据的量可用available函数来查看;

  • 输出流 OutputStream也是由javaio对象(如File)建立的,当新建一个OutputStream时,io对象建立了一个包含有数据流的管道并建立起io对象和管道的映射。 当调用OutputStream数据流的write方法时,内存对象里的数据就会流入管道里,而管道里的数据流输出(output)到io对象中,flush函数将促使数据缓冲区中的数据被写入到io设备(文件本身)中区;

clipboard.png

public class FilesTest {
    public static void main(String[] args) throws IOException {
       File file = new File(“1.txt");
       InputStream inputStream = new FileInputStream(file);
       byte[] buffer = new byte[1024];
       while(inputStream.read(buffer)!=-1)
        System.out.println(new String(buffer));
       inputStream.close();
    }
}

在这个例子里我们可以充分看出输入流创建和输入的过程,首先创建一个File对象来映射IO上的这个文件,依据这个File对象来创建输入流InputStream对象,注意,创建过后输入流里按序存储着IO文件里的数据内容(这个过程中可能InputStream并不是其存储作用的,因为若果这样大文件内的数据一次性存储可能会爆内存,所以这个过程应该是InputStream映射到IO文件),调用输入流InputStream对象的read方法,即可将流内的数据输入到程序中的之前创建的对象内,最终在使用完后关闭作为有限资源的输入流。这个过程完成了数据由IO对象输入到程序。

注意:如果是上次没有读完输入流内的内容,那么下一次程序到InputStream去读的时候是接着上次的结尾读的,这个可以根据InputStream对象的available方法看出来,所以在这个角度来看输入流就像是文件里的索引指针一样。

clipboard.png

    public class FilesTest {
        public static void main(String[] args) throws IOException {
           File file = new File("test.txt");
           OutputStream outputStream = new FileOutputStream(file);
           byte[] buffer = "hello".getBytes();
           outputStream.write(buffer);
           outputStream.close();
        }
    }

在这个例子里我们可以充分看出输出流创建和输出的过程,首先创建一个File对象来映射IO上的这个文件,依据这个File对象来创建输出流OutputStream对象,调用输出流OutputStream对象的write方法,即可将程序对象中的数据写到输出流中然后从输出流输出到IO文件中去,最终在使用完后关闭作为有限资源的输出流。这个过程完成了数据由程序输出到IO文件中。

字符输入输出流的道理是一样的,只不过字符流是直接处理字符的,而字节流的处理单位是字节。
read和write的API大同小异,无非就是把流里面的内容和缓冲区通过这些函数来进行交换。

流的流:

既然可以依据IO文件来创建流在文件和程序之间交换数据,那么我们可不可以从中间再加入一个流来作为中转处理一下数据呢?这个流的流构成的多流链称之为“流对象链”,这个过程说明不是所有的流都是直接和原始数据源打交道的,所以有如下定义:

节点流(Node Stream)直接连接到数据源,直接从IO文件上输入或输出数据;
处理流(Processing Stream)是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现增强的数 据读写功能,它并不直接连到数据源。

public class FilesTest {
    public static void main(String[] args) throws IOException {
        FileOutputStream outputStream = new FileOutputStream("1.txt");
        PrintStream printStream = new PrintStream(outputStream);
        printStream.print("xxx");
        printStream.close();
        outputStream.close();
    }
}

这个过程的原理如下图所示:

clipboard.png

在这个例子里我们可以充分看出流对象链形成的过程,首先创建一个File对象来映射IO上的这个文件,依据这个File对象来创建输出流OutputStream对象,利用这个输出流对象再创建一个PrintStream对象来链接输出流对象,调用PrintStream对象的print方法,即可将程序对象中的数据写到PrintStream中然后再输出到IO文件中去,最终在使用完后关闭作为有限资源的流。这个过程完成了数据由程序输出到IO文件中。

流中的缓冲技术

在内存中开辟一块区域,称为缓冲区,当缓冲区满时一次写入到磁盘中,提高了I/O的性能。和一般的输入输出流相比,这样的带有缓冲区的流可以做到更好的IO性能,在带缓冲的输出流时由于缓冲区的存在,需要在最后强制使用flush函数将缓冲区中剩余的内容全部输出到IO设备中去。

clipboard.png

public class FilesTest {
    public static void main(String[] args) {
        try {
            byte[] data = new byte[1];
            File srcFile = new File("1.txt");
            File desFile = new File("2.txt");
            BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(srcFile));
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(desFile));
            while (bufferedInputStream.read(data) != -1) {
                bufferedOutputStream.write(data);
            }
            bufferedOutputStream.flush();
            bufferedInputStream.close();
            bufferedOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里表现的是这个过程,IO文件里的数据经过文件输入流FileInputStream对象流入缓冲输入流BufferedInputStream对象,之后所有的数据(在数据量较小的时候,一般是小于8K时,后面会讨论到)流入输入流的缓冲区,之后每次在读取的时候都是直接从缓冲区读到临时的数组中去而不是再从流读入,然后临时数组的数据在write函数的作用下写到输出流的缓冲区中去,缓冲区满后数据会经由缓冲输出流BufferedOutputStream对象流入文件输出流FileOutputStream对象,并最终输出到IO文件中去,如果缓冲区不满的话是不会自发输出到缓冲输出流中去的,因此往往我们需要在最后缓冲区不满的情况下强制执行输出流的flush方法让缓冲区数据强制输出到输出流中去。这个过程完成了IO文件数据的流转,中间有一个缓冲区在暂存数据。

public class FilesTest {
    public byte[] generateString() {
        StringBuffer buffer = new StringBuffer();
        String content = "abcdefg\r\n";
        for (int i = 0; i < 10000; i++) {
            buffer.append(content);
        }
        return buffer.toString().getBytes();
    }

    public static void main(String[] args) throws IOException {
        FilesTest filesTest = new FilesTest();
        byte[] buffer = filesTest.generateString();
        InputStream inputStream = new ByteArrayInputStream(buffer);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        bufferedReader.readLine();
        System.out.println(dataInputStream.readline());
    }
}

在这个例子里我们可以更容易地发现BufferedReader这样的缓冲类输入流的缓冲作用,当首次调用readline(或者read等各种读取方法)函数读取这个输入流的时候,就会将流里的数据读进程序为BufferedInputStream对象分配的一个缓冲区中,而在此后的读取输入流的过程中就不需要去流中读取而只需要去缓冲区里读取就可以了,将开销较大的IO数据交换过程变成了开销小得多的内存数据交换,进而提高了IO效率,这是缓冲输入输出流的好处。但是这个缓冲区的大小是有限的,jdk为这个大小确定的固定值为8K字节,一旦超过这个值的话在第一次读取时就只能缓冲最多8K子节的数据,超出的部分只能在之后再缓冲。最后,如果要结束任务写入输出流的时候,要注意调用输出流的flush方法来将缓冲区强制清空使之全部输出到输出流中去。

public class FilesTest {
    public static void main(String[] args) throws IOException {
        //buffer:8192    
        File src = new File("1.txt");
        File des = new File("2.txt");
        FileWriter writer = new FileWriter(src);
        StringBuffer buffer = new StringBuffer();
        for(int i = 0;i<8193;i++){
            buffer.append("a");
        }
        writer.write(buffer.toString());
        writer.flush();
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(src));
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(des));
        byte[] eachBuffer = new byte[1024];
        int haveRead;
        while((haveRead = bufferedInputStream.read(eachBuffer))!=-1){
            bufferedOutputStream.write(eachBuffer);
        }
        //bufferedOutputStream.flush();
    }
}

上面这个过程演示了缓冲区的大小,当输入流的内容填不满缓冲区时(也就是不足8192字节时),如果不用flush没有办法自动写入文件,当原来缓冲区的大小大于这个值的时候,会一次性把上次的8192字节自动写入,下一次会再读入8192个字节,完成上面的过程。因此,这提醒我们,使用带有缓冲的输出流时务必要在最后强制清空缓冲进入输出流才能保证数据不出错。


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员