天天看點

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中進行聲明,這樣會自動關閉和重新整理。