天天看點

Java IO - FileInputStream&FileOutputStream

基本概念

  • FileInputStream 從檔案系統中的某個檔案中獲得輸入位元組
  • FileOutputStream 檔案輸出流是用于将資料寫入 File 或 FileDescriptor 的輸出流。檔案是否可用或能否可以被建立取決于基礎平台。特别是某些平台一次隻允許一個 FileOutputStream(或其他檔案寫入對象)打開檔案進行寫入。在這種情況下,如果所涉及的檔案已經打開,則此類中的構造方法将失敗。
  • 繼承結構
Java IO - FileInputStream&FileOutputStream
Java IO - FileInputStream&FileOutputStream

執行個體探究

  • 寫入檔案的位元組都會被底層按照系統預設編碼轉換成字元存到檔案中
  • 當使用檔案位元組輸出來的時候會被反轉成位元組
public class Test {

    private static final String TEMPFILE = "E:" + File.separator + "Test.txt";
    private static final String DESFILE = "E:" + File.separator + "Test2.txt";

    //英文字母 a ~ h
    private static final byte[] byteArray = { , , , , , , , ,  };

    public static void main(String[] args) throws IOException {
        write(TEMPFILE);
        read(TEMPFILE);
        copyFile(TEMPFILE, DESFILE);
    }

    public static void write(String path) {
        // 初始化,不然關閉流時編譯不通過
        FileOutputStream fos = null;

        try {
            // 建立流,這裡探究追加模式。假設文本内容現在為 1,2,3
            fos = new FileOutputStream(new File(path), true);

            // 寫入 a
            fos.write(byteArray[]);

            // 寫入 b,c,d,e,f
            fos.write(byteArray, , );

            fos.close();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void read(String path) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(new File(path));

            char temp = (char) fis.read();

            // 輸出 1
            System.out.print(temp);

            byte[] buffer = new byte[];
            int count = ;
            while ((count = fis.read(buffer)) != -) {
                // 輸出 234abcdefabcdefabcdef
                System.out.println(new String(buffer, , count));
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 檔案複制
    public static void copyFile(String srcPath, String desPath) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(new File(srcPath));
            fos = new FileOutputStream(new File(desPath));

            // 為了效率,一般采用按位元組數組讀取
            byte[] buffer = new byte[];
            int count = ;
            while ((count = fis.read(buffer)) != -) {
                fos.write(buffer, , count);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }

                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

源碼分析

1.FileInputStream

類結構圖如下:

Java IO - FileInputStream&FileOutputStream

首先來看類中的靜态代碼塊,它的作用是設定類中(也就是FileInputStream)的屬性的記憶體位址偏移量,便于在必要時操作記憶體給它指派。

static {
    initIDs();
}

private static native void initIDs();
           

成員變量

// 檔案描述符類,表示用來打開檔案的句柄
private FileDescriptor fd;

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

private Object closeLock = new Object(); 

private volatile boolean closed = false;

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

該類總共定義了 3 個構造函數,通過代碼可以發現可以 ①③ 都是通過擷取實際的檔案連接配接,在通過 open 方法來建立流。而 ② 是直接通過檔案描述符建立流。

// ①構造函數,通過檔案路徑建立
public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}

// ②構造函數,通過檔案描述符建立
public FileInputStream(FileDescriptor fdObj) {
    SecurityManager security = System.getSecurityManager();
    if (fdObj == null) {
        throw new NullPointerException();
    }
    if (security != null) {
        security.checkRead(fdObj);
    }
    fd = fdObj;

    fd.incrementAndGetUseCount();
}

// ③構造函數,通過檔案連接配接建立
public FileInputStream(File file) throws FileNotFoundException {

    String name = (file != null ? file.getPath() : null);

    // 操作檔案之前,檢查是否具有 read 權限
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }

    // 判斷路徑的合法性
    if (name == null) {
        throw new NullPointerException();
    }

    // 打開檔案
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    open(name);
}

// 關鍵-->打開系統檔案,native 方法
private native void open(String name) throws FileNotFoundException;
           

接下來看它 的 read 方法,觀察下面的代碼,發現在類中定義了常見的 3 種讀取方式,如①②③。① 本身就是個native 方法,而 ②③則是通過 readBytes 這個 native 方法用來實作檔案的讀取。

//① 從此輸入流中讀取一個資料位元組
public native int read() throws IOException;

//關鍵-->實際操作由它完成
private native int readBytes(byte b[], int off, int len) throws IOException;


//②從此輸入流中将最多 len 個位元組的資料讀入一個 byte 數組中
public int read(byte b[]) throws IOException {
    return readBytes(b, , b.length);
}

//③從此輸入流中将最多 b.length 個位元組的資料讀入一個 byte 數組中
public int read(byte b[], int off, int len) throws IOException {
    return readBytes(b, off, len);
}
           

接着來看其他流中沒有的而 FileInputStream 中獨有的幾個方法

//傳回檔案描述符,表示該檔案正在被 FileInputStream 使用
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, true, false, this);

            fd.incrementAndGetUseCount();

        }
        return channel;
    }
}

//重寫了 Object 的方法,確定該檔案輸入流的close方法被調用的時候它不用有引用
protected void finalize() throws IOException {
    if ((fd != null) && (fd != FileDescriptor.in)) {
        // 目前其他流操作該對象時,調用該方法無法釋放資源。但是可以調用 colse 方法強行釋放。
        runningFinalize.set(Boolean.TRUE);
        try {
            close();
        } finally {
            runningFinalize.set(Boolean.FALSE);
        }
    }
}
           

最後再來看看剩下的幾個方法

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 相關聯的電腦(當每獲得一個新的通道時,該電腦增加)
        fd.decrementAndGetUseCount();
        channel.close();
    }

    int useCount = fd.decrementAndGetUseCount();

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

private native void close0() throws IOException;
           

2.FileOutputStream

類結構圖

Java IO - FileInputStream&amp;FileOutputStream

首先來看靜态代碼塊,作用同上。

static {
    initIDs();
}

private static native void initIDs();
           

成員變量

private FileDescriptor fd;

private FileChannel channel = null;

private boolean append = false;

private Object closeLock = new Object();

private volatile boolean closed = false;

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

在分析構造函數之前先來看兩個 native 方法

//替換原檔案的内容
private native void open(String name) throws FileNotFoundException;

//檔案原有内容的末尾追加新内容
private native void openAppend(String name) throws FileNotFoundException;
           

該類定義了 5 個構造函數,其中 ① ~ ④ 都是通過 file 類來建立流,具體的實作都在 ④ 裡面。預設未指定 append 時為false,表示替換原檔案的内容,當 append 為 true 時,表示在檔案原有内容的末尾追加新内容。觀察 ④,發現該方法分别調用了 open ,openAppend 這兩個 native 方法來實作操作。而 ⑤ 則是通過檔案描述符完成建立

//①構造函數,根據檔案路徑建立,預設不追加内容
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();
    }
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    this.append = append;

    //判斷是否追加内容
    if (append) {
        openAppend(name);
    } else {
        open(name);
    }
}

//⑤構造函數,根據檔案描述符建立
public FileOutputStream(FileDescriptor fdObj) {
    SecurityManager security = System.getSecurityManager();
    if (fdObj == null) {
        throw new NullPointerException();
    }

    if (security != null) {
        security.checkWrite(fdObj);
    }

    fd = fdObj;

    fd.incrementAndGetUseCount();
}
           

接着來看 writer 方法,與 檔案輸入流的 read 方法類型,不再分析。

public native void write(int b) throws IOException;

private native void writeBytes(byte b[], int off, int len) throws IOException;

public void write(byte b[]) throws IOException {
    writeBytes(b, , b.length);
}

public void write(byte b[], int off, int len) throws IOException {
    writeBytes(b, off, len);
}
           

再來看看普通輸出流沒有,而 FileOutputStream 獨有的方法。

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, false, true, this, append);

            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);
            }
        }
    }
}
           

最後再來看看 close 方法

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 <= ) || !isRunningFinalize()) {
        close0();
    }
}

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

private native void close0() throws IOException;