I/O系統即輸入/輸出系統,對於一門程式語言來說,建立一個好的輸入/輸出系統並非易事。因為不僅存在各種I/O源端和想要與之通訊的接收端(檔案、控制檯、網路連結等),而且還需要支援多種不同方式的通訊(順序、隨機存取、緩衝、二進位制、按字元、按行、按字等)。
Java類庫的設計者通過建立大量的類來解決這個難題,比如面向位元組的類(位元組流,InputStream、OutputStream)、面向字元和基於Unicode的類(位元組流,Reader、Writer)、nio類(新I/O,為了改進效能及功能)等。所以,在充分理解Java I/O系統以便正確地運用之前,我們需要學習相當數量的類。因此一開始可能會對Java I/O系統提供的如此多的類感到迷惑,不過在我們系統地梳理完整個Java I/O系統並將這部分知識與融入到自我的整個知識體系中後,我們就能很快消除這種迷惑。
在I/O這個專題裡面,我會總結Java 中涉及到的大多數I/O相關類的用法,從傳統I/O諸如:File、位元組流、字元流、序列化到新I/O:nio。在本節中我會先總結File和RandomAccessFile的相關知識,按照如下順序:
1. File
1.1 File簡介常用方法
根據官方文件的解釋,Java中的File類是檔案和目錄路徑的抽象,使用者通過File直接執行與檔案或目錄相關的操作。我的理解就是File類的作用是用來指代檔案或者目錄的,通過File的抽象我們可以很方便的操作檔案或目錄,無需關心作業系統的差異。官方文件是這樣描述的:
An abstract representation of file and directory pathnames.
User interfaces and operating systems use system-dependent pathname strings to name files and directories. This class presents an abstract, system-independent view of hierarchical pathnames.
使用者介面和作業系統通過系統相關的路徑名來命名檔案和目錄。而File類提供了一個抽象地、系統無關的視角來描述分層次路徑名。File代表抽象路徑名,有兩個部分組成:
- 一個可選的系統相關的字首,比如磁碟驅動器說明符(disk-drive specifier),unix系統中是“/”而windows系統中則是“\”;
- 0或多個字串名稱組成的序列;
關於File的用法,我覺得直接通過示例來學習會比較高效:
public class FileDemo { public static void main(String[] args) throws IOException { File dir = new File("f:/dirDemo"); System.out.println("dir exists: " + dir.exists()); dir.mkdirs(); System.out.println("dir exists: " + dir.exists()); if(dir.isFile()) { System.out.println("dir is a file."); }else if(dir.isDirectory()) { System.out.println("dir is a directory"); } File file = new File("f:/dirDemo/fileDemo"); System.out.println( "\n Absolute path: " + file.getAbsolutePath() + "\n Can read: " + file.canRead() + "\n Can write: " + file.canWrite() + "\n getName: " + file.getName() + "\n getParent: " + file.getParent() + "\n getPath: " + file.getPath() + "\n length: " + file.length() + "\n lastModified: " + file.lastModified() + "\n isExist: " + file.exists()); file.createNewFile(); System.out.println("is file exist: " + file.exists()); if(file.isFile()) { System.out.println("file is a file."); }else if(file.isDirectory()) { System.out.println("file is a directory"); } System.out.println(); for(String filename : dir.list()) { System.out.println(filename); } } }
輸出結果:
dir exists: false dir exists: true dir is a directory Absolute path: f:\dirDemo\fileDemo Can read: false Can write: false getName: fileDemo getParent: f:\dirDemo getPath: f:\dirDemo\fileDemo length: 0 lastModified: 0 isExist: false is file exist: true file is a file. fileDemo
在這個簡單demo中我們用到多種不同的檔案特徵查詢方法來顯示檔案或目錄路徑的資訊:
- getAbsolutePath(),獲取檔案或目錄的絕對路徑;
- canRead()、canWrite(),檔案是否可讀/可寫;
- getName(),獲取檔名;
- getParent(),獲取父一級的目錄路徑名;
- getPath(),獲取檔案路徑名;
- length(),檔案長度;
- lastModified(),檔案最後修改時間,返回時間戳;
- exists(),檔案是否存在;
- isFile(),是否是檔案;
- isDirectory(),是否是目錄;
- mkdirs(),建立目錄,會把不存在的目錄一併建立出來;
- createNewFile(),建立檔案;
- list(),可以返回目錄下的所有File名,以字元陣列的形式返回;
exists()方法可以返回一個File例項是否存在,這裡的存在是指是否在磁碟上存在,而不是指File例項存在於虛擬機器堆記憶體中。一個File類的例項可能表示一個實際的檔案系統如檔案或目錄,也可能沒有實際意義,僅僅只是一個File類,並沒有關聯實際檔案,如果沒有則exists()返回false。
1.2 File過濾器
list()方法返回的陣列中包含此File下的所有檔名,如果想要獲得一個指定的列表,比如,希望得到所有副檔名為.java的檔案,可以使用“目錄過濾器”(實現了FilenameFilter介面),在這個類裡面可以指定怎樣顯示符合條件的File物件。我們把一個自己實現的FilenameFilter傳入list(FilenameFilter filter)方法中,在這個被當做引數的FilenameFilter中重寫其accept()方法,指定我們自己想要的邏輯即可,這其實是策略模式的體現。
比如我們只要獲取當前專案跟目錄下的xml檔案:
public class XmlList { public static void main(final String[] args) { File file = new File("."); String list; list = file.list(new FilenameFilter(){ @Override public boolean accept(File dir, String name) { Pattern pattern = Pattern.compile("(.*)\\.xml"); return pattern.matcher(name).matches(); } }); Arrays.sort(list,String.CASE_INSENSITIVE_ORDER); for(String dirItem : list) System.out.println(dirItem); } }
在這個例子中,我們用匿名內部類的方式給list()傳參,accept()方法內部我們指定了正則過濾策略,在呼叫File的list()方法時會自動為此目錄物件下的每個檔名呼叫accept()方法,來判斷是否要將該檔案包含在內,判斷結果由accept()返回的布林值來表示。
如上也只是羅列了一些個人認為File類較常用的方法,也只是一部分,若需要更詳細資訊請參考官方文件。
1.3 目錄工具
接下來我們來看一個實用工具,可以獲得指定目錄下的所有或者符合要求的File集合:
public class Directory { // local方法可以獲得指定目錄下指定檔案的集合 public static File[] local(File dir,String regex) { return dir.listFiles(new FilenameFilter() { private Pattern pattern = Pattern.compile(regex); @Override public boolean accept(File dir, String name) { return pattern.matcher(new File(name).getName()).matches(); } }); } public static File[] local(String dir,String regex) { return local(new File(dir),regex); } // walk()方法可以獲得指定目錄下所有符合要求的檔案或目錄,包括子目錄下 public static TreeInfo walk(String start,String regex) { return recurseDirs(new File(start),regex); } public static TreeInfo walk(File start,String regex) { return recurseDirs(start,regex); } public static TreeInfo walk(String start) { return recurseDirs(new File(start),".*"); } public static TreeInfo walk(File start) { return recurseDirs(start,".*"); } static TreeInfo recurseDirs(File startDir,String regex) { TreeInfo treeInfo = new TreeInfo(); for(File item : startDir.listFiles()) { if(item.isDirectory()) { treeInfo.dirs.add(item); treeInfo.addAll(recurseDirs(item,regex)); }else { if(item.getName().matches(regex)) treeInfo.files.add(item); } } return treeInfo; } public static class TreeInfo implements Iterable<File>{ public List<File> files = new ArrayList(); public List<File> dirs = new ArrayList(); @Override public Iterator<File> iterator() { return files.iterator(); } void addAll(TreeInfo other) { files.addAll(other.files); dirs.addAll(other.dirs); } } }
通過工具中的local()方法,我們可以獲得指定目錄下符合要求檔案的集合,通過walk()方法可以獲得指定目錄下所有符合要求的檔案或目錄,包括其子目錄下的檔案,這個工具只是記錄在這裡以備不時之需。
2. RandomAccessFile
因為File類知識檔案的抽象表示,並沒有指定資訊怎樣從檔案讀取或向檔案儲存,而向檔案讀取或儲存資訊主要有兩種方式:
- 通過輸入輸出流,即InputStream、OutputStream;
- 通過RandomAccessFile;
輸入輸出流的方式我們後面會專門總結,這也是Java I/O系統中很大的一塊,本文會講一下RandomAccessFile,因為它比較獨立,和流的相關性不大。
RandomAccessFile是一個完全獨立的類,其擁有和我們後面將總結的IO型別有本質不同的行為,可以在一個檔案內向前和向後移動。我們來看一下其主要方法:
- void write(int d) 向檔案中寫入1個位元組,寫入的是傳入的int值對應二進位制的低8位;
- int read() 讀取1個位元組,並以int形式返回,如果返回-1則代表已到檔案末尾;
- int read(byte[] data) 一次性從檔案中讀取位元組陣列總長度的位元組量,並存入到該位元組陣列中,返回的int值代表讀入的總位元組數,如果返回-1則代表未讀取到任何資料。通常位元組陣列的長度可以指定為1024*10(大概10Kb的樣子,效率比較好);
- int read(byte[] data, int off, int len) 一次性從檔案中讀取最多len個位元組,並存入到data陣列中,從下標off處開始;
- void write(int b) 往檔案中寫入1個位元組的內容,所寫的內容為傳入的int值對應二進位制的低8位;
- write(byte b[]) 往檔案中寫入一個位元組陣列的內容;
- write(byte b[], int off, int len) 往檔案中寫入從陣列b的下標off開始len個位元組的內容;
- seek(long pos) 設定檔案指標偏移量為指定值,即在檔案內移動至新的位置;
- long getFilePointer() 獲取檔案指標的當前位置;
- void close() 關閉RandomAccessFile;
上面只是一部分方法,更多請參考官方文件。我們再來看一個簡單demo學習一下:
public class RandomAccessFileDemo { public static void main(String[] args) { File file = new File("./test.txt"); if(!file.exists()) { try { file.createNewFile(); } catch (IOException e1) { e1.printStackTrace(); } } RandomAccessFile raf = null; try { raf = new RandomAccessFile("./test.txt","rw"); raf.write(1000); raf.seek(0); System.out.println(raf.read()); raf.seek(0); System.out.println(raf.readInt()); } catch (FileNotFoundException e) { System.out.println("file not found"); } catch (EOFException e) { System.out.println("reachs end before read enough bytes"); e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); }finally { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } }
輸出結果:
232
reachs end before read enough bytes
在RandomAccessFile的構造器中有兩個引數,第一個是檔案路徑或者File,代表該RandomAccessFile要操作的檔案,第二個是讀寫模式。如果操作的檔案不存在,在模式為“rw”時會直接建立檔案,如果是“r”則會丟擲異常。
這是一個簡單的例子,首先建立檔案test.txt,然後建立一個和該檔案關聯的RandomAccessFile,指定讀寫模式為讀寫,呼叫write()寫入1000,這裡只會寫入一個位元組,跳到檔案頭部,讀取1個位元組,輸出232(正好是1000對應二進位制的低8位),再跳到檔案頭部,呼叫readInt()讀取1個整數,這時候因為檔案中只有1個位元組,所以丟擲EOFException異常,最後關閉RandomAccessFile。
如上例子我們學習了RandomAccessFile的基本用法,這裡有一點需要注意,RandomAccessFile是基於檔案指標從當前位置來讀寫的,並且寫入操作是直接將插入點後面的內容覆蓋而不是插入。如果我們想實現插入操作,則需要將插入點後面的內容先儲存下來,再寫入要插入的內容,最後將儲存的內容新增進來,看下面的例子:
public class RandomAccessFileDemo { public static void main(String[] args) throws IOException { File file = new File("f:/test.txt"); file.createNewFile(); // 建立臨時空檔案用於緩衝,並指定在虛擬機器停止時將其刪除 File temp = File.createTempFile("temp", null); temp.deleteOnExit(); RandomAccessFile raf = null; try { // 首先往檔案中寫入下面的詩句,並讀取出來在控制檯列印 raf = new RandomAccessFile(file,"rw"); raf.write("明月幾時有,把酒問青天".getBytes()); raf.seek(0); byte[] b = new byte[60]; raf.read(b, 0, 30); System.out.println(new String(b)); // 接下來在詩句中間再插入一句詩 raf.seek(12); FileOutputStream fos = new FileOutputStream(temp); FileInputStream fis = new FileInputStream(temp); byte[] buffer = new byte[10]; int num = 0; while(-1 != (num = raf.read(buffer))) { fos.write(buffer, 0, num); } raf.seek(12); raf.write("但願人長久,千里共嬋娟。".getBytes()); // 插入完成後將緩衝的後半部分內容新增進來 while(-1 != (num = fis.read(buffer))) { raf.write(buffer, 0, num); } raf.seek(0); raf.read(b, 0, 60); System.out.println(new String(b)); System.out.println(); } catch (FileNotFoundException e) { e.printStackTrace(); }finally { raf.close(); } } }
輸出結果,插入詩句成功:
明月幾時有,把酒問青天
明月幾時有,但願人長久,千里共嬋娟。把酒問青天
3. 總結
本文是Java I/O系統系列第一篇,主要總結了File和RandomAccessFile的一些知識。
- File類是對檔案和目錄路徑的抽象,使用者通過File來直接執行與檔案或目錄相關的操作,無需關心作業系統的差異。
- RandomAccessFile類可以寫入和讀取檔案,其最大的特點就是可以在任意位置讀取檔案(random access的意思),是通過檔案指標實現的。