1. 程式人生 > >沉澱再出發:java的檔案讀寫

沉澱再出發:java的檔案讀寫

沉澱再出發:java的檔案讀寫

一、前言

  對於java的檔案讀寫是我們必須使用的一項基本技能,因此瞭解其中的原理,位元組流和字元流的本質有著重要的意義。

二、java中的I/O操作

 2.1、檔案讀寫的本質

 

 概念框架:

1     方式: 位元組流 Byte 和 字元流 Char
2     方向: 輸入 Input 和 輸出 Output ; 讀 Reader 和 寫 Writer
3     源: 字串 String, 陣列 Array, 物件 Object, 檔案 File, 通道 Channel, 管道 Pipe, 過濾器 Filter,控制檯 Console, 網路 Network Link ; 
一切可產生/接收資料的 Data
4 效能: 帶緩衝和不帶緩衝的 5 行為: Readable, Appendable, Closable, Flushable, Serializable/Externalizable

 概念組合:

    方式、方向、源、行為四個維度可以組合出各種特性的IO流。
JavaIO框架採用裝飾器模式,可以在現有IO流的基礎上新增新的IO特性,生成更強功能的IO流,體現了讀寫能力的可擴充套件性。 比如要構造一個帶緩衝的文字檔案讀取流:
File
-> FileInputStream -> InputStreamReader -> FileReader -> BufferedFileReader,
其中文字:字元流, 檔案:源, 讀:輸入方向, 帶緩衝的:效能。 讀寫耗時的字元輸入輸出流可以使用緩衝來提升效能,比如檔案讀寫、網路流讀寫等。
這是由於每個待讀寫的字元都要經過讀寫位元組、位元組
/字元轉換兩個操作。
緩衝實際上是將大量單個的讀寫操作轉換為一個大的單個的批量讀寫操作。

 位元組流:

1     位元組輸入流:InputStream, BufferedInputStream, FileInputStream, ByteArrayInputStream, PipedInputStream, FilterInputStream, DataInputStream, ObjectInputStream;
2     位元組輸出流: OuputStream, BufferedOuputStream, FileOuputStream, ByteArrayOuputStream, PipedOuputStream, FilterOuputStream, DataOuputStream, ObjectOuputStream, PrintStream;

 字元流:

1     字元輸入流: Reader, BufferedReader, FileReader, CharArrayReader, PipedReader, FilterReader, StringReader, LineNumberReader;
2     字元輸出流: Writer, BufferedWriter, FileWriter, CharArrayWriter, PipedWriter, FilterWriter, StringWriter, PrintWriter;

 位元組流與字元流之間的轉化:

1     InputStreamReader:輸入位元組流轉化為輸入字元流
2     OutStreamWriter: 輸出字元流轉化為輸出位元組流

 裝飾器模式:

    多個類實現同一個介面,並且這些實現類均持有一個該介面的引用,通過構造器傳入,從而可以在這些實現類的基礎上任意動態地組合疊加,構造出所需特性的實現來。裝飾器模式可以實現數學公式/邏輯公式運算函式。

  實現:

    在IO流的實現類中,通常有一個 Char[],Byte[], CharBuffer, StringBuffer, ByteBuffer的成員陣列或成員物件。將成員陣列/物件裡的資料寫到方法裡給定的引數陣列或返回值,即為讀實現;將方法裡給定的引數陣列寫到成員陣列或成員物件裡,即為寫實現。讀寫資料本質上就是資料在不同源之間的拷貝實現。
    IO流通常是會實現讀寫單個位元組/字元、讀寫位元組陣列/字元陣列、跳過若干位元組/字元的功能。成員 (next,pos,mark,count) 用於標識資料讀寫位置;mark 與 reset 方法組合可實現重複讀寫。

1     要實現一個自定義的 Reader, 可參考 StringReader 實現;StringReader 可以將字串作為字元流來讀取,內部成員為String;
而 StringWriter 內部成員為執行緒安全的 StringBuffer。兩者均要考慮併發讀寫的問題,使用一個 Object 鎖進行互斥訪問。
2 FileReader 是文字檔案讀取流,通過繼承 InputStreamReader, 轉化 FileInputStream 而得。
因檔案是對磁碟位元組資料的抽象,因此要獲得人類可讀的文字,必然存在從位元組流到字元流的轉換。
3 InputStreamReader 使用 StreamDecoder 將位元組流轉換為指定字元編碼的字元流,後續讀操作直接委託 StreamDecoder 讀取;
StreamDecoder 是一個位元組解碼器, 是 Reader 實現類。
OutputStreamWriter 使用 StreamEncoder 將字元流轉換為指定字元編碼的位元組流,
後續寫操作直接委託 StreamEncoder 寫入底層 OutputStream;StreamEncoder 是一個 Writer 實現類。
4 FilterReader, FilterWriter, FilterInputStream, FilterOutputStream: IO流的抽象類,分別持有一個[Reader,Writer,InputStream, OutputStream],
自定義 IO 流可以通過繼承 FilterXXX 來實現。CommonIO庫裡 ProxyReader, ProxyWriter 分別提供了 Reader, Writer 的一個示例實現,
java.io.DataInputStream 提供了 FilterInputStream 的一個示例實現,java.io.PrintStream 提供了 FilterOutputStream 的一個示例實現。
5 BufferedReader: 提供了緩衝讀及讀行的功能。
BufferedReader 主要成員為 [Reader in, char[defaultsize=8192] cb, int nextChar, int nChars, int markedChar];
nextChar 指示將要讀取的字元位置, nChars 指示可以讀取字元的終止位置,
markedChar 表示被標記的將要讀取字元的位置;
BufferedReader 從字元流 in 中讀取字元預存入緩衝字元陣列 cb 中,然後在需要的時候從 cb 中讀取字元。
BufferedReader 的實現由 fill 和 read, readLine 共同完成。
當緩衝字元陣列的字元數不足於填充要讀取的引數陣列時,就會使用 fill 方法從輸入流 in 中讀取資料再進行填充;
6 PipedReader: 管道輸入流,必須從 PipedWriter 中讀取。
底層實現有兩個執行緒、兩個位置索引以及一個字元陣列。
兩個執行緒分別是讀執行緒 readSide 和 寫執行緒 writeSide, 兩個位置索引分別用於指示字元陣列中將要寫的位置和將要讀的位置。
字元陣列是一個迴圈佇列,預設大小為 1024 chars 。初始化時必須將 PipedReader 的輸入連線至 PipedWriter 。
讀實現方法中,即是將底層字元陣列拷貝到方法中的引數陣列。PipedWriter 的實現相對簡單,其輸出連線至 PipedReader;
PipedWriter 的寫就是 PipedReader 的讀,因此 PipedWriter 的方法實現委託給 PipedReader 引用的方法。

 檔案:

1     檔案相關的類: File, RandomAccessFile, FileDescriptor, FileChannel, FilePermission, FilePermissionCollection, FileFilter, FileSystem, UnixFileSystem ;
2     檔案 File: 唯一性路徑標識【目錄路徑/檔名[.副檔名]】、檔案元資訊【描述符、型別、大小、建立時間、最近訪問時間、節點資訊】、檔案訪問許可權【讀/寫/執行組合】等; 
File 的操作會先使用 SecurityManager 檢查訪問許可權,然後通過 FileSystem 判斷和操作。
3 構造可以實際讀寫的檔案輸入輸出流需要使用 FileInputStream, FileOutputStream, FileReader, FileWriter, BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream 來包裝 File ,
比如 new BufferedReader(new FileReader(new File(filename))) 。 4 FileChannel: 操作檔案內容的通道;FileChannel 是執行緒安全的,使用 java.nio 引入的高效能機制來實現。 5 檔案描述符 FileDescriptor : 用於操作開啟的檔案、網路socket、位元組流的唯一標識控制代碼;
其成員是一個整型數 fd 和一個原子計數器 AtomicInteger useCount ;
標準輸入流 System.in.fd = 0 , 標準輸出流 System.out.fd = 1 , 標準錯誤流 System.err.fd = 2 6 檔案訪問許可權 FilePermission: 提供了【檔案可進行的操作 actions 與位掩碼錶示 mask 】之間的轉化方法。
檔案可進行的操作 actions 在類 SecurityConstants 的常量中定義;
mask 是一個整型數,表示檔案的執行(0x01)/寫(0x02)/讀(0x04)/刪(0x08)/讀連結(0x10)/無(0x00) 等許可權組合。 7 FileSystem:所使用的作業系統平臺的檔案系統。
指明該檔案系統所使用的【檔案分隔符、路徑分隔符、路徑標準化、路徑解析、檔案訪問安全控制、檔案元資訊訪問、檔案操作等】。
檔案分割符是指路徑名中分割目錄的符號,linux = '/', windows = '\' ;
路徑分割符是環境變數中分割多個路徑的符號,linux = ':' , windows = ';' ;
UnixFileSystem 提供了 FileSystem 的一個實現示例。

 列舉:

  存在 BA_EXISTS = 0x01; 
普通檔案 BA_REGULAR = 0x02;
目錄 BA_DIRECTORY = 0x04;
隱藏 BA_HIDDEN = 0x08;
可讀 ACCESS_READ = 0x04;
可寫 ACCESS_WRITE = 0x02;
可執行 ACCESS_EXECUTE = 0x01; 
判斷檔案屬性及訪問許可權採用與或非位運算實現, 優點是可以靈活組合。

 序列化:

    序列化是實現記憶體資料持久化的機制。可以將 Java 物件資料儲存到檔案中,或者將檔案中的資料恢復成Java物件。Java RPC, Dubbo,ORM 等都依賴於序列化機制。
    序列化相關的類及方法: Serializable/Externalizable, ObjectInputStream, ObjectOutputStream, readObject, writeObject 。
    使用 ObjectInputStream.readObject, ObjectOutputStream.writeObject 方法進行物件序列化: 
JDK提供的標準物件序列化方式;物件必須實現 Serializable 介面;
適用於任何 Java 物件圖的序列化,通用的序列化方案;輸出為可讀性差的二進位制檔案格式,很難為人所讀懂,無法進行編輯;
對某些依賴於底層實現的元件,存在版本相容和可移植性問題;只有基於 Java 的應用程式可以訪問序列化資料; 使用 XMLDecoder.readObject, XMLEncoder.writeObject 方法進行物件序列化:
物件是普通的 javaBean,無需實現 Serializable 介面;輸出為可讀性良好的XML檔案,容易編輯和二次處理;
其它平臺亦可訪問序列化資料;可以修改資料成員的內部結構的實現,只要保留原有bean屬性及setter
/getter簽名,就不影響序列化和反序列化;
可以解決版本相容的問題,提供長期持久化的能力;每個需要持久化的資料成員都必須有一個 Java bean 屬性。 實現 Externalizable 介面來進行物件序列化。
需在物件中實現 readObject, writeObject 方法,提供更高的序列化靈活性,由開發者自行決定例項化哪些欄位、何種順序、輸出格式等細節。

 NIO:

 java.nio 為 javaIO 體系引入了更好的效能改善,主要是 Buffer, Channel 和 file 相關的支撐類。
由於 java.io 類已經引用了 nio 裡的類,因此直接使用 java.io 裡的類就可以獲得 nio 帶來的好處了。

 2.2、案例分析

  首先我們看一個例子:

  位元組流讀寫測試,可以發現如果預先規定了Byte物件的大小,就會產生亂碼:

  1 package com.io.test;
  2 
  3 import java.io.File;
  4 import java.io.FileInputStream;
  5 import java.io.FileNotFoundException;
  6 import java.io.FileOutputStream;
  7 import java.io.IOException;
  8 import java.io.InputStream;
  9 import java.io.OutputStream;
 10 
 11 public class ByteTest {
 12 
 13     public static void main(String[] args) {
 14 
 15         // 第一種方式讀檔案,因為方法throws了異常,所以在這要捕獲異常
 16         try {
 17             ByteTest.readFromFileByByte();
 18         } catch (FileNotFoundException e) {
 19             e.printStackTrace();
 20             System.out.println("找不到檔案啊");
 21         } catch (IOException e) {
 22             e.printStackTrace();
 23             System.out.println("讀不成功啊!");
 24         }
 25 
 26         System.out.println("===========================");
 27 
 28         // 第二種方式讀檔案
 29         try {
 30             ByteTest.readFromFileByteTwo();
 31         } catch (IOException e) {
 32             e.printStackTrace();
 33             System.out.println("還是讀不成功啊!");
 34         }
 35         System.out.println("===========================");
 36         ByteTest.WriteToFile();
 37     }
 38 
 39     /**
 40      * 第一種方法讀檔案 通過位元組流讀取檔案中的資料
 41      * 
 42      * @throws IOException
 43      */
 44     public static void readFromFileByByte() throws IOException {
 45         File file = new File("abc.txt");
 46         // 如果檔案不存在則建立檔案
 47         if (!file.exists()) {
 48             file.createNewFile();
 49         }
 50         InputStream inputStream = new FileInputStream(file);
 51         // 這裡定義了陣列的長度是1024個位元組,如果檔案超出這位元組,就會溢位,結果就是讀不到1024位元組以後的東西
 52         byte[] bs = new byte[2048];
 53         // 這裡len獲得的是檔案中內容的長度
 54         int len = inputStream.read(bs);
 55         inputStream.close();
 56         System.out.println(len+new String(bs));
 57     }
 58 
 59     /**
 60      * 第二種方法讀檔案 通過位元組流讀取檔案中的資料
 61      * 
 62      * @throws IOException
 63      */
 64     public static void readFromFileByteTwo() throws IOException {
 65         // 注意這裡的不同,File.separator是分隔符,這裡指明絕對路徑,即D盤根目錄下的abc.txt檔案
 66         File file = new File("d:" + File.separator + "abc.txt");
 67         // 如果檔案不存在則建立檔案
 68         if (!file.exists()) {
 69             file.createNewFile();
 70         }
 71         InputStream inputStream = new FileInputStream(file);
 72         // 這裡也有不同,可以根據檔案的大小來宣告byte陣列的大小,確保能把檔案讀完
 73         byte[] bs = new byte[(int) file.length()];
 74         // read()方法每次只能讀一個byte的內容
 75         inputStream.read(bs);
 76         inputStream.close();
 77         System.out.println(new String(bs));
 78     }
 79 
 80     public static void WriteToFile() {
 81         File file = new File("D:" + File.separator + "write.txt");
 82         OutputStream outputStream = null;
 83         if (!file.exists()) {
 84             try {
 85                 // 如果檔案找不到,就new一個
 86                 file.createNewFile();
 87             } catch (IOException e) {
 88                 e.printStackTrace();
 89             }
 90         }
 91         try {
 92             // 定義輸出流,寫入檔案的流
 93             outputStream = new FileOutputStream(file);
 94         } catch (FileNotFoundException e) {
 95             e.printStackTrace();
 96         }
 97         // 定義將要寫入檔案的資料
 98         String string = "Hell Java, Hello World, 你好,世界!";
 99         // 把string轉換成byte型的,並存放在陣列中
100         byte[] bs = string.getBytes();
101         try {
102             // 寫入bs中的資料到file中
103             outputStream.write(bs);
104             outputStream.close();
105         } catch (IOException e) {
106             e.printStackTrace();
107         }
108 
109         // =================到此,檔案的寫入已經完成了!
110         // 如果想在檔案後面追加內容的話,用下面的方法
111         OutputStream outToFileEnd = null;
112         try {
113             outToFileEnd = new FileOutputStream(file, true);
114             String string2 = "Here I come!!";
115             byte[] bs2 = string2.getBytes();
116             outToFileEnd.write(bs2);
117             outToFileEnd.close();
118         } catch (FileNotFoundException e) {
119             e.printStackTrace();
120         } catch (IOException ex) {
121             ex.printStackTrace();
122         }
123     }
124 }

    字元流的讀寫,同樣的對於Byte物件的大小設定非常重要,同時寫入的時候並沒有識別出換行符:

 1 package com.io.test;
 2 
 3 import java.io.BufferedReader;
 4 import java.io.BufferedWriter;
 5 import java.io.File;
 6 import java.io.FileNotFoundException;
 7 import java.io.FileReader;
 8 import java.io.FileWriter;
 9 import java.io.IOException;
10 import java.io.Reader;
11 import java.io.Writer;
12 
13 public class CharTest {
14 
15     public static void main(String[] args) throws IOException {
16 
17         File file1 = new File("D:" + File.separator + "test1.txt");
18         File file2 = new File("D:" + File.separator + "test2.txt");
19         Writer writer = new FileWriter(file1);
20         Writer writer1 = new FileWriter(file2, true);
21         String string = "今天是教師節!";
22         writer.write(string);
23         String string2 = "祝願所有的老師教師節快樂!";
24         writer1.write(string);
25         writer1.write(string2);
26         // 在這一定要記得關閉流
27         writer.close();
28         writer1.close();
29         
30         ReadFromFile();
31         ReadFromFile2();
32     }
33 
34     public static void ReadFromFile() throws IOException {
35         File file = new File("d:" + File.separator + "test1.txt");
36         Reader reader = new FileReader(file);
37         char[] cs = new char[1024];
38         // 上面定義了一個大小為1024的char型陣列,如果檔案內容過大,程式就會報錯,而不是隻讀到1024的大小
39         reader.read(cs, 0, (int) file.length());
40         System.out.println(cs);
41         reader.close();
42     }
43 
44     public static void ReadFromFile2() throws IOException {
45         try {
46             // 宣告一個可變長的stringBuffer物件
47             StringBuffer sb = new StringBuffer("");
48 
49             Reader reader = new FileReader("d:" + File.separator + "test1.txt");
50             // 這裡我們用到了字元操作的BufferedReader類
51             BufferedReader bufferedReader = new BufferedReader(reader);
52             String string = null;
53             // 按行讀取,結束的判斷是是否為null,按位元組或者字元讀取時結束的標誌是-1
54             while ((string = bufferedReader.readLine()) != null) {
55                 // 這裡我們用到了StringBuffer的append方法,這個比string的“+”要高效
56                 sb.append(string + "/n");
57                 System.out.println(string);
58             }
59             // 注意這兩個關閉的順序
60             bufferedReader.close();
61             reader.close();
62 
63             /*
64              * 完整寫入檔案
65              */
66             Writer writer = new FileWriter("d:" + File.separator + "test3.txt");
67             BufferedWriter bw = new BufferedWriter(writer);
68             // 注意這裡呼叫了toString方法
69             bw.write(sb.toString());
70             // 注意這兩個關閉的順序
71             bw.close();
72             writer.close();
73 
74         } catch (FileNotFoundException e) {
75             e.printStackTrace();
76         } catch (IOException e) {
77             e.printStackTrace();
78         }
79     }
80 
81 }