天天看点

Java IO之OutputStream

Java io中通过InputStream字节输入流用来将数据读取到内存中,同时也提供了字节输出流OutputStream用来从内存中读取数据。

         和InputStream结构类似,我们也通过以下几个类来了解OutputStream。

Java IO之OutputStream
  • OutPutStream

OutputStream抽象类中主要提供了三个方法:

输出单个字节

public abstract void write(int b) throws IOException;
           

输出一个字节数组:

public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
}
           
public void write(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if ((off < 0) || (off > b.length) || (len < 0) ||
               ((off + len) > b.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return;
    }
    for (int i = 0 ; i < len ; i++) {
        write(b[off + i]);
    }
}
           

刷新缓冲流:

public void flush() throws IOException {
}
           

具体的实现 我们看OutputStream的具体实现类

  • FileOutputStream

将数据通过字节的方式输出到文件中:

先看下构造方法:

public FileOutputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null, false);
}
           
public FileOutputStream(String name, boolean append)
    throws FileNotFoundException
{
    this(name != null ? new File(name) : null, append);
}
           
public FileOutputStream(File file) throws FileNotFoundException {
    this(file, false);
}
           
public FileOutputStream(File file, boolean append)
    throws FileNotFoundException
{
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkWrite(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    this.fd = new FileDescriptor();
    this.append = append;
    this.path = name;
    fd.incrementAndGetUseCount();
    open(name, append);
}
           

可以看到虽然FileOutputStream提供了多个构造方法,但是最终调用的都是最后一个构造方法,两个参数一个参数时File对象,还有一个参数代表是否是添加操作,append=true会保留原有文件的内容,在后面增加新内容,append=false贼会覆盖原有内容。

Write(byte[])方法:

public void write(byte b[]) throws IOException {
    Object traceContext = IoTrace.fileWriteBegin(path);
    int bytesWritten = 0;
    try {
        writeBytes(b, 0, b.length, append);
        bytesWritten = b.length;
    } finally {
        IoTrace.fileWriteEnd(traceContext, bytesWritten);
    }
}
           
public void write(byte b[], int off, int len) throws IOException {
    Object traceContext = IoTrace.fileWriteBegin(path);
    int bytesWritten = 0;
    try {
        writeBytes(b, off, len, append);
        bytesWritten = len;
    } finally {
        IoTrace.fileWriteEnd(traceContext, bytesWritten);
    }
}
           
private native void writeBytes(byte b[], int off, int len, boolean append)
    throws IOException;
           

两种方式都是将内容写入这个文件输出流。

FileOutputStream比较常见,也比较简单,我们直接写一个实例,复制一个java文件到txt文件中:

测试代码:

package io;
/**
 * copy .java to .txt
 * 从内存中 先读再写
 */

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @author 18092106
 * @create 2018-09-05 19:58
 **/
public class TestFileOutputStream {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("Out.txt", false);
             FileInputStream fis = new FileInputStream("E:\\code\\leetcode\\src\\io\\TestFileOutputStream.java")) {
                byte[] bytes = new byte[1024];
                int hasRead = 0;
                while((hasRead = fis.read(bytes)) != -1){
                    fos.write(bytes,0,hasRead);
                }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

运行结果:

Java IO之OutputStream

将需要复制的文件先读取到内存中,在从内存中读取输出。

  • ByteArrayOutputStream

ByteArrayOutputStream自己声明了一个缓冲区,每次操作的都是自己缓冲区的字节数组,我们看下他是怎么工作的:

构造方法:

public ByteArrayOutputStream() {
    this(32);
}
           
public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}
           

每当声明一个ByteArrayOutputStream的时候,都会默认构造一个大小为32的字节数组。

再看下write(int)方法:

public synchronized void write(int b) {
    ensureCapacity(count + 1);
    buf[count] = (byte) b;
    count += 1;
}
           
private void ensureCapacity(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - buf.length > 0)
        grow(minCapacity);
}
           
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = buf.length;
    int newCapacity = oldCapacity << 1;
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity < 0) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    buf = Arrays.copyOf(buf, newCapacity);
}
           

在输出单个字节时,首先会去验证一下默认缓冲区和当前已使用容量的大小,如果当前使用的容量即将超过缓冲区,那么执行扩容操作:

int newCapacity = oldCapacity << 1;
           

直接扩容两倍,作为新的缓冲区

Write(byte[])方法类似,空间不够就先扩容,然后再将字节数组复制到缓冲区:

public synchronized void write(byte b[], int off, int len) {
    if ((off < 0) || (off > b.length) || (len < 0) ||
        ((off + len) - b.length > 0)) {
        throw new IndexOutOfBoundsException();
    }
    ensureCapacity(count + len);
    System.arraycopy(b, off, buf, count, len);
    count += len;
}
           

ByteArrayOutPutStream中还有两个方法介绍下:

将缓冲区中的字节数组输出

public synchronized byte toByteArray()[] {
    return Arrays.copyOf(buf, count);
}
           

将字节数组通过另一个输出流输出,比如可以和FileOutputStream配合,输出文件

public synchronized void writeTo(OutputStream out) throws IOException {
    out.write(buf, 0, count);
}
           

我们写个实例测试下上面的几个方法:

package io;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @author 18092106
 * @create 2018-09-05 20:10
 **/
public class TestByteArrayOutputStream {
    public static void main(String[] args) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(32);
        bos.write(97);
        byte[] bytes = {97,98,99,100};
        try {
            bos.write(bytes);
            for (byte b:bos.toByteArray()) {
                System.out.println((char) b);
            }
            bos.writeTo(new FileOutputStream("Out2.txt"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           

输出结果:

Java IO之OutputStream
Java IO之OutputStream
  • FilterOutputStream

和FilterInputStream一样,使用装饰器设计模式,本身不提供功能,只用来包装其他输出流,在其他输出流的方法基础上,实现一些功能。    

  • BufferedOutputStream

BufferedOutputStream就是继承了FilterOutputStream的一个实现类,用来实现缓冲功能,减少io的交互,看下源码:

构造方法:

public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}
           
public BufferedOutputStream(OutputStream out, int size) {
    super(out);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}
           

可以看到,BufferedOutputStream在实例化时会默认构造一个大小为8192的字节数组。

再看下最主要的write(byte[])方法:

public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {
        /* If the request length exceeds the size of the output buffer,
           flush the output buffer and then write the data directly.
           In this way buffered streams will cascade harmlessly. */
        flushBuffer();
        out.write(b, off, len);
        return;
    }
    if (len > buf.length - count) {
        flushBuffer();
    }
    System.arraycopy(b, off, buf, count, len);
    count += len;
}
           

在输出字节时,首先会进行判断,如果输出的字节数组的大小不超过缓冲区的大小,将这部分内容复制到默认的缓冲区中,如果输出的字节数组的大小超过了缓冲区的大小,那么调用flushBuffer()方法,我们看下这个方法的作用:

private void flushBuffer() throws IOException {
    if (count > 0) {
        out.write(buf, 0, count);
        count = 0;
    }
}
           

可以看到,这个方法调用包装的输出流的write方法,输出字节,同时将计数器清零

简单理解Buffered OutputStream的原理就是,它会默认构造一个8192也就是4K大小的一个缓冲区,当输出的字节数组的大小不超过它时,不会直接输出,而是被放到这个缓冲区中,当缓冲区中的空间不够时,再调用被包装的输出流的write(byte[])方法输出字节数组。

写个实例来看下效果:

package io;


import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

/**
 * @author 18092106
 * @create 2018-09-03 19:44
 **/
public class TestBufferedOutputStream {
    public static void main(String[] args) {
        try{
            FileOutputStream fos = new FileOutputStream("out3.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            bos.write(97);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}
           

可以看到,这个代码很简单,就是利用BufferedOutputStream包装一个FileOutputStream输出一个字节到文件中,我们执行上面的代码,看下那个文件的内容:

Java IO之OutputStream

可以看到文件的确被成功创建了,但是文件并没有出现我们输出的字节,我们加上一个方法再试一下:

bos.flush();
           
Java IO之OutputStream

可以看到,再加上了flush()这个方法以后,文件中出现了我们想要的内容,为什么会有这种情况呢?我们看下flush()做了什么事:

public synchronized void flush() throws IOException {
    flushBuffer();
    out.flush();
}
           

可以看到它调用强制刷洗缓冲区的方法,我们已经看过源码,里面调用就是out的write方法,flush起到的作用其实正是如此,通过强制调用输出流的write方法,将缓冲区的内容输出,避免出现上面第一次代码的情况,有内容在缓冲区中,没有输出。

细心的小伙伴可能发现了,之前的输出流我们都没有调用flush()方法,但是好像并没有影响,其实是因为这些类大部分都实现了Closeable和Flushable接口,然后jdk7以后支持try(XX x = new XX)catch{}的写法,像这种需要关闭,刷新的对象可以直接在try中进行声明,这样会自动关闭和刷新。