從本節開始,我們探討文件,本節主要介紹文件的一些基本概念和常識,Java中處理文件的基本思路和類,以及接下來的章節安排思路。 ...
我們在日常電腦操作中,接觸和處理最多的,除了上網,大概就是各種各樣的文件了,從本節開始,我們就來探討文件處理,本節主要介紹文件有關的一些基本概念和常識,Java中處理文件的基本思路和類結構,以及接來下章節的安排思路。
基本概念和常識
二進位思維
為了透徹理解文件,我們首先要有一個二進位思維。所有文件,不論是可執行文件、圖片文件、視頻文件、Word文件、壓縮文件、txt文件,都沒什麼可神秘的,它們都是以0和1的二進位形式保存的。我們所看到的圖片、視頻、文本,都是應用程式對這些二進位的解析結果。
作為程式員,我們應該有一個編輯器,能查看文件的二進位形式,比如UltraEdit,它支持以十六進位進行查看和編輯。比如說,一個文本文件,看到的內容為:
hello, 123, 老馬
打開十六進位編輯,看到的內容為:
左邊的部分就是其對應的十六進位,"hello"對應的十六進位是"68 65 6C 6C 6F",對應ASCII碼編號"104 101 108 108 111","馬"對應的十六進位是"E9 A9 AC",這是"馬"的UTF-8編碼。
文件類型
正如我們在第一節講到的,所有數據都是以二進位形式保存的,但為了方便處理數據,高級語言引入了數據類型的概念,文件處理也類似,所有文件都是以二進位形式保存的,但為了便於理解和處理文件,文件也有文件類型的概念。
文件類型通常以尾碼名的形式體現,比如,PDF文件類型的尾碼是.pdf,圖片文件的一種常見尾碼是.jpg,壓縮文件的一種常見尾碼是.zip。每種文件類型都有一定的格式,代表著文件含義和二進位之間的映射關係。比如一個Word文件,其中有文本、圖片、表格,文本可能有顏色、字體、字型大小等,doc文件類型就定義了這些內容和二進位表示之間的映射關係。有的文件類型的格式是公開的,有的可能是私有的,我們也可以定義自己私有的文件格式。
對於一種文件類型,往往有一種或多種應用程式可以解讀它,進行查看和編輯,一個應用程式往往可以解讀一種或多種文件類型。
在操作系統中,一種尾碼名往往關聯一個應用程式,比如.doc尾碼關聯Word應用。用戶通過雙擊試圖打開某尾碼名的文件時,操作系統查找關聯的應用程式,啟動該程式,傳遞該文件路徑給它,程式再打開該文件。
需要說明的是,給文件加正確的尾碼名是一種慣例,但並不是強制的,如果尾碼名和文件類型不匹配,應用程式試圖打開該文件時可能會報錯。另外,一個文件可以選擇使用多種應用程式進行解讀,在操作系統中,一般通過右鍵單擊文件,選擇打開方式即可。
文件類型可以粗略分為兩類,一類是文本文件,另一類是二進位文件。文本文件的例子有普通的.txt文件, 程式源代碼文件.java, HTML文件.html等,二進位文件的例子有壓縮文件.zip, pdf文件, mp3文件, excel文件等。
基本上,文本文件里的每個二進位位元組都是某個可列印字元的一部分,都可以用最基本的文本編輯器進行查看和編輯,如Windows上的notepad, Linux上的vi。
二進位文件中,每個位元組就不一定表示字元,可能表示顏色、可能表示字體、可能表示聲音大小等,如果用基本的文本編輯器打開,一般都是滿屏的亂碼,需要專門的應用程式進行查看和編輯。
文本文件的編碼
對於文本文件,我們還必須註意文件的編碼方式。文本文件中包含的基本都是可列印字元,但字元到二進位的映射,即編碼,卻有多種方式,如GB18030, UTF-8,我們在如何從亂碼中恢復一節詳細介紹過各種編碼,這裡就不贅述了。
對於一個給定的文本文件,它採用的是什麼編碼方式呢?一般而言,我們是不知道的。那應用程式用什麼編碼方式進行解讀呢?一般使用某種預設的編碼方式,可能是應用程式預設的,也可能是操作系統預設的,當然也可能採用一些比較智能的演算法自動推斷編碼方式。
對於UTF-8編碼的文件,我們需要特別說明一下,有一種方式,可以標記該文件是UTF-8編碼的,那就是在文件最開頭,加入三個特殊位元組 (0xEF 0xBB 0xBF),這三個特殊位元組被稱為BOM頭,BOM是Byte Order Mark (即位元組序標記) 的縮寫。比如,對前面的hello.txt文件,帶BOM頭的UTF-8編碼的十六進位形式為:
都是UTF-8編碼,看到的字元內容也一樣,但二進位內容不一樣,一個帶BOM頭,一個不帶BOM頭。
需要註意的是,帶BOM頭的UTF-8編碼文件不是所有應用程式都支持的,比如PHP就不支持BOM,如果你的PHP源代碼文件帶BOM頭的,PHP運行就會出錯,碰到這種問題時,前面介紹的二進位思維就特別重要,不要只看文件的顯示,還要看文件背後的二進位。
另外,我們需要說明下文本文件的換行符,在Windows系統中,換行符一般是兩個字元"\r\n",即ASCII碼的13('\r')和10('\n'),在Linux系統中,換行符一般是一個字元"\n"。
文件系統
文件一般是放在硬碟上的,一個機器上可能有多個硬碟,但各種操作系統都會隱藏物理硬碟概念,提供一個邏輯上的統一結構。在Windows中,可以有多個邏輯盤,C, D, E等,每個盤可以被格式化為一種不同的文件系統,常見的文件系統有FAT32和NTFS。在Linux中,只有一個邏輯的根目錄,用斜線/表示,Linux支持多種不同的文件系統,如Ext2/Ext3/Ext4等。不同的文件系統有不同的文件組織方式、結構和特點,不過,一般編程時,語言和類庫為我們提供了統一的API,我們並不需要關心其細節。
在邏輯上,Windows中就是有多個根目錄,Linux就是有一個根目錄,每個根目錄下就是一顆子目錄和文件構成的樹。每個文件都有文件路徑的概念,路徑有兩種形式,一種是絕對路徑,另一種是相對路徑。
所謂絕對路徑就是從根目錄開始到當前文件的完整路徑,在Windows中,目錄之間用反斜線分隔,如"C:\code\hello.java",在Linux中,目錄之間用斜線分隔,如"/Users/laoma/Desktop/code/hello.java"。在Java中,java.io.File類定義了一個靜態變數File.separator,表示路徑分隔符,編程時應使用該變數而避免硬編碼。
所謂相對路徑是相對於當前目錄而言的,在命令行終端上,通過cd命令進入到的目錄就是當前目錄,在Java中,通過System.getProperty("user.dir")可以得到運行Java程式的當前目錄,相對路徑不以根目錄開頭,比如在Windows上,當前目錄為"D:\laoma",相對路徑為"code\hello.java",則完整路徑為"D:\laoma\code\hello.java"。
每個文件除了有具體內容,還有元數據信息,如文件名、創建時間、修改時間、文件大小等。文件還有一個是否隱藏的性質,在Linux系統中,如果文件名以.開頭,則為隱藏文件,在Windows系統中,隱藏是文件的一個屬性,可以進行設置。
大部分文件系統,每個文件和目錄還有訪問許可權的概念,對所有者、用戶組可以有不同的許可權,許可權具體包括讀、寫、執行。
文件名有大小寫是否敏感的概念,在Windows系統中,一般是大小寫不敏感的,而Linux則一般是大小寫敏感的,也就是說,同一個目錄下,"abc.txt"和"ABC.txt"在Windows中被視為同一個文件,而Linux視為不同的文件。
操作系統中有一個臨時文件的概念,臨時文件位於一個特定目錄,比如Windows 7,一般位於"C:\Users\用戶名\AppData\Local\Temp",Linux系統,位於"/tmp",操作系統會有一定的策略自動清理不用的臨時文件。臨時文件一般不是用戶手工創建的,而是應用程式產生的,用於臨時目的。
文件讀寫
文件是放在硬碟上的,程式處理文件需要將文件讀入記憶體,修改後,需要寫回硬碟。操作系統提供了對文件讀寫的基本API,不同操作系統的介面和實現是不一樣的,不過,有一些共同的概念,Java封裝了操作系統的功能,提供了統一的API。
一個基本常識是,硬碟的訪問延時,相比記憶體,是很慢的,操作系統和硬碟一般是按塊批量傳輸,而不是按位元組,以攤銷延時開銷,塊大小一般至少為512位元組,即使應用程式只需要文件的一個位元組,操作系統也會至少將一個塊讀進來。一般而言,應儘量減少接觸硬碟,接觸一次,就一次多做一些事情,對於網路請求,和其他輸入輸出設備,原則都是類似的。
另一個基本常識是,一般讀寫文件需要兩次數據拷貝,比如讀文件,需要先從硬碟拷貝到操作系統內核,再從內核拷貝到應用程式分配的記憶體中,操作系統運行所在的環境和應用程式是不一樣的,操作系統所在的環境是內核態,應用程式是用戶態,應用程式調用操作系統的功能,需要兩次環境的切換,先從用戶態切到內核態,再從內核態切到用戶態,問題是,這種用戶態/內核態的切換是有開銷的,應儘量減少這種切換。
為了提升文件操作的效率,應用程式經常使用一種常見的策略,即使用緩衝區。讀文件時,即使目前只需要少量內容,但預知還會接著讀取,就一次讀取比較多的內容,放到讀緩衝區,下次讀取時,緩衝區有,就直接從緩衝區讀,減少訪問操作系統和硬碟。寫文件時,先寫到寫緩衝區,寫緩衝區滿了之後,再一次性的調用操作系統寫到硬碟。不過,需要註意的是,在寫結束的時候,要記住將緩衝區的剩餘內容同步到硬碟。操作系統自身也會使用緩衝區,不過,應用程式更瞭解讀寫模式,恰當使用往往可以有更高的效率。
操作系統操作文件一般有打開和關閉的概念,打開文件會在操作系統內核建立一個有關該文件的記憶體結構,這個結構一般通過一個整數索引來引用,這個索引一般稱為文件描述符,這個結構是消耗記憶體的,操作系統能同時打開的文件一般也是有限的,在不用文件的時候,應該記住關閉文件,關閉文件一般會同步緩衝區內容到硬碟,並釋放占據的記憶體結構。
操作系統一般支持一種稱之為記憶體映射文件的高效的隨機讀寫大文件的方法,將文件直接映射到記憶體,操作記憶體就是操作文件,在記憶體映射文件中,只有訪問到的數據才會被實際拷貝到記憶體,且數據只會拷貝一次,被操作系統以及多個應用程式共用。後面章節會進一步介紹。
Java文件概述
流
在Java中(很多其他語言也類似),文件一般不是單獨處理的,而是視為輸入輸出(IO - Input/Output)設備的一種。Java使用基本統一的概念處理所有的IO,包括鍵盤、顯示終端、網路等。
這個統一的概念是流,流有輸入流和輸出流,輸入流就是可以從中獲取數據,輸入流的實際提供者可以是鍵盤、文件、網路等,輸出流就是可以向其中寫入數據,輸出流的實際目的地可以是顯示終端、文件、網路等。
Java IO的基本類大多位於包java.io中,類InputStream表示輸入流,OutputStream表示輸出流,而FileInputStream表示文件輸入流,FileOutputStream表示文件輸出流。
有了流的概念,就有了很多面向流的代碼,比如對流做加密、壓縮、計算信息摘要、計算檢驗和等,這些代碼接受的參數和返回結果都是抽象的流,它們構成了一個協作體系,這類似於之前介紹的介面概念、面向介面的編程、以及容器類協作體系。一些實際上不是IO的數據源和目的地也轉換為了流,以方便參與這種協作,比如位元組數組,也包裝為了流ByteArrayInputStream和ByteArrayOutputStream。
裝飾器設計模式
基本的流按位元組讀寫,沒有緩衝區,這不方便使用,Java解決這個問題的方法是使用裝飾器設計模式,引入了很多裝飾類,對基本的流增加功能,以方便使用,一般一個類只關註一個方面,實際使用時,經常會需要多個裝飾類。
Java中有很多裝飾類,有兩個基類,過濾器輸入流FilterInputStream和過濾器輸出流FilterOutputStream,所謂過濾,就類似於自來水管道,流入的是水,流出的也是水,功能不變,或者只是增加功能,它有很多子類,這裡列舉一些:
- 對流起緩衝裝飾的子類是BufferedInputStream和BufferedOutputStream。
- 可以按八種基本類型和字元串對流進行讀寫的子類是DataInputStream和DataOutputStream。
- 可以對流進行壓縮和解壓縮的子類有GZIPInputStream, ZipInputStream, GZIPOutputStream, ZipOutputStream。
- 可以將基本類型、對象輸出為其字元串表示的子類有PrintStream。
眾多的裝飾類,使得整個類結構變的比較複雜,完成基本的操作也需要比較多的代碼,但優點是非常靈活,在解決某些問題時也很優雅。
Reader/Writer
以InputStream/OutputStream為基類的流基本都是以二進位形式處理數據的,不能夠方便的處理文本文件,沒有編碼的概念,能夠方便的按字元處理文本數據的基類是Reader和Writer,它也有很多子類:
- 讀寫文件的子類是FileReader和FileWriter。
- 起緩衝裝飾的子類是BufferedReader和BufferedWriter。
- 將字元數組包裝為Reader/Writer的子類是CharArrayReader和CharArrayWriter。
- 將字元串包裝為Reader/Writer的子類是StringReader和StringWriter。
- 將InputStream/OutputStream轉換為Reader/Writer的子類是InputStreamReader OutputStreamWriter。
- 將基本類型、對象輸出為其字元串表示的子類PrintWriter。
隨機讀寫文件
大部分情況下,使用流或Reader/Writer讀寫文件內容,但Java提供了一個獨立的可以隨機讀寫文件的類RandomAccessFile,適用於大小已知的記錄組成的文件,我們日常應用開發中用的會比較少,但在一些系統程式中用到的會比較多。
File
上面介紹的都是操作數據本身,而關於文件路徑、文件元數據、文件目錄、臨時文件、訪問許可權管理等,Java使用File這個類來表示。
Java NIO
以上介紹的類基本都位於包java.io下,Java還有一個關於IO操作的包java.nio,nio表示New IO,這個包下同樣包括大量的類。
NIO代表一種不同的看待IO的方式,它有緩衝區和通道的概念,利用緩衝區和通道往往可以達成和流類似的目的,不過,它們更接近操作系統的概念,某些操作的性能也更高。比如,拷貝文件到網路,通道可以利用操作系統和硬體提供的DMA機制(Direct Memory Access,直接記憶體存取) ,不用CPU和應用程式參與,直接將數據從硬碟拷貝到網卡。
除了看待方式不同,NIO還支持一些比較底層的功能,如記憶體映射文件、文件加鎖、自定義文件系統、非阻塞式IO、非同步IO等。
不過,這些功能要麼是比較底層,普通應用程式用到的比較少,要麼主要適用於網路IO操作,我們大多不會介紹,只會介紹記憶體映射文件。
序列化和反序列化
簡單來說,序列化就是將記憶體中的Java對象持久保存到一個流中,反序列化就是從流中恢復Java對象到記憶體。序列化/反序列化主要有兩個用處,一個是對象狀態持久化,另一個是網路遠程調用,用於傳遞和返回對象。
Java主要通過介面Serializable和類ObjectInputStream/ObjectOutputStream提供對序列化的支持,基本的使用是比較簡單的,但也有一些複雜的地方。
不過,Java的預設序列化有一些缺點,比如,序列化後的形式比較大、浪費空間,序列化/反序列化的性能也比較低,更重要的問題是,它是Java特有的技術,不能與其他語言交互。
XML是前幾年最為流行的描述結構性數據的語言和格式,Java對象也可以序列化為XML格式,XML容易閱讀和編輯,且可以方便的與其他語言進行交互。
XML強調格式化但比較"笨重",JSON是近幾年來逐漸流行的輕量級的數據交換格式,在很多場合替代了XML,也非常容易閱讀和編輯,Java對象也可以序列化為JSON格式,且與其他語言進行交互。
XML和JSON都是文本格式,人容易閱讀,但占用的空間相對大一些,在只用於網路遠程調用的情況下,有很多流行的、跨語言的、精簡且高效的對象序列化機制,如ProtoBuf, Thrift, MessagePack等。MessagePack是二進位形式的JSON,更小更快。
章節安排
文件看起來是一件非常簡單的事情,但實際卻沒有那麼簡單,Java的設計也不是太完美,包含了大量的類,這使得對於文件的理解變得困難。
為便於理解,我們將採用以下思路在接下來的章節中進行探討。
首先,我們介紹如何處理二進位文件,或者將所有文件看做二進位,介紹如何操作,對於常見操作,我們會封裝,提供一些簡單易用的方法。
下一步,我們介紹如何處理文本文件,我們會考慮編碼、按行處理等,同樣,對於常見操作,我們會封裝,提供簡單易用的方法。
接下來,我們介紹文件本身和目錄操作File類,我們也會封裝常見操作。
我們也會介紹比較底層的對文件的操作RandomAccessFile類,以及記憶體映射文件,我們會介紹它們的使用及應用。
實際處理文件時,經常針對的是具體的文件類型,我們會介紹一些常見類型的處理,比如CSV文件、Excel文件,圖片、HTML文件、壓縮文件等。
最後,對於序列化,除了介紹Java的預設序列化機制,我們還會介紹XML, JSON以及MessagePack。
小結
本節介紹了關於文件的一些基本概念和常識,Java中處理文件的基本思路和類結構,最後我們總結了接下來的章節安排思路。
文件看上去應該很簡單,但實際卻包含很多內容,讓我們耐住性子,下一節,先從二進位開始吧。
----------------
未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有版權。