天天看點

Java FileInputStream FileOutputStream類源碼解析FileInputStreamFileOutputStream

FileInputStream和FileOutputStream是比對的檔案輸出輸出流,讀取和寫入的是byte,是以适合用來處理一些非字元的資料,比如圖檔資料。因為涉及到大量關于檔案的操作,是以存在很多的native方法和利用作業系統的檔案系統實作,是以要深入了解檔案輸入輸出流還是需要加強作業系統和native源碼的知識。先來看一下簡單的使用示例:

public static void main(String args[]) throws IOException{
        FileOutputStream out = new FileOutputStream("D:/test/file.txt");
        out.write("1234567890".getBytes());//1234567890
        out.close();
        out = new FileOutputStream("D:/test/file.txt");
        out.write("765".getBytes());//765
        out.close();
        out = new FileOutputStream("D:/test/file.txt", true);
        out.write("asdfgh".getBytes());//765asdfgh
        out.close();
        new File("D:/test/file.txt");//765asdfgh
        out = new FileOutputStream("D:/test/file.txt");
        out.close();//内容為空
        
        //測試filechannel位置改變對stream的影響
        out = new FileOutputStream("D:/test/file.txt");
        out.write("1234567890".getBytes());//1234567890
        out.close();
        
        FileInputStream in = new FileInputStream("D:/test/file.txt");
        
        System.out.print(String.valueOf((byte)in.read() & 0xf));//1
        System.out.print(String.valueOf((byte)in.read() & 0xf));//2
        System.out.print(String.valueOf((byte)in.read() & 0xf));//3
        
        in.getChannel().position(5);
        System.out.print(String.valueOf((byte)in.read() & 0xf));//6
        in.getChannel().position(0);
        System.out.print(String.valueOf((byte)in.read() & 0xf));//1
        
        in.skip(-1);//向前跳一位
        System.out.print(String.valueOf((byte)in.read() & 0xf));//1
    }           

FileInputStream

實作了抽象類InputStream,FileInputStream從檔案系統中的檔案擷取bytes,檔案是否有效取決于主機環境。FileInputStream對于直接從流中讀取bytes資料非常有意義如圖像資料。如果要讀取字元資訊,考慮使用FileReader。我們可以看到除了close以外,沒有出現任何鎖,但是實際上如果我們使用多個線程讀取同一個檔案時,單次讀取是原子操作,見下方代碼:

package test;

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

public class FileThreadTest implements Runnable {
    private int type;// 0做skip操作,1做讀取操作
    
    private int gap;

    private FileInputStream in;

    public FileThreadTest(int type, FileInputStream in, int gap) {
        this.type = type;
        this.in = in;
        this.gap = gap;
    }

    @Override
    public void run() {
        byte[] body = new byte[gap];
        if (this.type == 0) {
            try {
                for(int i = 0; i < 4; i++) {
                    in.skip(gap);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            try {
                for(int i = 0; i < 10; i++) {
                    in.read(body);
                    System.out.println(Thread.currentThread().getName() + "-" + new String(body));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            
        }
    }

    public static void main(String args[]) throws IOException, InterruptedException {
        FileOutputStream out = new FileOutputStream("D:/test/file.txt");
        for (int i = 0; i < 1000; i++) {
            out.write("1234567890".getBytes());// 寫入測試資料
        }
        out.close();
        FileInputStream in = new FileInputStream("D:/test/file.txt");
        FileThreadTest t1 = new FileThreadTest(0, in, 2);
        FileThreadTest t2 = new FileThreadTest(1, in, 4);
        FileThreadTest t3 = new FileThreadTest(1, in, 3);
        Thread thread1 = new Thread(t1,"線程1");
        Thread thread2 = new Thread(t2,"線程2");
        Thread thread3 = new Thread(t3,"線程3");
        thread2.start();
        thread3.start();
        thread1.start();
        thread1.join();
        thread2.join();
        thread3.join();
        in.close();
        /*
        線程3-567
        線程3-678
        線程2-1234
        線程3-901
        線程3-678
        線程2-2345
        線程2-2345
        線程2-6789
        線程2-0123
        線程2-4567
        線程2-8901
        線程2-2345
        線程2-6789
        線程3-901
        線程3-456
        線程3-789
        線程2-0123
        線程3-012
        線程3-345
        線程3-678
        */
    }
}           

運作結果試機器配置每次運作會有所不同,但是我們可以看到無論怎麼運作,每一次read操作本身是不會被其他線程搶占而中斷的,它一定會完整的讀取到這次要讀取的内容,但是由于其他線程可以改變輸入流的位置,是以每個線程讀取時開始的位置是不可預知的,每個線程的read和skip操作都會改變流的位置。

先來看下内部變量,一個檔案輸出流有檔案路徑名、檔案描述符、檔案通道屬性,若由檔案描述符來建立,則檔案路徑名為null

/* 檔案描述符,用來打開檔案*/
    private final FileDescriptor fd;

    /**
     * 引用檔案的路徑,如果流是通過檔案描述符建立時該值為null
     */
    private final String path;

    private FileChannel channel = null;
    //用于保證close操作的線程安全性
    private final Object closeLock = new Object();
    private volatile boolean closed = false;           

然後是構造函數,主要分為通過檔案路徑名、檔案描述符、具體檔案,通過檔案描述符建立時檔案路徑名為null

/**
     * 通過打開一個連接配接實際檔案的連接配接來建立一個FileInputStream,檔案通過檔案系統中的路徑名name來命名。
     * 一個新的檔案描述符對象會被建立來表示這個檔案連接配接
     * 如果存在安全管理器,checkRead方法會被調用,name作為參數傳入
     * 如果檔案名不存在,或者是一個目錄而不是規則檔案,或者因為其他原因無法打開讀取,抛出FileNotFoundException
     */
    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
    //通過具體檔案來建立,其他情況同上
    public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);//name是file的路徑名
        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.attach(this);//綁定檔案描述符便于關閉對象
        path = name;
        open(name);//打開檔案
    }

    /**
     * 通過檔案描述符fdObj來建立FileInputStream,代表了對一個檔案系統中實際存在檔案的連接配接
     * 如果FileInputStream是null會抛出NullPointerException
     * 如果fdObj是無效的,構造器不會抛出異常,但是,如果調用這個流的IO方法,會抛出IOException
     */
    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.attach(this);
    }           

檔案本身需要打開才能進行操作,open隻能由構造函數來調用,最終是由native方法open0來完成的系統的互動

private void open(String name) throws FileNotFoundException {
        open0(name);
    }

    private native void open0(String name) throws FileNotFoundException;           

讀取根據參數重載分為讀取單個位元組和讀取字元數組,它們分别基于native方法read0和readBytes,前面的測試中,我們可以看到被多個線程共享的FileInputStream依然能夠保證單次的read操作讀取的資訊是完整的,這應該與readBytes的實作有關

//從流中讀取byte,如果沒有有輸出沒有完成會阻塞方法
    public int read() throws IOException {
        return read0();
    }
    //從流中讀取最大b.length的bytes資料到數組b中。該方法會被阻塞知道某些輸入完成
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }
    //從流中讀取長度為len的資料,放到b中從off開始的位置
    public int read(byte b[], int off, int len) throws IOException {
        return readBytes(b, off, len);
    }

    private native int read0() throws IOException;
    private native int readBytes(byte b[], int off, int len) throws IOException;           

skip也是native方法跳過并廢棄輸入流中n個bytes資料,skip方法可能因為一些不同的原因以跳過更少的bytes數結束,可能這個數量是0.如果n是負數,方法會嘗試往回跳(見本文最上方例子)。如果檔案不支援從目前位置往回掉,會抛出IOException。傳回的是實際跳過的bytes數量,為正是往後跳,為負是往前跳。可能會跳過比檔案中剩餘數量更多的bytes數,這個過程不會産生異常,跳過的bytes數可能包括了一些檔案中超過了EOF檔案結束符的bytes數,跳過了檔案結束符再嘗試讀取會傳回-1,說明已經到達檔案末尾。

public native long skip(long n) throws IOException;           

available也是native方法,傳回剩餘可讀取的或者可跳過的bytes數的估計值,這個過程不會阻塞下一個操作。檔案超過EOF時傳回0。下一個調用可能是相同的線程或不同的線程,一個讀取或者跳過這麼多bytes的操作不會被阻塞,但是可能讀取或跳過更少的bytes。一些情況下,一個非阻塞的讀取或者跳過可能在非常慢時被阻塞,比如從一個很慢的網絡中讀取大檔案。

public native int available() throws IOException;           

close關閉檔案輸入流并釋放相關聯的任何系統資源,如果流關聯通道則通道也要關閉。通過加鎖保證隻能進行一次,避免重複關閉。最終由檔案描述符通過close0來關閉檔案。

public void close() throws IOException {
        synchronized (closeLock) {//隻能由一個線程來執行關閉一次
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();//關閉關聯的通道
        }

        fd.closeAll(new Closeable() {//通知檔案描述符關閉檔案
            public void close() throws IOException {
               close0();
           }
        });
    }

    private native void close0() throws IOException;           

getChannel傳回關聯的通道,初始化通道的位置是目前位置從檔案中讀取的bytes數量。從流中讀取bytes會增加通道的位置,改變通道的位置會改變流中的檔案位置。延遲初始化,第一次調用該方法才會打開檔案通道。

public FileChannel getChannel() {
        synchronized (this) {//初始化單例
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);
            }
            return channel;
        }
    }           

finalize是protected方法必須通過繼承FileInputStram的類來使用,作用是確定close在沒有任何對流的引用時被調用,也就是避免其他線程中流還在讀取,另一個線程發起了close,因為可以通過檔案描述符來判斷是否是in狀态也就是正在讀取。

protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* 
             * 如果fd被共享,FileDescriptor中的引用會確定終結方法隻會在安全的時候調用。
             * 所有使用fd的引用都不可達時,我們調用close
             */
            close();
        }
    }           

FileOutputStream

FileOutputStream是對應的檔案輸出流,檔案輸出流将資料也到一個檔案或者是一個檔案描述符中,無論檔案是否有效或者可能根據所在平台建立一個新的檔案。在一些平台上,一個檔案隻允許被一個FileOutputStream或者其他檔案寫入對象進行操作,在這種環境下,本類的建構在檔案已經被打開時可能會出錯。FileOutputStream寫入的是比特,可以用于寫入圖檔資料,如果要寫入字元的話可以考慮使用FileWriter。

和上面的輸入流很相似,FileOutputStream除了close以外也是不加鎖的,但是write是一個原子性操作,必須在前一個byte串輸出完之後,下一個輸出才能開始。多線程可以強制輸出流進行輸出,但不能中斷未進行完的write。

package test;

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

public class FileOutputThreadTest implements Runnable {
    private byte[] txt;

    private FileOutputStream out;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                out.write(txt);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public FileOutputThreadTest(String txt, FileOutputStream out) {
        this.txt = txt.getBytes();
        this.out = out;
    }

    public static void main(String args[]) throws InterruptedException, IOException {
        FileOutputStream out = new FileOutputStream("D:/test/file.txt");
        StringBuilder build = new StringBuilder();
        Thread[] t = new Thread[10];
        for (int i = 0; i < 10; i++) {
            build.setLength(0);
            for (int j = 0; j < 5; j++) {
                build.append(i);
            }
            t[i] = new Thread(new FileOutputThreadTest(build.toString(), out));
        }
        for (int i = 0; i < 10; i++) {
            t[i].start();
        }
        for (int i = 0; i < 10; i++) {
            t[i].join();
        }
        out.close();

        // 根據輸出結果,每個字元應該連續出現5的倍數
        FileInputStream in = new FileInputStream("D:/test/file.txt");
        int pos = 0;
        while (in.available() > 0) {
            int text = in.read();
            pos++;
            for (int i = 1; i < 5; i++) {
                if (text != in.read()) {
                    System.out.println("error" + String.valueOf(pos));// 沒有出現
                    return;
                }
                pos++;
            }
        }
        /*
         * 00000000000000000000000000000000000000000000000000
         * 22222222222222211111222222222222222222222222222222輸入有交錯
         * 22222111111111111111111111111111111111111111111111
         * 33333333333333333333333333333333333333333333333333
         * 55555555555555555555555555555555555555555555555555
         * 44444444444444444444444444444444444444444444444444
         * 66666666666666666666666666666666666666666666666666
         * 99999999999999999999999998888888888777777777777777
         * 77777777777777777777777777777777777999999999999999
         * 99999999998888888888888888888888888888888888888888
         */
    }

}
           

FileOutputStream有兩種模式,清空檔案從頭開始輸入和保留原本内容從檔案末尾開始添加,這取決于内部屬性append為true時是添加模式

/**
     * 系統依賴的檔案描述符
     */
    private final FileDescriptor fd;

    /**
     * 檔案為擴充模式在末尾添加時為true
     */
    private final boolean append;

    /**
     * 相關聯的檔案通道,延遲初始化
     */
    private FileChannel channel;

    /**
     * 檔案路徑,如果該流是通過檔案描述符來建立,為null
     */
    private final String path;

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

構造方法傳入的參數同樣是三類:檔案路徑名、具體檔案和檔案描述符,append參數也在構造中指定,如果不輸入預設是false

/**
     * 通過具體的名字建立一個檔案輸出流來寫入到檔案。一個新的檔案描述符被建立來代表這個檔案.
     * 如果有一個安全管理器,它的checkWrite方法需要傳入name參數
     * 如果檔案存在但它是一個目錄而不是規則的檔案,或者檔案不出在但不能被建立,或者檔案因為其他原因不能被打開,抛出FileNotFoundException
     */
    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);
    }
    //建立一個檔案輸出流來向一個具體的file中寫入資料。一個新的檔案描述符被建立來代表這個檔案連接配接。
    public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);//清空檔案并且從頭開始輸入
    }
    
    public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        String name = (file != null ? file.getPath() : null);//file的路徑和檔案名
        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();
        fd.attach(this);//便于檔案描述符關閉檔案
        this.append = append;
        this.path = name;

        open(name, append);
    }
    
    /**
     * 建立一個檔案輸入流寫入到具體的檔案描述符中,該檔案描述符表示了對一個檔案系統中實際檔案存在的連結
     * 安全管理器的checkWrite參數是檔案描述符fdObj
     * 如果fdObj是null會抛出NullPointerException
     * 如果fdObj不可用不會抛出異常,但是,如果此時嘗試調用該流的IO方法會抛出IOException
     */
    public FileOutputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkWrite(fdObj);
        }
        this.fd = fdObj;
        this.append = false;//從頭寫入
        this.path = null;//使用檔案描述符時沒有路徑

        fd.attach(this);
    }           

同樣,檔案需要打開才能進行寫入,open方法隻能由構造函數調用,基于native方法open0完成

private void open(String name, boolean append)
        throws FileNotFoundException {
        open0(name, append);//調用native方法打開檔案
    }

    private native void open0(String name, boolean append)
        throws FileNotFoundException;           

write操作上面提到過寫入byte數組的操作是原子的,也就是native方法writeBytes是不可中斷的。append變量作用在兩個native方法中

//将具體的byte寫入到檔案輸出流中,實作了OutputStream.write方法
    public void write(int b) throws IOException {
        write(b, append);
    }

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

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

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

    private native void write(int b, boolean append) throws IOException;           

close關閉這個檔案輸出流并釋放任何關聯的系統資源,這個輸出流不能再用于寫入bytes,如果流關聯到了通道,則通道也關閉。通過加鎖保證隻會被關閉一次。

public void close() throws IOException {
        synchronized (closeLock) {//close隻能進行一次是以需要是線程安全的,隻能由一個線程進行
            if (closed) {
                return;
            }
            closed = true;
        }

        if (channel != null) {
            channel.close();//關閉檔案通道
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

    private native void close0() throws IOException;           

getChannel擷取的檔案通道,傳回通道的初始化等于到目前為止寫入檔案的bytes數量,除非目前的流是擴充模式,該模式下等于檔案的大小。寫入bytes将會增加通道的位置,無論是通過寫入或者指明來改變通道的位置都會改變流的檔案位置。檔案通道是延遲初始化的設計,在調用時才進行初始化。

public FileChannel getChannel() {
        synchronized (this) {//確定隻初始化一次
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, false, true, append, this);
            }
            return channel;
        }
    }           

finalize和FIleInputStream一樣也是要通過繼承類來調用的protected方法,清除所有到檔案的連接配接,確定當沒有其他對這個流的引用時,close方法被調用。通過檔案描述符的狀态來判斷目前是否在輸出。

protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {
                /* 
                 * 如果fd被共享,FileDescriptor中的引用會確定終結器隻在安全的時候被調用。
                 * 所有使用fd的引用都不可達時,我們調用close
                 */
                close();
            }
        }
    }