史上最騷最全最詳細的IO流教程,沒有之一!
- 2019 年 11 月 1 日
- 筆記
目錄
前言
io流用到的地方很多,就比如上傳下載,傳輸,設計模式等....基礎打紮實了,才能玩更高端的。
在部落客認為真正懂IO流的優秀程式設計師每次在使用IO流之前都會明確分析如下四點:
(1)明確要操作的數據是數據源還是數據目的(也就是要讀還是要寫)
(2)明確要操作的設備上的數據是位元組還是文本
(3)明確數據所在的具體設備
(4)明確是否需要額外功能(比如是否需要轉換流、高效流等)
以上四點將會在文章告白IO流的四點明確裡面小結一下,如果各位真能熟練以上四點,我覺得這篇文章你就沒必要看了,因為你已經把IO玩弄與股掌之中,萬物皆可被你盤也就也不再話下了。
@
1、告白IO流的四點明確
(1)明確要操作的數據是數據源還是數據目的(要讀還是要寫)
源:
InputStream Reader
目的:
OutputStream Writer
(2)明確要操作的設備上的數據是位元組還是文本
源:
位元組: InputStream
文本: Reader
目的:
位元組: OutputStream
文本: Writer
(3)明確數據所在的具體設備
源設備:
硬碟:文件
File開頭記憶體:數組,字元串
鍵盤:
System.in網路:
Socket
對應目的設備:
硬碟:文件
File開頭記憶體:數組,字元串
螢幕:
System.out網路:
Socket
(4)明確是否需要額外功能
需要轉換—— 轉換流 InputStreamReader 、OutputStreamWriter
需要高效—— 緩衝流Bufferedxxx
多個源—— 序列流 SequenceInputStream
對象序列化—— ObjectInputStream、ObjectOutputStream
保證數據的輸出形式—— 列印流PrintStream 、Printwriter
操作基本數據,保證位元組原樣性——DataOutputStream、DataInputStream
到這裡,我們再來看看IO流的分類吧

OK,準備好了告白IO流了咩?
2、File類
至於IO流,也就是輸入輸出流,從文件出發到文件結束,至始至終都離不開文件,所以IO流還得從文件File類講起。
1.1 File概述
java.io.File 類是專門對文件進行操作的類,只能對文件本身進行操作,不能對文件內容進行操作。
java.io.File 類是文件和目錄路徑名的抽象表示,主要用於文件和目錄的創建、查找和刪除等操作。
怎麼理解上面兩句話?其實很簡單!
第一句就是說File跟流無關,File類不能對文件進行讀和寫也就是輸入和輸出!
第二句就是說File主要表示類似D:\文件目錄1與D:\文件目錄1\文件.txt,前者是文件夾(Directory)後者則是文件(file),而File類就是操作這兩者的類。
1.2 構造方法
在java中,一切皆是對象,File類也不例外,不論是哪個對象都應該從該對象的構造說起,所以部落客來分析分析File類的構造方法。首先從API開始著手

我們主要來學習一下比較常用的三個:
1、 public File(String pathname) :通過將給定的路徑名字元串轉換為抽象路徑名來創建新的 File實例。
2、 public File(String parent, String child) :從父路徑名字元串和子路徑名字元串創建新的 File實例。
3、 public File(File parent, String child) :從父抽象路徑名和子路徑名字元串創建新的 File實例。
看字描述不夠生動不夠形象不得勁?沒得事,下面進行構造舉例,馬上就生動形象了,程式碼如下:
1. 一個File對象代表硬碟中實際存在的一個文件或者目錄。 2. File類構造方法不會給你檢驗這個文件或文件夾是否真實存在,因此無論該路徑下是否存在文件或者目錄,都不影響File對象的創建。 // 文件路徑名 String path = "D:\123.txt"; File file1 = new File(path); // 文件路徑名 String path2 = "D:\1\2.txt"; File file2 = new File(path2); -------------相當於D:\1\2.txt // 通過父路徑和子路徑字元串 String parent = "F:\aaa"; String child = "bbb.txt"; File file3 = new File(parent, child); --------相當於F:\aaa\bbb.txt // 通過父級File對象和子路徑字元串 File parentDir = new File("F:\aaa"); String child = "bbb.txt"; File file4 = new File(parentDir, child); --------相當於F:\aaa\bbb.txt File類的注意點:
- 一個File對象代表硬碟中實際存在的一個文件或者目錄。
- File類構造方法不會給你檢驗這個文件或文件夾是否真實存在,因此無論該路徑下是否存在文件或者目錄,都不影響File對象的創建。
1.3 常用方法
File的常用方法主要分為獲取功能、獲取絕對路徑和相對路徑、判斷功能、創建刪除功能的方法
1.3.1 獲取功能的方法
1、public String getAbsolutePath() :返回此File的絕對路徑名字元串。
2、public String getPath() :將此File轉換為路徑名字元串。
3、public String getName() :返回由此File表示的文件或目錄的名稱。
4、public long length() :返回由此File表示的文件的長度。
以上方法測試,程式碼如下【注意測試以自己的電腦文件夾為準】:
public class FileGet { public static void main(String[] args) { File f = new File("d:/aaa/bbb.java"); System.out.println("文件絕對路徑:"+f.getAbsolutePath()); System.out.println("文件構造路徑:"+f.getPath()); System.out.println("文件名稱:"+f.getName()); System.out.println("文件長度:"+f.length()+"位元組"); File f2 = new File("d:/aaa"); System.out.println("目錄絕對路徑:"+f2.getAbsolutePath()); System.out.println("目錄構造路徑:"+f2.getPath()); System.out.println("目錄名稱:"+f2.getName()); System.out.println("目錄長度:"+f2.length()); } } 輸出結果: 文件絕對路徑:d:aaabbb.java 文件構造路徑:d:aaabbb.java 文件名稱:bbb.java 文件長度:2116位元組 目錄絕對路徑:d:aaa 目錄構造路徑:d:aaa 目錄名稱:aaa 目錄長度:3236 注意:
length(),表示文件的長度。但是File對象表示目錄,則返回值未指定。
1.3.2 絕對路徑和相對路徑
絕對路徑:一個完整的路徑,以盤符開頭,例如F://aaa.txt。
相對路徑:一個簡化的路徑,不以盤符開頭,例如//aaa.txt//b.txt。
1、路徑是不區分大小寫
2、路徑中的文件名稱分隔符windows使用反斜杠,反斜杠是轉義字元,兩個反斜杠代表一個普通的反斜杠
//絕對路徑 public class FilePath { public static void main(String[] args) { // D盤下的bbb.java文件 File f = new File("D:\bbb.java"); System.out.println(f.getAbsolutePath()); // 項目下的bbb.java文件 File f2 = new File("bbb.java"); System.out.println(f2.getAbsolutePath()); } } 輸出結果: D:bbb.java D:javabbb.java 1.3.3判斷功能的方法
1、 public boolean exists() :此File表示的文件或目錄是否實際存在。
2、 public boolean isDirectory() :此File表示的是否為目錄。
3、public boolean isFile() :此File表示的是否為文件。
方法演示,程式碼如下:
public class FileIs { public static void main(String[] args) { File f = new File("d:\aaa\bbb.java"); File f2 = new File("d:\aaa"); // 判斷是否存在 System.out.println("d:\aaa\bbb.java 是否存在:"+f.exists()); System.out.println("d:\aaa 是否存在:"+f2.exists()); // 判斷是文件還是目錄 System.out.println("d:\aaa 文件?:"+f2.isFile()); System.out.println("d:\aaa 目錄?:"+f2.isDirectory()); } } 輸出結果: d:aaabbb.java 是否存在:true d:aaa 是否存在:true d:aaa 文件?:false d:aaa 目錄?:true 1.3.4 創建刪除功能的方法
public boolean createNewFile():文件不存在,創建一個新的空文件並返回true,文件存在,不創建文件並返回false。public boolean delete():刪除由此File表示的文件或目錄。
public boolean mkdir():創建由此File表示的目錄。public boolean mkdirs():創建由此File表示的目錄,包括任何必需但不存在的父目錄。
其中,mkdirs()和mkdir()方法類似,但mkdir(),只能創建一級目錄,mkdirs()可以創建多級目錄比如//a//b//c,所以開發中一般用mkdirs();
這些方法中值得注意的是createNewFile方法以及mkdir與mkdirs的區別
方法測試,程式碼如下:
public class FileCreateDelete { public static void main(String[] args) throws IOException { // 文件的創建 File f = new File("aaa.txt"); System.out.println("是否存在:"+f.exists()); // false System.out.println("是否創建:"+f.createNewFile()); // true System.out.println("是否創建:"+f.createNewFile()); // 以及創建過了所以再使用createNewFile返回false System.out.println("是否存在:"+f.exists()); // true // 目錄的創建 File f2= new File("newDir"); System.out.println("是否存在:"+f2.exists());// false System.out.println("是否創建:"+f2.mkdir()); // true System.out.println("是否存在:"+f2.exists());// true // 創建多級目錄 File f3= new File("newDira\newDirb"); System.out.println(f3.mkdir());// false File f4= new File("newDira\newDirb"); System.out.println(f4.mkdirs());// true // 文件的刪除 System.out.println(f.delete());// true // 目錄的刪除 System.out.println(f2.delete());// true System.out.println(f4.delete());// false } } 注意:
delete方法,如果此File表示目錄,則目錄必須為空才能刪除。
1.4 目錄的遍歷
public String[] list():返回一個String數組,表示該File目錄中的所有子文件或目錄。public File[] listFiles():返回一個File數組,表示該File目錄中的所有的子文件或目錄。
public class FileFor { public static void main(String[] args) { File dir = new File("G:游標"); //獲取當前目錄下的文件以及文件夾的名稱。 String[] names = dir.list(); for(String name : names){ System.out.println(name); } //獲取當前目錄下的文件以及文件夾對象,只要拿到了文件對象,那麼就可以獲取更多資訊 File[] files = dir.listFiles(); for (File file : files) { System.out.println(file); } } } 
listFiles在獲取指定目錄下的文件或者文件夾時必須滿足下面兩個條件
1,指定的目錄必須存在
2,指定的必須是目錄。否則容易引發返回數組為null,出現NullPointerException異常
1.5 遞歸遍歷文件夾下所有文件以及子文件
不說啥了,直接上程式碼:
package File; import java.io.File; //遞歸遍歷文件夾下所有的文件 public class RecursionDirectory { public static void main(String[] args) { File file=new File("D:\java專屬IO測試"); Recursion(file); } public static void Recursion(File file){ //1、判斷傳入的是否是目錄 if(!file.isDirectory()){ //不是目錄直接退出 return; } //已經確保了傳入的file是目錄 File[] files = file.listFiles(); //遍歷files for (File f: files) { //如果該目錄下文件還是個文件夾就再進行遞歸遍歷其子目錄 if(f.isDirectory()){ //遞歸 Recursion(f); }else { //如果該目錄下文件是個文件,則列印對應的名字 System.out.println(f.getName()); } } } } 如果對上面的程式碼有疑問,可以隨時聯繫我,部落客一直都在!
3、初探IO流
1.1 什麼是IO
我想在座各位肯定經歷都過這樣的場景。當你編輯一個文本文件也好用eclipse打程式碼也罷,忘記了ctrl+s ,在你關閉文件的哪一瞬間手殘點了個不該點的按鈕,但你反應過來,心早已拔涼拔涼的了。
我們把這種數據的傳輸,可以看做是一種數據的流動,按照流動的方向,以記憶體為基準,分為輸入input 和輸出output ,即流向記憶體是輸入流,流出記憶體的輸出流。
Java中I/O操作主要是指使用java.io包下的內容,進行輸入、輸出操作。輸入也叫做讀取數據,輸出也叫做作寫出數據。
1.2 IO的分類
根據數據的流向分為:輸入流 和 輸出流。
- 輸入流 :把數據從
其他設備上讀取到記憶體中的流。 - 輸出流 :把數據從
記憶體中寫出到其他設備上的流。
根據數據的類型分為:位元組流 和 字元流。
- 位元組流 :以位元組為單位,讀寫數據的流。
- 字元流 :以字元為單位,讀寫數據的流。
分類之後對應的超類(V8提示:超類也就是父類的意思)
| | 輸入流 | 輸出流
|--|--|--|
| 位元組流 | 位元組輸入流 InputStream |位元組輸出流 OutputStream |
| 字元流 | 字元輸入流 Reader|字元輸出流 Writer|
注:
由這四個類的子類名稱基本都是以其父類名作為子類名的後綴。
如:InputStream的子類FileInputStream。
如:Reader的子類FileReader。

1.3 關於IO的分流向說明
啥都不說了,看圖吧


位元組流OutputStream與InputStream的故事
OutputStream與InputStream的繼承關係

2.1 文件的世界裡一切皆為位元組
我們必須明確一點的是,一切文件數據(文本、圖片、影片等)在存儲時,都是以二進位數字的形式保存,都一個一個的位元組,那麼傳輸時一樣如此。所以,位元組流可以傳輸任意文件數據。在操作流的時候,我們要時刻明確,無論使用什麼樣的流對象,底層傳輸的始終為二進位數據。
2.2 位元組輸出流(OutputStream)
java.io.OutputStream抽象類是表示位元組輸出流的所有類的超類(父類),將指定的位元組資訊寫出到目的地。它定義了位元組輸出流的基本共性功能方法,不要問我OutputStream為啥可以定義位元組輸出流的基本共性功能方法,熊dei啊,上一句說過了OutputStream是位元組輸出流的所有類的超類,繼承知識,懂?(如果是真的不理解的小白同學,可以點擊藍色字體繼承進入補習)
位元組輸出流的基本共性功能方法:
1、
public void close():關閉此輸出流並釋放與此流相關聯的任何系統資源。
2、public void flush():刷新此輸出流並強制任何緩衝的輸出位元組被寫出。
3、public void write(byte[] b):將 b.length個位元組從指定的位元組數組寫入此輸出流。
4、public void write(byte[] b, int off, int len):從指定的位元組數組寫入 len位元組,從偏移量 off開始輸出到此輸出流。 也就是說從off個位元組數開始讀取一直到len個位元組結束
5、public abstract void write(int b):將指定的位元組輸出流。
以上五個方法則是位元組輸出流都具有的方法,由父類OutputStream定義提供,子類都會共享以上方法
FileOutputStream類
OutputStream有很多子類,我們從最簡單的一個子類FileOutputStream開始。看名字就知道是文件輸出流,用於將數據寫出到文件。
FileOutputStream構造方法
不管學啥子,只有是對象,就從構造方法開始!
1、
public FileOutputStream(File file):根據File對象為參數創建對象。
2、public FileOutputStream(String name): 根據名稱字元串為參數創建對象。
推薦第二種構造方法【開發常用】:
FileOutputStream outputStream = new FileOutputStream("abc.txt"); 就以上面這句程式碼來講,類似這樣創建位元組輸出流對象都做了三件事情:
1、調用系統功能去創建文件【輸出流對象才會自動創建】
2、創建outputStream對象
3、把foutputStream對象指向這個文件
注意:
創建輸出流對象的時候,系統會自動去對應位置創建對應文件,而創建輸出流對象的時候,文件不存在則會報FileNotFoundException異常,也就是系統找不到指定的文件異常。
當你創建一個流對象時,必須直接或者間接傳入一個文件路徑。比如現在我們創建一個FileOutputStream流對象,在該路徑下,如果沒有這個文件,會創建該文件。如果有這個文件,會清空這個文件的數據。有興趣的童鞋可以測試一下,具體程式碼如下:
public class FileOutputStreamConstructor throws IOException { public static void main(String[] args) { // 使用File對象創建流對象 File file = new File("G:\自動創建的文件夾\a.txt"); FileOutputStream fos = new FileOutputStream(file); // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("G:\b.txt"); } } FileOutputStream寫出位元組數據
使用FileOutputStream寫出位元組數據主要通過Write方法,而write方法分如下三種
public void write(int b) public void write(byte[] b) public void write(byte[] b,int off,int len) //從`off`索引開始,`len`個位元組 - 寫出位元組:
write(int b)方法,每次可以寫出一個位元組數據,程式碼如下:
public class IoWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 寫出數據 fos.write(97); // 寫出第1個位元組 fos.write(98); // 寫出第2個位元組 fos.write(99); // 寫出第3個位元組 // 關閉資源 fos.close(); } } 輸出結果: abc
- 雖然參數為int類型四個位元組,但是只會保留一個位元組的資訊寫出。
- 流操作完畢後,必須釋放系統資源,調用close方法,千萬記得。
- 寫出位元組數組:
write(byte[] b),每次可以寫出數組中的數據,程式碼使用演示:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字元串轉換為位元組數組 byte[] b = "麻麻我想吃烤山藥".getBytes(); // 寫出位元組數組數據 fos.write(b); // 關閉資源 fos.close(); } } 輸出結果: 麻麻我想吃烤山藥 - 寫出指定長度位元組數組:
write(byte[] b, int off, int len),每次寫出從off索引開始,len個位元組,程式碼如下:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 字元串轉換為位元組數組 byte[] b = "abcde".getBytes(); // 寫出從索引2開始,2個位元組。索引2是c,兩個位元組,也就是cd。 fos.write(b,2,2); // 關閉資源 fos.close(); } } 輸出結果: cd FileOutputStream實現數據追加續寫、換行
經過以上的程式碼測試,每次程式運行,每次創建輸出流對象,都會清空目標文件中的數據。如何保留目標文件中數據,還能繼續追加新數據呢?並且實現換行呢?其實很簡單,這個時候我們又要再學習FileOutputStream的另外兩個構造方法了,如下:
1、public FileOutputStream(File file, boolean append)
2、public FileOutputStream(String name, boolean append)
這兩個構造方法,第二個參數中都需要傳入一個boolean類型的值,true 表示追加數據,false 表示不追加也就是清空原有數據。這樣創建的輸出流對象,就可以指定是否追加續寫了,至於Windows換行則是 nr ,下面將會詳細講到。
實現數據追加續寫程式碼如下:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("fos.txt",true); // 字元串轉換為位元組數組 byte[] b = "abcde".getBytes(); // 寫出從索引2開始,2個位元組。索引2是c,兩個位元組,也就是cd。 fos.write(b); // 關閉資源 fos.close(); } } 文件操作前:cd 文件操作後:cdabcde Windows系統里,換行符號是rn ,具體程式碼如下:
public class FOSWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileOutputStream fos = new FileOutputStream("fos.txt"); // 定義位元組數組 byte[] words = {97,98,99,100,101}; // 遍曆數組 for (int i = 0; i < words.length; i++) { // 寫出一個位元組 fos.write(words[i]); // 寫出一個換行, 換行符號轉成數組寫出 fos.write("rn".getBytes()); } // 關閉資源 fos.close(); } } 輸出結果: a b c d e
- 回車符
r和換行符n:
- 回車符:回到一行的開頭(return)。
- 換行符:下一行(newline)。
- 系統中的換行:
- Windows系統里,每行結尾是
回車+換行,即rn;- Unix系統里,每行結尾只有
換行,即n;- Mac系統里,每行結尾是
回車,即r。從 Mac OS X開始與Linux統一。
2.3 位元組輸入流(InputStream)
java.io.InputStream抽象類是表示位元組輸入流的所有類的超類(父類),可以讀取位元組資訊到記憶體中。它定義了位元組輸入流的基本共性功能方法。
位元組輸入流的基本共性功能方法:
1、
public void close():關閉此輸入流並釋放與此流相關聯的任何系統資源。
2、public abstract int read(): 從輸入流讀取數據的下一個位元組。3、
public int read(byte[] b): 該方法返回的int值代表的是讀取了多少個位元組,讀到幾個返回幾個,讀取不到返回-1
FileInputStream類
java.io.FileInputStream類是文件輸入流,從文件中讀取位元組。
FileInputStream的構造方法
1、
FileInputStream(File file): 通過打開與實際文件的連接來創建一個 FileInputStream ,該文件由文件系統中的 File對象 file命名。
2、FileInputStream(String name): 通過打開與實際文件的連接來創建一個 FileInputStream ,該文件由文件系統中的路徑名name命名。
同樣的,推薦使用第二種構造方法:
FileInputStream inputStream = new FileInputStream("a.txt"); 當你創建一個流對象時,必須傳入一個文件路徑。該路徑下,如果沒有該文件,會拋出FileNotFoundException 。
構造舉例,程式碼如下:
public class FileInputStreamConstructor throws IOException{ public static void main(String[] args) { // 使用File對象創建流對象 File file = new File("a.txt"); FileInputStream fos = new FileInputStream(file); // 使用文件名稱創建流對象 FileInputStream fos = new FileInputStream("b.txt"); } } FileInputStream讀取位元組數據
- 讀取位元組:
read方法,每次可以讀取一個位元組的數據,提升為int類型,讀取到文件末尾,返回-1,程式碼測試如下【read.txt文件中內容為abcde】:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名稱創建流對象 FileInputStream fis = new FileInputStream("read.txt");//read.txt文件中內容為abcde // 讀取數據,返回一個位元組 int read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); read = fis.read(); System.out.println((char) read); // 讀取到末尾,返回-1 read = fis.read(); System.out.println( read); // 關閉資源 fis.close(); } } 輸出結果: a b c d e -1 循環改進讀取方式,程式碼使用演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名稱創建流對象 FileInputStream fis = new FileInputStream("read.txt"); // 定義變數,保存數據 int b ; // 循環讀取 while ((b = fis.read())!=-1) { System.out.println((char)b); } // 關閉資源 fis.close(); } } 輸出結果: a b c d e - 使用位元組數組讀取:
read(byte[] b),每次讀取b的長度個位元組到數組中,返回讀取到的有效位元組個數,讀取到末尾時,返回-1,程式碼使用演示:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名稱創建流對象. FileInputStream fis = new FileInputStream("read.txt"); // read.txt文件中內容為abcde // 定義變數,作為有效個數 int len ; // 定義位元組數組,作為裝位元組數據的容器 byte[] b = new byte[2]; // 循環讀取 while (( len= fis.read(b))!=-1) { // 每次讀取後,把數組變成字元串列印 System.out.println(new String(b)); } // 關閉資源 fis.close(); } } 輸出結果: ab cd ed 由於read.txt文件中內容為abcde,而錯誤數據d,是由於最後一次讀取時,只讀取一個位元組e,數組中,上次讀取的數據沒有被完全替換【注意是替換,看下圖】,所以要通過len ,獲取有效的位元組

程式碼如下:
public class FISRead { public static void main(String[] args) throws IOException{ // 使用文件名稱創建流對象. FileInputStream fis = new FileInputStream("read.txt"); // 文件中為abcde // 定義變數,作為有效個數 int len ; // 定義位元組數組,作為裝位元組數據的容器 byte[] b = new byte[2]; // 循環讀取 while (( len= fis.read(b))!=-1) { // 每次讀取後,把數組的有效位元組部分,變成字元串列印 System.out.println(new String(b,0,len));// len 每次讀取的有效位元組個數 } // 關閉資源 fis.close(); } } 輸出結果: ab cd e 在開發中一般強烈推薦使用數組讀取文件,程式碼如下:
package io; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class input2 { public static void main(String args[]){ FileInputStream inputStream = null; try { inputStream = new FileInputStream("a.txt"); int len = 0 ; byte[] bys = new byte[1024]; while ((len = inputStream.read(bys)) != -1) { System.out.println(new String(bys,0,len)); } } catch (IOException e) { e.printStackTrace(); }finally { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } 位元組流FileInputstream複製圖片
複製圖片原理

程式碼實現
複製圖片文件,程式碼如下:
public class Copy { public static void main(String[] args) throws IOException { // 1.創建流對象 // 1.1 指定數據源 FileInputStream fis = new FileInputStream("D:\test.jpg"); // 1.2 指定目的地 FileOutputStream fos = new FileOutputStream("test_copy.jpg"); // 2.讀寫數據 // 2.1 定義數組 byte[] b = new byte[1024]; // 2.2 定義長度 int len; // 2.3 循環讀取 while ((len = fis.read(b))!=-1) { // 2.4 寫出數據 fos.write(b, 0 , len); } // 3.關閉資源 fos.close(); fis.close(); } } 注:複製文本、圖片、mp3、影片等的方式一樣。
到這裡,已經從File類講到了位元組流OutputStream與InputStream,而現在將主要從字元流Reader和Writer的故事開展。
字元流Reader和Writer的故事
字元流Reader和Writer的故事從它們的繼承圖開始,啥都不說了,直接看圖

字元流
字元流的由來:因為數據編碼的不同,因而有了對字元進行高效操作的流對象,字元流本質其實就是基於位元組流讀取時,去查了指定的碼錶,而位元組流直接讀取數據會有亂碼的問題(讀中文會亂碼),這個時候小白同學就看不懂了,沒事,咋們先來看個程式:
package IO; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; public class CharaterStream { public static void main(String[] args) throws Exception { //FileInputStream為操作文件的字元輸入流 FileInputStream inputStream = new FileInputStream("a.txt");//內容為哥敢摸屎 int len; while ((len=inputStream.read())!=-1){ System.out.print((char)len); } } } 運行結果: ??¥??¢????± 具體現狀分析

話說,就是你哥我敢摸si,那你哥我肯定也不認識這玩意啊: ??¥??¢????±
位元組流讀取中文字元時,可能不會顯示完整的字元,那是因為一個中文字元佔用多個位元組存儲。
那位元組流就沒辦法了嗎?不,位元組流依舊有辦法,只是麻煩了點,程式碼如下:
public class CharaterStream { public static void main(String[] args) throws Exception { FileInputStream inputStream = new FileInputStream("a.txt"); byte[] bytes = new byte[1024]; int len; while ((len=inputStream.read(bytes))!=-1){ System.out.print(new String(bytes,0,len)); } } } 運行結果: 哥敢摸屎 這是為啥呢?沒錯解碼的正是String,查看new String()的源碼,String構造方法有解碼功能,並且默認編碼是utf-8,程式碼如下:
this.value = StringCoding.decode(bytes, offset, length); 再點進decode,循序漸進發現,默認編碼是UTF-8 儘管位元組流也能有辦法決絕亂碼問題,但是還是比較麻煩,於是java就有了字元流,字元為單位讀寫數據,字元流專門用於處理文本文件。如果處理純文本的數據優先考慮字元流,其他情況就只能用位元組流了(圖片、影片、等等只文本例外)。
從另一角度來說:字元流 = 位元組流 + 編碼表
1、 字元輸入流(Reader)
java.io.Reader抽象類是字元輸入流的所有類的超類(父類),可以讀取字元資訊到記憶體中。它定義了字元輸入流的基本共性功能方法。
字元輸入流的共性方法:
1、
public void close():關閉此流並釋放與此流相關聯的任何系統資源。
2、public int read(): 從輸入流讀取一個字元。
3、public int read(char[] cbuf): 從輸入流中讀取一些字元,並將它們存儲到字元數組cbuf中
FileReader類
java.io.FileReader類是讀取字元文件的便利類。構造時使用系統默認的字元編碼和默認位元組緩衝區。
構造方法
1、
FileReader(File file): 創建一個新的 FileReader ,給定要讀取的File對象。
2、FileReader(String fileName): 創建一個新的 FileReader ,給定要讀取的文件的字元串名稱。
構造方法的使用就算不寫應該都很熟悉了吧,程式碼如下:
public class FileReaderConstructor throws IOException{ public static void main(String[] args) { // 使用File對象創建流對象 File file = new File("a.txt"); FileReader fr = new FileReader(file); // 使用文件名稱創建流對象 FileReader fr = new FileReader("b.txt"); } } FileReader讀取字元數據
- 讀取字元:
read方法,每次可以讀取一個字元的數據,提升為int類型,讀取到文件末尾,返回-1,循環讀取,程式碼使用演示:
public class FRRead { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileReader fr = new FileReader("a.txt"); // 定義變數,保存數據 int b ; // 循環讀取 while ((b = fr.read())!=-1) { System.out.println((char)b); } // 關閉資源 fr.close(); } } 至於讀取的寫法類似位元組流的寫法,只是讀取單位不同罷了。
2、字元輸出流(Writer)
java.io.Writer抽象類是字元輸出流的所有類的超類(父類),將指定的字元資訊寫出到目的地。它同樣定義了字元輸出流的基本共性功能方法。
字元輸出流的基本共性功能方法:
1、
void write(int c)寫入單個字元。
2、void write(char[] cbuf)寫入字元數組。
3、abstract void write(char[] cbuf, int off, int len)寫入字元數組的某一部分,off數組的開始索引,len寫的字元個數。
4、void write(String str)寫入字元串。
5、void write(String str, int off, int len)寫入字元串的某一部分,off字元串的開始索引,len寫的字元個數。
6、void flush()刷新該流的緩衝。
7、void close()關閉此流,但要先刷新它。
FileWriter類
java.io.FileWriter類是寫出字元到文件的便利類。構造時使用系統默認的字元編碼和默認位元組緩衝區。
構造方法
1、 FileWriter(File file): 創建一個新的 FileWriter,給定要讀取的File對象。
2、FileWriter(String fileName): 創建一個新的 FileWriter,給定要讀取的文件的名稱。
依舊是熟悉的構造舉例,程式碼如下:
public class FileWriterConstructor { public static void main(String[] args) throws IOException { // 第一種:使用File對象創建流對象 File file = new File("a.txt"); FileWriter fw = new FileWriter(file); // 第二種:使用文件名稱創建流對象 FileWriter fw = new FileWriter("b.txt"); } } FileWriter寫出數據
寫出字元:write(int b) 方法,每次可以寫出一個字元數據,程式碼使用演示:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileWriter fw = new FileWriter("fw.txt"); // 寫出數據 fw.write(97); // 寫出第1個字元 fw.write('b'); // 寫出第2個字元 fw.write('C'); // 寫出第3個字元 //關閉資源時,與FileOutputStream不同。 如果不關閉,數據只是保存到緩衝區,並未保存到文件。 // fw.close(); } } 輸出結果: abC 【注意】關閉資源時,與FileOutputStream不同。 如果不關閉,數據只是保存到緩衝區,並未保存到文件。
關閉close和刷新flush
因為內置緩衝區的原因,如果不關閉輸出流,無法寫出字元到文件中。但是關閉的流對象,是無法繼續寫出數據的。如果我們既想寫出數據,又想繼續使用流,就需要flush 方法了。
flush :刷新緩衝區,流對象可以繼續使用。
close:先刷新緩衝區,然後通知系統釋放資源。流對象不可以再被使用了。
flush還是比較有趣的,童鞋們不自己運行一下還真不好體會,現在部落客就寫個程式讓你體會體會:
字元流
public class FlushDemo { public static void main(String[] args) throws Exception { //源 也就是輸入流【讀取流】 讀取a.txt文件 FileReader fr=new FileReader("a.txt"); //必須要存在a.txt文件,否則報FileNotFoundException異常 //目的地 也就是輸出流 FileWriter fw=new FileWriter("b.txt"); //系統會自動創建b.txt,因為它是輸出流! int len; while((len=fr.read())!=-1){ fw.write(len); } 注意這裡是沒有使用close關閉流,開發中不能這樣做,但是為了更好的體會flush的作用 } } 
運行效果是怎麼樣的呢?答案是b.txt文件中依舊是空的,是的並沒有任何東西,為啥呢?熊dei啊,我在上面就用紅色字體特別標註過了,就是這句話: 【注意】關閉資源時,與FileOutputStream不同。 如果不關閉,數據只是保存到緩衝區,並未保存到文件。這個時候反應過來了吧,可見實踐例子的重要性,編程就是這樣,不去敲,永遠學不會!!!所以一定要去敲,部落客沒敲過10萬行程式碼真的沒有臉出去說自己是學java的。所以,大家一定要多思考,多敲啊!!!
所以,我們在以上的程式碼中再添加下面三句程式碼,就完美了,b.txt文件就能複製到源文件的數據了!
fr.close(); fw.flush(); fw.close();
flush()這個函數是清空的意思,用於清空緩衝區的數據流,進行流的操作時,數據先被讀到記憶體中,然後再用數據寫到文件中,那麼當你數據讀完時,我們如果這時調用close()方法關閉讀寫流,這時就可能造成數據丟失,為什麼呢?因為,讀入數據完成時不代表寫入數據完成,一部分數據可能會留在快取區中,這個時候flush()方法就格外重要了。
好了,接下來close使用程式碼如下:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象 FileWriter fw = new FileWriter("fw.txt"); // 寫出數據,通過flush fw.write('刷'); // 寫出第1個字元 fw.flush(); fw.write('新'); // 繼續寫出第2個字元,寫出成功 fw.flush(); // 寫出數據,通過close fw.write('關'); // 寫出第1個字元 fw.close(); fw.write('閉'); // 繼續寫出第2個字元,【報錯】java.io.IOException: Stream closed fw.close(); } } 即便是flush方法寫出了數據,操作的最後還是要調用close方法,釋放系統資源。
FileWriter的續寫和換行
續寫和換行:操作類似於FileOutputStream操作(上一篇部落格講到過),直接上程式碼:
public class FWWrite { public static void main(String[] args) throws IOException { // 使用文件名稱創建流對象,可以續寫數據 FileWriter fw = new FileWriter("fw.txt",true); // 寫出字元串 fw.write("哥敢"); // 寫出換行 fw.write("rn"); // 寫出字元串 fw.write("摸屎"); // 關閉資源 fw.close(); } } 輸出結果: 哥敢 摸屎 FileReader和FileWriter類完成文本文件複製
直接上程式碼:
import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class CopyFile { public static void main(String[] args) throws IOException { //創建輸入流對象 FileReader fr=new FileReader("F:\新建文件夾\aa.txt");//文件不存在會拋出java.io.FileNotFoundException //創建輸出流對象 FileWriter fw=new FileWriter("C:\copyaa.txt"); /*創建輸出流做的工作: * 1、調用系統資源創建了一個文件 * 2、創建輸出流對象 * 3、把輸出流對象指向文件 * */ //文本文件複製,一次讀一個字元 copyMethod1(fr, fw); //文本文件複製,一次讀一個字元數組 copyMethod2(fr, fw); fr.close(); fw.close(); } public static void copyMethod1(FileReader fr, FileWriter fw) throws IOException { int ch; while((ch=fr.read())!=-1) {//讀數據 fw.write(ch);//寫數據 } fw.flush(); } public static void copyMethod2(FileReader fr, FileWriter fw) throws IOException { char chs[]=new char[1024]; int len=0; while((len=fr.read(chs))!=-1) {//讀數據 fw.write(chs,0,len);//寫數據 } fw.flush(); } } CopyFile 
最後再次強調:
字元流,只能操作文本文件,不能操作圖片,影片等非文本文件。當我們單純讀或者寫文本文件時 使用字元流 其他情況使用位元組流
IO異常的處理
我們在學習的過程中可能習慣把異常拋出,而實際開發中並不能這樣處理,建議使用try...catch...finally 程式碼塊,處理異常部分,格式程式碼如下:
public class HandleException1 { public static void main(String[] args) { // 聲明變數 FileWriter fw = null; try { //創建流對象 fw = new FileWriter("fw.txt"); // 寫出數據 fw.write("哥敢摸si"); //哥敢摸si } catch (IOException e) { e.printStackTrace(); } finally { try { if (fw != null) { fw.close(); } } catch (IOException e) { e.printStackTrace(); } } } } 如果對異常不是特別熟練的童鞋可以參考這篇文章【java基礎之異常】死了都要try,不淋漓盡致地catch我不痛快!
好了,到這裡,字元流Reader和Writer的故事的到這裡了!
前面主要寫了一些基本的流作為IO流的入門。從這裡開始將要見識一些更強大的流。比如能夠高效讀寫的緩衝流,能夠轉換編碼的轉換流,能夠持久化存儲對象的序列化流等等,而這些強大的流都是在基本的流對象基礎之上而來的!這些強大的流將伴隨著我們今後的開發!
1、緩衝流【掌握】
1.1 簡要概述
首先我們來認識認識一下緩衝流,也叫高效流,是對4個FileXxx 流的「增強流」。
緩衝流的基本原理:
1、使用了底層流對象從具體設備上獲取數據,並將數據存儲到緩衝區的數組內。
2、通過緩衝區的read()方法從緩衝區獲取具體的字元數據,這樣就提高了效率。
3、如果用read方法讀取字元數據,並存儲到另一個容器中,直到讀取到了換行符時,將另一個容器臨時存儲的數據轉成字元串返回,就形成了readLine()功能。
也就是說在創建流對象時,會創建一個內置的默認大小的緩衝區數組,通過緩衝區讀寫,減少系統IO次數,從而提高讀寫的效率。
緩衝書寫格式為BufferedXxx,按照數據類型分類:
- 位元組緩衝流:
BufferedInputStream,BufferedOutputStream - 字元緩衝流:
BufferedReader,BufferedWriter
1.2 位元組緩衝流
構造方法
public BufferedInputStream(InputStream in):創建一個新的緩衝輸入流,注意參數類型為InputStream。public BufferedOutputStream(OutputStream out): 創建一個新的緩衝輸出流,注意參數類型為OutputStream。
構造舉例程式碼如下:
//構造方式一: 創建位元組緩衝輸入流【但是開發中一般常用下面的格式申明】 FileInputStream fps = new FileInputStream(b.txt); BufferedInputStream bis = new BufferedInputStream(fps) //構造方式一: 創建位元組緩衝輸入流 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt")); ///構造方式二: 創建位元組緩衝輸出流 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt")); 感受緩衝流的高效
緩衝流讀寫方法與基本的流是一致的,我們通過複製370多MB的大文件,測試它的效率。
- 基本流,程式碼如下:
public class BufferedDemo { public static void main(String[] args) throws FileNotFoundException { // 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try ( FileInputStream fis = new FileInputStream("py.exe");//exe文件夠大 FileOutputStream fos = new FileOutputStream("copyPy.exe") ){ // 讀寫數據 int b; while ((b = fis.read()) != -1) { fos.write(b); } } catch (IOException e) { e.printStackTrace(); } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("普通流複製時間:"+(end - start)+" 毫秒"); } } 不好意思十分鐘過去了還在玩命複製中... - 緩衝流,程式碼如下:
public class BufferedDemo { public static void main(String[] args) throws FileNotFoundException { // 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try ( BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.exe")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.exe")); ){ // 讀寫數據 int b; while ((b = bis.read()) != -1) { bos.write(b); } } catch (IOException e) { e.printStackTrace(); } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("緩衝流複製時間:"+(end - start)+" 毫秒"); } } 緩衝流複製時間:8016 毫秒 有的童鞋就要說了,我要更快的速度!最近看速度與激情7有點上頭,能不能再快些?答案是當然可以
想要更快可以使用數組的方式,程式碼如下:
public class BufferedDemo { public static void main(String[] args) throws FileNotFoundException { // 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try ( BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.exe")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.exe")); ){ // 讀寫數據 int len; byte[] bytes = new byte[8*1024]; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0 , len); } } catch (IOException e) { e.printStackTrace(); } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("緩衝流使用數組複製時間:"+(end - start)+" 毫秒"); } } 緩衝流使用數組複製時間:521 毫秒 1.3 字元緩衝流
構造方法
相同的來看看其構造,其格式以及原理和位元組緩衝流是一樣一樣的!
public BufferedReader(Reader in):創建一個新的緩衝輸入流,注意參數類型為Reader。public BufferedWriter(Writer out): 創建一個新的緩衝輸出流,注意參數類型為Writer。
構造舉例,程式碼如下:
// 創建字元緩衝輸入流 BufferedReader br = new BufferedReader(new FileReader("b.txt")); // 創建字元緩衝輸出流 BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); 字元緩衝流特有方法
字元緩衝流的基本方法與普通字元流調用方式一致,這裡不再闡述,我們來看字元緩衝流具備的特有方法。
- BufferedReader:
public String readLine(): 讀一行數據。 讀取到最後返回null - BufferedWriter:
public void newLine(): 換行,由系統屬性定義符號。
readLine方法演示程式碼如下:
public class BufferedReaderDemo { public static void main(String[] args) throws IOException { // 創建流對象 BufferedReader br = new BufferedReader(new FileReader("a.txt")); // 定義字元串,保存讀取的一行文字 String line = null; // 循環讀取,讀取到最後返回null while ((line = br.readLine())!=null) { System.out.print(line); System.out.println("------"); } // 釋放資源 br.close(); } } newLine方法演示程式碼如下:
public class BufferedWriterDemo throws IOException { public static void main(String[] args) throws IOException { // 創建流對象 BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); // 寫出數據 bw.write("哥"); // 寫出換行 bw.newLine(); bw.write("敢"); bw.newLine(); bw.write("摸屎"); bw.newLine(); bw.write("你敢嗎?"); bw.newLine(); // 釋放資源 bw.close(); } } 輸出效果: 哥 敢 摸屎 你敢嗎? 1.4 字元緩衝流練習
字元緩衝流練習啥捏?先放鬆一下吧各位,先欣賞欣賞我寫的下面的詩篇
6.你說你的程式叫簡單,我說我的程式碼叫詩篇
1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁....哦nima個頭啊,完全不理人家受得了受不了
8.Just 簡單你和我 ,Just 簡單程式設計師
3.約了地點卻忘了見面 ,懂得寂寞才明白浩瀚
5.沉默是最大的發言權
2.總是喜歡坐在電腦前, 總是喜歡工作到很晚
7.向左走 又向右走,我們轉了好多的彎
4.你從來就不問我,你還是不是那個程式設計師
欣賞完了咩?沒錯咋們就練習如何使用緩衝流的技術把上面的詩篇歸順序,都編過號了~就是前面的1到8的編號~
分析:首先用字元輸入緩衝流創建個源,裡面放沒有排過序的文字,之後用字元輸出緩衝流創建個目標接收,排序的過程就要自己寫方法了哦,可以從每條詩詞的共同點「.」符號下手!
程式碼實現
public class BufferedTest { public static void main(String[] args) throws IOException { // 創建map集合,保存文本數據,鍵為序號,值為文字 HashMap<String, String> lineMap = new HashMap<>(); // 創建流對象 源 BufferedReader br = new BufferedReader(new FileReader("a.txt")); //目標 BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); // 讀取數據 String line = null; while ((line = br.readLine())!=null) { // 解析文本 String[] split = line.split("\."); // 保存到集合 lineMap.put(split[0],split[1]); } // 釋放資源 br.close(); // 遍歷map集合 for (int i = 1; i <= lineMap.size(); i++) { String key = String.valueOf(i); // 獲取map中文本 String value = lineMap.get(key); // 寫出拼接文本 bw.write(key+"."+value); // 寫出換行 bw.newLine(); } // 釋放資源 bw.close(); } } 運行效果
1.一想到你我就哦豁豁豁豁豁豁豁豁豁豁…哦nima個頭啊,完全不理人家受得了受不了 2.總是喜歡坐在電腦前, 總是喜歡工作到很晚 3.約了地點卻忘了見面 ,懂得寂寞才明白浩瀚 4.你從來就不問我,你還是不是那個程式設計師 5.沉默是最大的發言權 6.你說你的程式叫簡單,我說我的程式碼叫詩篇 7.向左走 又向右走,我們轉了好多的彎 8.Just 簡單你和我 ,Just 簡單程式設計師 2、轉換流【掌握】
何謂轉換流?為何由來?暫時帶著問題讓我們先來了解了解字元編碼和字符集!

2.1 字元編碼與解碼
眾所周知,電腦中儲存的資訊都是用二進位數表示的,而我們在螢幕上看到的數字、英文、標點符號、漢字等字元是二進位數轉換之後的結果。按照某種規則,將字元存儲到電腦中,稱為編碼 。反之,將存儲在電腦中的二進位數按照某種規則解析顯示出來,稱為解碼 。比如說,按照A規則存儲,同樣按照A規則解析,那麼就能顯示正確的文本符號。反之,按照A規則存儲,再按照B規則解析,就會導致亂碼現象。
簡單一點的說就是:
編碼:字元(能看懂的)--位元組(看不懂的)
解碼:位元組(看不懂的)-->字元(能看懂的)
程式碼解釋則是
String(byte[] bytes, String charsetName):通過指定的字符集解碼位元組數組 byte[] getBytes(String charsetName):使用指定的字符集合把字元串編碼為位元組數組 編碼:把看得懂的變成看不懂的 String -- byte[] 解碼:把看不懂的變成看得懂的 byte[] -- String 字元編碼
Character Encoding: 就是一套自然語言的字元與二進位數之間的對應規則。而編碼表則是生活中文字和電腦中二進位的對應規則
字符集
- 字符集
Charset:也叫編碼表。是一個系統支援的所有字元的集合,包括各國家文字、標點符號、圖形符號、數字等。
電腦要準確的存儲和識別各種字符集符號,需要進行字元編碼,一套字符集必然至少有一套字元編碼。常見字符集有ASCII字符集、GBK字符集、Unicode字符集等。

可見,當指定了編碼,它所對應的字符集自然就指定了,所以編碼才是我們最終要關心的。
- ASCII字符集 :
- ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼)是基於拉丁字母的一套電腦編碼系統,用於顯示現代英語,主要包括控制字元(回車鍵、退格、換行鍵等)和可顯示字元(英文大小寫字元、阿拉伯數字和西文符號)。
- 基本的ASCII字符集,使用7位(bits)表示一個字元,共128字元。ASCII的擴展字符集使用8位(bits)表示一個字元,共256字元,方便支援歐洲常用字元。
- ISO-8859-1字符集:
- 拉丁碼錶,別名Latin-1,用於顯示歐洲使用的語言,包括荷蘭、丹麥、德語、義大利語、西班牙語等。
- ISO-8859-1使用單位元組編碼,兼容ASCII編碼。
- GBxxx字符集:
- GB就是國標的意思,是為了顯示中文而設計的一套字符集。
- GB2312:簡體中文碼錶。一個小於127的字元的意義與原來相同。但兩個大於127的字元連在一起時,就表示一個漢字,這樣大約可以組合了包含7000多個簡體漢字,此外數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在ASCII里本來就有的數字、標點、字母都統統重新編了兩個位元組長的編碼,這就是常說的"全形"字元,而原來在127號以下的那些就叫"半形"字元了。
- GBK:最常用的中文碼錶。是在GB2312標準基礎上的擴展規範,使用了雙位元組編碼方案,共收錄了21003個漢字,完全兼容GB2312標準,同時支援繁體漢字以及日韓漢字等。
- GB18030:最新的中文碼錶。收錄漢字70244個,採用多位元組編碼,每個字可以由1個、2個或4個位元組組成。支援中國中國少數民族的文字,同時支援繁體漢字以及日韓漢字等。
- Unicode字符集 :
- Unicode編碼系統為表達任意語言的任意字元而設計,是業界的一種標準,也稱為統一碼、標準萬國碼。
- 它最多使用4個位元組的數字來表達每個字母、符號,或者文字。有三種編碼方案,UTF-8、UTF-16和UTF-32。最為常用的UTF-8編碼。
- UTF-8編碼,可以用來表示Unicode標準中任何字元,它是電子郵件、網頁及其他存儲或傳送文字的應用中,優先採用的編碼。互聯網工程工作小組(IETF)要求所有互聯網協議都必須支援UTF-8編碼。所以,我們開發Web應用,也要使用UTF-8編碼。它使用一至四個位元組為每個字元編碼,編碼規則:
- 128個US-ASCII字元,只需一個位元組編碼。
- 拉丁文等字元,需要二個位元組編碼。
- 大部分常用字(含中文),使用三個位元組編碼。
- 其他極少使用的Unicode輔助字元,使用四位元組編碼。
2.2 編碼問題導致亂碼
在java開發工具IDEA中,使用FileReader 讀取項目中的文本文件。由於IDEA的設置,都是默認的UTF-8編碼,所以沒有任何問題。但是,當讀取Windows系統中創建的文本文件時,由於Windows系統的默認是GBK編碼,就會出現亂碼。
public class ReaderDemo { public static void main(String[] args) throws IOException { FileReader fileReader = new FileReader("C:\a.txt"); int read; while ((read = fileReader.read()) != -1) { System.out.print((char)read); } fileReader.close(); } } 輸出結果:��� 那麼如何讀取GBK編碼的文件呢? 這個時候就得講講轉換流了!
從另一角度來講:字元流=位元組流+編碼表
2.3 InputStreamReader類-----(位元組流到字元流的橋樑)
轉換流java.io.InputStreamReader,是Reader的子類,從字面意思可以看出它是從位元組流到字元流的橋樑。它讀取位元組,並使用指定的字符集將其解碼為字元。它的字符集可以由名稱指定,也可以接受平台的默認字符集。
構造方法
InputStreamReader(InputStream in): 創建一個使用默認字符集的字元流。
InputStreamReader(InputStream in, String charsetName): 創建一個指定字符集的字元流。
構造程式碼如下:
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt")); InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK"); 使用轉換流解決編碼問題
public class ReaderDemo2 { public static void main(String[] args) throws IOException { // 定義文件路徑,文件為gbk編碼 String FileName = "C:\A.txt"; // 創建流對象,默認UTF8編碼 InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName)); // 創建流對象,指定GBK編碼 InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK"); // 定義變數,保存字元 int read; // 使用默認編碼字元流讀取,亂碼 while ((read = isr.read()) != -1) { System.out.print((char)read); // �����ʺ } isr.close(); // 使用指定編碼字元流讀取,正常解析 while ((read = isr2.read()) != -1) { System.out.print((char)read);// 哥敢摸屎 } isr2.close(); } } 2.4 OutputStreamWriter類-----(字元流到位元組流的橋樑)
轉換流java.io.OutputStreamWriter ,是Writer的子類,字面看容易混淆會誤以為是轉為字元流,其實不然,OutputStreamWriter為從字元流到位元組流的橋樑。使用指定的字符集將字元編碼為位元組。它的字符集可以由名稱指定,也可以接受平台的默認字符集。
構造方法
OutputStreamWriter(OutputStream in): 創建一個使用默認字符集的字元流。
OutputStreamWriter(OutputStream in, String charsetName): 創建一個指定字符集的字元流。
構造舉例,程式碼如下:
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("a.txt")); OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("b.txt") , "GBK"); 指定編碼構造程式碼
public class OutputDemo { public static void main(String[] args) throws IOException { // 定義文件路徑 String FileName = "C:\s.txt"; // 創建流對象,默認UTF8編碼 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName)); // 寫出數據 osw.write("哥敢"); // 保存為6個位元組 osw.close(); // 定義文件路徑 String FileName2 = "D:\A.txt"; // 創建流對象,指定GBK編碼 OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK"); // 寫出數據 osw2.write("摸屎");// 保存為4個位元組 osw2.close(); } } 
為了達到最高效率,可以考慮在 BufferedReader 內包裝 InputStreamReader
BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 3、序列化流【理解】
(1)可以把對象寫入文本文件或者在網路中傳輸
(2)如何實現序列化呢?
讓被序列化的對象所屬類實現序列化介面。
該介面是一個標記介面。沒有功能需要實現。
(3)注意問題:
把數據寫到文件後,在去修改類會產生一個問題。
如何解決該問題呢?
在類文件中,給出一個固定的序列化id值。
而且,這樣也可以解決黃色警告線問題
(4)面試題:
什麼時候序列化?
如何實現序列化?
什麼是反序列化?
3.1 何謂序列化
Java 提供了一種對象序列化的機制。用一個位元組序列可以表示一個對象,該位元組序列包含該對象的數據、對象的類型和對象中存儲的屬性等資訊。位元組序列寫出到文件之後,相當於文件中持久保存了一個對象的資訊。
反之,該位元組序列還可以從文件中讀取回來,重構對象,對它進行反序列化。對象的數據、對象的類型和對象中存儲的數據資訊,都可以用來在記憶體中創建對象。看圖理解序列化:

3.2 ObjectOutputStream類
java.io.ObjectOutputStream 類,將Java對象的原始數據類型寫出到文件,實現對象的持久存儲。
構造方法
public ObjectOutputStream(OutputStream out): 創建一個指定OutputStream的ObjectOutputStream。
構造程式碼如下:
FileOutputStream fileOut = new FileOutputStream("aa.txt"); ObjectOutputStream out = new ObjectOutputStream(fileOut); 序列化操作
- 一個對象要想序列化,必須滿足兩個條件:
該類必須實現java.io.Serializable 介面,Serializable 是一個標記介面,不實現此介面的類將不會使任何狀態序列化或反序列化,會拋出NotSerializableException 。
該類的所有屬性必須是可序列化的。如果有一個屬性不需要可序列化的,則該屬性必須註明是瞬態的,使用transient 關鍵字修飾。
public class Employee implements java.io.Serializable { public String name; public String address; public transient int age; // transient瞬態修飾成員,不會被序列化 public void addressCheck() { System.out.println("Address check : " + name + " -- " + address); } } 2.寫出對象方法
public final void writeObject (Object obj) : 將指定的對象寫出。
public class SerializeDemo{ public static void main(String [] args) { Employee e = new Employee(); e.name = "zhangsan"; e.address = "beiqinglu"; e.age = 20; try { // 創建序列化流對象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt")); // 寫出對象 out.writeObject(e); // 釋放資源 out.close(); fileOut.close(); System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年齡沒有被序列化。 } catch(IOException i) { i.printStackTrace(); } } } 輸出結果: Serialized data is saved 3.3 ObjectInputStream類
ObjectInputStream反序列化流,將之前使用ObjectOutputStream序列化的原始數據恢復為對象。
構造方法
public ObjectInputStream(InputStream in): 創建一個指定InputStream的ObjectInputStream。
反序列化操作1
如果能找到一個對象的class文件,我們可以進行反序列化操作,調用ObjectInputStream讀取對象的方法:
public final Object readObject (): 讀取一個對象。
public class DeserializeDemo { public static void main(String [] args) { Employee e = null; try { // 創建反序列化流 FileInputStream fileIn = new FileInputStream("employee.txt"); ObjectInputStream in = new ObjectInputStream(fileIn); // 讀取一個對象 e = (Employee) in.readObject(); // 釋放資源 in.close(); fileIn.close(); }catch(IOException i) { // 捕獲其他異常 i.printStackTrace(); return; }catch(ClassNotFoundException c) { // 捕獲類找不到異常 System.out.println("Employee class not found"); c.printStackTrace(); return; } // 無異常,直接列印輸出 System.out.println("Name: " + e.name); // zhangsan System.out.println("Address: " + e.address); // beiqinglu System.out.println("age: " + e.age); // 0 } } 對於JVM可以反序列化對象,它必須是能夠找到class文件的類。如果找不到該類的class文件,則拋出一個 ClassNotFoundException 異常。
反序列化操作2
另外,當JVM反序列化對象時,能找到class文件,但是class文件在序列化對象之後發生了修改,那麼反序列化操作也會失敗,拋出一個InvalidClassException異常。發生這個異常的原因如下:
1、該類的序列版本號與從流中讀取的類描述符的版本號不匹配
2、該類包含未知數據類型
2、該類沒有可訪問的無參數構造方法
Serializable 介面給需要序列化的類,提供了一個序列版本號。serialVersionUID 該版本號的目的在於驗證序列化的對象和對應類是否版本匹配。
public class Employee implements java.io.Serializable { // 加入序列版本號 private static final long serialVersionUID = 1L; public String name; public String address; // 添加新的屬性 ,重新編譯, 可以反序列化,該屬性賦為默認值. public int eid; public void addressCheck() { System.out.println("Address check : " + name + " -- " + address); } } 3.4 序列化集合練習
- 將存有多個自定義對象的集合序列化操作,保存到
list.txt文件中。 - 反序列化
list.txt,並遍歷集合,列印對象資訊。
案例分析
- 把若干學生對象 ,保存到集合中。
- 把集合序列化。
- 反序列化讀取時,只需要讀取一次,轉換為集合類型。
- 遍歷集合,可以列印所有的學生資訊
案例程式碼實現
public class SerTest { public static void main(String[] args) throws Exception { // 創建 學生對象 Student student = new Student("老王", "laow"); Student student2 = new Student("老張", "laoz"); Student student3 = new Student("老李", "laol"); ArrayList<Student> arrayList = new ArrayList<>(); arrayList.add(student); arrayList.add(student2); arrayList.add(student3); // 序列化操作 // serializ(arrayList); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt")); // 讀取對象,強轉為ArrayList類型 ArrayList<Student> list = (ArrayList<Student>)ois.readObject(); for (int i = 0; i < list.size(); i++ ){ Student s = list.get(i); System.out.println(s.getName()+"--"+ s.getPwd()); } } private static void serializ(ArrayList<Student> arrayList) throws Exception { // 創建 序列化流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt")); // 寫出對象 oos.writeObject(arrayList); // 釋放資源 oos.close(); } } 4、列印流【掌握】
4.1 何謂列印流
平時我們在控制台列印輸出,是調用print方法和println方法完成的,各位用了這麼久的輸出語句肯定沒想過這兩個方法都來自於java.io.PrintStream類吧,哈哈。該類能夠方便地列印各種數據類型的值,是一種便捷的輸出方式。
列印流分類:
位元組列印流PrintStream,字元列印流PrintWriter
列印流特點:
A:只操作目的地,不操作數據源
B:可以操作任意類型的數據
C:如果啟用了自動刷新,在調用println()方法的時候,能夠換行並刷新
D:可以直接操作文件
這個時候有同學就要問了,哪些流可以直接操作文件呢?答案很簡單,如果該流的構造方法能夠同時接收File和String類型的參數,一般都是可以直接操作文件的!
PrintStream是OutputStream的子類,PrintWriter是Writer的子類,兩者處於對等的位置上,所以它們的API是非常相似的。二者區別無非一個是位元組列印流,一個是字元列印流。
4.2 位元組輸出列印流PrintStream複製文本文件
import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.PrintStream; public class PrintStreamDemo { public static void main(String[] args) throws IOException { BufferedReader br=new BufferedReader(new FileReader("copy.txt")); PrintStream ps=new PrintStream("printcopy.txt"); String line; while((line=br.readLine())!=null) { ps.println(line); } br.close(); ps.close(); } } 4.3 字元輸出列印流PrintWriter複製文本文件
import java.io.BufferedReader; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; /** * 使用列印流複製文本文件 */ public class PrintWriterDemo { public static void main(String[] args) throws IOException { BufferedReader br=new BufferedReader(new FileReader("aa.txt")); PrintWriter pw=new PrintWriter("printcopyaa.txt"); String line; while((line=br.readLine())!=null) { pw.println(line); } br.close(); pw.close(); } } 5、Properties屬性類
我想各位對這個Properties類多多少少也接觸過了,首先Properties類並不在IO包下,那為啥要和IO流一起講呢?原因很簡單因為properties類經常和io流的聯合一起使用。
(1)是一個集合類,Hashtable的子類
(2)特有功能
A:public Object setProperty(String key,String value)
B:public String getProperty(String key)
C:public SetstringPropertyNames()
(3)和IO流結合的方法
把鍵值對形式的文本文件內容載入到集合中
public void load(Reader reader)
public void load(InputStream inStream)
把集合中的數據存儲到文本文件中
public void store(Writer writer,String comments)
public void store(OutputStream out,String comments)
5.1 Properties概述
java.util.Properties 繼承於Hashtable ,來表示一個持久的屬性集。它使用鍵值結構存儲數據,每個鍵及其對應值都是一個字元串。該類也被許多Java類使用,比如獲取系統屬性時,System.getProperties 方法就是返回一個Properties對象。
5.2 Properties類
構造方法
public Properties() :創建一個空的屬性列表。
基本的存儲方法
public Object setProperty(String key, String value): 保存一對屬性。
public String getProperty(String key):使用此屬性列表中指定的鍵搜索屬性值。public Set<String> stringPropertyNames():所有鍵的名稱的集合。
public class ProDemo { public static void main(String[] args) throws FileNotFoundException { // 創建屬性集對象 Properties properties = new Properties(); // 添加鍵值對元素 properties.setProperty("filename", "a.txt"); properties.setProperty("length", "209385038"); properties.setProperty("location", "D:\a.txt"); // 列印屬性集對象 System.out.println(properties); // 通過鍵,獲取屬性值 System.out.println(properties.getProperty("filename")); System.out.println(properties.getProperty("length")); System.out.println(properties.getProperty("location")); // 遍歷屬性集,獲取所有鍵的集合 Set<String> strings = properties.stringPropertyNames(); // 列印鍵值對 for (String key : strings ) { System.out.println(key+" -- "+properties.getProperty(key)); } } } 輸出結果: {filename=a.txt, length=209385038, location=D:a.txt} a.txt 209385038 D:a.txt filename -- a.txt length -- 209385038 location -- D:a.txt 與流相關的方法
public void load(InputStream inStream): 從位元組輸入流中讀取鍵值對。
參數中使用了位元組輸入流,通過流對象,可以關聯到某文件上,這樣就能夠載入文本中的數據了。現在文本數據格式如下:
filename=Properties.txt length=123 location=C:Properties.txt 載入程式碼演示:
public class ProDemo { public static void main(String[] args) throws FileNotFoundException { // 創建屬性集對象 Properties pro = new Properties(); // 載入文本中資訊到屬性集 pro.load(new FileInputStream("Properties.txt")); // 遍歷集合併列印 Set<String> strings = pro.stringPropertyNames(); for (String key : strings ) { System.out.println(key+" -- "+pro.getProperty(key)); } } } 輸出結果: filename -- Properties.txt length -- 123 location -- C:Properties.txt 文本中的數據,必須是鍵值對形式,可以使用空格、等號、冒號等符號分隔。
怎麼說呢,io流的基礎回顧就先告一段落了,淺嘗輒止。循序漸進,實踐中慢慢總結!更何況我還很low,依舊任重而道遠。
現在jdk已經出到13了,io流也有了許多的變化。有時間會從頭整理一下,一定會有機會的!
最後,歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術...

