Java編程拾遺『文件和IO概述』

從本文開始,講述Java中文件和IO的相關知識。在做企業web開發時,文件操作相對涉及的比較少,在桌面系統開發中比較常見。正因如此,導致很多人對文本文件、二進制文件、字節流、字符流等概念性的東西都不能完全搞清楚。其實在我在寫這篇文章之前,也是分不清的,但是因爲日常工作中也很少涉及相關的內容,所以一直也沒深究。本篇文章就來科普一下一些基本概念,同時介紹一下Java IO體系。

1. 基本概念

1.1 文件概述

提到文件,我們可以自然聯想到常見的可執行文件、圖片文件、視頻文件、Word文件、txt文件、pdf文件等,這些文件在計算機中都是以二進制存儲的,平時我們能看到文件的內容,是因爲計算機使用了相應的應用程序對二進制文件進行了解析。

舉個例子,我們保存存在一個txt文件,保存的內容是”hello 你好”,在notepad++中使用HEX-Editor插件可以看到在計算機磁盤中保存的形式如下圖所示:

文件編碼使用的是UTF-8,一個英文字母佔一個字節,一箇中文佔三個字節,68就是字符h的UTF-8編碼(16進制),e4bda0三個字節就是中文字符”你”的UTF-8編碼,e5a5bd就是中文字符”好”的UTF-8編碼。關於編碼規則,我在之前的一篇文章Java編程拾遺『搞定編碼』講的很清楚,建議去了解一下,對於理解本文有幫助。另外給一個工具網站,可以用來實踐字符編碼在線編碼轉換工具

上面展示了文本文件在磁盤中的二進制存儲,文件中每個二進制字節都是某個可打印字符的一部分,都可以用最基本的文本編輯器進行查看和編輯,如Windows上的notepad, Linux上的vi;但是如果不是文本文件,文件中每個二進制字節就不一定是可打印字符的一部分了,一個字節有可能表示顏色、可能表示字體、可能表示聲音大小等,這就是爲什麼非文本文件使用文本編輯器打開會看到一堆亂碼的原因,因爲文本編輯器是按照特定文本編碼方式解析二進制內容的(比如用記事本打開word文件)。

文件一般可以分爲兩種類型,一種是文本文件,另一種是二進制文件。比如txt文件、.java文件、html文件等都屬於文本文件;而zip文件、pdf文件、 mp3文件、 excel文件、word文件等都是二進制文件 。二進制文件必須使用特定的應用程序解析才能正確地看到其內容,而文本文件比較隨意,任何文本編輯器都可以查看。

至此,我們知道了文件可以分爲文本文件和二進制文件,上面也舉了一些文件後綴和文件類型的關係,比如.txt文件是文本文件、.pdf文件是二進制文件。但是這種方式其實是錯誤的,給文件加正確的後綴名是一種慣例,但並不是強制的,比如我是用notepad編輯了一個文本文件,在保存時後綴名保存爲.doc,但是當使用word軟件打開時,是會報錯的。

一個文件是文本文件還是二進制文件,跟文件的後綴名沒有直接關係。當我們講一個文件是文本文件時,說明這個文件在磁盤中二進制存儲的每個字節都是可打印字符的一部分,所有字節在文本編輯器中都是可以正常查看的。可以做個試驗,在linux下,新建一個test.doc文件,並在其中存儲文本內容,然後將文件拿到windows系統下通過記事本打開,可以發現內容是可以正常顯示的(之所以在linux下新建doc文件,是因爲在windows下,當記事本保存爲doc文件時,會添加一些附加信息)。如果一個文件在磁盤中的二進制存儲的二進制字節存在不可打印字符,那麼文件不是文本文件,比如Java序列化得到的文件,即使文件後綴名爲.txt,該txt文件也不是文本文件。本質上講文本文件也是二進制文件,只不過它比較特殊的是,二進制的每個字節都是可打印字符的一部分。

可以看到,即使文件後綴爲doc,但是存儲內容仍然只是文本”卓立123″的UTF-8編碼,並沒有一些word文件的控制信息。最後要提一下的是,最後一個字節0a是linux下的換行符\n,在windows下換行符爲\r\n,關於linux和windows下換行符的關聯,請參考這篇文章徹底解讀剪不斷理還亂的\r\n和\n, 以Windows和Linux爲例

1.2 文件系統

各種操作系統都會隱藏物理硬盤概念,提供一個邏輯上的統一結構。在windows中,可以有多個邏輯盤,比如C、D、E等,每個盤可以被格式化爲一種不同的文件系統,常見的文件系統有FAT32和NTFS。在linux中,只有一個邏輯的根目錄,用斜線/表示,linux支持多種不同的文件系統,如Ext2/Ext3/Ext4等。不同的文件系統有不同的文件組織方式,不過一般編程時,編程語言會提供了統一的API,屏蔽不同文件系統的底層細節。

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

絕對路徑就是從根目錄開始到當前文件的完整路徑,在windows中,目錄之間用反斜線分隔,如”C:\code\hello.java”,在linux中,目錄之間用正斜線分隔,如”/home/zhuoli/code/hello.java”。在Java中,java.io.File類定義了一個靜態變量File.separator,表示路徑分隔符,編程時應使用該變量而避免硬編碼。

相對路徑是相對於當前目錄而言的,在命令行終端上,通過cd命令進入到的目錄就是當前目錄,在Java中,通過System.getProperty(“user.dir”)可以得到運行Java程序的當前目錄,相對路徑不以根目錄開頭,比如在windows上,當前目錄爲”D:\zhuoli”,相對路徑爲”code\hello.java”,則完整路徑爲”D:\zhuoli\code\hello.java”。

每個文件除了有具體內容,還有元數據信息,如文件名、創建時間、修改時間、文件大小等。文件還有一個是否隱藏的性質,在linux系統中,如果文件名以.開頭,則爲隱藏文件,在windows系統中,隱藏是文件的一個屬性,可以進行設置。大部分文件系統,每個文件和目錄還有訪問權限的概念,對所有者、用戶組可以有不同的權限,權限具體包括讀、寫、執行。

在windows中,一般是大小寫不敏感的,而linux則一般是大小寫敏感的,同一個目錄下,”abc.txt”和”ABC.txt”在windows中被視爲同一個文件,而linux視爲不同的文件。

1.3 文件讀寫

文件是放在硬盤上的,程序處理文件需要將文件讀入內存,修改完成後,再寫回硬盤。操作系統提供了對文件讀寫的基本API,不同操作系統的接口和實現是不一樣的,Java封裝了不同操作系統文件讀寫的API,提供了統一的API。

爲了提升文件操作的效率,操作系統經常使用一種常見的策略,即使用緩衝區。讀文件時,即使目前只需要少量內容,但還會接着讀取,就一次讀取比較多的內容,放到讀緩衝區,下次讀取時,緩衝區有,就直接從緩衝區讀,減少訪問操作系統和硬盤。寫文件時,先寫到寫緩衝區,寫緩衝區滿了之後,再一次性的調用操作系統寫到硬盤。其實就是儘可能減少磁盤訪問次數,因爲每一次磁盤操作就需要一次磁盤IO,設計磁盤機械尋道等操作,是比較耗時的。

操作系統操作文件一般有打開和關閉的概念,打開文件會在操作系統內核建立一個有關該文件的內存結構,這個結構一般通過一個整數索引來引用,這個索引一般稱爲文件描述符,這個結構是消耗內存的,操作系統能同時打開的文件一般也是有限的,在不用文件的時候,應該記住關閉文件,關閉文件一般會同步緩衝區內容到硬盤,並釋放佔據的內存結構。

操作系統一般還支持一種稱之爲內存映射文件的高效的隨機讀寫大文件的方法,將文件直接映射到內存,操作內存就是操作文件,在內存映射文件中,只有訪問到的數據纔會被實際拷貝到內存,且數據只會拷貝一次,被操作系統以及多個應用程序共享。

2. Java文件概述

在Java中,文件不是單獨處理的,而是視爲輸入輸出(IO – Input/Output)設備的一種。Java使用流處理所有的IO,包括鍵盤、顯示終端、網絡等。Java中的流分爲輸入流和輸出流,輸入流就是可以從中獲取數據,輸入流的實際提供者可以是鍵盤、文件、網絡等,輸出流就是可以向其中寫入數據,輸出流的實際目的地可以是顯示終端、文件、網絡等。我本人都是這麼理解的,輸入與輸出都是針對內存而言的,輸入流的作用就是向內存中輸入數據,輸出流的作用是將內存中的數據輸出到外部,輸入流和輸出流就是上述動作的一個連接器。

Java IO的基本類大多位於包java.io中,類InputStream表示輸入流,OutputStream表示輸出流。有了流的概念,就有了很多面向流的代碼,比如對流做加密、壓縮、計算信息摘要、計算檢驗和等,這些代碼接受的參數和返回結果都是抽象的流。一些實際上不是IO的數據源和目的地也轉換爲了流,以方便參與這種協作,比如字節數組,也包裝爲了流ByteArrayInputStream和ByteArrayOutputStream。Java中的流的概念主要存在以下幾種:

  • InputStream/OutputStream: 基類,抽象類。
  • ByteArrayInputStream/ByteArrayOutputStream: 輸入源和輸出目標是字節數組的流。
  • FileInputStream/FileOutputStream: 輸入源和輸出目標是文件的流。
  • FilterInputStream/FilterOutputStream,所有包裝流的父類,一些“特殊”功能的流,比如DataInputStream/DataOutputStream、BufferedInputStream/BufferedOutputStream都繼承了改類。
  • ObjectInputStream/ObjectOuputStream:輸入源和輸出目標是對象的流,用於實現Java序列化。

上面介紹了Java中用於處理IO的流的概念,可以以字節爲單位處理文件,但是對於最基礎也是最常見的文本文件通過字節爲單位處理很明顯是不方便的,所以Java中另外提供了一套方便處理文本文件的Reader和Writer,可以以字符(Java中的char)爲單位處理文件,Reader和Writer是抽象類,它們有很多子類:

  • Reader/Writer:基類,抽象類。
  • BufferedReader/BufferedWriter:裝飾類,用於緩衝基本Reader/writer。
  • CharArrayReader/CharArrayWriter:輸入源和輸出目標是char數組的Reader/writer。
  • InputStreamReader/OutputStreamWriter:適配器類,輸入是InputStream,輸出是OutputStream,將字節流轉換爲字符流,最常用的處理文件的FileReader/FileWriter就是繼承自該類。
  • StringReader/StringWriter:輸入源和輸出目標是字符串的Reader/writer。

Java IO體系如下圖所示:

參考鏈接

1. 《Java編程的邏輯》

2. 文本文件和二進制文件的區別?

3. Java 中字節流與字符流的區別?

發佈了117 篇原創文章 · 獲贊 33 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章