本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營鏈接:http://item.jd.com/12299018.html
我們在日常電腦操作中,接觸和處理最多的,除了上網,大概就是各種各樣的文件了,從本節開始,我們就來探討文件處理,本節主要介紹文件有關的一些基本概念和常識,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編程及計算機技術的本質。用心原創,保留所有版權。