Java io中通過InputStream位元組輸入流用來将資料讀取到記憶體中,同時也提供了位元組輸出流OutputStream用來從記憶體中讀取資料。
和InputStream結構類似,我們也通過以下幾個類來了解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();
}
}
}
運作結果:
将需要複制的檔案先讀取到記憶體中,在從記憶體中讀取輸出。
-
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();
}
}
}
輸出結果:
-
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輸出一個位元組到檔案中,我們執行上面的代碼,看下那個檔案的内容:
可以看到檔案的确被成功建立了,但是檔案并沒有出現我們輸出的位元組,我們加上一個方法再試一下:
bos.flush();
可以看到,再加上了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中進行聲明,這樣會自動關閉和重新整理。