本文內(nèi)容非原創(chuàng),你可以點(diǎn)擊此處查看內(nèi)容來源聲明
輸入/輸出流
在Java API中,可以從其中讀出一個字節(jié)序列的對象叫做輸入流,可以從其中寫入一個字節(jié)序列的對象叫做輸出流。這些字節(jié)序列的來源地和目的地可以是文件,也可以是網(wǎng)絡(luò)連接,甚至是內(nèi)存塊
抽象類InputStream和OutputStream構(gòu)成了輸入輸出(I/O)類層次結(jié)構(gòu)的基礎(chǔ)。
讀寫字節(jié)
InpuStream類有一個抽象方法:
abstract int read()
這個方法將讀入一個字節(jié),并返回讀入的字節(jié),或者在遇到輸入源結(jié)尾時返回-1。在設(shè)計(jì)具體的輸入流類時,必須覆蓋這個方法已提供適當(dāng)?shù)墓δ堋?br>
InputStream類還有若干個非抽象防范,可以讀入一個字節(jié)數(shù)組,或者跳過大量的字節(jié)。這些方法都要調(diào)用抽象的read方法,因此,各個子類都只需覆蓋這一個方法。
與此類似,OutputStream類定義了下面的抽象方法:
abstract void write(int b)
它可以向某個輸出位置寫出一個字節(jié)。
read和write方法在執(zhí)行時都將阻塞,直至 字節(jié)確實(shí)被讀入或?qū)懗?。這就意味著如果流不能被立即訪問,那么當(dāng)前的線程將被阻塞。這使得這兩個防范等待制定的流變?yōu)榭捎?的這段時間里,其他的線程就有機(jī)會去執(zhí)行有用的工作。
available方法使我們可以去見檢查當(dāng)前可讀入的字節(jié)數(shù)量,這意味著下面的代碼片段不會被阻塞:
int bytesAvailable = in.available();
if(bytesAvailable > 0){
byte[] data = new byte[bytesAvailable];
in.read(data);
}
當(dāng)你完成對輸入輸出流的讀寫時,英國通過調(diào)用close方法來關(guān)閉它。
即使某個輸入輸出流類提供了原生的read和write功能的某些具體方法,應(yīng)用系統(tǒng)的程序員還是很少使用他們,因?yàn)榇蠹腋信d趣的數(shù)據(jù)可能包含數(shù)字、字符串和對象,而不是原生字節(jié)。
我們可以使用眾多的從基本的InputStream和OutputStream類導(dǎo)出的某個輸入/輸出類,而不只是直接使用字節(jié)。
完整的流家族
組合輸入/輸出流過濾器
Tips:
所有在java.io中的類都將被相對路徑名解釋為以用戶工作目錄開始,可以調(diào)用System.getProperty("user.dir");
Warning
由于反斜杠字符在java字符串中是轉(zhuǎn)義字符,因此要確保在Windows風(fēng)格的路徑名中使用\(eg:C:\Windows\win.ini)。在Windows中,可以使用單斜杠字符(C:/Windows/win.ini),因?yàn)榇蟛糠諻indows文件處理的系統(tǒng)調(diào)用都會將斜杠解釋成文件分隔符。但是并不推薦這么做,因?yàn)閃indows系統(tǒng)函數(shù)的行為會因與時俱進(jìn)而發(fā)生變化。因此對于可移植的程序來說,應(yīng)該使用程序所運(yùn)行平臺的文件分隔符,可以通過個常量字符串java.io.File.separator獲得。
FileInputStream和FileOutputStream可以提供附著在一個磁盤文件上的輸入流和輸出流,而你只需向構(gòu)造器提供文件名或文件的完整路徑名。
FileInput fin = new FileInputStream("employee.dat");
與抽象類InutStream和Outputstream一樣,這些類只支持在字節(jié)級別上的讀寫。也就是說,我們只能從fin對象中讀入字節(jié)和字節(jié)數(shù)組。
byte b = (byte) fin.read();
DataInputStream,就只能讀入數(shù)值類型:
DataInputStream din = ...;
double x = din.readDouble();
正如FileInputStream沒有任何讀入數(shù)值的方法一樣,DataInputStream也沒有任何從文件中獲取數(shù)據(jù)的方法。
Java使用了一種靈巧的機(jī)制來分離這兩種職責(zé)。某些輸入流(例如FileInputStream和由URL類的openStream方法返回的輸入流)可以從文件和其他更外部的位置上獲取字節(jié),而其他的輸入流(例如DataInputStream)可以將字節(jié)組裝到更有用的數(shù)據(jù)類型中。Java程序員必須對二者進(jìn)行組合。例如,為了從文件中讀入數(shù)字,首先需要創(chuàng)建一個FileInputStream,然后將其傳遞給DataInputStream的構(gòu)造器:
FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream din = new DataInputStream(fin);
double x = din.readDouble();
FilterInputStream和FilterOutputStream這些類的子類用于向處理字節(jié)的輸入/輸出流添加額外的功能。
可以通過嵌套過濾器添加多重功能。例如,輸入流在默認(rèn)情況下是不被緩沖區(qū)緩存的。每個對read的調(diào)用都會請求操作系統(tǒng)再分發(fā)一個字節(jié)。相比之下,請求一個數(shù)據(jù)塊并將置于緩沖區(qū)會顯得更加高效。如果使用緩沖機(jī)制,以及用于文件的數(shù)據(jù)輸入方法,就需要使用下面這種相當(dāng)恐怖的構(gòu)造器序列:
DataInputStream din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("employee.dat")));
注意,把DataInputStream置于構(gòu)造器的最后,是因?yàn)橄M褂肈ataInputStream的方法,并且希望使用它們能夠使用帶緩沖機(jī)制的read方法。
有時當(dāng)多個輸入流鏈接在一起,需要跟蹤各個中介輸入流(intermediate inputstream)。例如,當(dāng)讀入輸入流時,經(jīng)常需要預(yù)覽下一個字節(jié),以了解它是否是想要的值。Java提供了用于此目的的PushbackInputStream:
PushbackInputStream pbin = new PushbackInputStream(
new BuffererInputStream(
new FileInputStream("employee.dat")));
現(xiàn)在可以預(yù)讀下一個字節(jié):
int b = pbin.read();
并且在它并非你所期望的值時將其推回流中。
if(b != '<') pbin.unread(b);
但是讀入和推回是可應(yīng)用于可推回(pushable)輸入流的僅有的方法。如果希望能夠預(yù)先瀏覽并且可以讀入數(shù)字,就需要一個既是可推回輸入流,又是一個數(shù)據(jù)輸入流的引用。
DataInputStream din = new DataInputStream(
pbin = new PushbackInputStream(
new BufferedInputStream(
new FileInputStream("employee.dat")))
);
當(dāng)然在其它編程語言的輸入輸出流類庫中,諸如緩沖機(jī)制和預(yù)覽等細(xì)節(jié)都是自動處理的。
文本輸入與輸出
在保存數(shù)據(jù)時,可以選擇二進(jìn)制格式或文本格式。
在存儲文本字符串時,需要考慮編碼方式,在Java內(nèi)部使用的是UTF-16,字符串“1234”編碼為 00 31 00 32 00 33 0034 十六進(jìn)制。但是,許多程序都希望文本文件按照其他編碼方式編碼。在UTF-8這種在互聯(lián)網(wǎng)上最常用的編碼方式中,這個字符串將寫出為 4A 6F 73 C3 A9,其中并沒有用于前3個字母的任何0字節(jié),而字符e(e頭上加一個二聲)占用了兩個字節(jié)。(最后一句話沒看懂 ,中文書上就這么翻譯的,可能翻譯錯了吧。。。)
OutputStreamWriter類將使用選定的字符編碼方式,把Unicode碼元的輸出流轉(zhuǎn)換為字節(jié)流。
InputStreamReader類將包含字節(jié)(用某種字符編碼方式表示的字符)的輸入流轉(zhuǎn)換為可以產(chǎn)生Unicode碼元的讀入器。
如何寫出文本輸出
對于文本輸出可以使用PrinterWriter。這個類擁有以文本格式打印字符串和數(shù)字的方法,還有一個將Printerwriter鏈接到FileWriter的便捷方法,下面的語句:
PrintWriter out = new PrinterWriter("employee.txt","UTF-8");
等同于
PrintWriter out = new PrinterWriter(new FileOutStream("employee.txt"),"UTF-8");
為了輸出到打印寫出的器,需要使用與使用System.out時相同的print,println和printf方法。
如何讀入文本輸入
最簡單的處理任意文本的方式是使用廣泛使用的Scanner類,可以從任何輸入流中構(gòu)建Scanner對象。
或者,可以將短小的文本文件像下面這樣讀入一個字符串中:
Strng content = new String(File.readAllBytes(path),charset);
如果要將文件一行一行地讀入,可以調(diào)用:
List<String> lines = Files.readAllLiines(path,charset);
如果文件太大,可以將行惰性處理為一個Stream<String>對象:
try (Stream<String> lines = Files.lines(path,charset)){...}
在早期的Java版本中,處理文本的唯一輸入方式是通過BufferedReader類。它的readLine方法會產(chǎn)生一行文本,或者在無法獲得更多的輸入時返回null。典型的輸入循環(huán)看起來像下面這樣:
InputStream inputStream = ...;
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,StandardCharset.UTF_8))){
String line;
while((line = in.readLine() != null)){
do Something
}
}
如今,BufferedReader類又有了一個lines方法,可以產(chǎn)生一個Stream<String>對象。但是,與Scanner不同,BufferedReader沒有用于任何讀入數(shù)字的方法。
以文本格式存儲對象
字符編碼方式
輸入和輸出流都是用于處理字節(jié)序列的,但是在許多情況下,我們希望操作的是文本,即字符序列。于是,字符如何編碼成為字節(jié)就成了問題。
Java針對字符使用的是Unicode標(biāo)準(zhǔn)。每個字符或“編碼點(diǎn)”都具有一個21位的整數(shù)。有多種不同的字符編碼方式,就是說,將這些21位數(shù)字包裝成字節(jié)的方法有多種。
最常見的編碼方式是UTF-8,它會將每個Unicode編碼點(diǎn)編碼為1到4個字節(jié) 的序列(如下表)。UTF-8的好處是傳統(tǒng)的包含了英語中用到的所有字符 的ASCII字符集中的每個字符都會占用一個字節(jié)。
UTF-8編碼方式
| 字符范圍 | 編碼方式 |
|---|---|
| 0...7F | 0a6a5a4a3a2a1a0 |
| 80...7FF | 110a10a9a8a7a6??a5a4a3a2a1a0 |
| 800...FFFF | 1110a15a14a13a12a11a10a9a8a7a6??a5a4a3a2a1a0 |
| 10000...10FFFF | 11110a20a19a18a17a16a15a14a13a12a11a10a9a8a7a6??a5a4a3a2a1a0 |
另一種常用的編碼方式是UTF-16,他會將每個Unicode編碼點(diǎn)編碼為1個或2個16位值。
(待補(bǔ)充。。。)
StandardCharsets類具有類型為Charset的靜態(tài)變量,用于表示每種Java虛擬機(jī)都必須支持的字符編碼方式:
StandardCharsets.UTF_8
StandardCharsets.UTF_16
StandardCharsets.UTF_16BE
StandardCharsets.UTF_16LE
StandardCharsets.UTF_8859_1
StandardCharsets.UTF_US_ASCII
為了獲得另一種編碼方式的Charset,可以使用靜態(tài)的forName方法:
Charset shiftJIS = Charset.forName("Sift-JIS");
在讀入或?qū)懗鑫谋緯r,應(yīng)該使用Charset對象??梢韵裣旅孢@樣將一個字節(jié)數(shù)組轉(zhuǎn)換為字符串:
String str = new String(bytes,StandardCharsets.UTF_8);
讀寫二進(jìn)制數(shù)據(jù)
文本格式對于測試和調(diào)試而言很方便,但是并不像二進(jìn)制格式傳遞數(shù)據(jù)那樣高效。
DadaInput 和 DataOutput接口
DataInput接口定義了下面用于二進(jìn)制格式寫數(shù)組、字符、Boolean值和字符串的方法:
writeCharts
writeByte
writeInt
writeShort
writeLong
writeFloat
writeDouble
writeChar
writeBoolean
writeUTF
例如,writeInt總是將一個整數(shù)寫出為4字節(jié)的二進(jìn)制數(shù)量值,而不管它有多少位,writeDouble總是將一個double值寫出為8字節(jié)的二進(jìn)制數(shù)量值。這樣產(chǎn)生的結(jié)果并非人可閱讀的,但是對于每個給定的每個值,所需的空間都是相同的,而且將其讀回也比解析文本要快。
writeUTF方法使用修訂版的8位Unicode轉(zhuǎn)換格式寫出字符串。這種方式與直接使用標(biāo)準(zhǔn)的UTF-8編碼方式不同,其中,Unicode碼元序列首先用UTF-16表示,其結(jié)果之后使用UTF-8規(guī)則進(jìn)行編碼。修訂后的編碼方式對于編碼大于oxFFF的字符的處理有所不同,這是為了向后兼容在Unicode還沒有超過16位時構(gòu)建的虛擬機(jī)。
為了讀回?cái)?shù)據(jù),可以使用DataInput接口中定義 的下列方法:
readInt
readShort
readLong
readFloat
readDouble
readChar
readBoolean
readUTF
DataInputStream類實(shí)現(xiàn)了DataInput接口,為了從文件中讀入二進(jìn)制數(shù)據(jù),可以將DataInputStream與某個字節(jié)源相結(jié)合,例如FileInputStream:
DataInputStream in = new DataInputStream(new FileInputStream("employee.txt"));
與此類似,想要寫出二進(jìn)制數(shù)據(jù),可以使用實(shí)現(xiàn)了DataOutput接口的DataOutputStream類:
DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.txt"));
隨機(jī)訪問文件
RandomAccessFile類可以在文件中的任何位置查找或?qū)懭霐?shù)據(jù)。磁盤文件都是隨機(jī)訪問的,但是與網(wǎng)絡(luò)套接字通信的輸入/輸出流卻不是。可以打開一個隨機(jī)訪問文件,只是用于讀入或同時用于讀寫,可以通過使用字符串"r"(用于讀入訪問)或"rw"(用于讀入/寫出訪問)作為構(gòu)造器的第二個參數(shù)來指定這個選項(xiàng)。
RandomAccessFile in = new RandomAccesFiel("employee.txt","r");
RandomeAccessFile inOut = new RandomAcce("employee.txt","rw");
當(dāng)你將已有文件作為RandomAccessFile打開時,這個文件不會被刪除。
隨機(jī)訪問已有文件有一個表示下一個將被讀入或?qū)懗龅淖止?jié)所處位置 的文件指針,seek方法可以用來將這個文件指針設(shè)置到文件中的任意字節(jié)位置,seek的參數(shù)是一個long類型的整數(shù),它的值位于0到文件按照字節(jié)來度量的長度之間。
getFilePointer方法將返回文件指針的當(dāng)前位置。
RandomAccessFile類同時實(shí)現(xiàn)了DataInput和DataOutput接口。為了讀寫隨機(jī)訪問文件,可以使用在前面討論過的readInt/writeInt/和readChar/writeChar之類的方法。
(待補(bǔ)充。。。)
ZIP文檔
ZIP文檔通常以壓縮格式存儲了一個或多個文件,每個ZIP文檔都有一個頭,包含諸如每個文件名和使用的壓縮方法等信息。在Java中,可以使用ZipInputStream來讀入ZIP文檔??梢孕枰獮g覽文檔中的每個單獨(dú)的項(xiàng),getNextEntry方法可以返回一個描述這些項(xiàng)的ZipEntry類型的對象。向ZipInputStream的getInputStream方法傳遞該項(xiàng)可以獲取用于讀取該項(xiàng)的輸入流。然后調(diào)用closeEntry來讀入下一個項(xiàng)。下面是典型的通讀ZIP文件的代碼序列:
ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname));
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null){
InputStream in = zin.getInputStream(entry);
//read the contents of in
zin.closeEntry();
}
zin.close();
要寫出到ZIP文件,可以使用ZipOutputStream,而對于你希望放入到ZIP文件中的每一項(xiàng),都應(yīng)該創(chuàng)建一個ZipEntry對象,并將文件名傳遞給ZipEntry的構(gòu)造器,它將設(shè)置其他諸如文件日期和解壓縮方法等參數(shù)。如果需要,你可以 覆蓋這些設(shè)置。然后,你需要調(diào)用ZipOutputStream的putNextEntry方法來開始寫出新文件,并將文件數(shù)據(jù)發(fā)送到ZIP 輸出流中。完成時,需要調(diào)用closeEntry。然后,你需要對所有你希望存儲的文件都重復(fù)這個過程。下面是代碼框架:
FileOutputStream fout = new FileOutputStream("test.zip");
ZipOutputStream zout = new ZipOutputStream(fout);
//for all files
{
ZipEntry ze = new ZipEntry(filename);
zout.putNextEntry(ze);
//send data to zout
zout.closeEntry();
}
zout.close();
JAR文件只是帶一個特殊項(xiàng)的ZIP文件,這個項(xiàng)稱作清單??梢允褂肑arInputStream和JarOutputStream類來讀寫清單項(xiàng)。
ZIP輸入流是一個能夠展示流的抽象化的強(qiáng)大之處的實(shí)例。當(dāng)你讀入以壓縮格式存儲的數(shù)據(jù)時,不必?fù)?dān)心邊請求邊解壓數(shù)據(jù)的問題,而且ZIP格式的字節(jié)源并非是文件,也可以是來自網(wǎng)絡(luò)連接的ZIP數(shù)據(jù)。事實(shí)上,當(dāng)Applet的類加載器讀入JAR文件時,它就是在讀入和解壓來自網(wǎng)絡(luò)的數(shù)據(jù)。
對象輸入/輸出流與序列化
當(dāng)你需要存儲相同類型的數(shù)據(jù)時,使用固定長度的記錄格式是一個不錯的選擇。但是,在面向?qū)ο蟪绦蛑袆?chuàng)建的對象很少全部都具有相同的類型。例如,你可能有一個稱為staff的數(shù)組,它名義是一個Employee記錄數(shù)組,但是實(shí)際上卻包含諸如Manager這樣的子類實(shí)例。
Java語言支持一種稱為對象序列化的非常通用的機(jī)制,它可以將任何對象寫出到輸出流中,并在之后將其讀回。
保存和加載序列化對象
為了保存對象數(shù)據(jù),需要打開一個ObjectOupputStream對象:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
為了保存對象,可以使用ObjectOutputStream的writeObject方法:
Employee harry = new Employee("Harry Hacker",5000,1998,10,1);
Manager boss = new Manager("Carl Cracker",8000,1987,12,25);
out.writeObject(harry);
out.writeObbject(boss);
為了將這些對象讀回,首先需要獲得一個ObjectInputStream對象:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.txt"));
然后,用readObject方法以這些對象被寫出時的順序獲得他們:
Employee e1 = (Employee) in.readObject();
Employee e2 = (Employee) in.readObject();
但是對希望在對象輸出流中存儲或從對象輸入流中恢復(fù)的所有類都應(yīng)進(jìn)行一些修改,這些類必須實(shí)現(xiàn)Serializable接口:
class Employee implements Serializable {... }
Serializable接口沒有任何方法,因此不需要對這些類做任何改動。在這一點(diǎn)上,與Cloneable接口很相似。但是,為了使類可克隆,仍需要覆蓋object類中的clone方法,而為了使類序列化,不需要做任何事。
只有需要寫出對象時才使用witeObject/readObject,對于基本類型值,使用諸如writeInt/readInt或writeDouble/readDouble (對象流類都實(shí)現(xiàn)了DataInput/DataOutput接口)
在幕后,是ObjectOutputStream在瀏覽對象的所有域,并存儲他們的內(nèi)容。
此處的序列化是將對象集合保存到文件中,并按照它們被存儲的樣子獲取他們。序列化的另一種非常重要的應(yīng)用是通過網(wǎng)絡(luò)將對象集合傳送到另一臺計(jì)算機(jī)上。正如在文件中保存原生的內(nèi)存地址無意義一樣,這些地址對于不同的處理器之間的通信也是毫無意義的。因?yàn)樾蛄谢眯蛄刑柎媪藘?nèi)存地址,所有它允許對象集合從一臺機(jī)器傳送到另一臺機(jī)器。
理解對象序列化的文件格式
參見原書
修改默認(rèn)的序列化機(jī)制
某些數(shù)據(jù)域是不可序列化的,例如只對本地方法有意義的存儲文件句柄或窗口句柄的整數(shù)值,這種信息在稍后重新加載對象或?qū)⑵鋫魉偷狡渌麢C(jī)器上時都是沒用的。事實(shí)上,這種域的值如果不恰當(dāng),還會引起本地方法崩潰。Java擁有一種很簡單的機(jī)制來防止這種域被序列化,那就是將它們標(biāo)記成transient的。如果這些域?qū)儆诓豢尚蛄谢念?,也需要將它們?biāo)記成transient的。瞬時的域在對象被序列化時總是被跳過的。
序列化機(jī)制為單個的類提供了一種方式,去向默認(rèn)的讀寫行為添加驗(yàn)證或任何其他想要的行為??尚蛄谢念惪梢远x具有下列簽名的方法:
private void readObject(ObjectInputStream in) throws IOException,ClassNoutFoundException;
private void writeObject(ObjectOutputStream out) throws IOException;
之后數(shù)據(jù)域就再不會被自動序列化,取而代之的是調(diào)用這些方法。
除了讓序列化機(jī)制來保存和恢復(fù)對象數(shù)據(jù),類還可以定義它自己的機(jī)制。為了做到這一點(diǎn),這個類必須實(shí)現(xiàn)Externalizable接口,這需要定義兩個方法:
public void readExternal(ObjectInputStream in) throws IOException ,ClassNotFoundException;
Public void writeExternal(ObjectOutputStream out) throws IOException;
序列化單例和實(shí)例安全的枚舉
在序列化和反序列化時,如果目標(biāo)對象時唯一的,必須加倍小心。
如果使用Java語言的enum結(jié)構(gòu),就不必?fù)?dān)心序列化。但是假設(shè)在維護(hù)遺留代碼,其中包含下面這樣的枚舉類型:
public class Orientation {
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
}
為了解決這個問題,需要定義另一種稱為readResolve的特殊序列化方法。如果定義了該方法,在對象被序列化后就會調(diào)用它。它必須返回一個對象,而該對象之后會成為一個readObject的返回值。在上面的情況下,readResolve方法將檢查value域并返回恰當(dāng)?shù)拿杜e常量:
protected Object readResolve() throws ObjectStreamException{
if(value == 1) return Orientation.HORIZONTAL;
if(value == 2) return Orientation.VERTICAL;
throw new ObjectStreamException();
}
為克隆使用序列化
序列化機(jī)制是一種很有趣的用法:即提供了一種克隆對象的漸變途徑,只要對應(yīng)的類是可序列化的即可。做法很簡單:直接將對象序列化到輸出流中,然后將其讀回。這樣產(chǎn)生的新對象是對現(xiàn)有對象的一個深拷貝。在此過程中,我們不必將對象寫出到文件中,因?yàn)榭梢杂肂yteArrayOutputStream將數(shù)據(jù)保存到字節(jié)數(shù)組中。
操作文件
Path接口和Files類封裝了在用戶機(jī)器上處理文件系統(tǒng)所需的所有功能,他們是在JavaSE7中新添加進(jìn)來的,比自JDK1.0以來一直使用的File類方便很多。
Path
Path表示的是一個目錄名序列,其后還可以跟著一個文件名。路徑中的第一個部件可以是根部件,例如 / 或 C:\ ,而允許訪問的根部件取決于文件系統(tǒng)。
Path absolute = Paths.get("/home","harry");
Path relative = Paths.get("myprog","conf","user.properties");
靜態(tài)的Paths.get方法接受一個或多個字符串,并將它們用默認(rèn)的文件系統(tǒng)路徑分隔符(/,\)連接起來,然后解析連接起來的結(jié)果,如果表示的不是給定文件系統(tǒng)的合法路徑,就拋出InvalidPathException異常。這個連接起來的結(jié)果就是一個Path對象。
get方法可以獲取多個部件構(gòu)成的單個字符串。
組合或解析路徑是常見的操作,調(diào)用p.resolve(q)將按照下列規(guī)則返回一個路徑:
- 如果q是絕對路徑,結(jié)果就是q
- 否則,根據(jù)文件系統(tǒng)規(guī)則,將“p后跟q"作為結(jié)果
resolve方法有一種快捷方式,它接受一個字符串而不是路徑:
Path workPath = basePath.resolve("work");
還有一個很方便的方法:resolveSibling,它通過解析指定路徑的父路徑產(chǎn)生其兄弟路徑。例如,如果workPath是/opt/myapp/work,那么下面的調(diào)用
Path tempPath = workPath.resolveSibling("temp");
將創(chuàng)建/opt/myapp/temp.
resolve的對立面是relativize,即調(diào)用p.relativize(r)將產(chǎn)生路徑q,而對q進(jìn)行的解析的結(jié)果正是r 。例如,以”/home/cay"為目標(biāo)對"/home/fred/myprog"進(jìn)行相對化操作,會產(chǎn)生“../fred/yprog",其中假設(shè)..表示的是文件系統(tǒng)的父目錄。
normalize方法將移除所有冗余的.和..部件(或文件系統(tǒng)認(rèn)為冗余的部件)
toAbsolutePath方法將產(chǎn)生給定路徑的絕對路徑,該絕對路徑從根部件開始。
Path類有許多有用的方法用來將路徑斷開。
Path p = Paths.get("/home","fred","myproperties");
Path parent = p.getParent();
Path file = p.getFileName();
Path root = p.getRoot();
還可以從Path對象中構(gòu)建Scanner對象:
Scanner in = new Scanner(Paths.get("/home/fred/input.txt"));
需要與遺留系統(tǒng)交互時,可以使用Path的toFile方法,或者File類的toPath方法
讀寫文件
Files類可以使得普通文件操作變得快捷。例如可以使用下面方式很容易地讀取文件的所有內(nèi)容:
byte[] bytes = Files.readAllBytes(path);
如果想將文件當(dāng)作字符串讀入,可以在調(diào)用readAllBytes之后執(zhí)行:
String content = new String(bytes,charset);
如果希望文件當(dāng)作行序列讀入,可以調(diào)用:
List<String> lines = Files.readAllLines(path,charset);
相反,如果希望寫出一個字符串到文件中,可以調(diào)用:
Files.write(path,content.getBytes(charset));
向指定文件追加內(nèi)容:可以調(diào)用:
Files.write(path,content,getBytes(charset),StandardOpenOption.APPEND);
可以用下面的語句將一個行的集合寫出到文件中:
Files.write(path,lines);
這些簡便方法適用于處理中等長度的文本文件,如果要處理的文本長度較大,或者是二進(jìn)制文件,還是應(yīng)該使用所熟知的輸入輸出流或者讀入器/寫出器。
創(chuàng)建文件和目錄
創(chuàng)建新目錄可以調(diào)用:
Files.createDirectory(path);
其中路徑中除最后一個部件外,其他部分都必須是已存在的。要創(chuàng)建路徑中的中間目錄,應(yīng)該使用:
Files.createDirectories(path);
可以使用下面的語句創(chuàng)建一個空文件:
Files.createFile(path);
如果文件已存在,那么這個調(diào)用就會異常。
有些便捷方法可以用來在指定位置或者系統(tǒng)指定位置創(chuàng)建臨時文件和臨時目錄:
Path newPath = Files.createTempFile(dir,prefix,suffix);
Pah newPath = Files.createTempFile(prefix,suffix);
Pah newPath = Files.createTempDirectory(dir,prefix);
Pah newPath = Files.createTempDirectory(prefix);
其中,dir是一個Path對象,prefix和suffix是可以為null的字符串。
創(chuàng)建文件或目錄,可以制定屬性,例如文件的擁有者和權(quán)限。。
復(fù)制、移動和刪除文件
將文件從一個位置復(fù)制到另一個位置可以直接調(diào)用
Files.copy(fromPath,toPath);
移動文件可以調(diào)用:
Files.move(fromPath,toPath);
如果目標(biāo)路徑已經(jīng)存在,那么復(fù)制或移動將失敗。如果想要覆蓋已有的目標(biāo)路徑,可以使用REPLACE_EXISTING選項(xiàng)。如果想要復(fù)制所有的文件屬性,可以使用COPY_ATTRIBUTES選項(xiàng)??赏瑫r選擇這兩個選項(xiàng)。
可以使用ATOMIC_MOVE 將移動操作定義為原子性的。
還可以將一個輸入流復(fù)制到Path中,這表示你想要將該輸入流存儲到硬盤上。類似地,可以將一個Path復(fù)制到輸出流中??梢允褂孟旅娴恼{(diào)用:
Files.copy(inputStream,toPath);
Files.copy(fromPath,outputStream);
刪除文件可以調(diào)用
Files.delete(path);
如果要刪除的文件不存在,這個方法就會拋出異常。因此可轉(zhuǎn)而使用:
boolean deleted = Files.deleteIfExits(path);
獲取文件信息
下面的靜態(tài)方法將返回boolean值,表示檢查路徑的某個屬性的結(jié)果:
- exists
- isHidden
- isReadadble,isWritable,isExecuutable
- isRegularFile, isDirectory, isSymbolicLink
size方法將返回文件的字節(jié)數(shù):
long filesize = Files.size(path);
getOwner方法將文件的擁有者作為java.nio.file.attribute.UserPrincipal的一個實(shí)例返回。
所有的文件兄臺那個都會報(bào)告一個基本屬性集,他們被封裝在BasicFileAttributes接口中,這些屬性與上述信息有部分重疊。基本文件屬性包括:
- 創(chuàng)建文件、最后一次訪問、最后一次修改的時間,這些時間都表示成java.nio.file.attribute.FileTime
- 文件是常規(guī)文件、目錄還是符號鏈接,或者都不是
- 文件尺寸
- 文件主鍵,這是某種類的對象,具體所屬類與文件系統(tǒng)相關(guān),有可能是文件的唯一標(biāo)識符,也可能不是
要獲取這些屬性,可以調(diào)用:
BasicFileAttributes attributes = Files .readAttributes(path,BasicFileAttributes.class);
如果了解到用戶的文件兄臺那個兼容POSIX,你可以獲取一個PosixFileAttributes實(shí)例:
PosixFileAttributes attributes = Files .readAttributes(path,PosixFilesAttributes.class);
然后從中找到擁有者,以及文件的擁有者、組或訪問權(quán)限。
訪問目錄的項(xiàng)
靜態(tài)的Files.list方法會返回一個可以讀取目錄中各個項(xiàng)的Stream<Path>對象。目錄是被惰性讀取的,這使得處理具有大量項(xiàng)的目錄可以變得更高效。
因?yàn)樽x取目錄設(shè)涉及需要關(guān)閉的系統(tǒng)資源,所以應(yīng)使用try塊:
try( Stream<Path> entries = Files.list(pathToDirectory){...}
list 方法不會進(jìn)入子目錄。為了處理目錄中的所有子目錄,需要使用File.walk方法。
使用目錄流
Files.walk方法會產(chǎn)生一個可以遍歷目錄中所有子目錄的Stream<Path>對象。有時需要對遍歷過程進(jìn)行更加細(xì)粒度的控制。在這種情況下,應(yīng)該使用File.newDirectoryStream對象,它會產(chǎn)生一個DirectoryStream。注意它不是java.util.stream.Stream的子接口,而是專門用于目錄遍歷的接口。它是Iterable的子接口,可以在增強(qiáng)的for循環(huán)中使用目錄流。
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir)){
for (Path entry: entries){
Process entries
}}
try語句塊用來確保目錄流可以被正確關(guān)閉。訪問目錄中的 項(xiàng)并沒有具體的順序??梢杂?strong>glob模式來過濾文件:
try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir,"*.java"))
glob模式待完善。。
ZIP文件系統(tǒng)
Paths類會在默認(rèn)文件系統(tǒng)中查找路徑,即在用戶本地磁盤中的文件,最有用之一的是ZIP文件系統(tǒng)。如果zipname 是某個ZIP文件的名字,那么下面的調(diào)用
FileSystem fs = FileSystem.newFileSystem(Path.get(zipname),null);
將建立一個文件系統(tǒng),它包含ZIP文檔中的所有文件。如果知道文件名,那么從ZIP文檔中復(fù)制出這個文件就會變得很容易:
Files.copy(fs.getPath(soureceName),targetPath);
其中,fs.getPath對于任意文件系統(tǒng)來說,都與Paths.get類似
要列出ZIP文檔中的所有文件,可以遍歷文件樹:
FileSystem fs = FileSystem.newFileSystem(Path.get(zipname),null);
Files.walkFileTreee(fs.getPath("/"),new SimpleFileVisitor<Path>(){
public FileVisitResult visitFile(Path file,BasicFileAttributes attrs) throws IOException{
System.out.pintln(file);
return FileVisitResult.CONTINUE;}
});
內(nèi)存映射文件
大多數(shù)操作系統(tǒng)都可以利用虛擬內(nèi)存實(shí)現(xiàn)來將一個文件或文件的一部分“映射”到內(nèi)存中。然后,這個文件就可以當(dāng)作是內(nèi)存數(shù)組一樣訪問,這比傳統(tǒng)的文件操作要快得多。
內(nèi)存映射文件的性能
與隨機(jī)訪問相比,性能提高總是很明顯的。另一方面,對于中等尺寸文件的順序讀入則沒有必要使用內(nèi)存映射。
java.nio包使用內(nèi)存映射變得十分簡單。
首先,從文件中獲得一個通道(channel),通道是用于磁盤文件的一種抽象,它使我們可以訪問諸如內(nèi)存映射、文件加鎖機(jī)制以及文件間快速數(shù)據(jù)傳遞等操作系統(tǒng)特性。
FileChannel channel = FileChannel.open(path,optios);
然后,通過調(diào)用FileChannel類的map方法從這個通道中獲得一個ByteBuffer??梢灾付ㄏ胍成涞奈募^(qū)域與映射模式,支持的模式有三種:
- FileChannel.MapMode.READ_ONLY: 所產(chǎn)生的緩沖區(qū)是只讀的,任何對緩沖區(qū)寫入的嘗試都會導(dǎo)致ReadOnlyBufferedException異常。
- FileChannel.MapMode.READ_WRTE:所產(chǎn)生的緩沖區(qū)是可寫的,任何修改都會在某個時刻寫回到文件中。注意:其他映射同一個文件的程序可能不能立即看到這些修改,多個程序同時進(jìn)行文件映射的確切行為是依賴于操作系統(tǒng)的。
- FileChannel.MapMode.PRIVATE: 所產(chǎn)生的緩沖區(qū)是科協(xié)的,但是任何修改過這個緩沖區(qū)來說是私有的,不會傳播到文件中。
一旦有了緩沖區(qū),就可以使用ByteBuffer類和Buffer超類的方法讀寫數(shù)據(jù)了。
緩沖區(qū)支持順序和隨機(jī)訪問數(shù)據(jù)訪問,它有一個可以通過get和put操作來移動的位置。例如,可以像下面這樣順序遍歷緩沖區(qū)的所有字節(jié):
while (buffer.hasRemaining()){
byte b = buffer.get();
}
或者下下面這樣進(jìn)行隨機(jī)訪問:
for ( int i=0;i < buffer.limit(); i++){
byte b = buffer.get(i);
}
可以用下面 的方法讀寫字節(jié)數(shù)組:
get(byte[] byte)
get(byte[], int offset, int length)
最后,還有下面的方法:
getInt
getLong
getShort
getChar
getFloat
getDouble
用來讀入文件中存儲為二進(jìn)制的基本類型值。
Java對二進(jìn)制數(shù)據(jù)使用高位在前 的排序機(jī)制,但是,如果需要以地位在前的排序方式處理包含二進(jìn)制數(shù)字的文件,只需調(diào)用
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查詢緩沖區(qū)內(nèi)當(dāng)前的字節(jié)順序,可以調(diào)用:
ByteOrder b = buffer.order();
要向緩沖區(qū)寫數(shù)字,可以使用下列方法:
putInt
putLong
putShort
putChar
putFloat
putDouble
在恰當(dāng)?shù)臅r候,以及當(dāng)通道關(guān)閉時,會將這些修改寫回到文件中。
緩沖區(qū)數(shù)據(jù)結(jié)構(gòu)
本節(jié)簡要介紹Buffer對象上的基本操作。
緩沖區(qū)是由具有相同類型的數(shù)值構(gòu)成的數(shù)組,Buffer類是一個抽象類,它由眾多的具體子類,包括ByteBufer,CharBuffer,DoubleBuffer,IntBuffer,LongBuffer和ShortBuffer。
StringBuffer類與這些緩沖區(qū)沒有關(guān)系。
在實(shí)踐中,最常用的是ByteBuffer和CharBuffer,如圖,每個緩沖區(qū)都具有:
- 一個容量,它永遠(yuǎn)不能改變
- 一個讀寫位置,下一個值將在此進(jìn)行讀寫
- 一個界限,超過它進(jìn)行讀寫沒有意義
- 一個可選的標(biāo)記,用于重復(fù)一個讀入或?qū)懗霾僮?/li>

這些值滿足下面的條件:
0<=標(biāo)記<=位置<=界限<=容量
使用緩沖區(qū)的主要目的是執(zhí)行“寫,然后讀入”循環(huán)。假設(shè)我們有一個緩沖區(qū),在一開始,他的位置為0,界限等于容量。我們不斷地調(diào)用put將值添加到這個緩沖區(qū)中,當(dāng)我們耗盡所有的數(shù)據(jù)或者寫出的數(shù)據(jù)量達(dá)到容量大小時,就該切換到讀入操作了。
這時調(diào)用flip方法將界限設(shè)置到當(dāng)前位置,并把位置復(fù)位到0.現(xiàn)在在remaining方法返回正整數(shù)時,不斷地調(diào)用get。在我們緩沖區(qū)素養(yǎng)的值都讀入之后,調(diào)用clear使緩沖區(qū)為下一次寫循環(huán)做好準(zhǔn)備。clear方法將位置復(fù)位到0,并將界限復(fù)位到容量。
如果想重讀緩沖區(qū),可以使用rewined或mark/reset方法。
要獲取緩沖區(qū),可以調(diào)用諸如ByteBuffer.allocate或ByteBuffer.wrap這樣的靜態(tài)方法。
然后,可以用來自某個通道的數(shù)據(jù)填充緩沖區(qū),或者將緩沖區(qū)的內(nèi)容寫出通道中。例如:
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
這是一種非常有用的方法,可以替代隨機(jī)訪問文件。
文件加鎖機(jī)制
考慮一下多個同時執(zhí)行的程序需要修改同一個文件的情形,很明顯,這些程序需要以某種方式進(jìn)行通信,不然這個文件很容易被損壞。文件鎖可以解決問題,它可以控制對文件或文件中某個范圍的字節(jié)的訪問。
假設(shè)你的應(yīng)用程序?qū)⒂脩舻钠么鎯υ谝粋€配置文件中,當(dāng)用戶調(diào)用這個應(yīng)用的兩個實(shí)例時,這兩個實(shí)例就有可能會同時希望寫這個配置文件。這種情況下,第一個實(shí)例應(yīng)該鎖定這個文件,當(dāng)?shù)诙€實(shí)例發(fā)現(xiàn)這個文件被鎖定時,它必須決策是等待直至這個文件解鎖,還是直接跳過這個文件寫操作。
要鎖定一個問價,可以調(diào)用FileChannel類的lock和tryLock方法:
FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();
或
FileLock lock = channel.rtyLock();
第一個調(diào)用會阻塞直至可獲得鎖,而第二個調(diào)用將立即返回,要么返回鎖,要么在鎖不可獲得的情況下返回null。這個文件將保持鎖定狀態(tài),直至這個通道關(guān)閉,或者在鎖上調(diào)用了release方法。
還可以通過下面的調(diào)用鎖定文件的一部分:
FileLock lock (long start,long size,boolean shared)
或
FileLock tryLock(long start,long size,boolean shared)
如果shared標(biāo)志為false,則鎖定文件的目的是讀寫,而如果為true,則這是一個共享鎖,它允許多個進(jìn)程從文件中讀入,并阻止任何進(jìn)程獲得獨(dú)占的鎖。并非所有的操作系統(tǒng)都支持共享鎖,因此你可能會在請求共享鎖的時候得到的是獨(dú)占的鎖。
調(diào)用FileLock類的isShared方法可以查詢所持有的鎖的類型。
如果鎖定了文件的尾部,而這個文件的長度隨后增長了超過鎖定的部分,那么增長出來的額外區(qū)域是未鎖定的,要想鎖定所有字節(jié),可以使用Long.MAX_VALUE來表示尺寸。
要確保在操作完成時釋放鎖,與往常一樣,最好在一個try語句中執(zhí)行釋放鎖的操8作:
try(FileLock lock = channel.lock()){
access the locked file or segment
}
文件加鎖機(jī)制時依賴操作系統(tǒng)的
- 在某些系統(tǒng)中,文件加鎖僅僅是建議性的,如果是一個應(yīng)用未能得到鎖,它仍舊可以向被另一個應(yīng)用并發(fā)鎖定的文件執(zhí)行寫操作。
- 在某些系統(tǒng)中,不能在鎖定一個文件的同時將其映射到內(nèi)存中。
文件是由整個Java虛擬機(jī)持有的。如果有兩個程序是由同一個虛擬機(jī)啟動的,那么它們不可能每一個都獲得一個在同一個文件上的鎖。當(dāng)調(diào)用lock和tryLock方法時,如果虛擬機(jī)已經(jīng)在同一個文件上持有了另一個重疊的鎖,那么這兩個方法將拋出OverlappingFileLockException。 - 在一些系統(tǒng)中,關(guān)閉一個通道會釋放由Java虛擬機(jī)持有的底層文件上的所有鎖。因此,在同一個鎖定文件山海關(guān)應(yīng)避免使用多個通道。
- 在網(wǎng)絡(luò)文件系統(tǒng)上鎖定文件時高度依賴與系統(tǒng)的,因此應(yīng)盡量避免。
正則表達(dá)式
這部分單獨(dú)作為一個章節(jié)列出。