計算機程式的思維邏輯 (56) – 檔案概述

swiftma發表於2019-02-18

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (56) – 檔案概述

我們在日常電腦操作中,接觸和處理最多的,除了上網,大概就是各種各樣的檔案了,從本節開始,我們就來探討檔案處理,本節主要介紹檔案有關的一些基本概念和常識,Java中處理檔案的基本思路和類結構,以及接來下章節的安排思路。

基本概念和常識

二進位制思維

為了透徹理解檔案,我們首先要有一個二進位制思維。所有檔案,不論是可執行檔案、圖片檔案、視訊檔案、Word檔案、壓縮檔案、txt檔案,都沒什麼可神祕的,它們都是以0和1的二進位制形式儲存的。我們所看到的圖片、視訊、文字,都是應用程式對這些二進位制的解析結果。

作為程式設計師,我們應該有一個編輯器,能檢視檔案的二進位制形式,比如UltraEdit,它支援以十六進位制進行檢視和編輯。比如說,一個文字檔案,看到的內容為:

hello, 123, 老馬
複製程式碼

開啟十六進位制編輯,看到的內容為:

計算機程式的思維邏輯 (56) – 檔案概述

左邊的部分就是其對應的十六進位制,”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編碼的十六進位制形式為:

計算機程式的思維邏輯 (56) – 檔案概述

都是UTF-8編碼,看到的字元內容也一樣,但二進位制內容不一樣,一個帶BOM頭,一個不帶BOM頭。

需要注意的是,帶BOM頭的UTF-8編碼檔案不是所有應用程式都支援的,比如PHP就不支援BOM,如果你的PHP原始碼檔案帶BOM頭的,PHP執行就會出錯,碰到這種問題時,前面介紹的二進位制思維就特別重要,不要只看檔案的顯示,還要看檔案背後的二進位制

另外,我們需要說明下文字檔案的換行符,在Windows系統中,換行符一般是兩個字元”
“,即ASCII碼的13(`
`)和10(`
`),在Linux系統中,換行符一般是一個字元”
“。

檔案系統

檔案一般是放在硬碟上的,一個機器上可能有多個硬碟,但各種作業系統都會隱藏物理硬碟概念,提供一個邏輯上的統一結構。在Windows中,可以有多個邏輯盤,C, D, E等,每個盤可以被格式化為一種不同的檔案系統,常見的檔案系統有FAT32和NTFS。在Linux中,只有一個邏輯的根目錄,用斜線/表示,Linux支援多種不同的檔案系統,如Ext2/Ext3/Ext4等。不同的檔案系統有不同的檔案組織方式、結構和特點,不過,一般程式設計時,語言和類庫為我們提供了統一的API,我們並不需要關心其細節。

在邏輯上,Windows中就是有多個根目錄,Linux就是有一個根目錄,每個根目錄下就是一顆子目錄和檔案構成的樹。每個檔案都有檔案路徑的概念,路徑有兩種形式,一種是絕對路徑,另一種是相對路徑

所謂絕對路徑就是從根目錄開始到當前檔案的完整路徑,在Windows中,目錄之間用反斜線分隔,如”C:codehello.java”,在Linux中,目錄之間用斜線分隔,如”/Users/laoma/Desktop/code/hello.java”。在Java中,java.io.File類定義了一個靜態變數File.separator,表示路徑分隔符,程式設計時應使用該變數而避免硬編碼。

所謂相對路徑是相對於當前目錄而言的,在命令列終端上,通過cd命令進入到的目錄就是當前目錄,在Java中,通過System.getProperty(“user.dir”)可以得到執行Java程式的當前目錄,相對路徑不以根目錄開頭,比如在Windows上,當前目錄為”D:laoma”,相對路徑為”codehello.java”,則完整路徑為”D:laomacodehello.java”。

每個檔案除了有具體內容,還有後設資料資訊,如檔名、建立時間、修改時間、檔案大小等。檔案還有一個是否隱藏的性質,在Linux系統中,如果檔名以.開頭,則為隱藏檔案,在Windows系統中,隱藏是檔案的一個屬性,可以進行設定。

大部分檔案系統,每個檔案和目錄還有訪問許可權的概念,對所有者、使用者組可以有不同的許可權,許可權具體包括讀、寫、執行。

檔名有大小寫是否敏感的概念,在Windows系統中,一般是大小寫不敏感的,而Linux則一般是大小寫敏感的,也就是說,同一個目錄下,”abc.txt”和”ABC.txt”在Windows中被視為同一個檔案,而Linux視為不同的檔案。

作業系統中有一個臨時檔案的概念,臨時檔案位於一個特定目錄,比如Windows 7,一般位於”C:Users使用者名稱AppDataLocalTemp”,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程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (56) – 檔案概述

相關文章