Java的I/O是一個龐大的文件操作系統,初學者往往對I/O的使用比較迷茫,優點丈二和尚摸不著頭腦的感覺。即便是使用java I/O處理了自己的實際需求仍然不知其所以然。當然我也是這樣,所以幾天以前我決定好好地看看java的I/O系統,到現在感覺還行,當然用好不敢自夸,但是對于I/O的那個套路差不多已經走通了,并不像以前那樣云里霧里不知所云了。我學習的資料是《java編程思想》,這個總結并沒有多少我自己的東西,因為我的水平有限,倒是想自己造一個輪子但是畢竟能力有限嗎。
好了廢話不多說了,我下面說一下我的學習思路,只是一個思路,當然我已經按照這個路子能比較清楚的使用java的I/O了,所以當大家發現這都是我摘抄的編程思想的內容是大家不要詫異,歡迎拍磚...
先來說一下File類。File類一個容易讓我們"顧名思義"的類。通常我們看到這個類,就會想到這個類可能指代的是一個文件。但是實際上不是這樣子的,他指的是一組文件名或單個文件名。Java API上面說:"文件和目錄路徑名的抽象表示形式"。我們先來看一段簡單的代碼就可以一目了然了,心中的疑惑就煙消云散了:
package review; import java.io.File; public class TestFile { public static void main(String[] args) { File file = new File("D://test.txt"); System.out.println(file); System.out.println(file.isFile()); } } Result: D:\test.txt true
讓我們看一下輸出地結果,當我們打印file對象的時候輸出了一個window下的文件路徑,第二個打印結果是true。當然我提前已經在D盤創建了這個test.txt文件,第二個輸出地意思是這個test.txt對象是不是在D盤下面。我們稍微看一下File類的源碼也可以看到設計者設計File類的意圖:
public class File implements Serializable, Comparable<File> { ... private String path; ... /** * Creates a new <code>File</code> instance by converting the given * pathname string into an abstract pathname. If the given string is * the empty string, then the result is the empty abstract pathname. * * @param pathname A pathname string * @throws NullPointerException * If the <code>pathname</code> argument is <code>null</code> */ public File(String pathname) { if (pathname == null) { throw new NullPointerException(); } this.path = fs.normalize(pathname); this.prefixLength = fs.prefixLength(this.path); } /** * Converts this abstract pathname into a pathname string. The resulting * string uses the {@link #separator default name-separator character} to * separate the names in the name sequence. * * @return The string form of this abstract pathname */ public String getPath() { return path; } /** * Returns the pathname string of this abstract pathname. This is just the * string returned by the <code>{@link #getPath}</code> method. * * @return The string form of this abstract pathname */ public String toString() { return getPath(); }
看了這樣的一部分源碼我們對File類的大概用途差不多已經知道了。首先聲明了一個String類型的path對象,在File的構造方法中有對path的賦值,toString方法也是返回了getPath()取得path值,FIle類一直在和String類型的path對象打交道,所以,這個File類的功能我們也大概知道了。至于具體的File如何使用,如何創建一個目錄,如何打印一個目錄就沒必要舉例了吧!自己對照著API實驗吧。不過我們要注意一下FilenameFiter這個類,他的用法下面舉了一個例子:
package org.wk.io; import java.io.File; import java.io.FilenameFilter; import java.util.Arrays; import java.util.regex.Pattern; /** * 作用:初探File類,獲取文件名稱的小工具類,并檢查某個文件名稱是否在此文件集合中 */ public class DirectoryList { public static void main(String[] args) { File path = new File("."); String list[] = null; if (args.length == 0) { list = path.list(); } else list = path.list(new DirFilter(args[0])); Arrays.sort(list, String.CASE_INSENSITIVE_ORDER); for(String str : list) { System.out.println("File Name: " + str); } } } class DirFilter implements FilenameFilter { private Pattern pattern; public DirFilter(String regex) { this.pattern = Pattern.compile(regex); } @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }
下面是比較核心的部分,也是為什么我們在使用java I/O的時候比較迷惑的地方---java的輸入輸出流。
關于流的概念:它代表任何有能力產出數據的數據源對象或者有能力接受數據的接收端對象,"流"屏蔽了實際I/O設備中處理數據的細節。實際上是為用戶提供了接口,用戶只需關心使用"流"就行了,不必在操心數據的處理。
Java的I/O流分成輸入和輸出兩大部分,但是輸入和輸出又有基于字符和字節之分,那我們就具體展開來看一下其中的細節。
基于字符的輸入和輸出:
輸入流:任何由InputStream派生的類,他們均包含read()方法。
輸出流:任何有OutputStream派生的類,他們均包含write()方法。
FileInputStream類型
類 |
功能 |
構造器 |
如何使用 |
||
ByteArrayInputStream |
允許將內存的緩沖區當做InputStream使用。關閉 ByteArrayInputStream 無效,此類中的方法在關閉此流后仍可被調用,而不會產生任何 IOException。 |
緩沖區,字節將從中取出作為數據源;配合FilterInputStream對象使用提供有用的接口。 |
StringBufferInputStream |
將String對象轉換成InputStream |
字符串,底層實現實際使用StringBuffer作為數據源;配合FilterInputStream對象使用提供有用的接口。 |
FileInputStream |
用于從文件中讀取信息 |
字符串,表示文件名,或FileDescriptor對象作為一種數據源;配合FilterInputStream對象使用提供有用的接口。 |
PipedInputStream |
產生用于寫入相關PipedOutputStream的數據。實現了"管道化"的概念。 |
PipedOutputStream作為多線程中數據源;配合FilterInputStream對象使用提供有用的接口。 |
SequenceInputStream |
將兩個或多個InputStream對象轉換成為一個InputStream。 |
兩個InputStream或者一個容納InputStream的Enumeration作為一種數據源;配合FilterInputStream對象使用提供有用的接口。 |
FilterInputStream |
抽象類,作為裝飾器的接口。其中為其他的InputStream提供有用的接口。 |
詳細建見表"FileterInputStream類型"。 |
OutputStream類型
類 |
功能 |
構造器 |
如何使用 |
||
ByteArrayOutputStream |
在內存中創建緩沖區,所有送往"流"的數據都要放置在此緩沖區中。 |
緩沖區初始化大小(可選),用于指定數據的目的地;配合FilterOutputStream對象使用提供有用的接口。 |
FileOutputStream |
用于將信息寫至文件 |
字符串,表示文件名,文件或FileDescriptor對象指定數據的目的地;配合FilterOutputStream對象使用提供有用的接口。 |
PipedOutputStream |
任何寫入其中的信息都會自動作為相關PipedInputStream的輸出。實現"管道化"概念。 |
PipedOutputStream指定用于多線程的數據目的地;配合FilterOutputStream對象使用提供有用的接口。 |
FilterOutputStream |
抽象類,作為裝飾器的接口。其中為其他的OutputStream提供有用的接口。 |
見表"FilterOutputStream類型"。 |
前面的表中有好多的地方在賣關子,什么裝飾器類的接口,什么配合FilterInputStream提供有用的接口。裝飾器類式設計模式中比較重要和常用的一種設計模式,可以上網上搜一下了解一下裝飾器模式。裝飾器模式的特點之一是比較靈活這也導致了我們在使用java I/O的時候比較困惑。BufferedInputStream buffer = new BufferedInputStream(...);里面的內容我們到底如何決定呢,正是裝飾器模式給我們帶來了這種麻煩。那我們看看這兩個裝飾器類的子類到底有什么也方便我們的選擇。
FileterInputStream類型
類 |
功能 |
構造器參數 |
如何使用 |
||
DataInputStream |
與DataOutputStream搭配之用,因此我們可以按照可移植的方式讀取基本類型。 |
InputStream 包含用于讀取基本數據類型的全部接口。 |
BufferInputStream |
使用它可以防止每次讀取都進行實際寫操作,代表使用緩沖區。 |
InputStream可以指定緩沖區大小 本質上不提供接口只不過是向進程中添加緩沖區是必需的。與接口對象搭配。 |
LineNumInputStream |
跟蹤輸入流中的行號;可調用getLineNum(),setLineNum()。 |
InputStream 僅增加了行號,與接口對象搭配之用。 |
PushBackInputStream |
具有"能彈出一個子節的緩沖區"的功能,因此可以將讀到的最后一個字符回退。 |
inputStream 通常最為編譯器的掃描器,之所以包含在內是因為java編譯器的需要,我們可能不會用到。 |
所以,如果我們通常這樣寫程序
BufferedInputStream b = new BufferedInputStream(new DataInputStream(new FileInputStream("xxx")));
如果對裝飾器模式稍微有點了解的話,就會很容易理解這段程序。首先new FileInputStream("xxx") 作為DataInputStream 的構造器參數new DataInputStream(new FileInputStream("xxx")) 又作為BufferedInputStream 的構造參數,非常明顯的裝飾器模式。
我們再來看一下FilterOutputStream的實現類
FilterOutputStream類型
類 |
功能 |
構造器參數 |
如何使用 |
||
DataOutputStream |
與DataIntputStream搭配之用,因此我們可以按照可移植的方式讀取基本類型。 |
OutputStream 包含用于讀取基本數據類型的全部接口。 |
PrintStream |
用于產生格式化輸出。其中DataOutputStream處理存儲,PrintStream處理顯示。 |
OutputStream,可以用boolean值表示在每次換行的時候是否清空緩沖區 |
BufferedOutputStream |
使用它可以防止每次讀取都進行實際寫操作,代表使用緩沖區。代表使用緩沖區,可以用flush()清空緩沖區 |
OutputStream,指定緩沖區大小 本質上不是接口,只不過是向進程中添加緩沖區所必需的。與接口搭配之用。 |
所以,類比輸入流的程序我們可以這樣寫:
DataOutputStream d = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("...")));
道理與輸入流一樣,所以兩者掌握一個另一個也就可以掌握了,但是并不是所有的輸入和輸出流的API都是有對應的,靈活掌握才是硬道理。
基于字節流的輸入輸出:
Reader和Writer兩個類的操作是基于字符,提供兼容Unicode的功能,同時也起到國際化的作用。但是這兩個類又不是完全沒有關聯的,我們可以通過適配器模式將InputStream轉換成為Reader,這個適配器類就是InputStreamReader;同樣我們可以通過適配器類OutputStreamWriter將OutputStream轉換成為Writer。
這樣我們在操作文件的時候就擁有了兩個利器,一個是面向字節的InputStream,OutputStream;和面向字符的Reader,Writer。那么究竟什么情況下使用Reader,Writer,什么情況下使用InputStream,OutputStream呢?由于Reader和Writer是后來添加上的類,所以它的操作效率比InputStream和OutputStream高,于是在編程思想中給出了這樣的結論:盡量常識使用Reader和Writer,一旦程序無法成功的編譯,那是在選擇InputStream和OutputStream。
下面我們看一下這兩個類層次結構的關系:
面向字節 |
面向字符 |
適配器 |
InputStream |
Reader |
InputStreamReader |
OutputStream |
Writer |
OutputStreamWriter |
FileInputStream |
FileReader |
|
FileOutputStream |
FileWriter |
|
StringBufferInputStream |
StringReader |
|
無 |
StringWriter |
|
ByteArrayInputStream |
CharArrayReader |
|
ByteArrayOutputStream |
CharArrayWriter |
|
PipedInputStream |
PipedReader |
|
PipedOutputStream |
PipedWriter |
所以剛才寫的兩段代碼就可以這樣來通過Reader和Writer來改寫:
BufferedInputStream b = new BufferedInputStream(new DataInputStream(new FileInputStream("xxx"))); BufferedReader reader = new BufferedReader(new FileReader("...")); DataOutputStream d = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("..."))); BufferedWriter writer = new BufferedWriter(new FileWriter("..."));
下面我們來看兩種典型的應用方式,以后我們在進行I/O操作是就可以直接使用代碼,或者直接套用這樣的格式。
應用一:
package typical.usage; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; /** * @author Administrator 一個讀寫文件的小工具 */ public class TextFile extends ArrayList<String> { private static final long serialVersionUID = 1L; // 讀文件 public static String read(String file) { StringBuilder builder = new StringBuilder(); try { BufferedReader bf = new BufferedReader(new FileReader( new File(file).getAbsoluteFile())); try { String s = null; while ((s = bf.readLine()) != null) { builder = builder.append(s); builder.append("\n"); } } catch (IOException e) { e.printStackTrace(); } finally { try { bf.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (FileNotFoundException e) { e.printStackTrace(); } return builder.toString(); } // 寫文件 public static void write(String file, String content) { try { PrintWriter pr = new PrintWriter(new File(file).getAbsoluteFile()); try { pr.print(content); } finally { pr.close(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } // 根據指定的正則表達式拆分文件 public TextFile(String file, String regex) { super(Arrays.asList(read(file).split(regex))); if (get(0).equals("")) remove(0); } // 讀文件 public TextFile(String file) { this(file, "\n"); } // 寫文件 public void write(String file) { try { PrintWriter pw = new PrintWriter(new File(file).getAbsoluteFile()); try { for (String item : this) { pw.print(item); } } finally { pw.close(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { String file = read("D://IAReg.txt"); System.out.println(file); write("D://b.txt", file); TextFile tf = new TextFile("D://b.txt"); tf.write("D://c.txt"); TextFile tf2 = new TextFile("D://b.txt", "\\W+"); System.out.println(tf2); } }
這個例子展示了讀寫文件的基本方法,同時還實現了一個文件的過濾器。
應用二:
package typical.usage; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; /** * * @author wangkang * 打開一個文件用于字符輸入,同時為了提高速度運用了緩沖。 */ public class BufferedInputFile { public static String read(String fileName) throws IOException { BufferedReader bf = new BufferedReader(new FileReader(fileName)); String s = null; StringBuilder bs = new StringBuilder(); while ((s = bf.readLine()) != null) { bs = bs.append(s + "\n"); } //關閉文件輸入流 bf.close(); return bs.toString(); } public static void main(String[] args) { try { System.out.println(read("D:\\IAReg.txt")); } catch (IOException e) { e.printStackTrace(); } } }
這個示例展示了利用緩沖打開一個文件,并將其內容防止字符串對象中,當然我們獲取了文件內容以后操作是根據我們的實際需求來。
應用三:
package typical.usage; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; public class FormattedMemoryInputStream2 { public static void main(String[] args) throws IOException { DataInputStream in = new DataInputStream(new ByteArrayInputStream( BufferedInputFile.read("D:\\IAReg.txt").getBytes())); while (in.available() != 0) { System.out.println(in.readByte()); } } }
這個示例展示了格式化的內存輸入,new ByteArrayInputStream(
BufferedInputFile.read("D:\\IAReg.txt").getBytes())產生了一個格式化的字節數組共DataInputStream使用。
應用四:
//基本的本件輸出 package typical.usage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; public class BasicFileOutput { public static void main(String[] args) throws IOException { String file = "D:\\test.out"; BufferedReader in = new BufferedReader(new StringReader( BufferedInputFile.read("D:\\IAReg.txt"))); PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter( file))); int lineNum = 1; String s = null; while ((s = in.readLine()) != null) { lineNum++; System.out.println(lineNum + ":" + s); //將字符串的內容寫進文件 out.write(s); } in.close(); out.close(); } } //文件輸出的快捷方式 package typical.usage; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; public class ShortCutFileOutput { public static void main(String[] args) throws IOException { String file = "D:\\test.out"; BufferedReader in = new BufferedReader(new StringReader( BufferedInputFile.read("D:\\IAReg.txt"))); PrintWriter out = new PrintWriter(file); int lineNum = 1; String s = null; while ((s = in.readLine()) != null) { lineNum++; // 將字符串的內容寫進文件 out.write(lineNum + ":" + s); } in.close(); out.close(); System.out.println(BufferedInputFile.read("D:\\test.out")); } }
應用五:
package typical.usage; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class StoringAndRecoveringData { public static void main(String[] args) throws IOException { DataOutputStream out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream("D://a.java"))); out.writeInt(12345); out.writeDouble(12345.000); out.writeUTF("上面的是一個整數和一個浮點數!!"); out.writeChars("上面的是一個整數和一個浮點數!!"); out.close(); DataInputStream in = new DataInputStream(new BufferedInputStream( new FileInputStream("D://a.java"))); System.out.println(in.readInt()); System.out.println(in.readDouble()); System.out.println(in.readUTF()); System.out.println(in.readChar()); in.close(); } }
這個示例展示了如何存儲和恢復數據。我們在程序中使用DataOutputStream來存儲數據,用DataInputStream來讀取恢復數據。我們需要知道,只要是DataOutputStream寫入的數據都何以利用DataInputStream來準確的得到數據,盡管是在跨平臺應用中,這也體現了java的跨平臺性。writeUTF()以與機器無關方式使用 UTF-8 修改版編碼將一個字符串寫入基礎輸出流,所以我們可以將字符串和其他數據類型相混合。
應用六:
package typical.usage; import java.io.IOException; import java.io.RandomAccessFile; public class UsingRandomAccessingFile { static String file = "D://IAReg.txt"; static void play() throws IOException { RandomAccessFile rf = new RandomAccessFile(file, "r"); for(int i = 0; i < 11; i ++) { System.out.println(rf.readLine()); } rf.close(); } public static void main(String[] args) throws IOException { //讀取數據 play(); RandomAccessFile rf = new RandomAccessFile(file, "rw"); for(int i = 0; i < 3; i ++) { rf.writeUTF("哈哈哈"); } rf.close(); //測試修改結果 play(); } }
這個示例展示了如何讀寫隨機訪問文件。我們是通過RandomAccessFile來實現這樣的功能的,RandomAccessFile適用于大小已知的記錄組成的文件。這個類的構造器的參數需要一個特別的String字符串,用來指定"隨機讀----r"或者"既讀又寫-----rw"
到這里就告一段落吧,說的都是一些皮毛的東西,至于一些高級的應用,我也是不太明白。但是只要入門了一切都好說了,面包會有的一切會有的。這一章我們必須要掌握裝飾器和適配器模式,這樣我們才能理解類之間紛繁復雜的配合使用。