Java IO 之 FileInputStream & FileOutputStream 原始碼分析

泥沙磚瓦漿木匠發表於2015-10-25

一、引子

檔案,作為常見的資料來源。關於操作檔案的位元組流就是 — FileInputStream & FileOutputStream。它們是Basic IO位元組流中重要的實現類。

二、FileInputStream原始碼分析

FileInputStream原始碼如下:

/**
 * FileInputStream 從檔案系統的檔案中獲取輸入位元組流。檔案取決於主機系統。
 *  比如讀取圖片等的原始位元組流。如果讀取字元流,考慮使用 FiLeReader。
 */
public class SFileInputStream extends InputStream
{
    /* 檔案描述符類---此處用於開啟檔案的控制程式碼 */
    private final FileDescriptor fd;

    /* 引用檔案的路徑 */
    private final String path;

    /* 檔案通道,NIO部分 */
    private FileChannel channel = null;

    private final Object closeLock = new Object();
    private volatile boolean closed = false;

    private static final ThreadLocal<Boolean> runningFinalize =
        new ThreadLocal<>();

    private static boolean isRunningFinalize() {
        Boolean val;
        if ((val = runningFinalize.get()) != null)
            return val.booleanValue();
        return false;
    }

    /* 通過檔案路徑名來建立FileInputStream */
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

    /* 通過檔案來建立FileInputStream */
    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        }
        fd = new FileDescriptor();
        fd.incrementAndGetUseCount();
        this.path = name;
        open(name);
    }

    /* 通過檔案描述符類來建立FileInputStream */
    public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;
        fd.incrementAndGetUseCount();
    }

    /* 開啟檔案,為了下一步讀取檔案內容。native方法 */
    private native void open(String name) throws FileNotFoundException;

    /* 從此輸入流中讀取一個資料位元組 */
    public int read() throws IOException {
        Object traceContext = IoTrace.fileReadBegin(path);
        int b = 0;
        try {
            b = read0();
        } finally {
            IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);
        }
        return b;
    }

    /* 從此輸入流中讀取一個資料位元組。native方法 */
    private native int read0() throws IOException;

    /* 從此輸入流中讀取多個位元組到byte陣列中。native方法 */
    private native int readBytes(byte b[], int off, int len) throws IOException;

    /* 從此輸入流中讀取多個位元組到byte陣列中。 */
    public int read(byte b[]) throws IOException {
        Object traceContext = IoTrace.fileReadBegin(path);
        int bytesRead = 0;
        try {
            bytesRead = readBytes(b, 0, b.length);
        } finally {
            IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
        }
        return bytesRead;
    }

    /* 從此輸入流中讀取最多len個位元組到byte陣列中。 */
    public int read(byte b[], int off, int len) throws IOException {
        Object traceContext = IoTrace.fileReadBegin(path);
        int bytesRead = 0;
        try {
            bytesRead = readBytes(b, off, len);
        } finally {
            IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
        }
        return bytesRead;
    }

    public native long skip(long n) throws IOException;

    /* 返回下一次對此輸入流呼叫的方法可以不受阻塞地從此輸入流讀取(或跳過)的估計剩餘位元組數。 */
    public native int available() throws IOException;

    /* 關閉此檔案輸入流並釋放與此流有關的所有系統資源。 */
    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           fd.decrementAndGetUseCount();
           channel.close();
        }

        int useCount = fd.decrementAndGetUseCount();

        if ((useCount <= 0) || !isRunningFinalize()) {
            close0();
        }
    }

    public final FileDescriptor getFD() throws IOException {
        if (fd != null) return fd;
        throw new IOException();
    }

    /* 獲取此檔案輸入流的唯一FileChannel物件 */
    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
                fd.incrementAndGetUseCount();
            }
            return channel;
        }
    }

    private static native void initIDs();

    private native void close0() throws IOException;

    static {
        initIDs();
    }

    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            runningFinalize.set(Boolean.TRUE);
            try {
                close();
            } finally {
                runningFinalize.set(Boolean.FALSE);
            }
        }
    }
}

1. 三個核心方法

三個核心方法,也就是Override(重寫)了抽象類InputStream的read方法。

int read() 方法,即

public int read() throws IOException

程式碼實現中很簡單,一個try中呼叫本地native的read0()方法,直接從檔案輸入流中讀取一個位元組。IoTrace.fileReadEnd(),字面意思是防止檔案沒有關閉讀的通道,導致讀檔案失敗,一直開著讀的通道,會造成記憶體洩露。

int read(byte b[]) 方法,即

public int read(byte b[]) throws IOException

程式碼實現也是比較簡單的,也是一個try中呼叫本地native的readBytes()方法,直接從檔案輸入流中讀取最多b.length個位元組到byte陣列b中。

int read(byte b[], int off, int len) 方法,即

public int read(byte b[], int off, int len) throws IOException

程式碼實現和 int read(byte b[])方法 一樣,直接從檔案輸入流中讀取最多len個位元組到byte陣列b中。

可是這裡有個問答:

Q: 為什麼 int read(byte b[]) 方法需要自己獨立實現呢? 直接呼叫 int read(byte b[], int off, int len) 方法,即read(b , 0 , b.length),等價於read(b)?

A:待完善,希望路過大神回答。。。。向下相容?? Finally??

2. 值得一提的native方法

上面核心方法中為什麼實現簡單,因為工作量都在native方法裡面,即JVM裡面實現了。native倒是不少一一列舉吧:

native void open(String name) // 開啟檔案,為了下一步讀取檔案內容

native int read0() // 從檔案輸入流中讀取一個位元組

native int readBytes(byte b[], int off, int len) // 從檔案輸入流中讀取,從off控制程式碼開始的len個位元組,並儲存至b位元組陣列內。

native void close0() // 關閉該檔案輸入流及涉及的資源,比如說如果該檔案輸入流的FileChannel對被獲取後,需要對FileChannel進行close。

其他還有值得一提的就是,在jdk1.4中,新增了NIO包,優化了一些IO處理的速度,所以在FileInputStream和FileOutputStream中新增了FileChannel getChannel()的方法。即獲取與該檔案輸入流相關的 java.nio.channels.FileChannel物件。

三、FileOutputStream 原始碼分析

FileOutputStream 原始碼如下:

/**
 * 檔案輸入流是用於將資料寫入檔案或者檔案描述符類
 *  比如寫入圖片等的原始位元組流。如果寫入字元流,考慮使用 FiLeWriter。
 */
public class SFileOutputStream extends OutputStream
{
    /* 檔案描述符類---此處用於開啟檔案的控制程式碼 */
    private final FileDescriptor fd;

    /* 引用檔案的路徑 */
    private final String path;

    /* 如果為 true,則將位元組寫入檔案末尾處,而不是寫入檔案開始處 */
    private final boolean append;

    /* 關聯的FileChannel類,懶載入 */
    private FileChannel channel;

    private final Object closeLock = new Object();
    private volatile boolean closed = false;
    private static final ThreadLocal<Boolean> runningFinalize =
        new ThreadLocal<>();

    private static boolean isRunningFinalize() {
        Boolean val;
        if ((val = runningFinalize.get()) != null)
            return val.booleanValue();
        return false;
    }

    /* 通過檔名建立檔案輸入流 */
    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);
    }

    /* 通過檔案描述符類建立檔案輸入流 */
    public FileOutputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkWrite(fdObj);
        }
        this.fd = fdObj;
        this.path = null;
        this.append = false;

        fd.incrementAndGetUseCount();
    }

    /* 開啟檔案,並確定檔案寫入起始處模式 */
    private native void open(String name, boolean append)
        throws FileNotFoundException;

    /* 將指定的位元組b寫入到該檔案輸入流,並指定檔案寫入起始處模式 */
    private native void write(int b, boolean append) throws IOException;

    /* 將指定的位元組b寫入到該檔案輸入流 */
    public void write(int b) throws IOException {
        Object traceContext = IoTrace.fileWriteBegin(path);
        int bytesWritten = 0;
        try {
            write(b, append);
            bytesWritten = 1;
        } finally {
            IoTrace.fileWriteEnd(traceContext, bytesWritten);
        }
    }

    /* 將指定的位元組陣列寫入該檔案輸入流,並指定檔案寫入起始處模式 */
    private native void writeBytes(byte b[], int off, int len, boolean append)
        throws IOException;

    /* 將指定的位元組陣列b寫入該檔案輸入流 */
    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);
        }
    }

    /* 將指定len長度的位元組陣列b寫入該檔案輸入流 */
    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);
        }
    }

    /* 關閉此檔案輸出流並釋放與此流有關的所有系統資源 */
    public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }

        if (channel != null) {
            fd.decrementAndGetUseCount();
            channel.close();
        }

        int useCount = fd.decrementAndGetUseCount();

        if ((useCount <= 0) || !isRunningFinalize()) {
            close0();
        }
    }

     public final FileDescriptor getFD()  throws IOException {
        if (fd != null) return fd;
        throw new IOException();
     }

    public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, false, true, append, this);

                fd.incrementAndGetUseCount();
            }
            return channel;
        }
    }

    protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {

                runningFinalize.set(Boolean.TRUE);
                try {
                    close();
                } finally {
                    runningFinalize.set(Boolean.FALSE);
                }
            }
        }
    }

    private native void close0() throws IOException;

    private static native void initIDs();

    static {
        initIDs();
    }

}

1. 三個核心方法

三個核心方法,也就是Override(重寫)了抽象類OutputStream的write方法。

void write(int b) 方法,即

public void write(int b) throws IOException

程式碼實現中很簡單,一個try中呼叫本地native的write()方法,直接將指定的位元組b寫入檔案輸出流。IoTrace.fileReadEnd()的意思和上面FileInputStream意思一致。

void write(byte b[]) 方法,即

public void write(byte b[]) throws IOException

程式碼實現也是比較簡單的,也是一個try中呼叫本地native的writeBytes()方法,直接將指定的位元組陣列寫入該檔案輸入流。

void write(byte b[], int off, int len) 方法,即

public void write(byte b[], int off, int len) throws IOException

程式碼實現和 void write(byte b[]) 方法 一樣,直接將指定的位元組陣列寫入該檔案輸入流。

2. 值得一提的native方法

上面核心方法中為什麼實現簡單,因為工作量都在native方法裡面,即JVM裡面實現了。native倒是不少一一列舉吧:

native void open(String name) // 開啟檔案,為了下一步讀取檔案內容

native void write(int b, boolean append) // 直接將指定的位元組b寫入檔案輸出流

native native void writeBytes(byte b[], int off, int len, boolean append) // 直接將指定的位元組陣列寫入該檔案輸入流。

native void close0() // 關閉該檔案輸入流及涉及的資源,比如說如果該檔案輸入流的FileChannel對被獲取後,需要對FileChannel進行close。

相似之處:

其實到這裡,該想一想。兩個原始碼實現很相似,而且native方法也很相似。其實不能說“相似”,應該以“對應”來概括它們。

它們是一組,是一根吸管的兩個孔的關係:“一個Input一個Output”。

四、使用案例

下面先看程式碼:

package org.javacore.io;

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

/*
 * Copyright [2015] [Jeff Lee]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author Jeff Lee
 * @since 2015-10-8 20:06:03
 * FileInputStream&FileOutputStream使用案例
 */
public class FileIOStreamT {
    private static final String thisFilePath =
            "src" + File.separator +
            "org" + File.separator +
            "javacore" + File.separator +
            "io" + File.separator +
            "FileIOStreamT.java";
    public static void main(String[] args) throws IOException {
        // 建立檔案輸入流
        FileInputStream fileInputStream = new FileInputStream(thisFilePath);
        // 建立檔案輸出流
        FileOutputStream fileOutputStream =  new FileOutputStream("data.txt");

        // 建立流的最大位元組陣列
        byte[] inOutBytes = new byte[fileInputStream.available()];
        // 將檔案輸入流讀取,儲存至inOutBytes陣列
        fileInputStream.read(inOutBytes);
        // 將inOutBytes陣列,寫出到data.txt檔案中
        fileOutputStream.write(inOutBytes);

        fileOutputStream.close();
        fileInputStream.close();
    }
}

執行後,會發現根目錄中出現了一個“data.txt”檔案,內容為上面的程式碼。

1. 簡單地分析下原始碼:

1、建立了FileInputStream,讀取該程式碼檔案為檔案輸入流。

2、建立了FileOutputStream,作為檔案輸出流,輸出至data.txt檔案。

3、針對流的位元組陣列,一個 read ,一個write,完成讀取和寫入。

4、關閉流

2. 程式碼呼叫的流程如圖所示:

iostream

3. 程式碼雖簡單,但是有點小問題:

FileInputStream.available() 是返回流中的估計剩餘位元組數。所以一般不會用此方法。

一般做法,比如建立一個 byte陣列,大小1K。然後read至其返回值不為-1,一直讀取即可。邊讀邊寫。

五、思考與小結

FileInputStream & FileOutputStream 是一對來自 InputStream和OutputStream的實現類。用於本地檔案讀寫(二進位制格式按順序讀寫)。

本文小結:

1、FileInputStream 原始碼分析

2、FileOutputStream 資源分析

3、FileInputStream & FileOutputStream 使用案例

4、其原始碼呼叫過程

相關文章