【小白看的Java教程】第三十七章,Mr.R和Mr.W:Java中的IO【薦】
File類(掌握)
File課理解為檔案和資料夾(目錄),用於表示磁碟中某個檔案或資料夾的路徑。該類包含了檔案的建立、刪除、重新命名、判斷是否存在等方法。
只能設定和獲取檔案本身的資訊(檔案大小,是否可讀),不能設定和獲取檔案裡面的內容。
-
Unix: 嚴格區分大小寫,使用”/”來表示路徑分隔符。
-
Windows: 預設情況下是不區分大小寫的,使用”\”來分割目錄路徑。但是在Java中一個”\”表示轉義,所以在Windows系統中就得使用兩個”\”。
操作File常見方法:
-
String getName():獲取檔名稱
-
String getPath():獲取檔案路徑
-
String getAbsolutePath():獲取絕對路徑
-
File getParentFile():獲取上級目錄檔案
-
boolean exists():判斷檔案是否存在
-
boolean isFile() :是否是檔案
-
boolean isDirectory():判斷是否是目錄
-
boolean delete() :刪除檔案
-
boolean mkdirs():建立當前目錄和上級目錄
-
File[] listFiles() :列出所有檔案物件
public class FileDemo { public static void main(String[] args) throws Exception { File f = new File("C:/test/123.txt"); System.out.println(f.getName());//123.txt System.out.println(f.getPath());//C:/test/123.txt System.out.println(f.getAbsolutePath());//C:/test/123.txt System.out.println(f.getParentFile().getName());//test System.out.println(f.exists());//true System.out.println(f.isFile());//true System.out.println(f.isDirectory());//false //如果當前檔案的父資料夾不存在,則建立 if(!f.getParentFile().exists()) { f.getParentFile().mkdirs(); } //列出當前資料夾中所有檔案 File[] fs = f.getParentFile().listFiles(); for (File file : fs) { System.out.println(file); } } }
列出給定目錄中的全部檔案的路徑,包括給定目錄下面的所有子目錄。
public static void list(File file) { if (file.isDirectory()) { // 如果是資料夾,則繼續列出 File[] fs = file.listFiles(); if (fs != null) { for (File f : fs) { list(f); } } } System.out.println(file); }
字元編碼
字元編碼的發展歷程(瞭解)
階段一:
計算機只認識數字,在計算機裡一切資料都是以數字來表示,因為英文符號有限,所以規定使用的位元組的最高位是0。每一個位元組都是以0~127之間的數字來表示,比如A對應65,a對應97。此時把每一個位元組按照順序存放在一張表格中,這就是美國標準資訊交換碼——ASCII編碼表。
階段二:
隨著計算機在全球的普及,很多國家和地區都把自己的字元引入了計算機,比如漢字。此時發現一個位元組(128個)能表示數字範圍太小,而漢字太多,128個數字不能包含所有的中文漢字,那麼此時就規定使用兩個位元組一起來表示一個漢字。
規定:原有的ASCII字元的編碼保持不變,仍然使用一個位元組表示,為了區別一箇中文字元與兩個ASCII碼字元,中文字元的每個位元組最高位(符號位)規定為1(中文的二進位制是負數),該規範就是GB2312編碼表。後來在GB2312碼錶的基礎上增加了更多的中文漢字,也就出現了更強大的GBK碼錶。
階段三:
中國人是認識漢字的,現在需要和外國人通過網路交流,此時需要把把漢字資訊傳遞給外國人,但外國的碼錶中沒有收錄漢字,此時就會把漢字顯示為另一個符號甚至不能識別的亂碼。為了解決各個國家因為本地化字元編碼帶來的影響,就乾脆把全世界所有的符號統一收錄進Unicode編碼表。
如果使用Unicode碼錶,那麼某一個字元在全世界任何地方都是固定的。比如’哥’這個字,在任何地方都是以十六進位制的54E5來表示,因此說Unicode是國際統一編碼。
常見的字元編碼和操作(瞭解)
常見的字符集
-
ASCII:佔一個位元組,只能包含128個符號。不能表示漢字。
-
ISO-8859-1:也稱之為latin-1,佔一個位元組,收錄西歐語言,不能表示漢字。
-
GB2312/GBK/GB18030:佔兩個位元組,支援中文。
-
ANSI:佔兩個位元組,在簡體中文的作業系統中ANSI 就指的是 GBK。
-
UTF-8:是一種針對Unicode的可變長度字元編碼,是Unicode的實現方式之一,支援中文。在開發中建議使用。
-
UTF-8 BOM:是微軟搞出來的一種編碼,不要使用。
儲存字母、數字、漢字:
儲存字母和數字無論是什麼字符集都佔1個位元組.
儲存漢字,GBK家族佔兩個位元組,UTF-8家族佔3個位元組。
不能使用單位元組的字符集(ASCII、ISO-8859-1)來儲存中文,否則會亂碼。
字元的編碼和解碼操作(掌握)
資料在網路上傳輸是以二進位制的格式,二進位制格式就是byte陣列,此時需要把資訊做編碼和解碼處理。
-
編碼:把字串轉換為byte陣列 String—>byte[]
-
解碼:把byte陣列轉換為字串 byte[]—>String
注意:一定要保證編碼和解碼的字元相同,才能正確解碼出資訊。
經典案例:在表單中填寫中文,為什麼在服務端看到的是亂碼問題。
情景分析,比如瀏覽器使用UTF-8編碼,伺服器使用ISO-8859-1編碼。
此時編碼和解碼的字元型別不同,那麼亂碼就出現了。
先來分析亂碼產生的原因:
亂碼的解決方案:
public class CodeDemo {
public static void main(String[] args) throws Exception {
String input = "龍哥";//模擬使用者輸入的中文資料
//編碼操作:String -> byte[]
byte[] data = input.getBytes("UTF-8");
System.out.println(Arrays.toString(data));//[-23, -66, -103, -27, -109, -91]
//解碼操作:byte[] -> String
//因為伺服器時老外寫的,老外在解碼的時候使用ISO-8859-1,此時就亂碼了
String ret = new String(data, "ISO-8859-1");
System.out.println(ret);//輸出:龙哥
//---------------------------------------------
//解決方案:重新對亂碼編碼回到byte[],重新按照UTF-8解碼
data = ret.getBytes("ISO-8859-1");
System.out.println(Arrays.toString(data));//[-23, -66, -103, -27, -109, -91]
ret = new String(data,"UTF-8");
//---------------------------------------------
System.out.println(ret);//輸出:龍哥
}
}
IO流操作
IO概述(瞭解)
什麼是IO,Input和Output,即輸入和輸出。
電腦相關的IO裝置:和電腦通訊的裝置,此時要站在電腦的角度,把資訊傳遞給電腦叫輸入裝置,把電腦資訊傳遞出來的叫輸出裝置。
-
輸入裝置:麥克風、掃描器、鍵盤、滑鼠等
-
輸出裝置:顯示器、印表機、投影儀、耳機、音響等
為什麼程式需要IO呢?
案例1:打遊戲操作,需要儲存遊戲的資訊。
- 此時需要把遊戲中的資料儲存起來,資料只能儲存在檔案中。
案例2:打遊戲操作,需求讀取之前遊戲的記錄資訊,資料儲存在一個檔案中的。
- 此時遊戲程式需要去讀取檔案中的資料,並顯示在遊戲中。
IO操作是一個相對的過程,一般的,我們在程式角度來思考(程式的記憶體)。
-
程式需要讀取資料:檔案——>程式,輸入操作
-
程式需要儲存資料:程式——>檔案,輸出操作
IO操作示意圖(瞭解)
講解IO知識點的時候,習慣和生活中的水流聯絡起來,一起來看看復古的水井和水缸。
此時站在水缸的角度,分析IO的操作方向:
輸入操作:水井——>水缸
輸出操作:水缸——>飯鍋
注意:誰擁有資料,誰就是源,把資料流到哪裡,哪裡就是目標。那麼,請問水缸是源還是目標。
流的分類(掌握)
根據流的不同特性,流的劃分是不一樣的,一般按照如下情況來考慮:
-
按流動方向:分為輸入流和輸出流
-
按資料傳輸單位:分為位元組流和字元流,即每次傳遞一個位元組(byte)或一個字元(char)
-
按功能上劃分:分為節點流和處理流,節點流功能單一,處理流功能更強
流的流向是相對的,我們一般站在程式的角度:
-
程式需要資料 → 把資料讀進來 → 輸入操作(read):讀進來
-
程式儲存資料 → 把資料寫出去 → 輸出操作(write):寫出去
六字箴言:讀進來,寫出去(仔細揣摩這六個字有什麼高深的含義)
四大基流
操作IO流的模板:
1):建立源或者目標物件(挖井).
輸入操作: 把檔案中的資料流向到程式中,此時檔案是源,程式是目標.
輸出操作: 把程式中的資料流向到檔案中,此時檔案是目標,程式是源.
2):建立IO流物件(水管).
輸入操作: 建立輸入流物件.
輸出操作: 建立輸出流物件.
3):具體的IO操作.
輸入操作: 輸入流物件的read方法.
輸出操作: 輸出流物件的write方法.
4):關閉資源(勿忘). 一旦資源關閉之後,就不能使用流物件了,否則報錯.
輸入操作: 輸入流物件.close();
輸出操作: 輸出流物件.close();
注意:
- 四大抽象流是不能建立物件的,一般的我們根據不同的需求建立他們不同的子類物件,比如操作檔案時就使用檔案流。
- 不管是什麼流,操作完畢都必須呼叫close方法,釋放資源。
常見輸入輸出流
##InputStream(位元組輸入流) 類的宣告為:
public abstract class InputStream extends Object implements Closeable
表示位元組輸入流的所有類的超類。
常用方法:
-
public void close() throws IOException:關閉此輸入流並釋放與該流關聯的所有系統資源。 InputStream 的 close 方法不執行任何操作。
-
public abstract int read() throws IOException:從輸入流中讀取一個位元組資料並返回該位元組資料,如果到達流的末尾,則返回 -1。
-
public int read(byte[] buff) throws IOException:從輸入流中讀取多個位元組資料,並存儲在緩衝區陣列 buff 中。返回已讀取的位元組數量,如果已到達流的末尾,則返回 -1。
##OutputStream(位元組輸出流) 類的宣告為:public abstract class OutputStream extends Object implements Closeable, Flushable,表示位元組輸出流的所有類的超類。 常用方法:
-
public void close() throws IOException:關閉此輸出流並釋放與此流有關的所有系統資源。
-
public abstract void write(int b) throws IOException:將指定的一個位元組資料b寫入到輸出流中。
-
public void write(byte[] buff) throws IOException:把陣列buff中所有位元組資料寫入到輸出流中。
-
public void write(byte[] b, int off,int len) throws IOException:把陣列buff中從索引off 開始的len 個位元組寫入此輸出流中。
##Reader(字元輸入流) 類的宣告為:
public abstract class Reader extends Object implements Readable, Closeable
表示字元輸入流的所有類的超類。
常用方法:
- public abstract void close() throws IOException:關閉此輸入流並釋放與該流關聯的所有系統資源。
- public int read() throws IOException:從輸入流中讀取一個字元資料並返回該字元資料,如果到達流的末尾,則返回 -1。
- public int read(char[] cbuf) throws IOException:從輸入流中讀取多個字元,並存儲在緩衝區陣列 cbuf 中。返回已讀取的字元數,如果已到達流的末尾,則返回 -1。
##Writer(字元輸出流)
類的宣告為:
public abstract class Writer extends Object implements Appendable, Closeable, Flushable
表示字元輸出流的所有類的超類。
常用方法:
-
public abstract void flush() throws IOException:重新整理此輸出流並強制寫出所有緩衝的輸出字元。
-
public abstract void close() throws IOException:關閉此輸入流並釋放與該流關聯的所有系統資源。
-
public void write(int c) throws IOException:將指定的一個字元資料c寫入到輸出流中。
-
public void write(char[] cbuf) throws IOException:把陣列cbuf中cbuf.length 個字元資料寫入到輸出流中。
-
public abstract void write(char[] cbuf, int off,int len) throws IOException:把陣列cbuf中從索引off 開始的len 個字元寫入此輸出流中。
-
public void write(String str) throws IOException:將str字串資料寫入到輸出流中。
##檔案流(重點) 當程式需要讀取檔案中的資料或者把資料儲存到檔案中去,此時就得使用檔案流,但是注意只能操作純文字檔案(txt格式),不要使用Word、Excel。檔案流比較常用。 需求1:使用檔案位元組輸出流,把程式中資料儲存到result1.txt檔案,操作英文
private static void test1() throws Exception {
//1):建立源或者目標物件
File dest = new File("file/result1.txt");
//2):建立IO流物件
FileOutputStream out = new FileOutputStream(dest);
//3):具體的IO操作
out.write(65);//輸出A
out.write(66);//輸出B
out.write(67);//輸出C
String str = "to be or not to be";
out.write(str.getBytes());//輸出str字串中所有內容
//4):關閉資源(勿忘)
out.close();
}
需求2:使用檔案位元組輸入流,讀取result1.txt檔案中的資料
private static void test2() throws Exception {
//1):建立源或者目標物件
File src = new File("file/result1.txt");
//2):建立IO流物件
FileInputStream in = new FileInputStream(src);
//3):具體的IO操作
System.out.println((char)in.read());//讀取A位元組
System.out.println((char)in.read());//讀取B位元組
System.out.println((char)in.read());//讀取C位元組
byte[] buff = new byte[5];//準備容量為5的緩衝區
int len = in.read(buff);//讀取5個位元組資料,並存儲到buff陣列中
System.out.println(Arrays.toString(buff));//[116, 111, 32, 98, 101]
System.out.println(len);//返回讀取了幾個位元組
//4):關閉資源(勿忘)
in.close();
}
需求3:使用檔案字元輸出流,把程式中資料儲存到result2.txt檔案,操作中文
private static void test3() throws Exception {
//1):建立源或者目標物件
File dest = new File("file/result2.txt");
//2):建立IO流物件
FileWriter out = new FileWriter(dest);
//3):具體的IO操作
out.write('辛');//輸出A
out.write('棄');//輸出B
out.write('疾');//輸出C
String str = "眾裡尋他千百度,驀然回首,那人卻在,燈火闌珊處。";
out.write(str.toCharArray());
out.write(str);//String的本質就是char[]
//4):關閉資源(勿忘)
out.close();
}
需求4:使用檔案字元輸入流,讀取result2.txt檔案中的資料
private static void test4() throws Exception {
//1):建立源或者目標物件
File src = new File("file/result2.txt");
//2):建立IO流物件
FileReader in = new FileReader(src);
//3):具體的IO操作
System.out.println(in.read());//讀取辛字元
System.out.println(in.read());//讀取棄字元
System.out.println(in.read());//讀取疾字元
char[] buff = new char[5];//準備容量為5的緩衝區
int len = in.read(buff);//讀取5個字元資料,並存儲到buff陣列中
System.out.println(Arrays.toString(buff));//[眾, 裡, 尋, 他, 千]
System.out.println(len);//返回讀取了幾個位元組
//4):關閉資源(勿忘)
in.close();
}
##位元組流和字元流選用問題 使用記事本開啟某個檔案,如果可以看到內容的就是文字檔案,否則可以暫時認為是二進位制格式的。 一般的,操作二進位制檔案(圖片、音訊、視訊等)必須使用位元組流。操作文字檔案使用字元流,尤其是操作帶有中文的檔案,使用字元流不容易導致亂碼,因為使用位元組流可能出現讀取半個漢字的尷尬(漢字由兩個或三個位元組組成)。當然,如果不清楚屬於哪一型別檔案,都可以使用位元組流。
JavaIO流常見使用方式
檔案拷貝操作
需求:把copy_before.txt檔案中的資料拷貝到copy_after.txt檔案中
private static void copy() throws Exception {
//1):建立源或者目標物件
File src = new File("file/copy_before.txt");
File dest = new File("file/copy_after.txt");
//2):建立IO流物件
FileReader in = new FileReader(src);
FileWriter out = new FileWriter(dest);
//3):具體的IO操作
int len = -1;//記錄以及讀取了多個字元
char[] buff = new char[1024];//每次可以讀取1024個字元
len = in.read(buff);//先讀取一次
while(len > 0) {
//邊讀邊寫
out.write(buff, 0, len);
len = in.read(buff);//再繼續讀取
}
//4):關閉資源(勿忘)
out.close();
in.close();
}
如何,正確處理異常:
private static void copy2() {
//1):建立源或者目標物件
File src = new File("file/copy_before.txt");
File dest = new File("file/copy_after.txt");
//把需要關閉的資源,宣告在try之外
FileReader in = null;
FileWriter out = null;
try {
//可能出現異常的程式碼
//2):建立IO流物件
in = new FileReader(src);
out = new FileWriter(dest);
//3):具體的IO操作
int len = -1;//記錄以及讀取了多個字元
char[] buff = new char[1024];//每次可以讀取1024個字元
len = in.read(buff);//先讀取一次
while (len > 0) {
out.write(buff, 0, len);
len = in.read(buff);//再繼續讀取
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//4):關閉資源(勿忘)
try {
if (out != null) {
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (in != null) {
in.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
此時關閉資源的程式碼,又臭又長,在後續的學習中為了方便就直接使用throws丟擲IO異常了,在實際開發中需要處理。
緩衝流
節點流的功能都比較單一,效能較低。處理流,也稱之為包裝流,相對於節點流更高階,這裡存在一個設計模式——裝飾設計模式,此時撇開不談。
包裝流如何區分?寫程式碼的時候,發現建立流物件時,需要傳遞另一個流物件,類似:
new 流類A( new 流類B(..));
那麼流A就屬於包裝流,當然B可能屬於節點流也可能屬於包裝流。
有了包裝流之後,我們只關係包裝流的操作即可,比如只需要關閉包裝流即可,無需在關閉節點流。
非常重要的包裝流——緩衝流,根據四大基流都有各自的包裝流:
BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter
緩衝流內建了一個預設大小為8192個位元組或字元的快取區,緩衝區的作用用來減少磁碟的IO操作,拿位元組緩衝流舉例,比如一次性讀取8192個位元組到記憶體中,或者存滿8192個位元組再輸出到磁碟中。
操作資料量比較大的流,都建議使用上對應的快取流。
需求:把郭德綱-報菜名.mp3檔案中的資料拷貝到郭德綱-報菜名2.mp3檔案中
private static void copy3() throws Exception {
//1):建立源或者目標物件
File src = new File("file/郭德綱-報菜名.mp3");
File dest = new File("file/郭德綱-報菜名2.mp3");
//2):建立IO流物件
BufferedInputStream bis =
new BufferedInputStream(new FileInputStream(src), 8192);
BufferedOutputStream bos =
new BufferedOutputStream(new FileOutputStream(dest), 8192);
//3):具體的IO操作
int len = -1;//記錄以及讀取了多個字元
byte[] buff = new byte[1024];//每次可以讀取1024個字元
len = bis.read(buff);//先讀取一次
while (len > 0) {
//邊讀邊寫
bos.write(buff, 0, len);
len = bis.read(buff);//再繼續讀取
}
//4):關閉資源(勿忘)
bos.close();
bis.close();
}
物件序列化
序列化:指把Java堆記憶體中的物件資料,通過某種方式把物件資料儲存到磁碟檔案中或者傳遞給給網路上傳輸。序列化在分散式系統在應用非常廣泛。
反序列化:把磁碟檔案中的物件的資料或者把網路節點上的物件資料恢復成Java物件的過程。
需要做序列化的類必須實現序列化介面:java.io.Serializable(這是標誌介面[沒有抽象方法])
可以通過IO中的物件流來做序列化和反序列化操作。
-
ObjectOutputStream:通過writeObject方法做序列化操作的
-
ObjectInputStream:通過readObject方法做反序列化操作的
如果欄位使用transient 修飾則不會被序列化。
class User implements **Serializable** {
private String name;
private transient String password;
private int age;
public User(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
public String getName() {
return name;
}
public String getPassword() {
return password;
}
public int getAge() {
return age;
}
public String toString() {
return "User [name=" + name + ", password=" + password + ", age=" + age + "]";
}
}
測試程式碼
public class ObjectStreamDemo {
public static void main(String[] args) throws Exception {
String file = "file/obj.txt";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
User u = new User("Will", "1111", 17);
out.writeObject(u);
out.close();
//--------------------------------------
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Object obj = in.readObject();
in.close();
System.out.println(obj);
}
}
obj.txt檔案
序列化的版本問題
當類實現Serializable介面後,在編譯的時候就會根據欄位生成一個預設的serialVersionUID值,並在序列化操作時,寫到序列化資料檔案中。
但隨著專案的升級系統的class檔案也會升級(增加一個欄位/刪除一個欄位),此時再重新編譯,物件的serialVersionUID值又會改變。那麼在反序列化時,JVM會把物件資料資料中的serialVersionUID與本地位元組碼中的serialVersionUID進行比較,如果值不相同(意味著類的版本不同),那麼報異常InvalidCastException,即:類版本不對應,不能進行反序列化。如果版本號相同,則可以進行反序列化。
為了避免程式碼版本升級而造成反序列化因版本不相容而失敗的問題,在開發中我們可以故意在類中提供一個固定的serialVersionUID值。
class User implements Serializable {
private static final long serialVersionUID = 1L;
//TODO
}
查漏補缺
列印流
列印流是一種特殊是輸出流,可以輸出任意型別的資料,比一般的輸出流更好用。可以作為處理流包裝一個平臺的節點流使用,平時我們使用的System.out.println其實就是使用的列印流。
-
PrintStream :位元組列印流
-
PrintWriter :字元列印流
列印流中的方法:
-
提供了print方法:列印不換行
-
提供了println方法:先列印,再換行
private static void test5() throws Exception {
//1):建立源或者目標物件
File dest = new File("file/result3.txt");
//2):建立IO流物件
PrintStream ps = new PrintStream(new FileOutputStream(dest));
//3):具體的IO操作
ps.println("Will");
ps.println(17);
ps.println("眾裡尋他千百度,驀然回首,那人卻在,燈火闌珊處。");
//4):關閉資源(勿忘),列印流可以不用關閉
}
標準IO
標準的輸入:通過鍵盤錄入資料給程式
標準的輸出:在螢幕上顯示程式資料
在System類中有兩個常量int和out分別就表示了標準流:
InputStream in = System.in;
PrintStream out = System.out;
需求:做一個ECHO(回聲)的小案例
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in); //接受使用者輸入資料後敲回車
while (sc.hasNextLine()) { //判斷使用者是否輸入一行資料
String line = sc.nextLine(); //獲取使用者輸入的資料
System.out.println("ECHO:" + line);//顯示使用者輸入的資料
}
}
IO流小結
在開發中使用比較多的還是位元組和字元流的讀寫操作,務必要非常熟練,再體會一下六字箴言(讀進來,寫出去),到底有何深意。
綜合練習題:做一個統計程式碼行數的程式,掃描一個目錄能統計出該目錄中包括所有子目錄中所有Java檔案的行數,不統計空行。
若要獲得最好的學習效果,需要配合對應教學視訊一起學習。需要完整教學視訊,請參看https://ke.qq.com/course/272077。