Java IO流學習
Java流操作有關的類或介面:
Java流類圖結構:
流的概念和作用
流是一組有順序的,有起點和終點的位元組集合,是對資料傳輸的總稱或抽象。即資料在兩裝置間的傳輸稱為流,流的本質是資料傳輸,根據資料傳輸特性將流抽象為各種類,方便更直觀的進行資料操作。
在IO中涉及的裝置檔案包括檔案、控制檯、網路連結等,這其中又根據流的方向可以將兩端的裝置檔案分為資料來源物件和接收端物件
- 資料來源物件:有能力產出資料
- 接收端物件:有能力接收資料
而IO流實際上遮蔽了在實際裝置中的處理資料的細節,這些處理方式也叫通訊方式可以包括順序、隨機存取、緩衝、二進位制、按字元、按位元組、按行等。
字元流和位元組流。io預設都是直接操作位元組的,多用於讀取或書寫二進位制資料,這些類的基類為InputStream或OutputStream。而字元流操作的是為了支援Unicode編碼,用於字元國際化,一個字元佔用兩個位元組,這些類的基類為Reader或Writer。java的io在jdk1.1以後新增了字元流的支援,為我們直接操作字元流提供了方便。
- 關於字元類庫和位元組類庫的選擇
最明智的做法是儘量嘗試使用Reader和Writer,一旦程式程式碼無法成功編譯,我們就會發現自己不得不使用面向位元組的類庫。
根據資料的流向,流分為輸入流和輸出流,這裡指的都是記憶體,記憶體輸入為讀,輸出為寫,I讀O寫。
Java提供了針對不同情況的處理流的類,以便於我們直觀地進行資料操作,這些類就在java的io包之中。下面介紹java io包的類,整個io包大量應用了裝飾模式。在實際使用過程中,涉及多層套用的建構函式必然都會有自己的一個裝飾模式的架構,包括被裝飾類(原構建基類),裝飾超類,裝飾具體類,用的過程中懂得去區分理解會讓你更加靈活地使用它們。
- 節點流:檔案(File),管道(Piped)和陣列(Array)(他們每個類都分別包括輸入輸出和位元組字元四種流)
- 處理流:其餘的都是處理類,他們都是屬於節點流的裝飾類,下面我整理了一個關於處理流的表格。
處理流 | who | 個數 | 裝飾功能 |
---|---|---|---|
緩衝區 | Buffered開頭的類 | 4((IO/BC四種) | 可將流放在緩衝區內操作 |
轉化流 | InputStreamReader/OutputStreamWriter | 2 | 可將位元組流轉化為字元流 |
基本型別 | DataXXXStream | 2(IO替換XXX) | 可傳輸基本型別資料 |
物件流 | ObjectXXXStream | 2(IO替換XXX) | 可傳輸物件型別資料(序列化) |
列印流 | PrintStream/PrintWriter | 2 | 包含print和println的輸出流 |
合併流 | SequenceInputStream | 1 | 可邏輯串聯其他輸入流 |
行號讀入流 | LineNumberReader | 1 | 可得到一個攜帶行號的字元讀入流 |
推回輸入流 | PushbackInputStream/PushbackReader | 2 | 可將輸入流push back或unread一個位元組 |
字串讀寫流 | StringWriter/StringReader | 2 | 可在緩衝區讀寫字串 |
注意:
- 預設命名規則:位元組流是輸入輸出(Input/Output),字元流是讀寫(Writer/Reader),當又有輸入輸出又有讀寫的時候,那一定是轉化流(InputStreamReader/OutputStreamWriter)。
- 預設都是操作位元組,所有操作字元的類都需要先經過轉化流將位元組流轉為字元流再使用。
- LineNumberInputStream已過時,因為它是基於位元組輸入流的,而錯誤假定位元組能充分表示字元,現已被LineNumberReader取代。
- StringBufferInputStream已過時,因為此類未能正確地將字元轉換為位元組,現已被StringReader取代。
- 這裡面還有一個過濾流的概念,它也包括輸入輸出位元組字元四種流,我在上面沒有表示出來。它是一個抽象類,作為“裝飾器”的介面,其中,“裝飾器”為其他輸入輸出字元位元組類提供有用功能。
我們知道在字元流處理類加入java io類庫之前,所有的類都是面向位元組流的,在jdk1.1以後,新增了字元流的支援,根據“開閉原則”,所以在不改變原有類的基礎上,有了轉化流:InputStreamReader和OutputStreamWriter,這兩個類正是所謂的“介面卡類”,InputStreamReader可以吧InputStream轉換為Reader,而OutputStreamWriter可以將OutputStream轉換為Writer。位元組流和字元流都有各自獨立的一整套繼承層次結構,而通過介面卡類,可以在不改變原有類的前提下有效將他們結合起來。
- java io 裝飾器模式的研究:
Java I/O類庫需要多種不同功能的組合,存在filter類的原因就是抽象類filter是所有裝飾器類的基類,裝飾器必須具有和它所裝飾物件相同的介面。FilterInputStream和FilterOutputStream是用來提供裝飾器類介面以控制特定輸入流(InputStream)和輸出流(OutputStream)的兩個類,他們的名字並不是很直觀,包括DataInput/OutputStream, BufferedInput/OutputStream,LineNumberInputStream, PushbackInputStream,PrintStream等,這些過濾流類在下面都會有詳細介紹。FilterInputStream和FilterOutputStream分別自I/O類庫中的基類InputStream和OutputStream派生而立,這兩個類是裝飾器的必要條件(以便能為所有正在被修飾的物件提供通用介面)。
一、File / 檔案流 / RandomAccessFile
節點流是直接與資料來源相連,進行IO操作,檔案是資料來源中最常見的,下面介紹檔案相關的內容。
1.File
File類可以表示檔案也可以表示資料夾目錄,直接展示一段程式碼,就懂了。
package javaS.IO;
+import java.io.File;
/**
* 基於磁碟IO操作的類 java.io.File
*
* 可以表示檔案,也可以表示資料夾目錄
*
* @author Evsward
*
*/
public class FileS extends IOBaseS {
@Test
public void testFileMethods() throws IOException {
logger.info("Start testing file methods.");
File file = new File(root);
if (!file.exists())
/**
* 建立目錄 mkdir();
*/
file.mkdir();
if (file.isDirectory()) {
File file1 = new File(root+"/UME.txt");
File file2 = new File(root+"/HongXing.txt");
/**
* 建立檔案 createNewFile();
*/
file1.createNewFile();
file2.createNewFile();
File file3 = new File(root+"/Cinema");
file3.mkdir();
/**
* 列出檔案路徑下的所有檔案(包括檔案和目錄)
*/
File[] files = file.listFiles();
for (File f : files) {
/**
* 判斷該檔案路徑是否為目錄
*/
if (f.isDirectory()) {
logger.info("The directory in 'Files' is: " + f.getName());
} else {
logger.info("The file in 'Files' is: " + f.getName());
}
logger.info("Whose path is: " + f.getAbsolutePath());
}
} else {
logger.info("FileS is not a directory!");
}
logger.info("Complete testing file methods.");
/**
* 輸出:
* 15:12:56[testFileMethods]: Start testing file methods.
* 15:12:56[testFileMethods]: The file in 'Files' is: HongXing.txt
* 15:12:56[testFileMethods]: Whose path is: /home/work/github/mainbase/resource/StudyFile/HongXing.txt
* 15:12:56[testFileMethods]: The directory in 'Files' is: Cinema
* 15:12:56[testFileMethods]: Whose path is: /home/work/github/mainbase/resource/StudyFile/Cinema
* 15:12:56[testFileMethods]: The file in 'Files' is: UME.txt
* 15:12:56[testFileMethods]: Whose path is: /home/work/github/mainbase/resource/StudyFile/UME.txt
* 15:12:56[testFileMethods]: Complete testing file methods.
*/
}
}
為了下面測試方便,我們在FileS類中加入一個靜態方法,用來初始化目錄:
/**
* 清空一個檔案,以便於我們測試使用
*
* @param filePath
* @throws IOException
*/
public static void initEV(String filePath) throws IOException {
File f = new File(filePath);
if (f.exists())
f.delete();
f.createNewFile();
}
然後,File還支援過濾器的功能,例如我們想要列出某目錄下的所有文字檔案的名字。
/**
* 測試檔案目錄過濾器,例如列出目錄下所有"*.txt"
*/
public void testFileFilter() {
logger.info("start testing file filter.");
String filterStr = "(./*)+(txt)$";
File file = new File(root);
String list[] = file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return Pattern.matches(filterStr, name);
}
});
for (String s : list)
logger.info(s);
}
/**
* 輸出:
*
* 12:57:17[testFileFilter]: start testing file filter.
*
* 12:57:17[testFileFilter]: HuaYi.txt
*
* 12:57:17[testFileFilter]: HongXing.txt
*
* 12:57:17[testFileFilter]: UME.txt
*/
正常的String list[] = file.list(); 我們都能用到,它的意思是列出該目錄下的所有檔名,如果我們想過濾這些檔名列出我們只想要的文字檔名,則需要通過匿名內部類建立一個FilenameFilter介面的實現類物件,建立時必須實現其方法accept方法,該方法是一個回撥函式,每一個查詢出來的檔名都要回撥該方法,是否能夠通過全靠accept的布林返回值所決定,這也是策略模式的體現,因為在accept方法體的具體實現留給了我們去自定義,所以這個函式更加有了一個“漏斗”的作用。我們只想得到文字檔名,那麼定義一個正規表示式去過濾即可。
關於java的正規表示式,未來會寫博文專門細說。
File其他方法的測試:
public void testFileTool() {
File file = new File(root);
PPrint.pprint(file.listFiles());
// 轉為List
@SuppressWarnings("unused")
List<File> fl = Arrays.asList(file.listFiles());
logger.info("file.length(): " + file.length());
logger.info("file.getName(): " + file.getName());
logger.info("file.getParent(): " + file.getParent());
// file.renameTo(new File("resource/S"));// 重新命名檔案
logger.info("file.canRead(): " + file.canRead());
logger.info("file.canWrite(): " + file.canWrite());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:SS:SSS");
Date a = new Date(file.lastModified());
logger.info("file.lastModified(): " + sdf.format(a));
}
輸出:
[
resource/StudyFile/access
resource/StudyFile/HuaYi.txt
resource/StudyFile/HongXing.txt
resource/StudyFile/Cinema
resource/StudyFile/UME.txt
]
13:59:20[testFileTool]: file.length(): 4096
13:59:20[testFileTool]: file.getName(): StudyFile
13:59:20[testFileTool]: file.getParent(): resource
13:59:20[testFileTool]: file.canRead(): true
13:59:20[testFileTool]: file.canWrite(): true
13:59:20[testFileTool]: file.lastModified(): 2017-11-30 13:55:00:000
一個用於運算元組或列表的實用列印輸出類,其實這種寫法我們自己也會經常在業務程式碼中寫出。
package javaS.IO;
import java.util.Arrays;
import java.util.Collection;
public class PPrint {
public static String pFormat(Collection<?> c) {// 泛型方法
if (c.size() == 0)
return "[]";
StringBuilder result = new StringBuilder("[");
for (Object elem : c) {
if (c.size() != 1) {
result.append("\n");
}
result.append(elem);
}
if (c.size() != 1) {
result.append("\n");
}
result.append("]");
return result.toString();
}
/**
* 列印一個視覺化的集合
*
* @param c
*/
public static void pprint(Collection<?> c) {
System.out.println(pFormat(c));
}
/**
* 列印一個視覺化的陣列
*
* @param c
*/
public static void pprint(Object[] c) {
System.out.println(pFormat(Arrays.asList(c)));
}
}
2.檔案流
檔案流包括輸入輸出字元位元組四種類,首先來看檔案位元組流處理。
package javaS.IO;
+import java.io.BufferedOutputStream;
/**
* 位元組流的學習
*
* 基於位元組I/O操作的基類:InputStream和OutputStream
*
* 對應的快取類:BufferedInputStream和BufferedOutputStream
*
* 出入的主語是“記憶體”,出記憶體就是寫入檔案,入記憶體就是讀取檔案
*
* @author Evsward
*
*/
public class ByteStreamS extends IOBaseS {
@Test
/**
* 使用輸出流OutputStream.write,將記憶體中的內容寫入裝置檔案(這裡的裝置檔案為File:磁碟檔案)
*/
public void testWrite2OutputStream() throws IOException {
OutputStream fos = new FileOutputStream(root+"/UME.txt");//找不到該檔案會自動建立(包括路徑)
/**
* 內容中的字串內容content
*/
String content = "哈哈哈\n嘿嘿";
fos.write(content.getBytes());// 直接寫入位元組
fos.close();// 操作完注意將流關閉
/**
* 檔案後面追加內容,建構函式加第二個引數true
*/
OutputStream fosadd = new FileOutputStream(root+"/UME.txt", true);
fosadd.write(" 你好".getBytes());
fosadd.close();
}
@Test
/**
* 使用輸入流讀取InputStream.read,將裝置檔案(這裡的磁碟檔案是File)讀到記憶體buffer中去。
*/
public void testRead2InputStream() throws IOException {
int bufferSize = 200;
FileInputStream fis = new FileInputStream(root+"/UME.txt");
byte buffer[] = new byte[bufferSize];
int length;
while ((length = fis.read(buffer, 0, bufferSize)) > -1) {
String str = new String(buffer, 0, length);
logger.info(str);
}
fis.close();// 操作完注意將流關閉
/**
* 輸出:
* 13:41:02[testInputStreamS]: 舉杯邀明月床前明月光
*/
}
}
- Buffered 緩衝區的作用
可以將資料流從資料來源中處理完畢都存入記憶體緩衝區,然後統一一次性與底層IO進行操作,可以有效降低程式直接操作IO的頻率,提高io執行速度。以下為緩衝區處理流的使用。
/**
* 緩衝區處理流:BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter,
* 一次性寫入,降低佔用IO的頻率
* 避免每次和硬碟打交道,提高資料訪問的效率。
*/
@Test
public void testWrite2BufferedOutputStream() throws IOException {
// OutputStream為基類
OutputStream fosaddOnce = new FileOutputStream(root+"/UME.txt");
OutputStream bs = new BufferedOutputStream(fosaddOnce);
bs.write("舉杯邀明月".getBytes());
bs.flush();// 每次flush會將記憶體中資料一齊刷入到外部檔案中,但不會close該流。
bs.write("床前明月光".getBytes());
/**
* close方法除了有關閉流的作用,在其關閉流之前也會執行一次flush。
* 注意一定要先關閉BufferedOutputStream,再關閉FileOutputStream,從外到內開啟,要從內到外關閉。
*/
bs.close();
fosaddOnce.close();// 兩個流都要關閉
}
- 使用建構函式套接的方式優化程式碼
我們很少使用單一的類來建立流物件,而是通過疊合多個物件來提供所期望的功能。(這就是裝飾器模式)
public void testWrite2BufferedOutputStream() throws IOException {
OutputStream bs = new BufferedOutputStream(new FileOutputStream(root+"/UME.txt"));
bs.write("舉杯邀明月".getBytes());
bs.close();//只關閉一次即可。
}
下面再看字元流的處理,這裡也包括了檔案字元讀寫流(FileWriter/FileReader)的應用。
package javaS.IO;
+import java.io.BufferedReader;
/**
* 字元流的學習
*
* 基於字元I/O操作的基類: Reader和Writer
*
* 對應的快取類:BufferedReader和BufferedWriter
*
* @author Evsward
*
*/
public class CharacterStreamS extends IOBaseS {
@Test
/**
* OutputStreamWriter,位元組到字元的轉化橋樑,轉化過程中需指定編碼字符集,否則採用預設字符集。
*/
public void testWriter() throws IOException {
// 檔案輸出流不變
FileOutputStream fos = new FileOutputStream(root + "HongXing.txt");
/**
* 輸出流寫入類(這是比起位元組流多出來的類)專門用來寫入字元流,注意字元編碼的引數
*
* 如果只保留fos一個引數,編碼預設為工作區預設編碼,這裡是“UTF-8",
*
* 位元組編碼為字元 -> 請轉到 http://www.cnblogs.com/Evsward/p/huffman.html#ascii編碼
*
* 為了保證寫入和讀取的編碼統一,請每次都要指定編碼
*
* 輸出體系中提供的兩個轉換流,用於實現將位元組流轉換成字元流。
*/
OutputStreamWriter osw = new OutputStreamWriter(fos);
// 快取寫入類,對應BufferedOutputStream
BufferedWriter bw = new BufferedWriter(osw);
bw.write("感時花濺淚,恨別鳥驚心");
bw.close();
osw.close();
fos.close();
/**
* 終版:將close部分縮短
*/
BufferedWriter bwA = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(root + "HongXing.txt", true), "UTF-8"));// 注意層級,加入指定編碼的引數
bwA.write("\n烽火連三月,家書抵萬金");
bwA.close();
}
@Test
/**
* InputStreamReader,位元組到字元的轉化橋樑,轉化過程中需指定編碼字符集,否則採用預設字符集。
*/
public void testReader() throws IOException {
FileInputStream fis = new FileInputStream(root + "HongXing.txt");
/**
* 輸出流讀取類(這是比起位元組流多出來的類)專門用來讀取字元流,注意字元編碼的引數要與寫入時的編碼相同,否則會亂碼
*
* 輸入體系中提供的兩個轉換流,用於實現將位元組流轉換成字元流。
*/
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String str;// 這裡也可以用StringBuilder來累計檔案的全部內容,最後輸出。
while ((str = br.readLine()) != null) {
logger.info(str);
}
br.close();// 顯示呼叫close方法關閉流
isr.close();
fis.close();
/**
* 終版:將close部分縮短
*/
BufferedReader brA = new BufferedReader(
new InputStreamReader(new FileInputStream(root + "HongXing.txt"), "UTF-8"));
String strA;
while ((strA = brA.readLine()) != null) {
logger.info(strA);
}
brA.close();
/**
* 輸出: 15:04:07[testReader]: 感時花濺淚,恨別鳥驚心 15:04:07[testReader]:
* 烽火連三月,家書抵萬金
*/
}
/**
* File提供了支援字元讀寫的封裝類:FileWriter和FileReader
* 所以不必每次都使用InputStreamReader和OutputStreamWriter來轉換。
*/
@Test
public void testFileWriter() throws IOException {
FileWriter fw = new FileWriter(root + "HongXing.txt", true);
fw.write("\n杜甫《春望》");
fw.close();
}
@Test
public void testFileReader() throws IOException {
FileReader fr = new FileReader(root + "HongXing.txt");
// FileReader直接read方法沒有readLine方便,所以套上裝飾類BufferedReader借它的readLine用一用
BufferedReader br = new BufferedReader(fr);
String str;
while ((str = br.readLine()) != null) {
logger.info(str);
}
br.close();
fr.close();
}
}
注意:File提供了支援字元讀寫的封裝類:FileWriter和FileReader,所以不必每次都使用InputStreamReader和OutputStreamWriter來轉換。
3.RandomAccessFile
RandomAccessFile 是任意位置進入檔案的意思,適用於由大小已知的記錄組成的檔案,它有一個seek方法定義了檔案的位置,所以要注意在對檔案進行RandomAccessFile操作時,要記住檔案的內容的位置和大小,否則會發生內容複寫更改的後果。
package javaS.IO;
+import java.io.File;
/**
* RandomAccessFile:有一個指標seek,對檔案任意位置操作的類
*
* @author Evsward
*
*/
public class RandomAccessFileS extends IOBaseS {
@Before
public void testWrite2RAFile() throws IOException {
FileS.initEV(root + "/access");// 首先清空access檔案。
RandomAccessFile raf = new RandomAccessFile(root + "/access", "rw");// rw是採用讀寫的方式開啟檔案
logger.info(raf.length());
Student Jhon = new Student(1001, "Jhon", 26, 1.85d);
Student Jack = new Student(1002, "Jack", 25, 1.75d);
Jhon.write(raf);// 寫入檔案以後,指標到當前文字結尾
// 當前seek是從seek(raf.length)開始的
logger.info(raf.length());
Jack.write(raf);// 繼續寫入,指標繼續移動到末尾,相當於追加
logger.info(raf.length());
raf.close();
}
@After
public void testReadRAFile() throws IOException {
RandomAccessFile raf = new RandomAccessFile(root + "/access", "r");
// 獲取raf時,seek就是在檔案開始位置
logger.info(raf.length());
Student Lily = new Student();
Lily.read(raf);
logger.info(Lily);
Lily.read(raf);
logger.info(Lily);
// 讀入次數是有限的,一定要預先知道最多讀幾次,否則會報EOFException。
Lily.read(raf);
logger.info(Lily);
raf.close();
/**
* 輸出: 16:14:30[testReadRAFile]: id:1001 name:Jhon age:26 height:1.85
*/
}
}
RandomAccessFile就像資料庫一樣,是將資料一條一條的寫入的,而這些資料必須是類物件,不能是基本型別資料,因為該類物件要寫兩個方法write和read用來將物件寫入RandomAccessFile,這兩個方法也不是繼承於誰,可以起別的名字,呼叫的時候也是自己呼叫。
public class Student extends IOBaseS{
private long id;
private String name;
private int age;
private double height;
public Student() {
}
public Student(long id, String name, int age, double height) {
super();
this.id = id;
this.name = name;
this.age = age;
this.height = height;
}
public void write(RandomAccessFile raf) throws IOException {
raf.writeLong(id);
raf.writeUTF(name);// 採用UTF的編碼方式寫入字串
raf.writeInt(age);
raf.writeDouble(height);
}
/**
* 要嚴格按照寫入順序讀取,這也是ORM的意義
*
* @param raf
* @throws IOException
*/
public void read(RandomAccessFile raf) throws IOException {
this.id = raf.readLong();
this.name = raf.readUTF();
this.age = raf.readInt();
this.height = raf.readDouble();
}
}
按照類的結構,將欄位按順序寫入檔案的任意位置,讀取的時候也要按照相同的順序讀出。
- RandomAccessFile 追加內容
追加的時候要使用seek方法,將游標定位到檔案末尾,如果不重新定位,預設從起點開始寫入,就會覆蓋原有資料。
/**
* 追加內容
*/
public void testWriteAppend2RAFile() throws IOException {
RandomAccessFile raf = new RandomAccessFile(root + "/access", "rw");// rw是採用讀寫的方式開啟檔案
Student Mason = new Student(1003, "Mason", 26, 1.82d);// 這裡的“Mason”比上面的兩條資料多一位字元
// 追加內容要先調整seek的位置到raf.length,然後開始追加內容
raf.seek(raf.length());
Mason.write(raf);
logger.info(raf.length());
raf.close();
}
- RandomAccessFile 在任意位置插入資料
@Test
public void insert() {
Student Hudson = new Student(1005, "Hudson", 45, 1.76d);
try {
insert(root + "/access", 26, Hudson);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 在RandomAccessFile指定位置插入資料,先將位置後面的資料放入緩衝區,插入資料以後再將其寫回來。
*
* @param file
* 能找到該檔案的路徑,是字串型別
* @param position
* 其實外部呼叫的時候能找到這個位置比較難,因為不確定資料長度是多少,弄不好就會將資料拆分引起混亂。
* @param content
*/
public void insert(String file, long position, Student s) throws IOException {
/**
* 建立一個臨時檔案
*
* 在使用完以後就將其刪除
*/
File tempFile = File.createTempFile("temp", null);
tempFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream("temp");
/**
* 將插入位置後面的資料快取到臨時檔案
*/
RandomAccessFile raf = new RandomAccessFile(file, "rw");
raf.seek(position);
byte[] buffer = new byte[20];
while (raf.read(buffer) > -1) {
fos.write(buffer);
}
raf.seek(position);
/**
* 向RandomAccessFile寫入插入內容
*/
s.write(raf);
/**
* 從臨時檔案中寫回快取資料到RandomAccessFile
*/
FileInputStream fis = new FileInputStream("temp");
while (fis.read(buffer) > -1) {
raf.write(buffer);
}
fos.close();
fis.close();
raf.close();
tempFile.delete();//刪除臨時檔案tempFile
}
插入資料的時候,RandomAccessFile並沒有提供相關的方法,由於在舊的位置寫入資料會覆蓋原有資料,所以我們要將插入位置後面的資料快取到一個臨時檔案中,插入資料以後再將臨時檔案的內容接著被插入的資料後面。
- JUnit生命週期
完整JUnit執行順序:
- @BeforeClass –> @Before –> @Test –> @After –> @AfterClass
- @BeforeClass和@AfterClass只執行一次,且必須為static void
- @Ignore 在執行整個類的時候會跳過該方法
下面我們定義一個完整測試流程:先初始化一個空白檔案,然後新增兩行資料Jhon和Jack,然後在他倆中間插入Hudson,最後讀出該檔案資料,驗證結果輸出為:
/**
* 輸出:
* 17:10:31[testWrite2RAFile]: 0 17:10:31[testWrite2RAFile]: 26
* 17:10:31[testWrite2RAFile]: 52 17:10:31[testReadRAFile]: 94
* 17:10:31[testReadRAFile]: id:1001 name:Jhon age:26 height:1.85
* 17:10:31[testReadRAFile]: id:1005 name:Hudson age:45 height:1.76
* 17:10:31[testReadRAFile]: id:1002 name:Jack age:25 height:1.75
*/
二、DataOutputStream / DataInputStream
這一對類可以直接寫入java基本型別資料(沒有String),但寫入以後是一個二進位制檔案的形式,不可以直接檢視。DataOutputStream / DataInputStream是常用的過濾流類,如果物件的序列化是整個物件轉換為一個位元組序列的話,DataOutputStream / DataInputStream就是將欄位序列化,轉為二進位制資料。下面看程式碼。
package javaS.IO;
+import java.io.BufferedInputStream;
public class DataStream extends IOBaseS {
@Test
/**
* DataOutputStream,可以直接寫入java基本型別資料(沒有String),但寫入以後是一個二進位制檔案的形式,不可以直接檢視。
*
* 文字檔案是二進位制檔案的特殊形式,這是通過轉儲實現的,相關內容請轉到
* http://www.cnblogs.com/Evsward/p/huffman.html#二進位制轉儲
*/
public void testWrite2DataOutputStream() throws IOException {
OutputStream fosaddOnce = new FileOutputStream(root + "/UME.txt");
OutputStream bs = new BufferedOutputStream(fosaddOnce);
DataOutputStream dos = new DataOutputStream(bs);
dos.writeInt(22);
dos.writeShort(1222222222);
dos.writeLong(20L);
dos.writeByte(3);
dos.writeChar(42);
dos.close();
bs.close();
fosaddOnce.close();
/**
* 終版:上面的close階段要從內向外關閉三次,比較麻煩,下面直接採用裝飾模式標準寫法,套接物件。
* 套接物件:最裡面的一定是節點流,它之外的無論幾層都是處理流
* FileOutputStream:屬於節點流,其他節點流還包括管道和陣列,剩下的都是處理流
* BufferedOutputStream:緩衝技術(也屬於處理流) DataOutputStream:處理流
*/
DataOutputStream dosA = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(root + "/UME.txt")));
dosA.writeInt(22);
dosA.writeShort(65538);// DataOutputStream並不會檢查資料是否越界,越界的資料按照二進位制方式擷取,只保留界限以內的資料。
dosA.writeLong(20L);
dosA.writeByte(3);
dosA.writeChar(42);
dosA.writeDouble(3.1415926);
dosA.close();// 只關閉一次。
}
@Test
/**
* 通過DataInputStream處理流讀取二進位制檔案,一定要按照寫入的順序去讀取java基本型別的檔案內容,否則會出現亂碼或者不準確的資訊
*/
public void testRead2DataInputStream() throws IOException {
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(root + "/UME.txt")));
logger.info(dis.readInt());
/**
* 即使存入越界的樹65538,也不會報錯,因為超出部分不會被存入,存入的只是超出的部分。
* short型別佔據16位的空間,因此將65538轉為二進位制數,超出16位的部分自動截掉,只保留16為以內的資料,所以就變成了2。
*/
logger.info(dis.readShort());
logger.info(dis.readLong());
logger.info(dis.readByte());
logger.info(dis.readChar());
logger.info(dis.readDouble());
dis.close();
/**
* 輸出:
* 13:39:03[testDataInputStream]: 22
* 13:39:03[testDataInputStream]: 2
* 13:39:03[testDataInputStream]: 20
* 13:39:03[testDataInputStream]: 3
* 13:39:03[testDataInputStream]: *
* 13:39:03[testDataInputStream]: 3.1415926
*/
}
}
註釋比較齊全,這裡不再過多贅述。
三、物件流 / 序列化
資料傳輸過程中,都會預設採用二進位制檔案的方式,因為計算機的底層識別方式就是二進位制,不依賴任何執行環境或是程式設計語言,所以這是實現資料傳輸跨平臺跨網路的基礎。序列化可以直接將java物件轉化為一個位元組序列,並能夠在以後將這個位元組序列完全恢復為原來的物件,這一過程甚至可以通過網路進行,這意味著序列化機制能自動彌補不同作業系統之間的差異。
如果要讓一個類的物件可以成功序列化,那麼它必須實現Serializable介面,這個介面就像Cloneable介面一樣,他們只是一個空的標識介面。
下面這段話用來描述物件序列化真的非常受啟發。
當你建立物件時,只要你需要,它就會一直存在,但是在程式終止時,無論如何它都不會繼續存在。儘管這麼做肯定是有意義的,但是仍舊存在某些情況,如果物件能夠在程式不執行的情況下仍能存在並儲存其資訊,那將非常有用。這樣,在下次執行程式時,該物件將被重建並且擁有的資訊與在程式上次執行時它所擁有的資訊相同。當然,你可以通過將資訊寫入檔案或資料庫來達到相同的效果,但是在使萬物都成為物件的物件導向的精神中,如果能夠將一個物件宣告是“永續性”的,併為我們處理掉所有細節,那將會顯得十分方便。
相對於資料庫儲存那種重量級永續性來說,物件的序列化可以實現輕量級永續性。
永續性意味著一個物件的生命週期並不取決於程式是否正在執行,它可以生存於程式的呼叫之間。
輕量級是因為不能用某個關鍵字來簡單定義一個物件,並讓系統自動維護其他細節問題,相反,物件必須在程式中顯示地序列化和反序列化。
如果需要一個更嚴格的永續性機制,可以考慮像Hibernate之類的工具。TODO: 會有一篇文章深入介紹Hibernate。
物件序列化的意義:
- 支援java的遠端方法呼叫RMI,它使存活於其他計算機上的物件使用起來就像是存活在本機上一樣。
- 對於java bean,一定要儲存下來它在設計階段對他的狀態資訊的配置,在程式啟動時進行後期恢復,這中具體工作就是由物件序列化完成的。
+package javaS.IO;
/**
* Serializable為標記介面,表示這個類的物件可以被序列化。
*
* @author Evsward
*
*/
public class Student extends IOBaseS implements Serializable {
/**
* 類中的宣告
*
* transient和static的變數不會被序列化
*/
/**
* 序列號:避免重複序列化
* 如果serialVersionUID被修改,反序列化會失敗。
* 當程式試圖序列化一個物件時,會先檢查該物件是否已經被序列化過,只有該物件從未(在本次虛擬機器中)被序列化,系統才會將該物件轉換成位元組序列並輸出。
*/
private static final long serialVersionUID = -6861464712478477441L;
private long id;
private String name;
private int age;
private transient double height;
public Student() {
}
public Student(long id, String name, int age, double height) {
super();
this.id = id;
this.name = name;
this.age = age;
this.height = height;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("id:");
sb.append(this.id);
sb.append(" ");
sb.append("name:");
sb.append(this.name);
sb.append(" ");
sb.append("age:");
sb.append(this.age);
sb.append(" ");
sb.append("height:");
sb.append(this.height);
return sb.toString();
}
/**
* 相當於重寫了ObjectOutputStream.writeObject方法,ObjectOutputStream寫入該物件的時候會呼叫該方法
*
* 作用:可以在序列化過程中,採用自定義的方式對資料進行加密
*
* 參考原始碼:
*
* public final void writeObject(Object obj) throws IOException {
if (enableOverride) {// 如果發現引數Object有重寫該方法,則去執行重寫的方法,否則繼續執行本地方法。
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
*
*
* readObject方法的分析同上。
*
* @param out
* @throws IOException
*/
private final void writeObject(ObjectOutputStream out) throws IOException {
logger.info("Start writing data to Object.");
out.writeLong(this.id);
/**
* 下面的writeObject是StringBuffer原始碼中的:
*
* readObject is called to restore the state of the StringBuffer from a stream.
private synchronized void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
java.io.ObjectOutputStream.PutField fields = s.putFields();
fields.put("value", value);
fields.put("count", count);
fields.put("shared", false);
s.writeFields();
}
*/
out.writeObject(new StringBuffer(name));
out.writeInt(this.age);
out.writeDouble(this.height);// 這裡重寫以後,就忽略了transient的設定
}
private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
logger.info("Start reading data to Object.");
this.id = in.readLong();
/**
* 下面的readObject是StringBuffer原始碼中的:
*
* readObject is called to restore the state of the StringBuffer from a stream.
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
java.io.ObjectInputStream.GetField fields = s.readFields();
value = (char[])fields.get("value", null);
count = fields.get("count", 0);
}
*/
this.name = ((StringBuffer) in.readObject()).toString();
this.age = in.readInt();
this.height = in.readDouble();
}
}
這個類內容比較多,因為為了更好的去分析,我將jdk的原始碼也貼上到註釋區域了。但是不影響,文末會有原始碼位置,感興趣的同學可以去下載原始碼檢視。在這個類中,我們重寫了readObject和writeObject方法,外部ObjectXXXStream在呼叫的時候會先找物件類中是否有重寫的readObject和writeObject方法,如果有則使用沒有才呼叫自己內部預設的實現方法。
package javaS.IO;
+import java.io.FileInputStream;
/**
* 研究物件序列化(跨平臺,跨網路的基礎)
*
* 記憶體 -> 磁碟/網路
*
* Java物件 -> 二進位制檔案
*
* @author Evsward
*
*/
public class ObjectStreamS extends IOBaseS {
/**
* ObjectOutputStream:物件流(物件序列化),不同於DataOutputStream是操作基本型別。
* 測試序列化物件儲存結構
*/
@Test
public void testWriteSerialObject() throws IOException {
FileS.initEV(root + "/access");// 先將access檔案清空。
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(root + "/access"));
Student Lu = new Student(2001, "Luxa", 31, 1.81d);
// 可以寫入不同的序列化物件資料,但要記錄寫入順序
oos.writeObject(Lu);
oos.close();
/**
* access內容:由於寫入的是一個二進位制檔案,所以開啟是亂碼
*
* ¬í^@^Esr^@^PjavaS.IO.Student Ç.2<95>׳^?^B^@^DI^@^CageD^@^FheightJ^@^BidL^@^Dnamet^@^RLjava/lang/String;xp^@^@^@^_?üõÂ<8f>\(ö^@^@^@^@^@^@^GÑt^@^DLuxa
*/
}
/**
* ObjectInputStream:物件反序列化
* 讀取二進位制檔案(序列號檔案)
*
* @throws IOException
* @throws ClassNotFoundException
*/
@Test
public void testReadSerialObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(root + "/access"));
// 可以讀取不同的物件的資料,但是要按照寫入順序去讀取。
Student s = (Student) ois.readObject();
logger.info(s);
ois.close();
/**
* ①輸出:
*
* 10:24:08[testReadSerialObject]: id:2001 name:Luxa age:31 height:1.81
*
* ②若height屬性變數被宣告為transient,則該變數在序列化過程中不會被寫入,為初始值。輸出為:
*
* 10:29:34[testReadSerialObject]: id:2001 name:Luxa age:31 height:0.0
*/
}
}
transient變數可標識出來不被序列化的欄位,這些欄位可能攜帶敏感資訊,例如密碼。但是這個關鍵字在我們重寫了writeObject和readObject方法以後,不好使了。
序列化的使用忠告:
- 謹慎實現Serializable介面,需要隨時保持uuid的時效性以及一致性,對於結構性框架中要避免不可序列化的類與已序列化的類之間的繼承關係。
- 要考慮實現自定義的序列化形式,正如以上程式碼中我們所做的那樣。
- 保護性的編寫readObject方法。加入異常處理,讓無效的序列化物件在反序列化過程中終端丟擲異常。
四、PrintStream / PrintWriter
首先請問java的標準輸入流是什麼?是InputStream,正確。那麼java的標準輸出流是什麼?是OutputSteam?No!而是PrintStream。
因為標準輸入輸出流是System類的定義,System中有三個欄位,in是InputStream型別,對應的是標準輸入流,err和out都是PrintStream物件,out對應的是標準輸出流。我們常用的System.out.println(data)方法的返回值就是PrintStream物件,此流預設輸出在控制檯,也可以重定向輸出位置:
System.setOut(new PrintStream(new FileOutputStream(file)));
System.out.println("這些內容只能在file物件的檔案中才能看到哦!");
PrintWriter就是PrintStream的字元操作的版本。PrintStream都是針對位元組流進行操作的,如果要操作字元流,可以使用PrintWriter。下面直接看程式碼的註釋吧。
package javaS.IO;
+import java.io.FileOutputStream;
public class PrintStreamS extends IOBaseS {
/**
* 列印流的使用非常類似於FileWriter,但是它支援更多的方法,同時也有著豐富的構造方法。
*/
@Test
public void testPrintStream() throws IOException {
FileS.initEV(root + "HongXing.txt");
// PrintStream p = new PrintStream(root + "HongXing.txt");
// PrintStream p = new PrintStream(new File(root + "HongXing.txt"));
// PrintStream p = new PrintStream(new FileOutputStream(root +
// "HongXing.txt"), true, "UTF-8");
PrintStream p = System.out;// 資料來源切換到控制檯,標準輸出,相當於System.out.xxx();
p.append("海上升明月");
p.println("潤物細無聲");
p.print("當春乃發生");
p.write("無敵心頭好".getBytes());
p.flush();// 刷入記憶體資料到資料來源
System.out.write("asdfas".getBytes());
p.close();
/**
* 輸出:
*
* 海上升明月潤物細無聲
*
* 當春乃發生無敵心頭好
*/
}
/**
* PrintWriter與PrintStream的兩點區別:
*
* write方法一個是寫入位元組,一個是寫入字元。
*
* 一般來講,使用PrintStream多一些。
*/
@Test // 如果忘記寫該註解,執行JUnit會報錯initializationError
public void testPrintWriter() throws IOException {
FileS.initEV(root + "HongXing.txt");
// PrintWriter p = new PrintWriter(root + "HongXing.txt");
// PrintWriter p = new PrintWriter(new File(root + "HongXing.txt"));
// 第二個引數為autoflush,如果為true的話,println、printf和format會自動執行flush。
// PrintWriter p = new PrintWriter(new FileOutputStream(root + "HongXing.txt"), true);
System.setOut(new PrintStream(new FileOutputStream(root + "HongXing.txt")));// 輸出重定向,從預設的控制檯轉到檔案,這也是日誌系統的基本思想。
PrintWriter p = new PrintWriter(System.out, true);// 將PrintWriter的列印位置改到標準輸出
p.append("海上升明月");
p.println("潤物細無聲");
p.print("當春乃發生");
p.write("無敵心頭好");// 這是與PrintStream唯一區別了
p.flush();// PrintWriter也支援刷入操作
p.close();
}
/**
* 測試標準輸入輸出
*/
public void testStandardIO() throws IOException {
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
String str;
while ((str = stdin.readLine()) != null && str.length() != 0)
logger.info(str);
}
}
五、SequenceInputStream
合併流,有兩種建構函式:
- 傳入兩個InputStream型別的物件
- 傳入一個列舉的InputStream型別的物件的集合,將它們合併起來進行操作
合併流以後,操作可以是讀寫到另一個檔案,或者列印到控制檯。下面看程式碼:
package javaS.IO;
+import java.io.FileInputStream;
public class SequenceInputStreamS extends IOBaseS {
/**
* 合併兩個讀入的位元組流
*
* @throws IOException
*/
@Test
public void testSequenceInputStream() throws IOException {
// buffer的空間要設定為2的次方才能有效分割,否則會出現某漢字被中途分割顯示不完整的情況,
int bufferSize = 16;
InputStream is1 = new FileInputStream(root + "UME.txt");
InputStream is2 = new FileInputStream(root + "HongXing.txt");
SequenceInputStream sis = new SequenceInputStream(is1, is2);// 構造引數必須為InputStream
byte[] buffer = new byte[bufferSize];
while (sis.read(buffer, 0, bufferSize) != -1) {
// 開始讀合併後的資料流,這裡可以針對這些資料流做任何操作(讀寫到任何檔案或者列印到控制檯)
String str = new String(buffer, 0, bufferSize);
logger.info(str);// 列印到控制檯
}
is1.close();
is2.close();
sis.close();
}
@Test
public void testMergeEnumInputStream() throws IOException {
// 實際上它可以合併不同型別資料,然而如果是物件流的話,讀取時涉及反序列化工作,要找準與其他資料的分割點,比較麻煩。
InputStream is1 = new FileInputStream(root + "UME.txt");
InputStream is2 = new FileInputStream(root + "HongXing.txt");
InputStream is3 = new FileInputStream(root + "HuaYi.txt");
ArrayList<InputStream> list = new ArrayList<InputStream>();
list.add(is1);
list.add(is2);
list.add(is3);
Iterator<InputStream> it = list.iterator();// 傳入一個迭代器用於建立列舉
SequenceInputStream sis = new SequenceInputStream(new Enumeration<InputStream>() {
@Override
public boolean hasMoreElements() {
return it.hasNext();
}
@Override
public InputStream nextElement() {
return it.next();
}
});
int bufferSize = 32;
byte[] buffer = new byte[bufferSize];
while (sis.read(buffer, 0, bufferSize) != -1) {
// 開始讀合併後的資料流,這裡可以針對這些資料流做任何操作(讀寫到任何檔案或者列印到控制檯)
String str = new String(buffer, 0, bufferSize);
logger.info(str);// 列印到控制檯
}
is1.close();
is2.close();
is3.close();
sis.close();
}
}
六、LineNumberReader
LineNumberReader是可以將讀入的字元流加入行號,下面看程式碼。
package javaS.IO;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import org.junit.Test;
public class LineNumberReaderS extends IOBaseS {
@Test
public void testLineNumberReader() throws IOException {
FileReader fr = new FileReader(root + "UME.txt");
// 構造引數為Reader
LineNumberReader lnr = new LineNumberReader(fr);
lnr.setLineNumber(1);// 設定行號從2開始。
String str;
while ((str = lnr.readLine()) != null) {
// 核心方法:lnr.getLineNumber(),獲得行號
logger.info("行號:" + lnr.getLineNumber() + " 內容:" + str);
}
fr.close();
lnr.close();
/**
* 輸出:
*
* 12:11:27[testLineNumberReader]: 行號:2 內容:舉杯邀明月
*
* 12:11:27[testLineNumberReader]: 行號:3 內容:床前明月光
*/
}
}
七、陣列IO / PushbackInputStream / PushbackReader
把單一字元推回輸入流。這裡採用了陣列的讀取方法,同樣分為位元組和字元,位元組陣列流為ByteArrayInputStream,字元陣列流為CharArrayReader。
package javaS.IO;
import java.io.ByteArrayInputStream;
import java.io.CharArrayReader;
import java.io.IOException;
import java.io.PushbackInputStream;
import java.io.PushbackReader;
import org.junit.Test;
public class PushBackS extends IOBaseS {
@Test
public void testPushbackInputStream() throws IOException {
String content = "Superman VS Batman";
// 構造引數為一個位元組陣列
ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes());
// 構造引數為一個InputStream物件。
PushbackInputStream pbis = new PushbackInputStream(bais);
pbis.unread("Ssdfasdf".getBytes(), 0, 1);// 將S推到源字串的最前方
// pr.unread('S');// 這裡的'S'是按照整型值操作
int n;
String str = "";
while ((n = pbis.read()) != -1) {
str += (char) n;
// pbis.unread(n);將剛讀出來的字元再推回去,就會死迴圈。
}
logger.info(str);
pbis.close();
bais.close();
/**
* 輸出:
*
* 12:32:48[testPushBackInputStream]: SSuperman VS Batman
*/
}
@Test
/**
* PushbackInputStream的字元流版本
*/
public void testPushbackReader() throws IOException {
// 構造引數為Reader物件,使用字元陣列讀取
PushbackReader pr = new PushbackReader(new CharArrayReader("go go Gan.".toCharArray()));
pr.unread("Ssdfasdf".toCharArray(), 0, 1);// 將S推到源字串的最前方
// pr.unread('S');// 這裡的'S'是按照整型值操作
int n;
String str = "";
while ((n = pr.read()) != -1) {
str += (char) n;
// pr.unread(n);將剛讀出來的字元再推回去,就會死迴圈。
}
logger.info(str);
pr.close();
/**
* 輸出:
*
* 12:45:55[testPushbackReader]: Sgo go Gan.
*/
}
}
八、StringWriter / StringReader
這沒什麼太多可說的,就是對一個字串的讀寫操作,一般很少單獨使用,因為直接使用String就可以將他們代替,然而當需要一個流的時候,可以與其他IO流進行組合使用。
package javaS.IO;
+import java.io.IOException;
public class StringIOS extends IOBaseS {
@Test
public void testStringWriter() throws IOException {
StringWriter sw = new StringWriter();
sw.write("Hello");
sw.append("A");
sw.close();
logger.info(sw);
StringReader sr = new StringReader("Hello");
int c;
StringBuilder sb = new StringBuilder();
while ((c = sr.read()) != -1) {
sb.append((char)c);
}
logger.info(sb);
/**
* Output:
*
* 12:56:47[testStringWriter]: HelloA
*
* 12:56:47[testStringWriter]: Hello
*/
}
}
總結
-
關於節點流,以上部分我們展示了檔案字元位元組輸入輸出流和陣列各種流的使用。
本文唯獨沒有管道相關的內容,管道的部分會在NIO中去研究。
-
關於處理流,我們展示了緩衝區,物件流,基本型別資料流,轉化流,列印流,合併流,行號讀入流,推回輸入流以及字串讀寫流。
參考資料
- 《Java程式設計思想》
- 《effective java》
- JDK API Document