Java.io package
通過數據流,序列化和文件系統提供系統輸入和輸出。
File
概述
一種文件或目錄的路徑名抽象表示。
用戶界面和操作系統使用以來系統的路徑字符串來命名文件和目錄。
File類表達一種與系統無關的分層路徑名視圖抽象,這種抽象主要作用是以不依賴操作系統的方式處理很多文件和路徑名依賴操作系統的復雜問題。這種抽象路徑由兩部分組件:
- 一個可選的依賴系統的首字符串,例如磁盤驅動說明符,"/"Unix根目錄,"\\"表示Windows UNC路徑名;
- 一個空的或者多個"name"的序列。
在抽象路徑中
name
可以表示以下幾種含義:第一個name
可能是目錄名,或者是hostname(Windows UNC的一部分);后續的每一個name
(除了最后一個)都表示目錄名;最后一個name
表示目錄名或者文件名。
路徑分類
無論是字符串形式,還是File的抽象表示,路徑總是分為絕對路徑和相對路徑。
絕對路徑名是完整的,包含文件名和它的完整路徑以及磁盤驅動說明符,是依賴操作系統的。不需要其他信息來定位它所表示的文件。
而相對路徑必須根據從其他路徑獲取的信息來解釋。默認情況java.io包下的類都是一句當前用戶目錄來解決相對路徑。這個目錄時根據系統屬性user.dir來命名,而且通常是JVM被調用的目錄(執行java className命令的目錄就是當前目錄)。
使用絕對路徑不利于代碼平臺移植,所以盡量使用相對路徑。相對路徑目錄分隔符是斜杠(/)。
路徑中的首字符串
這個prefix概念是依賴系統的,例如表示Unix根目錄,Windows的磁盤驅動器說明符和根目錄...
在Unix系統中,"/"表示跟目錄;在Windows中,磁盤字母+:
形式,如果是絕對路徑,后面還會跟"\"。
路徑字符串中的符號
分隔符
依賴系統的路徑字符串和File路徑抽象表示之間的轉換都涉及到分隔符。其作用是將路徑中各name分隔,形成分層。不同操作系統的分隔符都是不一樣的。
依賴系統特定的分隔符可以通過給System.getProperty()
傳入關鍵詞file.separator獲取(更多關鍵詞可以去查看文檔)。
除了目錄分隔符還有路徑分隔符和行分隔符,它們都是依賴系統的(關鍵詞分別是:path.separator和line.separator)。
其他符號
符號 | 描述 |
---|---|
"." | 表示當前目錄 |
".." | 表示上一層目錄 |
"../../" | 表示上一層目錄的上一層目錄 |
"/" | 表示根目錄 |
"~/" | 表示用戶目錄的根目錄(表示當前虛擬目錄下) |
注意表格中使用的分隔符(/)可以替換成任一系統特定的分隔符。
FileSystem Hierarchy
FileSystem Hierarchy的主要作用是規定了操作系統各層次目錄的作用。
好處在于軟件可以預測已安裝文件和文件夾的位置;用戶可以預測已安裝文件和文件夾的位置。這就好比MacOS目錄有/Library,/Application,/Users...一個Android項目也有相應的路徑分別存放Java代碼和資源文件。
詳細規定可以看文件系統層次結構。
empty abstract name
文檔中介紹File的一種抽象形式empty abstract name,它不包含任何prefix和name sequence。那么表示哪個文件或目錄呢?
public class TestPathCharacter {
public static void main(String[] args) {
File file = new File("");
if(!file.exists()) {
System.out.println("no such file or directory");
//打印了,說明無法定位到指定文件或目錄
}
//猜想表示當前用戶目錄
//File f = new File(file, "/TestPathString.java"); //執行后打印異常,無法找到相應文件
File f = new File("./", "TestPathString.java");//執行后正常打印,證明不需要使用絕對路徑
if(!f.exists()) {
System.out.println("no such file or directory");
}
Scanner input = null;
try {
input = new Scanner(f);
String line = null;
while(input.hasNextLine()) {
line = input.nextLine();
System.out.println(line);
}
}catch(IOException e) {
e.printStackTrace();
}finally{
input.close();
}
}
}
執行結果可以看注釋,證明不表示任何文件或目錄。
Path文檔中empty path定位到的是file system默認的目錄。
與java.nio.file互通性
java.nio.file包定義了讓JVM訪問文件,文件屬性和文件系統的接口和類。這些API克服了File類的限制。調用一個File對象的toPath()可以得到一個Path去定位相應文件。返回得到的Path配合Files使用,提供了更高效和更廣泛的額外文件操作,文件屬性和有利于診斷文件操作時錯誤的I/O異常的訪問。
Path是一個可以用于在file system中定位文件得到類。它一般代表依賴系統的文件路徑。一個Path對象表示一個分層的,由被系統特定的分隔符分隔的目錄和文件名元素的序列組成。也有可能會包含一個標示著系統層次的根組件。
FileSystem是一個提供file system的接口,同時是對象訪問file system中的文件和其他對象的工廠。通過FileSystems.getFileSystem()文檔介紹,自己對file system初步理解:
This method iterates over the installed providers to locate the provider that is identified by the URI scheme of the given URI.
給一個對象提供外部訪問接口,也就是提供程序。可以比作Android系統中為了安全考慮程序之間數據共享使用ContentProvider,根據特定的uri去定位文件。而這個ContentProvider就是該程序的file system。
java.nio.file包中還提供了幾個名稱類似的類,它們的對應關系:
類名 | 描述 |
---|---|
Path | 一個可以用于在file system中定位文件得到類。它一般代表依賴系統的文件路徑。 |
Paths | 只包含通過轉換路徑字符串或URI返回Path的靜態方法 |
FileSystem | 一個提供file system的接口 |
FileSystems | 是file system的工廠方法,用于獲取或者構建file system |
Files | 只包含對文件,目錄或其他類型文件操作的靜態方法 |
FileSystems.getDefault()返回的file system是指JVM可用的。而工作目錄就是當前用戶目錄,被系統屬性命名(System.getProperty(user.dir)),這就允許了和java.io.File的互通性。
API理解
大部分File的方法直接看文檔就可以,一下幾個自己理解有誤,實驗了一下。
- mkdir()&mkdirs(),兩者都是創建目錄,注意不會創建文件。但是使用mkdir()時,File對象指示的路徑中有不存在的目錄,導致創建失敗。此時可以用mkdirs()。
- getParent(),返回File對象的父目錄字符串。
- list()&listFiles()&listRoots(),list()和listFiles()都是返回當前目錄下所有文件和目錄,前者返回字符串數組,后者返回File數組;listRoots()返回系統可用的系統盤。
- getPath()&getAbsolutePath()&getCanonicalPath(),getPath()返回構建File對象時路徑的字符串形式(完全不改動);getAbsolutePath()返回File對象指示的絕對路徑;getCanonicalPath()返回File對象的絕對路徑,但是不包括路徑中的符號(除了分隔符)。
FilenameFilter,用于過濾文件。
RandomAccessFile
文檔概述
RandomAccessFile實例支持隨機訪問文件。隨機訪問文件的行為類似存儲在file system中大字節數組。該隱含的數組中有一種光標或索引,稱為文件指針。讀取操作是在指針處讀取開始讀取字節,并且推進指針至下一個字節。如果使用"rw"模式構建的對象,寫操作一樣可用。寫入操作過程和讀取類似。一旦寫入數據超過隱含的數組長度后,數組被擴展。文件指針可以被getFilePointer()
獲取當前位置,通過seek()
修改位置。
作用
java.io包下的其它流都是只讀或只寫。而且它們的外部文件都是順序的,意思是讀操作時從文首到文末、寫操作時覆蓋或者從文末追加,無法更新文件(可以刪除重新創建)。而RandomAccessFile可以做到。
二進制IO中的字符與字符串
DataOutput
該接口規定了從任何Java基本類型數據到一系列字節的轉換,然后將這些字節寫入二進制流中。同時也提供字符串對象轉換成改進版UTF-8格式,并且將這些字節寫出。
DataInput定義了讀取基本數據類型和字符串的方法。
- writeChar(int),將UTF-16字節寫入輸出流中;
- writeChars(String),將字符串中所有字符的UTF-16字節寫入輸出流中;
- writeBytes(String),將字符串中所有字符的UTF-16低字節寫入輸出流,高字節拋棄。該方法適用于純ASCII碼字符組成的字符串。
- writeUTF(String),將字符串中字符轉換成改進版UTF-8格式,然后前兩個字節存儲轉換后的字節數,后面接上轉換后的字節系列。
改進版UTF-8編碼規則
改進版的UTF-8方案采用一個字節,二個字節或三個字節來存儲字符。
- 字符Unicode編碼小于或等于0x7F,該字符編碼大小為一個字節,且首位為0標示為一個字節存儲;
- 字符Unicode編碼大于0x7F且小于或等于0x7FF,該字符編碼大小為二個字節,且前三位是110標示為兩個字節中第一位;
- 字符unicode編碼大于0x7FF,該字符編碼大小為三個字節,且前四位是1110標示為三個字節中第一個。
實戰
public class TestDataOutput {
public static void main(String[] args) {
String s = "我是\uD835\uDD46";
String result = null;
System.out.println(s);
System.out.println("字符串字符個數: " + s.length());
System.out.println("字符串code point個數: " + s.codePointCount(0, s.length()));
RandomAccessFile file = null;
byte[] b = null;
try {
try {
file = new RandomAccessFile("../file/TestUTF8Stream.txt", "rw");
file.setLength(0);
file.writeUTF(s);
int count = (int)(file.length());
System.out.println("隨機訪問文件字節數有:" + count + "個.");
b = new byte[count];
file.seek(0);
file.read(b, 0, count);
for(byte i : b) {
System.out.println(i & 0xff);
}
file.seek(0);
result = file.readUTF();
System.out.println(result);
}finally {
file.close();
}
}catch(FileNotFoundException e) {
e.printStackTrace();
}catch(IOException e) {
e.printStackTrace();
}
}
}
Console打印輸出:
上圖中打印了字符串,字符串代碼單元數量以及代碼點數量,寫入文件后轉換的字節系列個數,分別打印該系列,最后使用readUTF()
讀取寫入文件的字符串。
這里遇到三個誤區:
第一,當寫完數據后,直接從文件中獲取所有字節系列,是無法獲取到的。這是因為此時文本指針在文本末尾。同理,使用readUTF()
前也需要調用seek(0)
。
第二,當文件關閉后對文件重新寫入數據(程序重啟),會覆蓋文本內的內容。這里需要注意了如果第二次輸入的數據字節長度小于第一次,只會覆蓋第二次輸入數量的字節,剩下的會保留。為了避免誤操作,可以在每一次寫入時調用setLength(0)
清空。
第三,使用RandomAccessFile構造函數實例化對象,如果給定的是文件路徑字符串或者File,沒有該文件自動創建。但是給定的是包含未創建的目錄或者目錄路徑字符串或者File,文件/目錄不會自動創建,并且拋出FileNotFounException異常。
RandomAccessFile中讀寫操作調用需要對應起來。也就是在文本寫入時,哪里調用了
writeUTF()
就在讀取過程中的相應順序調用readUTF()
。
只支持文件,且沒有只寫模式
RandomAccessFile類只能夠為文件創建訪問流,目錄等其他類型對象不可以。
RandomAccessFile構造器有兩個:
- RandomAccessFile(File file, String mode)
- RandomAccessFile(String name, String mode)
都會拋出FileNotFoundException異常,原因有以下幾種:
- 如果模式是"r",給定的文件不是固定存在,會拋出。
- 如果模式是"rw"開頭,給定的文件不是一個存在且可寫的固定文件,若不存在又無法創建或者創建,打開文件時出現錯誤,會拋出。
File類可以抽象的表示文件或者目錄路徑,操作時無需處理系統依賴問題。FileSystem是file system接口,且提供了對象訪問file system中文件或其他對象方法(不僅僅只有文件和目錄)。而Path則是FileSystem中定位文件或其他對象的路徑,與系統相關。
模式
RandomAccessFile模式常用的有兩種:
- "r",只讀模式。如果使用該模式實例去調用任何write重載方法會拋出IOException異常。
- "rw",讀寫模式。如果文件不存在,會去嘗試創建一個新文件。
還有兩種模式,看文檔不是很理解:
- "rws"
- "rwd"
同樣是支持讀寫操作,還有別的作用,不太理解。
注意不支持只寫模式。
方法
RandomAccessFile的方法主要是向文本記錄數據。每一條數據大小不必相等,但是它們大小和位置必須可知。這樣做的目的,一方面為了后期讀取時,必須按照寫入時順序調用相應方法;另一方面可以通過seek()方法準確定位到想要的數據。所以,她只能操作文件。
RandomAccessFile雖然是二進制流,但是和InputStream和OutputStream沒有繼承關系。它直接繼承Object,實現DataOutput和DataInput接口的方法。除了這兩個接口的讀寫方法,還實現了特有方法。
- getFilePointer(),返回從文本頭部到指針處(下一個可讀字節)字節偏移量。
- seek(),設置文件指針的偏移量,到下一個可讀或寫的字節。偏移量從文首開始計算。偏移量可以超出文末,但是不會改變文本長度屬性。
- length(),返回文本長度,即字節數。
- setLength(),設置文本長度,一般傳入0可以用于清空文本。
效率
RandomAccessFile的讀寫方法都有native關鍵詞。說明每調用一次讀或寫方法,都需要與磁盤進行一次I/O操作。如果讀取大文件,效率非常低。
在文檔中介紹RandomAccessFile行為好比內存中有一個巨大數組存儲文件字節,其實并沒有。
在java.io包中解決辦法是給流添加一個緩沖區,減少與磁盤I/O操作次數,從而提高效率。例如BufferedInputStream。
但是這樣沒有了RandomAccessFile的特性-隨機訪問文件。可以使用jdk 1.4 nio中的內存映射替換RandomAccessFile,或者擴展RandomAccessFile實現帶有Buffer的RandomAccessFile。
內存映射文件概念:由于文件過大,無法直接放入內存中進行I/O操作,所以一般都是通過緩沖區來提高效率。而有了內存映射概念,可以認為通過一次與磁盤的I/O操作,把文件放入內存,存儲在一個數組中進行訪問,大大提高了效率。
問題
boolean類型數據占幾個字節,值是多少?
boolean類型數據占一個字節,true的字節值為1,false的字節值為0。