JAVA IO中的文件流涉及流式部分的轉換流InputStreamReader和OutputStreamWriter、文件流FlieReader和FileWriter、FileInputStream和FileOutputStream。還有非流式部分輔助流式部分的類File類、RandomAccessFile類和FileDescriptor等。
3.1 轉換流
轉換流用於在字節流和字符流之間的轉換。在IO包中實際上只有字節流,字符流是在字節流的基礎上轉換出來的,JDK提供了兩種轉換流InputStreamReader和OutputStreamWriter。
3.1.1 InputStreamReader
java.io.InputStreamReader
是字節流通向字符流的橋梁,它使用指定的
InputStreamReader有四個構造方法,需要傳入InputStream對象以及指定的charset(或者使用默認字符集)。其相關方法都是通過私有常量StreamDecoder對象調用對應的方法實現。
private final StreamDecoder sd; public String getEncoding() {return sd.getEncoding();} public int read() throws IOException {return sd.read(); } public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); } public boolean ready() throws IOException { return sd.ready(); } public void close() throws IOException { sd.close(); }
而sun.nio.cs.StreamDecoder
也是Reader的子類。它涉及五個對象java.nio.charset.Charset
, java.nio.charset.CharsetDecoder
, java.nio.ByteBuffer
, java.io.InputStream
, java.nio.channels.ReadableByteChannel
。在其字節流相關的構造函數中,指定了字符集、解碼器和輸入源InputStream以及緩沖區的大小。
private Charset cs;//字符集,16 位的 Unicode 代碼單元序列和字節序列之間的指定映射關系 private CharsetDecoder decoder;//解碼器,能夠把特定 charset 中的字節序列轉換成 16 位 Unicode 字符序列的引擎。 private ByteBuffer bb;//字節緩沖區。 // 兩者是兩種輸入方式的輸入源,必須有一個對象是非空 private InputStream in;//字節輸入流 private ReadableByteChannel ch;//可讀取字節的通道。 //字節流輸入相關的構造方法 StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { super(lock); this.cs = dec.charset(); this.decoder = dec; // 這個if已經常為false,因為這個FileInputStream相關方法暫時並不會加快讀取速度 if (false && in instanceof FileInputStream) { ch = getChannel((FileInputStream)in); if (ch != null) bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE); } if (ch == null) { this.in = in; this.ch = null; bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);//分配緩沖區大小8192 } bb.flip(); // 清空字符緩沖對象 }
StreamDecoder實現了read(char cbuf[], int offset, int length)
方法和read()
方法,其具體實現是參考了
3.1.2 OutputStreamWriter
java.io.OutputStreamWriter
是字符流通向字節流的橋梁,它使用指定的
OutputStreamWriter也有四個構造方法,需要傳入OutputStream對象以及指定的charset(或者使用默認字符集)。其相關方法都是通過私有常量StreamEncoder 對象調用對應的方法實現。
而sun.nio.cs.StreamEncoder
也是Writer的子類。它涉及五個對象java.nio.charset.Charset
, java.nio.charset.CharsetEncoder
, java.nio.ByteBuffer
, java.io.OutputStream
, java.nio.channels.WritableByteChannel
。在其字節流相關的構造函數中,指定了字符集、解碼器和字節輸出流以及緩沖區的大小。
private Charset cs; private CharsetEncoder encoder; private ByteBuffer bb; // Exactly one of these is non-null private final OutputStream out; private WritableByteChannel ch; //字節流相關構造函數 private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc) { super(lock); this.out = out; this.ch = null; this.cs = enc.charset(); this.encoder = enc; // This path disabled until direct buffers are faster if (false && out instanceof FileOutputStream) { ch = ((FileOutputStream)out).getChannel(); if (ch != null) bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE); } if (ch == null) { bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);//8192 } }
在StreamEncoder的三個write方法中,
-
其他兩個方法最終都是調用
write(char cbuf[], int off, int len)
方法 -
而
write(char cbuf[], int off, int len)
方法調用implWrite(cbuf, off, len)
-
implWrite
方法則是調用writeBytes()
實現的 -
writeBytes()
最終調用指定字節輸出流的write方法
//implWrite方法 void implWrite(char cbuf[], int off, int len) throws IOException { CharBuffer cb = CharBuffer.wrap(cbuf, off, len);//獲得寫入目標數組的字符緩沖區對象 if (haveLeftoverChar) flushLeftoverChar(cb, false); while (cb.hasRemaining()) {//判斷字符緩沖去是否還有元素 CoderResult cr = encoder.encode(cb, bb, false); if (cr.isUnderflow()) { assert (cb.remaining() <= 1) : cb.remaining(); if (cb.remaining() == 1) {//remaining()方法則會返回緩沖區中剩余的元素數 haveLeftoverChar = true; leftoverChar = cb.get(); } break; } if (cr.isOverflow()) { assert bb.position() > 0; writeBytes(); continue; } cr.throwException(); } } //writeBytes方法 private void writeBytes() throws IOException { bb.flip(); int lim = bb.limit(); int pos = bb.position(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (rem > 0) { if (ch != null) { if (ch.write(bb) != rem) assert false : rem; } else { out.write(bb.array(), bb.arrayOffset() + pos, rem);//最終調用指定字節輸出流的write方法 } } bb.clear(); }
3.2 字節文件流FileInputStream和FileOutputStream
FileInputStream從文件系統中的某個文件(可以是Flie或者FileDescriptor)獲得輸入字節流,FileOutputStream是用於將數據寫入File或者FileDescriptor的輸出流。文件是否可用或者能否可以創建取決於基礎平台。
無論是FileInputStream還是FileOutputStream都是用於讀取或寫入諸如圖像數據之類的原始字節的流。若是要操作字符流,應當考慮使用FileReader和FileWriter。
java.io.FileInputStream
是InputStream的子類,提供了三個構造方法,可以通過文件名,文件(File),已有的文件連接(FileDescriptor)來打開一個到實際文件的連接。在InputStream中重寫了read(),skip()、available()等方法,不過這些方法都是通過調用本地方法來實現的。
java.io.FileOutputStream
是OutputStream的子類,提供了五個構造方法,除了提供文件名,文件(File),已有的文件連接(FileDescriptor)三種打開到實際文件連接的方法,還添加了布爾變量append用以區別是否使用文件的追加模式。
3.3 字符文件流FileReader和FileWriter
FileReader和FileWriter是用來讀取或者寫入字符文件的便攜類。此兩類的構造方法假定默認字符編碼和默認字節緩沖區大小都是可接受的。要自己指定這些值,可以在其上構造轉換流類InputStreamReader和OutputStreamWriter。
文件是否可用或是否可以被創建取決於底層平台。字符文件流用於操作字符流,要操作原始字節流,考慮使用字節文件流FileInputStream和FileOutputStream。
java.io.FileReader
繼承轉換流InputStreamReader,其三個構造方法對應FileInputStream的三個構造方法,通過傳入生成的FileInputStream對象進行字節流和字符流的轉換來實現,並沒有其他實現。
public class FileReader extends InputStreamReader { public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); } public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } public FileReader(FileDescriptor fd) { super(new FileInputStream(fd)); } }
java.io.FileWriter
繼承轉換流OutputStream,其五個構造方法也與FileOutputStream一一對應。
public class FileWriter extends OutputStreamWriter { public FileWriter(String fileName) throws IOException { super(new FileOutputStream(fileName)); } public FileWriter(String fileName, boolean append) throws IOException { super(new FileOutputStream(fileName, append)); } public FileWriter(File file) throws IOException { super(new FileOutputStream(file)); } public FileWriter(File file, boolean append) throws IOException { super(new FileOutputStream(file, append)); } public FileWriter(FileDescriptor fd) { super(new FileOutputStream(fd)); } }
3.4 File類
java.io.File
是文件和目錄路徑名的抽象表示形式。用戶界面和操作系統使用與系統相關的路徑名字字符串來命名文件和目錄,此類呈現分層路徑名一個抽象的、與系統無關的視圖。無論是抽象路徑名還是路徑名字符串,都可以是絕對路徑或相對路徑,默認情況下,java.io包中的類總是根據當前用戶目錄來解析相對路徑名,此目錄由系統屬性user.dir指定,通常是java虛擬機的調用目錄。
此類的實例可能表示(也可能不表示)實際文件系統對象,如文件或目錄。
文件系統可以實現對實際文件系統對象上的某些操作(如讀、寫、執行)進行限制,這些限制統稱為訪問權限。對象上的訪問權限可能導致File類的某些方法執行失敗。
File類的實例是不可變的,即一旦創建,File對象表示的抽象路徑名將永不改變。
private final String path;
File類的構造函數只是創建一個File實例,並沒有以文件做讀取等操作,因此即使路徑是錯誤的,也可以創建實例不報錯。構造函數包含四種。其構造方法最終都是對抽象路徑名進行初始化。其中的
private static final FileSystem fs = DefaultFileSystem.getFileSystem();//平台的本地文件系統的抽象 private final String path; //抽象路徑名 private final transient int prefixLength;//抽象路徑的前綴長度 public File(String pathname) { if (pathname == null) { throw new NullPointerException(); } this.path = fs.normalize(pathname);// this.prefixLength = fs.prefixLength(this.path);// }
File類主要的方法比較多,但是並不包含文件讀取和寫入、執行:
-
一些文件、目錄操作(創建或者刪除)
-
boolean createNewFile()
,當且僅當不存在具有此抽象路徑名指定名稱的文件時,不可分地創建一個新的空文件。 -
static File createTempFile(String prefix, String suffix)
,在默認臨時文件目錄中創建一個空文件,使用給定前綴和后綴生成其名稱。 -
static File createTempFile(String prefix, String suffix, File directory)
,在指定目錄中創建一個新的空文件,使用給定的前綴和后綴字符串生成其名稱。 -
boolean delete()
,刪除此抽象路徑名表示的文件或目錄。 -
void deleteOnExit()
,在虛擬機終止時,請求刪除此抽象路徑名表示的文件或目錄。 -
boolean mkdir()
,創建此抽象路徑名指定的目錄。 -
boolean mkdirs()
,創建此抽象路徑名指定的目錄,包括所有必需但不存在的父目錄。 -
boolean renameTo(File dest)
,重新命名此抽象路徑名表示的文件。
-
-
一些有關抽象路徑的設置(讀寫權限)
-
boolean setExecutable(boolean executable)
,設置此抽象路徑名所有者執行權限的一個便捷方法。 -
boolean setExecutable(boolean executable, boolean ownerOnly)
,設置此抽象路徑名的所有者或所有用戶的執行權限。 -
boolean setLastModified(long time)
,設置此抽象路徑名指定的文件或目錄的最后一次修改時間。 -
boolean setReadable(boolean readable)
,設置此抽象路徑名所有者讀權限的一個便捷方法。 -
boolean setReadable(boolean readable, boolean ownerOnly)
,設置此抽象路徑名的所有者或所有用戶的讀權限。 -
boolean setReadOnly()
,標記此抽象路徑名指定的文件或目錄,從而只能對其進行讀操作。 -
boolean setWritable(boolean writable)
,設置此抽象路徑名所有者寫權限的一個便捷方法。 -
boolean setWritable(boolean writable, boolean ownerOnly)
,設置此抽象路徑名的所有者或所有用戶的寫權限。
-
-
一些有關文件狀態的判斷(測試文件是否可讀可寫可執行,抽象路徑是否存在、是否是絕對路徑、是否是文件、是否目錄、是否隱藏)
-
boolean canExecute()
,測試應用程序是否可以執行此抽象路徑名表示的文件。 -
boolean canRead()
,測試應用程序是否可以讀取此抽象路徑名表示的文件。 -
boolean canWrite()
,測試應用程序是否可以修改此抽象路徑名表示的文件。 -
boolean exists()
,測試此抽象路徑名表示的文件或目錄是否存在。 -
boolean isAbsolute()
,測試此抽象路徑名是否為絕對路徑名。 -
boolean isDirectory()
,測試此抽象路徑名表示的文件是否是一個目錄。 -
boolean isFile()
,測試此抽象路徑名表示的文件是否是一個標准文件。 -
boolean isHidden()
,測試此抽象路徑名指定的文件是否是一個隱藏文件。
-
-
獲取文件、目錄信息
-
File getAbsoluteFile()
,返回此抽象路徑名的絕對路徑名形式。 -
String getAbsolutePath()
,返回此抽象路徑名的絕對路徑名字符串。 -
File getCanonicalFile()
,返回此抽象路徑名的規范形式。 -
String getCanonicalPath()
,返回此抽象路徑名的規范路徑名字符串。 -
long getFreeSpace()
,返回此抽象路徑名指定的分區中未分配的字節數。 -
String getName()
,返回由此抽象路徑名表示的文件或目錄的名稱。 -
String getParent()
,返回此抽象路徑名父目錄的路徑名字符串;如果此路徑名沒有指定父目錄,則返回 null。 -
File getParentFile()
,返回此抽象路徑名父目錄的抽象路徑名;如果此路徑名沒有指定父目錄,則返回 null。 -
String getPath()
,將此抽象路徑名轉換為一個路徑名字符串。 -
long getTotalSpace()
,返回此抽象路徑名指定的分區大小。 -
long getUsableSpace()
,返回此抽象路徑名指定的分區上可用於此虛擬機的字節數。 -
long lastModified()
,返回此抽象路徑名表示的文件最后一次被修改的時間。 -
long length()
,返回由此抽象路徑名表示的文件的長度。 -
String[] list()
,返回一個字符串數組,這些字符串指定此抽象路徑名表示的目錄中的文件和目錄。 -
String[] list(FilenameFilter filter)
,返回一個字符串數組,這些字符串指定此抽象路徑名表示的目錄中滿足指定過濾器的文件和目錄。 -
File[] listFiles()
,返回一個抽象路徑名數組,這些路徑名表示此抽象路徑名表示的目錄中的文件。 -
File[] listFiles(FileFilter filter)
,返回抽象路徑名數組,這些路徑名表示此抽象路徑名表示的目錄中滿足指定過濾器的文件和目錄。 -
File[] listFiles(FilenameFilter filter)
,返回抽象路徑名數組,這些路徑名表示此抽象路徑名表示的目錄中滿足指定過濾器的文件和目錄。 -
static File[] listRoots()
,列出可用的文件系統根。 -
String toString()
,返回此抽象路徑名的路徑名字符串。
-
-
其他
-
int compareTo(File pathname)
,按字母順序比較兩個抽象路徑名。 -
boolean equals(Object obj)
,測試此抽象路徑名與給定對象是否相等。 -
int hashCode()
,計算此抽象路徑名的哈希碼。 -
URI toURI()
,構造一個表示此抽象路徑名的 file: URI。
-
3.5 FileDescriptor類
java.io.FileDescriptor
與字節文件流FileInputstream以及FileOutputStream相關,字節文件流實例的構造過程最終會對一個私有常量FileDescriptor實例進行初始化。
FileInputstream是一個final修飾的類,而在api中是這樣子描述該類的:文件描述符類的實例用作與基礎機器有關的某種不透明句柄。該結構表示開放文件、開放套接字或者字節的另一個源或者接收者。文件描述符的主要實際用途是創建一個包含該結構的FileInputStream或FileOutputStream。應用程序不應創建自己的文件描述符。
如果單單看以上語句似乎有些雲里霧里,參考
內核(kernel)利用文件描述符(file descriptor)來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。
文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維度的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些設計底層的程序編寫往往會圍繞着文件描述符展開,但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。在Windows操作系統上,文件描述符被稱為文件句柄。
FileDescriptor
一個打開的文件通過唯一的描述符進行引用,該描述符是打開文件的元數據到文件本身的映射。在Linux內核中,這個描述符被稱為文件描述符,文件描述符用一個整數表示(C語言中的類型為int),簡寫為fd。
文件描述符在用戶空間(相對於內核空間而言,也就是我們應用程序的那層)中共享,允許用戶程序用文件描述符直接訪問文件。
同一個文件能被不同或者相同的進程多次打開,每一個打開文件的實例(譬如java中的File實例、FileInputStream實例)都產生一個唯一的文件描述符。同一個描述符可以被多個進程使用。不同進程能同時對一個文件進行讀寫,所以存在並發修改問題。
而有關於java.io.FileDescriptor
的實現,以下源碼及注釋引自:
// jdk7 package java.io; import java.util.concurrent.atomic.AtomicInteger; /** * 文件描述符類的實例用作與基礎機器有關的某種結構的不透明句柄,該結構表示開放文件、開放套接字或者字節的另一個源或接收者。 * 文件描述符的主要實際用途是創建一個包含該結構的 FileInputStream 或 FileOutputStream。 * * 應用程序不應創建自己的文件描述符。 * @since JDK1.0 */ public final class FileDescriptor { private int fd; private long handle; /** * 用於跟蹤使用此FileDescriptor的FIS / FOS / RAF實例的使用計數器。 * 如果FileDescriptor仍被任何流使用,FIS / FOS.finalize()將不會釋放。 */ private AtomicInteger useCount; /** * 構造一個(無效)FileDescriptor對象。 * Constructs an (invalid) FileDescriptor object. */ public /**/ FileDescriptor() { fd = -1; handle = -1; useCount = new AtomicInteger(); } static { // 例行初始化該類的JNI字段偏移量 initIDs(); }a // Set up JavaIOFileDescriptorAccess in SharedSecrets // 在SharedSecrets中設置JavaIOFileDescriptorAccess static { sun.misc.SharedSecrets.setJavaIOFileDescriptorAccess( new sun.misc.JavaIOFileDescriptorAccess() { public void set(FileDescriptor obj, int fd) { obj.fd = fd; } public int get(FileDescriptor obj) { return obj.fd; } public void setHandle(FileDescriptor obj, long handle) { obj.handle = handle; } public long getHandle(FileDescriptor obj) { return obj.handle; } } ); } /** * 標准輸入流的句柄。 * 通常,該文件描述符不直接使用,而是通過稱為System.in的輸入流。 * @see java.lang.System#in */ public static final FileDescriptor in = standardStream(0); /** * 標准輸出流的句柄。 * 通常,此文件描述符不是直接使用,而是通過稱為System.out的輸出流。 * @see java.lang.System#out */ public static final FileDescriptor out = standardStream(1); /** * 標准錯誤流的句柄。 * 通常,該文件描述符不直接使用,而是通過稱為System.err的輸出流。 * @see java.lang.System#err */ public static final FileDescriptor err = standardStream(2); /** * 測試此文件描述符對象是否有效。 */ public boolean valid() { return ((handle != -1) || (fd != -1)); } /** * 強制所有系統緩沖區與基礎設備同步 * @since JDK1.1 */ public native void sync() throws SyncFailedException; // 例行初始化該類的JNI字段偏移量 private static native void initIDs(); private static native long set(int d); private static FileDescriptor standardStream(int fd) { FileDescriptor desc = new FileDescriptor(); desc.handle = set(fd); return desc; } // FIS,FOS和RAF使用的私有方法。 // package private methods used by FIS, FOS and RAF. int incrementAndGetUseCount() { return useCount.incrementAndGet(); } int decrementAndGetUseCount() { return useCount.decrementAndGet(); } }
3.6 RandomAccessFile類
java.io.RandomAccessFile
類既可以讀取文件內容,也可以向文件輸出數據,同時RandomAccessFile支持“隨機訪問”的方式,程序可以直接跳轉到文件的任意地方來讀寫數據。RandomAccessFile只能讀寫文件不能讀寫其他IO節點,其主要應用場景包含:
-
訪問文件的部分內容而不是把文件從頭讀到尾。
-
程序需要向已存在的文件后追加內容
-
RandomAccessFile的一個重要使用場景就是網絡請求中的多線程下載及斷點續傳。
3.6.1 RandomAccessFile的構造方法
RandomAccessFile·有兩個構造方法,可通過直接傳入File對象或者傳入文件名來進行構建。參數model用來指定RandomAccessFile的訪問模式,一共有4種模式:
-
'r'
:以只讀方式打開。調用結果對象的任何write方法都將導致拋出IOException。 -
'rw'
:打開以便讀取和寫入 -
'rws'
:打開以便讀取和寫入。相比於'rw'
,'rws'
還要求對“文件的內容”或“元數據”的每個更新都同步寫入到基礎存儲設備。 -
'rwd'
:打開以便讀取和寫入。相比於'rw'
,'rwd'
還要求對“文件的內容”的每個更新都同步寫入到基礎存儲設備。
3.6.2 RandomAccessFile的重要方法
RandomAccessFile既可以讀文件,也可以寫文件,所以具備了許多類型的read()方法和write()方法。其中比較特殊的是readline()方法。
String readline()
用來從文件中讀取下一行的文本。在其源碼中,通過判斷換行符'\r'和'\n'來實現。
public final String readLine() throws IOException { StringBuffer input = new StringBuffer(); int c = -1; boolean eol = false; while (!eol) { switch (c = read()) { case -1: case '\n': eol = true; break; case '\r': eol = true; long cur = getFilePointer(); if ((read()) != '\n') { seek(cur); } break; default: input.append((char)c); break; } } if ((c == -1) && (input.length() == 0)) { return null; } return input.toString(); }
除了讀寫操作,RandomAccessFile還支持隨機訪問。RandomAccessFile對象包含了一個記錄指針,用以標識當前讀寫處的位置,當程序新創建一個RandomAccessFile對象時,該對象的文件指針記錄位於文件頭(也就是0處),當讀/寫了n個字節后,文件記錄指針將會后移n個字節。除此之外,RandomAccessFile還可以自由移動該記錄指針。下面就是RandomAccessFile具有的兩個特殊方法:
-
long getFilePointer()
:返回文件記錄指針的當前位置。 -
void seek(long pos)
:將文件指針定位到pos位置。
3.7 文件訪問總結
在IO包中實際上只有字節流,字符流是在字節流的基礎上轉換出來的,轉換流InputStreamReader (OutputStreamWriter)是字節流和字符流之間的橋梁,字符文件流FileReader (FileWriter)通過繼承InputStreamReader (OutputStreamWriter)將字節文件流FileInputStream (OutputStream)轉換成字符流。
用戶界面和操作系統使用與系統相關的路徑名字字符串(絕對路徑或相對路徑)來命名文件和目錄,而File類是文件和目錄路徑名的抽象表示形式,在構造函數中File類會初始化一個私有常量path,一旦File實例創建,該抽象路徑不可更改(該抽象路徑可能指向實際目錄或文件,可能不)。
而在操作文件的輸入輸出流中會維護一個常量FileDescriptor(文件描述符)實例,因為內核(kernel)是利用文件描述符(file descriptor)來訪問文件的。
而RandomAccessFile類是一個特殊的類,它既可以讀取文件也可以寫文件,也可以進行隨機訪問,它通過構造函數中的mode來指定文件訪問模式,通過文件指針來進行隨機訪問。