JAVA输入/输出流详细讲解
應用程序經常需要訪問文件和目錄,讀取文件信息或寫入信息到文件,即從外界輸入數據或者向外界傳輸數據,這些數據可以保存在磁盤文件、內存或其他程序中。在Java中,對這些數據的操作是通過I/O技術來實現的。所謂I/O技術,就是數據的輸入(Input)、輸出(Output)技術。本章將對Java的?I/O系統進行講解,包括I/O的體系結構、流的概念、字節流、處理字節流的基本類InputStream和OutputStream、字符流、處理字符流的基本類Reader和Writer、文件管理、序列化和反序列化等。
12.1 I/O流概述
Java將數據的輸入/輸出操作當作“流”來處理,“流”是一組從源頭到目的地的有序的字節序列。在Java程序中,從某個數據源讀取數據到程序的流稱為輸入流,通過程序使用數據流將數據寫入到目的地的稱為輸出流。輸入流和輸出的讀取和寫入流程如圖12.1所示。
?
(a)輸入流 (b)輸出流
圖12.1 輸入/輸出流示意圖
當程序需要從某個數據源讀入數據的時候,就會開啟一個輸入流,數據源可以是文件、內存或網絡等。相反,需要寫出數據到某個數據源目的地的時候,也會開啟一個輸出流,這個數據源目的地也能夠是文件、內存或網絡等。I/O流有很多種,按操作數據單位不同可分為字節流和字符流,按數據流的方向不同分為輸入流和輸出流,如表12.1所示。
表12.1 流的分類
| 輸入/輸出 | 字節流 | 字符流 |
| 輸入流 | InputStream | Reader |
| 輸出流 | OutputStream | Writer |
????輸入流和輸出流的區別是以程序為中心來進行判斷,從外部設備讀取數據到程序是輸入流,從程序寫入數據到外部設備是輸出流。字節流的單位是一個字節,即8bit;字符流的單位是兩個字節,即16bit。表12.1是I/O流的簡單分類,實際開發中需要使用的的I/O流共涉及40多個類,都是從這4個抽象基類派生的。接下來,我們先學習輸入/輸出流的體系結構。
Java.io包中的最重要的部分是由5個類和一個接口組成。5個類是指File、RandomAccessFile、InputStream、OutputStream、Writer、Reader,一個接口指的是Serializable。掌握了這些I/O的核心操作,那么對于Java中的I/O體系也就有了一個初步的認識了。總體上看,Java I/O主要包括如下3個部分:
??流式部分:I/O的主體部分。
??非流式部分:主要包含一些輔助流式部分的類,如File類、RandomAccessFile類和FileDescriptor類等。
??其他類:主要是文件讀取部分的與安全相關的類(如SerializablePermission類),以及與本地操作系統相關的文件系統的類,如(FileSystem類、Win32FileSystem類和WinNTFileSystem類)。
這里,將Java I/O中主要的類簡單介紹如下:
??File類(文件特征與管理類):用于文件或者目錄的描述信息等(An abstract representation of file and directory pathnames),如生成新目錄、修改文件名、刪除文件、判斷文件所在路徑等。
??InputStream類(二進制格式操作類):基于字節輸入操作的抽象類,是所有輸入流的父類,定義了所有輸入流都具有的共同特征。
??OutputStream類(二進制格式操作類):基于字節輸出操作的抽象類,是所有輸出流的父類,定義了所有輸出流都具有的共同特征。
??Reader類(文件格式操作類):抽象類,基于字符的輸入操作。
??Writer類(文件格式操作類):抽象類,基于字符的輸出操作。
??RandomAccessFile類(隨機文件操作類):它的功能豐富,可以從文件的任意位置進行存取(輸入輸出)操作。
綜上所述,Java中I/O流的體系結構如圖12.2所示。
圖12.2 I/O流體系結構圖
12.2 File類
File類可以用于處理文件目錄。在對一個文件進行輸入/輸出,必須先獲取有關該文件的基本信息,如文件是否可以讀取、能否被寫入、路徑是什么等。java.io.File類不屬于Java流系統,但它是文件流進行文件操作的輔助類,提供了獲取文件基本信息以及操作文件的一些方法,通過調用File類提供的相應方法,能夠完成創建文件、刪除文件以及對目錄的一些操作。
12.2.1?File類的常用方法
File類的對象是一個“文件或目錄”的抽象,它并不打開文件或目錄,而是指定要操作的文件或目錄。File類的對象一旦創建,就不能再修改。要創建一個新的File對象,需要使用它的構造方法,如表12.2所示。
表12.2 File類構造方法
| 構造方法 | 功能描述 |
| public File(String filename) | 創建File對象,filename表示文件或目錄的路徑 |
| public File(String parent,String child) | 創建File對象,parent表示上級目錄,child表示指定的子目錄或文件名 |
| public File(File obj,String child) | 設置File對象,obj表示File對象,child表示指定的子目錄或文件名 |
使用表12.2所列的哪種構造方法要由其他被訪問的文件來決定。例如,當在應用程序中只用到一個文件時,使用第1種構造方法最合適;如果使用了一個公共目錄下的幾個文件,那么使用第2種或第3種構造方法會更方便。
創建File類的對象后,就可以使用File的相關方法來獲取文件信息。接下來,先了解一下File類的常用方法,如表12.3所示。
表12.3?File類常用方法
| 常用方法 | 功能描述 | 備注 |
| String getName() | 獲取相關文件名 | 與文件名相關的方法 |
| String getPath() | 獲取文件路徑 | |
| String getAbsolutePath() | 獲取文件絕對路徑 | |
| String getParent() | 獲取文件上級目錄名稱 | |
| boolean renameTo(File newName) | 更改文件名,成功則返回true,否則返回false | |
| boolean exists() | 檢測文件對象是否存在 | 文件測定相關方法 |
| boolean canWrite() | 檢測文件對象是否可寫 | |
| boolean canRead() | 檢測文件對象是否可讀 | |
| boolean isFile() | 檢測文件對象是否是文件 | |
| boolean isDirectory() | 檢測文件對象是否是目錄 | |
| boolean isAbsolute() | 檢測文件對象是否是絕對路徑 | |
| long lastModified() | 返回此File對象表示的文件或目錄最后一次被修改的時間 | 常用文件信息和方法 |
| long length() | 返回此File對象表示的文件或目錄的長度 | |
| boolean delete() | 刪除文件或目錄。如果File對象為目錄,則該目錄為空,方可刪除。刪除成功,返回true,否則返回false | |
| boolean mkdir() | 創建File對象指定目錄。如果創建成功,則返回true,否則返回false | 目錄相關類工具 |
| boolean mkdirs() | 創建File對象指定的目錄,如果此目錄的父級不存在,則還會創建父目錄。如創建成功,則返回true,否則返回false | |
| String []list() | 返回此File對象表示的目錄中的文件和目錄的名稱所組成字符串數組 |
????接下來,通過一個案例來演示File類常用方法的基本使用,先在當前目錄創建一個1201.txt文件,在里面輸入“AAA軟件教育歡迎您!”,然后編寫代碼,如例12-1所示。
例12-1?Demo1201.java
1?package com.aaa.p120201;
2?import java.io.*;
3?import java.util.*;
4?import java.text.SimpleDateFormat;
5
6?public class Demo1201 {
7?????public static void main(String[] args) {
8?File file = new File("src/1201.txt");
9?System.out.println("文件是否存在-->" + file.exists());
10?System.out.println("文件是否可寫-->" + file.canWrite());
11?System.out.println("文件是否可讀-->" + file.canRead());
12?System.out.println("文件是否是文件-->" + file.isFile());
13?System.out.println("文件是否是目錄-->" + file.isDirectory());
14?System.out.println("文件是否是絕對路徑-->" + file.isAbsolute());
15?System.out.println("文件名是-->" + file.getName());
16?System.out.println("文件的路徑是-->" + file.getPath());
17?System.out.println("文件的絕對路徑是-->" + file.getAbsolutePath());
18?System.out.println("文件的上級路徑是-->" + file.getParent());
19?SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
20?System.out.print("最后修改時間-->");
21?System.out.println(sdf.format(new Date(file.lastModified())));
22?System.out.println("文件長度是-->" + file.length());
23?????}
24?}
程序的運行結果如下:
文件是否存在-->true
文件是否可寫-->true
文件是否可讀-->true
文件是否是文件-->true
文件是否是目錄-->false
文件是否是絕對路徑-->false
文件名是-->1201.txt
文件的路徑是-->src\1201.txt
文件的絕對路徑是-->D:\work\AAA課程研發\教材編寫\javaIO\src\1201.txt
文件的上級路徑是-->src
最后修改時間-->2021-06-15
文件長度是-->25
例12-1在程序中構造了File類的對象,運用File類的各個方法得到文件的各種相關屬性。在第19~21行代碼中,通過格式化時間信息,獲取文件最后修改時間,最后打印文件1201.txt相關屬性的信息。
12.2.2?遍歷目錄下的文件
File類用來操作文件和獲得文件的信息,但是不提供對文件讀取的方法,這些方法由文件流提供。File類中提供了list()方法和listFiles()方法,用來遍歷目錄下所有文件。兩者不同之處是list()方法只返回文件名,沒有路徑信息;而listFiles()方法不但返回文件名稱,還包含有路徑信息。
接下來,通過案例來演示list()方法與listFiles()方法的使用,如例12-2所示。
例12-2?Demo1202.java
1?package com.aaa.p120202;
2?import java.io.*;
3
1?public class Demo1202 {
2?public static void main(String[] args) {
3?System.out.printf("***********list()方法***********");
4?File file = new File("D:\\javaCode");?????????//?創建File對象
5?if (file.isDirectory()) {?????????//?判斷file目錄是否存在
6?String[] list = file.list();
7?for (String fileName : list) {
8?System.out.println(fileName);?????????//?打印文件名
9?}
10?}
11?System.out.printf("***********listFiles()方法***********");
12?files(file);
13?}
14?public static void files(File file) {
15?File[] listFile = file.listFiles();?????????//?遍歷目錄下所有文件
16?for (File f : listFile) {
17?if (f.isDirectory()) {?????????//?判斷是否是目錄
18?files(f);?????????????//?遞歸調用
19?}
20?System.out.println(f.getAbsolutePath());
21?}
22?}
23?}
程序的運行結果如下:
***********list()方法***********
chapter02
test.txt
***********listFiles()方法***********
D:\javaCode\chapter02\.idea\.gitignore
D:\javaCode\chapter02\.idea\misc.xml
D:\javaCode\chapter02\.idea\modules.xml
D:\javaCode\chapter02\.idea\uiDesigner.xml
D:\javaCode\chapter02\.idea\workspace.xml
D:\javaCode\chapter02\.idea
D:\javaCode\chapter02\chapter02.iml
D:\javaCode\chapter02\out\production\chapter02\Demo02.class
D:\javaCode\chapter02\out\production\chapter02\Demo0201.class
例12-2中,首先創建File對象,指定File對象的目錄。第5~10行代碼先判斷file目錄是否存在,若存在,則調用list()方法,第6行代碼以String數組的形式得到所有文件名,最后循環遍歷數組內容并打印。如果目錄下仍然有子目錄則不能遍歷到,此時就需要用到File類的listFiles()方法,遍歷目錄下所有文件之后,循環判斷遍歷到的是否是目錄,如果是目錄,則再次遞歸調用file(file)方法本身。第14~22行代碼是自定義的靜態方法,直到遍歷完到文件。通過程序運行結果可以看到,listFiles()方法輸出的信息比list()方法輸出的信息更加詳細,而且listFiles()方法返回值是File類型,可以直接使用該文件。
注意:在Windows系統中,目錄的分隔符是反斜杠(\)。但是,在Java語言中,使用反斜杠表示轉義字符,所以如果需要在Windows系統的路徑下包括反斜杠,則應該使用兩條反斜線,如D:\\javaCode或者直接用斜線(/)也可以。
12.2.3?刪除文件及目錄
在程序設計中,除了遍歷文件,文件的刪除操作也很常見,Java中通過使用File類的delete()方法來對文件進行刪除操作。
接下來,演示如何刪除文件及目錄,如例12-3所示。
例12-3?Demo1203.java
1?package com.aaa.p120203;
2?import java.io.*;
3
4?public class Demo1203 {
5?public static void main(String[] args) {
6?String path = "D:/javaCode/chapter02/out";
7?deleteD(path);
8?}
9?private static void deleteD(String pp) {
10?File file = new File(pp);
11?if (file.isFile()) {
12?while (file.exists()) {
13?System.out.println("刪除了文件:" + file.getName());
14?file.delete();
15?}
16?} else {
17?File[] listFiles = file.listFiles();
18?for (File file2 : listFiles) {
19?try {
20?deleteD(file2.getAbsolutePath());
21?} catch (Exception e) {
22?System.out.println(e.getMessage());
23?}
24?}
25?file.delete();
26?}
27?}
28?}
程序的運行結果如下:
刪除了文件:Demo02.class
刪除了文件:Demo0201.class
刪除了文件:Demo0202.class
刪除了文件:Demo03.class
刪除了文件:Demo05.class
刪除了文件:Demo06.class
例12-3中,在main()方法中deleteD(File file)方法中將待刪除的內容以字符串形式傳入,調用方法時創建File對象,然后遍歷該目錄下所有文件,判斷遍歷到的是否是目錄,如果是目錄,繼續遞歸調用方法本身,如果是文件則輸出文件信息然后直接刪除,刪除文件完成后,將目錄刪除。第18~23行代碼增加了確保程序健壯性的異常信息處理。
注意:File類的delete()方法只是刪除一個指定的文件,如果目錄下還有子目錄,是無法直接刪除的,需要遞歸刪除。另外,在Java中是直接從虛擬機中將文件或目錄刪除,可以不經過回收站,文件無法恢復,所以使用delete()操作時要謹慎。
12.2.4?RandomAccessFile類
Java提供的RandomAccessFile類,允許從文件的任何位置進行數據的讀寫。它不屬于流,是Object類的子類,但它融合了InputStream類和OutStream類的功能,既能提供read()方法和write()方法,還能提供更高級的直接讀寫各種基本數據類型數據的讀寫方法,如readInt()方法和writeInt()方法等。
RandomAccessFile類的中文含義為隨機訪問文件類,隨機意味著不確定性,指的是不需要從頭讀到尾,可以從文件的任意位置開始訪問文件。使用RandomAccessFile類,程序可以直接跳到文件的任意地方讀、寫文件,既支持只訪問文件的部分數據,又支持向已存在的文件追加數據。
為支持任意讀寫,RandomAccessFile類將文件內容存儲在一個大型的byte數組中。RandomAccessFile類設置指向該隱含的byte數組的索引,稱為文件指針,通過從文件開頭就開始計算的偏移量來標明當前讀寫的位置。
????RandomAccessFile類有兩個構造方法,其實這兩個構造方法基本相同,只是指定文件的形式不同而已,一個使用String參數來指定文件名,一個使用File參數來指定文件本身。具體示例如下:
//?訪問file參數指定的文件,訪問的形式由mode參數指定
public RandomAccessFile(File file, String mode)
//?訪問name參數指定的文件,訪問的形式由mode參數指定
public RandomAccessFile(String name, String mode)
????在創建RandomAccessFile對象時還要設置該對象的訪問形式,具體使用一個參數mode進行指定,mode的值及對應的訪問形式如表12.4所示。
表12.4 mode的值及含義
| mode值 | 含義 |
| “r” | 以只讀的方式打開,如果試圖對該RandomAccessFile執行寫入方法,都將拋出IOException異常 |
| “rw” | 以讀、寫方式打開指定文件,如果該文件不存在,則嘗試創建該文件 |
| “rws” | 以讀、寫方式打開指定文件,相較于“rw”模式,還需要對文件的內容或元數據的每個更新都同步寫入到底層存儲設備 |
| “rwd” | 以讀、寫方式打開指定文件,相較于“rw”模式,還要求對文件內容的每個更新都同步寫入到底層存儲設備 |
????隨機訪問文件是由字節序列組成,一個稱為文件指針的特殊標記定位這些字節中的某個字節的位置,文件的讀寫操作就是在文件指針所在的位置上進行的。打開文件時,文件指針置于文件的起始位置,在文件中進行讀寫數據后,文件指針就會移動到下一個數據項。如表12.5所示,列出了RandomAccessFile類所擁有的用來操作文件指針的方法。
表12.5 RandomAccessFile類操作指針的方法
| 方法聲明 | 功能描述 |
| long getFilePointer() | 獲取當前讀寫指針所處的位置 |
| void seek(long pos) | 指定從文件起始位置開始的指針偏移量,即設置讀指針的位置 |
| int skipBytes(int n) | 使讀寫指針從當前位置開始,跳過n個字節 |
| void setLength(long num) | 設置文件長度 |
????接下來,通過案例演示?RandomAccessFile類操作文件指針的方法的使用,如例12-4所示。
例12-4?Demo1204.java
1?package com.aaa.p1202;
2?import java.io.*;
3?import java.io.IOException;
4?import java.io.RandomAccessFile;
5
6?public class Demo1204 {
7?public static void main(String[] args) {
8?File file = new File("d:/javaCode/test.txt");
9?RandomAccessFile raf = null;?????????//?聲明RandomAccessFile對象
10
11?try{
12?raf = new RandomAccessFile(file,"rw");
13?for(int n = 0;n < 10;n++){
14?raf.writeInt(n);
15?}
16?System.out.println("當前指針位置:" + raf.getFilePointer());
17?System.out.println("文件長度:" + raf.length() + "字節");
18?raf.seek(0);?????????????????????????????//?返回數據的起始位置
19
20?System.out.println("當前指針位置:" + raf.getFilePointer());
21?System.out.println("讀取數據");
22?for(int n = 0;n < 6;n++){
23?System.out.println("數值:" + raf.readInt() + "-->" +
24?(raf.getFilePointer() - 4));
25?if(n == 3)raf.seek(32);?????????????????//?指針跳過?4 5 6 7
26?}
27?raf.close();?????????????????//?關閉隨機訪問文件流
28?}catch (IOException e){
29?e.printStackTrace();
30?}
31?}
32?}
程序的運行結果如下:
第0個值:0
當前指針位置:40
文件長度:40字節
當前指針位置:0
讀取數據
數值:0-->0
數值:1-->4
數值:2-->8
數值:3-->12
數值:8-->32
數值:9-->36
例12-4中,先向test.txt文件寫入0~9十個數字,此時文件的長度為10個int字節,數據指針位置為40。如果要讀取文件的數據信息,則需要把文件指針移動到文件的起始位置,而執行seek(0)可以達到目的。雖然開始時會循環讀取數據,但在i為3時,將指針移動到32,即跳過5、6、7、8,直接開始讀取8和9,對應指針位置時32和36。
12.3?字節流
在前面小節中,我們學習了File類對文件或目錄進行操作的方法,但是File類不包含向文件讀寫數據的方法。為了進一步進行文件輸入/輸出操作,需要使用正確的Java I/O類來創建對象。在程序設計中,程序如果要讀取或寫入8位bit的字節數據,應該使用字節流來處理。字節流一般用于讀取或寫入二進制數據,如圖片、音頻文件等。一般而言,只要是“非文本數據”就應該使用字節流來處理。
12.3.1?字節流概述
在計算機中,無論是文本、圖片、音頻還是視頻,所有的文件都能以二進制(bit,1字節為8bit)形式傳輸或保存。Java中針對字節輸入/輸出操作提供了一系列流,統稱為字節流。程序需要數據的時候要使用輸入流來讀取數據,而當程序需要將一些數據保存起來的時候就需要使用輸出流來完成。在Java中,字節流提供了兩個抽象基類InputStream和OutputStream,分別用于處理字節流的輸入和輸出。因為抽象類不能被實例化,所以在實際使用中,使用的是這兩個類的子類。這里還需要強調的是,輸入流和輸出流的概念是有一個參照物的,參照物就是站在程序的角度來理解這兩個概念,如圖12.3所示。
圖12.3中,從文件到程序是輸入流(InputStream),通過程序,讀取文件中的數據;從程序到文件是輸出流(OutputStream),將數據從程序輸出到文件。????
????InputStream類和OutputStream類都是抽象類,不能被實例化,所以如果要實現不同數據源的操作功能,須要用到它們的子類,這些子類可以在JDK的API文檔里的類層次結構中查看,如圖12.4和圖12.5所示。
?
圖12.4 InputStream子類結構圖 圖12.5 OutputStream子類結構圖
從圖12.4和圖12.5中可看出,InputStream和OutputStream的子類雖然較多,但都有規律可循。因為輸入流或輸出流的數據源或目標的數據格式不同,如字節數組、文件、管道等,所以子類在命名的時候采用的格式是數據類型加抽象基類名。例如,FileInputStream的子類表示從文件中讀取信息,InputStream為后綴。此外,InputStream和OutputStream的子類大多都是成對出現的,如數據過濾流FilterInputStream和FilterOutputStream。
InputStream類定義了輸入流的一般方法,是字節輸入流的父類,其他字節輸入流都是在其基礎上做功能上的增強。因此,了解了InputStream類就為了解其他輸入流打下了基礎,表12.6列出了InputStream類的常用方法。
表12.6 InputStream類的常用方法
| 方法聲明 | 功能描述 |
| public int available() | 獲取輸入流中可以不受阻塞地讀取的字節數 |
| public void close() | 關閉輸入流并釋放與該流關聯的所有系統資源,該方法由子類重寫 |
| public void mark(int readlimit) | 在此輸入流中標記當前的位置,該方法由子類重寫 |
| public boolean markSupported() | 判斷當前輸入流是否允許標記。若允許,則返回true,否則返回false |
| public long skip(long n) | 從輸入流中跳過指定n個指定的字節,并返回跳過的字節數 |
| public int read() | 從輸入流中讀取數據的下一個字節 |
| public int read(byte[] b) | 從輸入流中讀取一定數量的字節,并將其存儲在緩沖區數組 b 中,返回讀取的字節數。如果已經到達末尾,則返回-1 |
| public int read(byte[] b, int off, int len) | 將輸入流中最多 len 個數據字節讀入 byte 數組。然后將讀取的b數據以int返回。如果已經到達末尾,則返回-1 |
| public void reset() | 將輸入流重新定位到最后一次對此輸入流設置標記的起始處 |
????表12.6中列出了InputStream類的方法,上述所有方法都聲明拋出IOException異常,因此使用時要注意處理異常。InputStream使用最多的方法為read()和close()方法,前者從已存在的文件中讀取字節,在工作做完之后,由后者關閉字節流,釋放系統資源,如果不關閉會浪費一定量的系統資源,會導致計算機運行效率下降。read()方法有構成函數重載的3種形式,無參的read()方法可以用來將字節挨個讀入,另外兩個可以指定一個字節數組作為讀取字節的批量,甚至可以通過定義off和len,指定讀取字節的起始位置和長度。
下面我們來看一看OutputStream類,它擁有和InputStream類似的用法和相對的功能方法。
表12.7 OutputStream類的常用方法
| 方法聲明 | 功能描述 |
| void close() | 關閉此輸出流,并釋放與之有關的所有系統資源,由子類重寫該方法 |
| void flush() | 刷新此輸出流,并強制寫出所有緩沖的輸出字節 |
| void write(byte[] b) | 將 數組b的數據寫到輸出流 |
| void write(int b) | 將指定的int字節b寫入此輸出流 |
| void write(byte[] b, int off, int len) | 將指定 byte 數組b中寫入到輸出流,從偏移量 off 開始的 len 個字節寫入此輸出流 |
????表12.7所列的OutputStream類的常用方法可分為兩類:3個重載write()方法能夠向文件中寫入數據,可以選擇挨個或以數組的方式;flush()方法和close()方法能夠操作輸出流本身,close()方法關閉此流并釋放系統資源,flush()方法會強制將緩沖區中的字節寫入文件中,即使緩沖區還沒有裝滿,該方法可在流關閉前調用,用于清空緩沖區。
12.3.2?讀寫文件
FileInputStream類和FileOutputStream類用于從文件/向文件讀取/寫入字節數據,FileInputStream是InputStream的子類,用來從文件中讀取數據,操作文件的字節輸入流;FileOutputStream是OutputStream的子類,可以指定文件名創建實例,一旦創建文檔就開啟,接著就可以用來寫入數據。二者在使用時,都不需要用close()關閉文檔。
????接下來,通過實例來演示讀取本地文件的流程。為了方便,我們先在當前目錄下新建一個名為“read.txt”的文件,并向其中寫入“AAA軟件教育”,接著編寫程序將文件中的內容讀出并打印到控制臺,如例12-5所示。
例12-5?Demo1205.java
1?package com.aaa.p120302;
2?import java.io.*;
3
4?public class Demo1205 {
5?public static void main(String[] args) {
6?FileInputStream fileInput = null;
7?try {
8?fileInput = new FileInputStream("read.txt");?????????//?創建文件輸入流對象
9?int n = 1024;??????????????????????????????????//?設定讀取的字節數
10?byte buffer[] = new byte[n];
11?//?讀取輸入流
12?while ((fileInput.read(buffer, 0, n) != -1) && (n > 0)) {
13?System.out.print(new String(buffer));
14?}
15?} catch (Exception e) {
16?System.out.println(e);
17?} finally {
18?if (fileInput != null){
19??????????try {
20?????????????fileInput.close();?????????????????????????//?釋放資源
21?????????????} catch (IOException e) {
22?????????????e.printStackTrace();
23?????????????}
24?}
25?}
26?}
27?}
程序的運行結果如下:
AAA軟件教育
在例12-5中,我們建立了一個長度為1024的byte數組,將其傳入read()方法中,并設置始末位置為0到n,此時read()方法一次讀1024個字節。運行之后,我們看到控制臺打印出“AAA軟件教育”。
注意:在例12-5中,如果程序中途出現錯誤,程序將直接中斷,所以一定要將關閉資源的close()方法寫到finally中。另外,由于finally中不能直接訪問try中的內容,所以要將FileInputStream定義在try的外面。由于篇幅有限,后面的代碼不再重復異常處理的標準寫法,直接將異常拋出。
需要注意的是,當創建文件輸入流時,一定要保證目錄下有對應文件存在,否則會報FileNotFoundException異常,提示“java.io.FileNotFoundException: read.txt (系統找不到指定的文件)”。
明白了FileInputStream的用法,下面我們來看看與之相對的FileOutputStream,它使用字節流向一個文件中寫入內容,兩者用法相似,如例12-6所示。
例12-6?Demo1206.java
1?package com.aaa.p120302;
2?import java.io.*;
3
4?public class Demo1206 {
5?public static void main(String[] args) throws Exception {
6?System.out.print("請輸入要保存到文件的內容:");
7?int count, n = 1024;
8?byte buffer[] = new byte[n];
9?count = System.in.read(buffer);?????????????????//?讀取標準輸入流
10?//?創建文件輸出流對象
11?FileOutputStream fileOutput = new FileOutputStream("read.txt");
12?fileOutput.write(buffer, 0, count);?????????//?寫入輸出流
13?System.out.println("已保存到read.txt!");
14?fileOutput.close();?????????????//?釋放資源
15?}
16?}
程序的運行結果如下:
請輸入要保存到文件的內容:AAA軟件歡迎你
已保存到read.txt!
例12-6程序的運行結果顯示已保存到read.txt,此時文件內容如下:
AAA軟件歡迎你
????與輸入流不同的是,當文件不存在時,輸出流會先創建文件再向其中寫入內容。當文件已經存在時,會先將原本的內容清空,再向其中寫入。例12-6中執行之后,原本的內容被替換成了新的內容。如果想要保留原來內容,只需要在原來的基礎上構造輸出流時追加一個Boolean類型的參數,該參數用于指定是否為追加寫入,如果為true,就能夠在源文件尾部的下一行寫入內容了,如例12-7所示。
例12-7?Demo1207.java
1?package com.aaa.p120302;
2?import java.io.*;
3
4?public class Demo1207 {
5?public static void main(String[] args) throws Exception {
6?System.out.print("請輸入要保存到文件的內容:");
7?int count, n = 1024;
8?byte buffer[] = new byte[n];
9?count = System.in.read(buffer);?????????????????//?讀取標準輸入流
10?//?創建文件輸出流對象
11?FileOutputStream fileOutput = new FileOutputStream("read.txt", true);
12?fileOutput.write(buffer, 0, count);?????????//?寫入輸出流
13?System.out.println("已保存到read.txt!");
14?fileOutput.close();?????????????//?釋放資源
15?}
16?}
程序的運行結果如下所示:
請輸入要保存到文件的內容:專業的軟件培訓機構
已保存到read.txt!
運行結果顯示已保存到read.txt,由于我們是自行創建了read.txt?文件,并在例12-6中重寫,而本次運行的結果是將對應內容追加到read.txt中,此時文件內容如下:
AAA軟件歡迎你
專業的軟件培訓機構
????通過例12-7可以看出,構造FileOutputStream時聲明append參數為true,即可在原文件基礎上寫入新內容。
12.3.3?文件拷貝
前面我們分別講解了文件輸入流和文件輸出流的使用,現在我們將二者結合起來,就能夠完成更復雜的操作,這也是我們在日常開發中可能使用到的。
輸入流和文件輸結合使用可以實現文件的復制,首先我們來做一些準備工作。在當前目錄下建立兩個文件夾,分別命名為image、target,之后向image目錄下存放一張圖片,并命名為img.png。然后,開始編寫代碼,如例12-8所示。
例12-8?Demo1208.java
1?package com.aaa.p1203;
2?import java.io.*;
3
4?public class Demo1208 {
5?public static void main(String[] args) throws Exception {
6?//?創建文件輸入流對象
7?FileInputStream input = new FileInputStream("image\\img.png");
8?//?創建文件輸出流對象
9?FileOutputStream output = new FileOutputStream("target\\img.png");
10?int len;????????????????????????????????????????//?定義len,記錄每次讀取的字節
11?long begin = System.currentTimeMillis();????//?拷貝文件前的系統時間
12?while ((len = input.read()) != -1) {?????????//?讀取文件并判斷是否到達文件末尾
13?output.write(len);?????????????????//?將讀到的字節寫入文件
14?}
15?long end = System.currentTimeMillis();?????????//?拷貝文件后的系統時間
16?System.out.println("拷貝文件耗時:" + (end - begin) + "毫秒");
17?output.close();?????????????????//?釋放資源
18?input.close();
19?}
20?}
程序的運行結果如下所示:
拷貝文件耗時:875毫秒
控制臺中打印出了程序拷貝文件所消耗的時間,而圖片就在這段時間內由字節流的方式實現了拷貝,如圖12.6所示。
?
圖12.6 InputStream子類結構圖 圖12.7 OutputStream子類結構圖
由于不同計算機性能不同,或同一個計算機在不同情況下負載不同,拷貝圖片所消耗的時間都有可能會有差別,具體時間以現實情況為準。
注意:在例12-8中,指定image和target的目錄用“\\”,這是因為windows系統目錄用反斜杠“\”表示,但Java中反斜杠是特殊字符,所以寫成“\\”指定路徑,也可以使用“/”指定目錄,如“image/img.png”。
12.3.4?字節流的緩沖區
前文講解了字節流拷貝文件,還有一種更高效的拷貝方式,那就是在使用中加上緩沖區,緩沖區可以幫助提升字節傳輸效率。因為,不加緩沖區的時候是一個字節一個字節地傳輸,而加了緩沖區后則是先將字節填滿一個緩沖區,再將整個緩沖區的字節一并傳輸,這樣可以顯著降低傳輸次數,提升傳輸效率。每次傳輸都會消耗一定的時間,但是使用緩沖區會在本地占用一定的空間,這屬于空間換時間的方式,
接下來,通過案例來演示緩沖區在字節流拷貝中的用法,如例12-9所示。
例12-9?Demo1209.java
1?package com.aaa.p120304;
2?import java.io.*;
3
4?public class Demo1209 {
5?public static void main(String[] args) throws Exception {
6?//?創建文件輸入流對象
7?FileInputStream input = new FileInputStream("image\\img.png");
8?//?創建文件輸出流對象
9?FileOutputStream output = new FileOutputStream("target\\img.png");
10?byte[] b = new byte[1024];?????????????//?定義緩沖區大小
11?int len;?????????????????//?定義len,記錄每次讀取的字節
12?long begin = System.currentTimeMillis();//?拷貝文件前的系統時間
13?while ((len = input.read(b)) != -1) {????//?讀取文件并判斷是否到達文件末尾
14?output.write(b, 0, len);?????????????//?從第1個字節開始,向文件寫入len個字節
15?}
16?long end = System.currentTimeMillis();????//?拷貝文件后的系統時間
17?System.out.println("拷貝文件耗時:" + (end - begin) + "毫秒");
18?output.close();?????????????//?釋放資源
19?input.close();
20?}
21?}
程序的運行結果如下:
拷貝文件耗時:38毫秒
從例12-9的運行結果可以看出,與例12-8相比,拷貝所耗的時間大大地降低了,說明使用緩沖區有效減少了字節流的傳輸次數,從而提升了程序的運行效率。
除了上面這種方式,還有一種封裝性更好,更易用的方式來使用帶緩沖區的I/O流,那就是BufferedInputStream和BufferedOutputStream,它們的構造器接收對應的I/O流,并返回帶緩沖的BufferedInputStream對象和BufferedOutputStream對象,這體現出了裝飾設計模式的思想,其接收的參數為裝飾對象,返回的類為裝飾結果,結構如圖12.8所示。
從圖12.8可以看出,在程序和文件之間的核心由節點流傳輸數據,如我們在之前所講到的FileInputStream和FileOutputStream。在外層為節點流的封裝,如我們現在講的BufferedInputStream和BufferedOutputStream。
接下來,我們通過案例來演示緩沖流的使用,如例12-10所示。
例12-10?Demo1210.java
1?package com.aaa.p120304;
2?import java.io.*;
3
4?public class Demo1210 {
5?public static void main(String[] args) throws Exception {
6?//?創建文件輸入流對象
7?FileInputStream fInput = new FileInputStream("image\\img.png");
8?//?創建文件輸出流對象
9?FileOutputStream fOutput = new FileOutputStream("target\\img.png");
10?//?將創建的節點流的對象作為形參傳遞給緩沖流的構造方法中
11?BufferedInputStream bInput = new BufferedInputStream(fInput);
12?BufferedOutputStream bOutput = new BufferedOutputStream(fOutput);
13?int len;????????????????????????????????????//?定義len,記錄每次讀取的字節
14?long begin = System.currentTimeMillis();//?拷貝文件前的系統時間
15?while ((len = bInput.read()) != -1){????????//?讀取文件并判斷是否到達文件末尾
16?bOutput.write(len);?????????//?將讀到的字節寫入文件
17?}
18?long end = System.currentTimeMillis();????//?拷貝文件后的系統時間
19?System.out.println("拷貝文件耗時:" + (end - begin) + "毫秒");
20?bInput.close();
21?bOutput.close();
22?}
23?}
程序的運行結果如下:
拷貝文件耗時:51毫秒
例12-10的運行結果如上所示,拷貝img文件的時間為51毫秒,和未使用緩沖時相比,拷貝的速度明顯加快,因為緩沖流內部定義了一個長度為8192的字節數組作為緩沖區,在使用read()方法或write()方法進行讀寫時首先將數據存入該數組中,然后以數組為對象進行操作,這顯著降低了操作次數,讓程序完成同樣的工作花費了更少的時間。
12.4?字符流
前文講解了使用InputStream和OutputStream來處理字節流,也就是二進制文件,而Reader和Writer是用來處理字符流的,也就是文本文件。與文件字節輸入/輸出流的功能一樣,文件字符輸入/輸出類Reader和Writer只是建立了一條通往文本文件的通道,而要實現對字符數據的讀寫操作,還需要相應的讀方法和寫方法來完成。
12.4.1?字符流概述
除了字節流,Java還提供了字符流,用于操作字符。與字節流類似,字符流也有兩個抽象基類,分別是Reader和Writer。Reader是字符輸入流,用于從目標文件讀取字符;Writer是字符輸出流,用于向目標文件寫入字符。字符流也是由兩個抽象基類衍生出很多子類,由子類來實現功能,先來了解一下它們的子類結構,如圖12.9和圖12.10所示。
?
圖12.9 Reader子類結構圖 圖12.10 Writer子類結構圖
可以看出,字符流與字節流相似,也是很有規律的,這些子類都是以它們的抽象基類為結尾命名的,并且大多以Reader和Writer結尾,如CharArrayReader和CharArrayWriter。接下來,我們詳細講解字符流的使用。
12.4.2?操作文件
Reader和Writer有眾多子類,其中FileReader和FileWriter是兩個很常用的子類,FileReader類是用來從文件中讀取字符的,操作文件的字符輸入流。
接下來,通過案例來演示如何從文件中讀取字符。首先在當前目錄新建一個文本文件read.txt,文件內容如下:
AAA軟件教育
fileReader
????創建文件完成后,開始編寫代碼,如例12-11所示。
例12-11?Demo1211.java
1?package com.aaa.p120402;
1?import java.io.*;
2
3?public class?Demo1211?{
4?public static void main(String[] args) throws Exception {
5?File file = new File("read.txt");
6?FileReader fileReader = new FileReader(file);
7?int len;?????????????????????????????//?定義len,記錄讀取的字符
8?while ((len = fileReader.read()) != -1){????//?判斷是否讀取到文件的末尾
9?System.out.print((char) len);?????????????//?打印文件內容
10?}
11?fileReader.close();?????????????????????//?釋放資源
12?}
13?}
程序的運行結果如下:
AAA軟件教育
fileReader
例12-11中,首先定義一個文件字符輸入流,然后在創建輸入流實例時,將文件以參數傳入,讀取到文件后,用變量len記錄讀取的字符,然后循環輸出。這里要注意len是int類型,所以輸出時要強轉類型,第10行中將len強轉為char類型。
????與FileReader類對應的是FileWriter類,它是用來將字符寫入文件,操作文件字符輸出流的。
接下來,通過案例來演示如何將字符寫入文件,如例12-12所示。
例12-12?Demo1212.java
1?package com.aaa.p120402;
2?import java.io.*;
3
4?public class?Demo1212?{
5?public static void main(String[] args) throws Exception {
6?File file = new File("read.txt");
7?FileWriter fileWriter = new FileWriter(file);
8?fileWriter.write("AAA軟件專業的Java學習平臺");?????????????//?寫入文件的內容
9?System.out.println("已保存到read.txt!");
10?fileWriter.close();?????????????????????????????????????????//?釋放資源
11?}
12?}
程序的運行結果如下:
已保存到read.txt!
例12-12運行結果顯示已保存到read.txt文件,文件內容如下:
AAA軟件專業的Java學習平臺
FileWriter與FileOutputStream類似,如果指定的目標文件不存在,則先新建文件,再寫入內容,如果文件存在,會先清空文件內容,然后寫入新內容,但是結尾不加換行符。如果想在文件內容的末尾追加內容,則需要調用構造方法FileWriter?(String FileName,boolean append)來創建文件字符輸出流對象,將參數append指定為true即可,將例12-12第5行代碼修改如下:
FileWriter fileWriter = new FileWriter(file, true);
????再次運行程序,輸出流會將字符追加到文件內容的末尾,不會清除文件本身的內容,結尾同樣是沒有換行符的。
12.4.3?轉換流
前文分別講解了字節流和字符流,有時字節流和字符流之間可能也需要進行轉換,在JDK中提供了可以將字節流轉換為字符流的兩個類,分別是InputStreamReader類和OutputStreamWriter類,它們被稱之為轉換流。其中,OutputStreamWriter類可以將一個字節輸出流轉換成字符輸出流,而InputStreamReade類可以將一個字節輸入流轉換成字符輸入流。轉換流的出現方便了對文件的讀寫,它在字符流與字節流之間架起了一座橋梁,使原本沒有關聯的兩種流的操作能夠進行轉換,提高了程序的靈活性。通過轉換流進行讀寫數據的過程,如圖12.11所示。
圖12.11 轉換流示意圖
圖12.11中,程序向文件寫入數據時將輸出的字符流轉變為字節流,程序從文件讀取數據時將輸入的字節流變為字符流,有效地提高了讀寫效率。
接下來,通過案例來演示轉換流的使用。首先在當前目錄新建一個文本文件Conversion.txt,文件內容為“AAA軟件教育”。創建文件完成后,開始編寫代碼,如例12-13所示。
例12-13?Demo1213java
1?package com.aaa.p120403;
2?import java.io.*;
3
4?public class Demo1213 {
5?public static void main(String[] args) throws IOException {
6?//?創建字節輸入流
7?FileInputStream input = new FileInputStream("Conversion.txt");
8?//?將字節輸入流轉換為字符輸入流
9?InputStreamReader inputReader = new InputStreamReader(input);
10?//?創建字節輸出流
11?FileOutputStream output = new FileOutputStream("target.txt");
12?//?將字節輸出流轉換成字符輸出流
13?OutputStreamWriter outputWriter = new OutputStreamWriter(output);
14?int str;
15?while ((str = inputReader.read()) != -1) {
16?outputWriter.write(str);
17?}
18?outputWriter.close();
19?inputReader.close();
20?}
21?}
例12-14程序運行結束后,會在當前目錄生成一個target.txt文件,如圖12.12和圖12.13所示。
?
圖12.12 文件拷貝前 圖12.13 文件拷貝后
在例12-13中實現了字節流與字符流之間的互相轉換,將字節流轉換為字符流,從而實現直接對字符的讀寫。這里要注意,如果用字符流操作非文本文件,如操作視頻文件,很有可能會造成部分數據丟失。
12.5?對象序列化方式
Java提供了一種對象序列化的機制,該機制中一個對象可以被表示為一個字節序列,該字節序列包括該對象的數據、對象的類型和存儲在對象中的數據的類型。將序列化對象寫入文件之后,可以從文件中讀取出來,并且對它進行反序列化。也就是說,對象的類型信息、對象的數據,還有對象中的數據類型可以用來在內存中新建對象。上述整個過程都是Java虛擬機(JVM)獨立完成的,這樣在一個平臺上序列化的對象可以在另一個完全不同的平臺上反序列化該對象。
12.5.1?對象序列化概述
序列化機制可以將實現序列化的Java對象轉換成字節序列,而這些字節序列可以保存在磁盤上,或者通過網絡傳輸,以備以后重新恢復成原來的對象繼續使用。序列化機制可以使Java對象脫離程序的運行而獨立存在。
對象的序列化(Serialize)是指將一個Java對象寫入I/O流中,與此對應,對象的反序列化(Deserialize)則是指從I/O流中恢復該Java對象。
如果需要讓某個對象支持序列化機制,則必須讓它的類是可序列化的(Serializable)。為了讓某個類是可序列化的,該類就需要實現Serializable或者Externalizable這兩個接口之一,一般推薦使用Serializable接口,因為Serializable接口只需實現不需要重寫任何方法,使用起來較為簡單。
Java的很多類其實已經實現了Serializable,該接口是一個標記接口,實現該接口時無須實現任何方法,它只是表明該類的實例是可序列化的。所有可能在網絡上傳輸的對象的類都必須是可序列化的,否則程序可能會出現異常,如RMI(Remote Method Invoke,即遠程方法調用,是Java EE的基礎)過程中的參數和返回值。所有需要保存到磁盤里的對象的類都必須可序列化,如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象。
因為序列化是RMI過程的參數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎,所有的分布式應用常常需要跨平臺、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化。因此,序列化機制是Java EE平臺的基礎,通常建議程序創建的每個JavaBean類都實現Serializable接口。
12.5.2?如何實現對象序列化的持久化
如果需要將某個對象保存到磁盤上或者通過網絡傳輸,那么這個類就需要實現Serializable接口或者Extermalizable接口之一。
使用Serializable來實現序列化非常簡單,主要讓目標類實現Serializable接口即可,無須實現任何方法。一旦某個類實現了Serializable接口,該類的對象就是可序列化的,程序可以通過如下兩個步驟來序列化該對象:
??創建一個ObjectOutputStream,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎之上,代碼如下:
//?創建個?ObjectOutputStreamn輸出流
FileOutputStream fos = new FileOutputStream("person.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
??調用ObjectOutputStream對象的writeObject()方法輸出可序列化對象,代碼如下:
//?將一個Person對象輸出到輸出流中
oos.writeObject(person);
下面的程序定義了一個Person類,這個類就是一個普通的Java類,只是實現了Serializable接口,該接口代表該類的對象是可序列化的,代碼如下:
1?import java.io.Serializable;
2
3?public class Person implements Serializable {
4?private String name;
5?private Integer age;
6?public Person(String name, Integer age) {
7?this.name = name;
8?this.age = age;
9?}
10?public String getName() {
11?return name;
12?}
13?public void setName(String name) {
14?this.name = name;
15?}
16?public Integer getAge() {
17?return age;
18?}
19?public void setAge(Integer age) {
20?this.age = age;
21?}
22?@Override
23?public String toString() {
24?return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
25?}
26?}
接下來,通過案例來演示使用ObjectOutputStream將一個Person對象寫入磁盤文件,如例12-14所示。
例12-14?Demo1214.java
1?package com.aaa.p120502;
2
3?public class Demo1214 {
4?public static void main(String[] args) {
5?try (FileOutputStream fos = new FileOutputStream("person.txt");
6?ObjectOutputStream oos = new ObjectOutputStream(fos)) {
7?Person person = new Person("小喬", 18);
8?oos.writeObject(person);
9?} catch (IOException e) {
10?e.printStackTrace();
11?}
12?}
13?}
例12-14中,第6行代碼創建了一個ObjectOutputStream輸出流,這個ObjectOutputStream輸出流建立在一個文件輸出流的基礎之上,第8行代碼使用writeObject()方法將一個Person對象寫入輸出流。運行這段代碼,將會看到生成了一個Person.txt文件,該文件的內容就是Person對象。
如果想從二進制流中恢復Java對象,則需要使用反序列化。反序化的的步驟如下:
??創建一個ObjectInputStream輸入流,這個輸入流是個處理流,所以必須建立在其他節點流的基礎之上,代碼如下:
//?創建一個ObjectInputStream輸入流
FileInputStream fis = new FileInputStream("person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
??調用ObjectInputStream對象的readObject()方法讀取流中的對象,該方法返回一個Object類型的Java對象,如果程序知道該Java對象的類型,則可以將該對象強制類型轉換成其真實的類型,代碼如下:
//?從輸入流中讀取一個Java對象,并將其強制類型轉換為Person類
Person person = (Person)?ois.readObject();
接下來,通過案例來演示從剛剛生成的person.txt文件中讀取Person對象,如例12-15所示。
例12-15?Demo1215.java
1?package com.aaa.p120502;
2
3?public class Demo1215 {
4?public static void main(String[] args) {
5?try (FileInputStream fis = new FileInputStream("person.txt");
6?ObjectInputStream ois = new ObjectInputStream(fis)) {
7?Person person = (Person) ois.readObject();
8?System.out.println(person);
9?} catch (Exception e) {
10?e.printStackTrace();
11?}
12?}
13?}
例12-15中,第6行代碼將一個文件輸入流包裝成ObjectInputStream輸入流,第7行代碼使用readObject()讀取了文件中的Java對象,這就完成了反序列化過程。
必須指出的是,反序列化讀取的僅僅是Java對象的數據,而不是Java類,因此采用反序列化恢復Java對象時,必須提供該Java對象所屬類的class文件,否則將會引發ClassNotFoundException異常。
注意:在ObjectInputStream輸入流中的readObject()方法聲明拋出了ClassNotFoundException異常,也就是說,當反序列化時找不到對應的Java類時將會引發該異常。
如果使用序列化機制向文件中寫入了多個Java對象,使用反序列化機制恢復對象時必須按實際寫入的順序讀取。
當一個可序列化類有多個父類時(包括直接父類和間接父類),這些父類要么有無參數的構造器,?要么也是可序列化的,否則反序列化時將拋出InvalidClassException異常。如果父類是不可序列化的,只是帶有無參數的構造器,則該父類中定義的成員變量值不會序列化到二進制流中。
12.5.3?引用對象的序列化控制
前文中的Person類的兩個成員變量分別是String類型和Integer類型,如果某個類的成員變量的類型不是基本類型或String類型,而是另一個引用類型,那么這個引用類必須是可序列化的,否則擁有該類型成員變量的類也是不可序列化的。
下面的程序中,Teacher類持有一個Student類的引用,只有Student類是可序列化的,Teacher類才是可序列化?的。如果Student類不可序列化,則無論Teacher類是否實現Serilizable或Externalizable接口,則Teacher類都是不可序列化的。代碼如下:
1?public class Teacher implements Serializable {
2?private String name;
3?private Student student;
4?public Teacher(String name, Student student) {
5?this.name = name;
6?this.student = student;
7?}
8?public String getName() {
9?return name;
10?}
11?public void setName(String name) {
12?this.name = name;
13?}
14?public Student getStudent() {
15?return student;
16?}
17?public void setStudent(Student student) {
18?this.student = student;
19?}
20?@Override
21?public String toString() {
22?return "Teacher{" +?"name='" + name + '\'' +", student=" + student +'}';
23?}
24?}
25
26?class Student implements Serializable {
27?private String name;
28?private Integer age;
29?public Student(String name, Integer age) {
30?this.name = name;
31?this.age = age;
32?}
33?public String getName() {
34?return name;
35?}
36?public void setName(String name) {
37?this.name = name;
38?}
39?public Integer getAge() {
40?return age;
41?}
42?public void setAge(Integer age) {
43?this.age = age;
44?}
45?@Override
46?public String toString() {
47?return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
48?}
49?}
注意:當程序序列化一個Teacher對象時,如果該Teacher對象持有一個Student對象的引用,為了在反序列化時可以正常恢復該Teacher對象,程序會順帶將該Student對象也進行序列化,所以Student類也必須是可序列化的,否則Teacher類將不可序列化。
現在假設有如下特殊情形:程序中有兩個Teacher對象,它們的student實例變量都引用同一個Student對象,而且該Student對象還有一個引用變量引用它,代碼如下:
Student student = new Student("小喬", 18);
Teacher teacher1 = new Teacher("周瑜", student);
Teacher teacher2 = new Teacher("曹操", student);
上述代碼創建了兩個Teacher對象和一個Student對象,這3個對象在內存中的存儲示意圖如圖12.14所示。
這里產生了一個問題,如果先序列化teacher1對象,則系統將該teacherl對象所引用的Student對象一起序列化。當程序再次序列化teacher2對象時,系統則一樣會序列化該teacher2對象,并且會再次序列化teacher2對象所引用的Student對象。如果程序再顯式序列化student對象,系統將再次序列化該Student對象。這個過程似乎會向輸出流中輸出3個Student對象。
如果系統向輸出流中寫入了3個Student對象,那么后果是當程序從輸入流中反序列化這些對象時,將會得到3個Student對象,從而導致teacher1和teacher2所引用的Student對象不是同一個對象,這顯然與圖12.12所示的效果不一致,也違背了Java序列化機制的初衷。所以,Java序列化機制采用了一種特殊的序列化算法,其算法內容如下:
??所有保存到磁盤中的對象都有一個序列化編號。
??當程序試圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化過,系統才會將該對象轉換成字節序列并輸出。
??如果某個對象已經序列化過,程序將只是直接輸出一個序列化編號,而不是再次重新序列化該對象。
根據上面的序列化算法,可以得到一個結論,當第2次、第3次序列化Student對象時,程序不會再次將Student對象轉換成字節序列并輸出,而是僅僅輸出一個序列化編號。例如,有如下順序的序列化代碼:
oos.writeObject(teacher1);
oos.writeObject(teacher2);
oos.writeObject(student);
上面代碼一次序列化了teacher1、teacher2和student對象,序列化后磁盤文件的存儲示意圖如圖12.15所示,通過改圖可以很好地理解Java序列化的底層機制。不難看出,當多次調用writeObject()方法輸出同一個對象時,只有當第1次調用writeObject()方法時才會將該對象轉換成字節序列并輸出。
接下來,通過案例來演示序列化兩個Teacher對象,兩個Teacher對象都持有一個引用同一個Student對象的引用,而且程序兩次調用writeObject()方法輸出同一個Teacher對象,如例12-16所示。
例12-16?Demo1216.java
1?package com.aaa.p120503;
2
3?public class?Demo1216?{
4?public static void main(String[] args) {
5?try (FileOutputStream fos = new FileOutputStream("teacher.txt");
6?ObjectOutputStream oos = new ObjectOutputStream(fos)) {
7?Student student = new Student("小喬", 18);
8?Teacher teacher1 = new Teacher("周瑜", student);
9?Teacher teacher2 = new Teacher("曹操", student);
10?oos.writeObject(teacher1);
11?oos.writeObject(teacher2);
12?oos.writeObject(student);
13?oos.writeObject(teacher2);
14?} catch (Exception e) {
15?e.printStackTrace();
16?}
17?}
18?}
例12-16中,4次調用了writeObject0方法來輸出對象,實際上只序列化了3個對象,而且序列的兩個Teacher對象的student引用實際是同一個Student對象。
接下來,通過案例來演示讀取序列化文件中的對象,如例12-17所示。
例12-17?Demo1217.java
1?package com.aaa.p120503;
2
3?public class Demo1217 {
4?public static void main(String[] args) {
5?try (FileInputStream fis = new FileInputStream("teacher.txt");
6?ObjectInputStream ois = new ObjectInputStream(fis)) {
7?Teacher t1 = (Teacher) ois.readObject();
8?Teacher t2 = (Teacher) ois.readObject();
9?Student s = (Student) ois.readObject();
10?Teacher t3 = (Teacher) ois.readObject();
11?System.out.println("t1的student引用和s是不是相同對象:"
12?+ (t1.getStudent() == s));
13?System.out.println("t2的student引用和s是不是相同對象:"
14?+ (t2.getStudent() == s));
15?System.out.println("t2和t3是不是相同對象:" + (t2 == t3));
16?} catch (Exception e) {
17?e.printStackTrace();
18?}
19?}
20?}
程序運行結果如下:
t1的student引用和s是不是相同對象:true
t2的student引用和s是不是相同對象:true
t2和t3是不是相同對象:true
例12-17中,代碼依次讀取了序列化文件中的4個Java對象,但通過后面的比較判斷,不難發現t2和t3是同一個Java對象,tl、t2和s?的引用變量引用的也是同一個Java對象,這證明了圖12.15所示的序列化機制。
根據Java序列化機制,如果多次序列化同一個Java對象時,只有第1次序列化時才會把該Java?對象轉換成字節序列并輸出,這樣也可能會引發一個潛在的問題,即當程序序列化一個可變對象時,只有第1次使用writeObject()方法輸出時才會將該對象轉換成字節序列并輸出,當程序再次調用writeObject()方法時,程序只是輸出前面的序列化編號,即使后面該對象的實例變量值已被改變,改變的實例變量值也不會被輸出,如例12-18所示。
例12-18?Demo1218.java
1?package com.aaa.p120503;
2
3?public class Demo1218 {
4?public static void main(String[] args) {
5?try (FileOutputStream fos = new FileOutputStream("teacher.txt");
6?ObjectOutputStream oos = new ObjectOutputStream(fos);
7?FileInputStream fis = new FileInputStream("teacher.txt");
8?ObjectInputStream ois = new ObjectInputStream(fis)) {
9?Student student1 = new Student("小喬", 18);
10?oos.writeObject(student1);
11?student1.setName("大喬");
12?System.out.println("修改name后:" + student1);
13?oos.writeObject(student1);
14?Student s2 = (Student) ois.readObject();
15?Student s3 = (Student) ois.readObject();
16?System.out.println("s2與s3進行對比:" + (s2 == s3));
17?System.out.println("s2反序列化后:" + s2);
18?System.out.println("s3反序列化后:" + s3);
19?} catch (Exception e) {
20?e.printStackTrace();
21?}
22?}
23?}
程序的運行結果如下:
修改name后:Student{name='大喬', age=18}
s2與s3進行對比:true
s2反序列化后:Student{name='小喬', age=18}
s3反序列化后:Student{name='小喬', age=18}
例12-18中,先使用writeObject()方法寫入了一個Student對象,接著改變了Student對象的實例變量name的值,然后程序再次序列化輸出Student對象,但這次不會將Student對象轉換成字節序列輸出了,而是僅輸出了一個序列化編號。第14行和第15行的代碼兩次調用readObject()方法讀取了序列化文件中的Java對象,比較兩次讀取的Java對象結果為true,證明是同一對象。然后,程序再次輸出兩個對象,兩個對象的name值依然是“小喬”,表明改變后的Student對象并沒有被寫入,這與Java序列化機制相符。
注意:當使用Java序列化機制去序列化可變對象時一定要注意,只有第一次調用writeObject()方法來輸出對象時才會將對象轉換成字節序列,并寫入到ObjectOutputStream。在后面程序中,即使該對象的實例變量發生了改變,再次調用writeObject()方法輸出該對象時,改變后的實例變量也不會被輸出。
12.6?本章小結
? Java I/O系統負責處理程序的輸入和輸出,I/O類庫位于java.io包中,它對各種常見的輸入流和輸出流進行了抽象。
? 通過調用File類提供的各種方法,能夠完成創建文件、刪除文件、重命名文件、判斷文件的讀寫權限以及文件是否存在、設置和查詢文件的創建時間和權限等操作。
? 根據數據的讀取或寫入分類,流可以分為輸入流和輸出流。
? 根操作對象分類,流可以分為字節流和字符流。字節流可以處理所有類型數據,如圖片、MP3、AVI視頻文件等,而字符流只能處理字符數據。只要是處理純文本數據,就要優先考慮使用字符流,除此之外都用字節流。
? Java的序列化機制可以將實現序列化的Java對象轉換成字節序列,而這些字節序列可以保存在磁盤上,或者通過網絡傳輸,以備以后重新恢復成原來的對象繼續使用。
? Java的反序列化機制是客戶端從文件中或網絡上獲得序列化后的對象字節流后,根據字節流中所保存的對象狀態及描述信息,通過反序列化重建對象。
12.7?理論試題與實踐練習
1.填空題
1.1????關于文件的類都放在?包下面。
1.2????字節流有兩個抽象基類?和?,分別處理字節流的輸入和輸出。
2.選擇題
2.1 下面哪個流類屬于面向字符的輸入流( )
A.BufferedWriter????????????????????B.FileInputStream
C.ObjectInputStream????????????????D.InputStreamReader
2.2 新建一個流對象,下面哪個選項的代碼是錯誤的( )
A.new BufferedWriter(new FileWriter("a.txt"));
????B.new BufferedReader(new FileInputStream("a.dat"));
C.new GZIPOutputStream(new FileOutputStream("a.zip"));
????D.new ObjectInputStream(new FileInputStream("a.dat"));
2.3 要從文件“file.dat”中讀出第10個字節到變量c中,下列哪個方法適合( )
A.FileInputStream in = new FileInputStream("file.dat"); in.skip(9); int c = in.read();
????B.FileInputStream in = new FileInputStream("file.dat"); in.skip(10); int c = in.read();
C.FileInputStream in = new FileInputStream("file.dat"); int c = in.read();
????D.RandomAccessFile in = new RandomAccessFile("file.dat"); in.skip(9);
2.4 Java I/O程序設計中,下列描述正確的是( )
A.OutputStream用于寫操作????????B.InputStream用于寫操作
C.只有字節流可以進行讀操作????????D.I/O庫不支持對文件可讀可寫API
2.5 下列哪個不是合法的字符編碼( )
A.UTF-8????????????????????????B.ISO8859-1
C.GBL????????????????????????????D.ASCII
3.思考題
3.1????請簡述Java中有幾種類型的流??????
4.編程題
4.1 編寫一個程序,要求用戶輸入一個路徑,運行程序列出路徑下的所有文件。
4.2 編寫一個程序實現文件拷貝功能,要求用戶輸入源文件路徑和目標文件路徑,運行程序實現文件拷貝。
4.3 編寫一個程序,產生50個1~9999之間的隨機數,然后利用BufferedWriter類將其寫入到文件file.txt中,之后再讀取這些數字,并進行升序排列。
總結
以上是生活随笔為你收集整理的JAVA输入/输出流详细讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GALGAME文字提取agth 特殊码大
- 下一篇: 图像3尺度全小波包分解matlab,小波