java-IO程式設計
IO基礎
io流
IO流是一種順序讀寫資料的模式:
- 單向流動
- 以byte為最小單位(位元組流)
如果字元不是單子節表示的ASCLL碼,Java還提供一下解決方案:
- java還提供了reader、writer表示字元流
- 字元流傳輸的最小單位是char
- 字元流輸出byte取決於編碼方式
reader、writer本質上是一個能自動編解碼的InputStream/OutputStream: 使用reader雖然讀入的資料來源是子節,但是我們讀入的資料都是char型別的字元
使用InputStream雖然讀入的資料和源一樣都是byte子節,但是我們可以根據編碼將二進位制byte陣列轉換成字串達到和reader一樣的效果 那麼如何選擇reader和InputStream?這取決於資料來源,如果資料來源不是文字就只能使用InputStream,如果是文字使用reader更加方便。同步io與非同步io
同步io:
- 讀寫io時程式碼等待資料返回後才繼續執行後續程式碼
- 程式碼編寫簡單,cpu執行效率低
非同步io:
- 讀寫io時僅發出請求,然後立刻執行後續程式碼
- 程式碼編寫複雜,cpu執行效率高
JDK提供的java.io是同步io,java.nio是非同步io
File物件
java.io.File表示檔案系統的一個檔案或者目錄 建立方法:可以是相對路徑或者絕對路徑
// windows
File f=new File("c:\\win\\note.exe");
// linux
File f=new File("/usr/bin/javac");
複製程式碼
獲取路徑/絕對路徑/規範路徑:getPath() / getAbsolutePath() / getCanonicalPath() 判斷檔案或目錄:
- isFile():是否是檔案
- isDirectory():是否是目錄
需要注意的是構造一個File物件,即使我們傳入的檔案或目錄不存在也不會報錯,因為沒有對磁碟進行任何操作,只有在呼叫某些方法的時候如果檔案或目錄不存在才會報錯。
檔案操作:
- canRead():是否允許讀取該檔案
- canWrite():是否允許寫入該檔案
- canExecute():是否允許執行該檔案
- length():獲取檔案大小
- createNewFile():建立一個新檔案
- static createTempFile():建立一個臨時檔案
- delete():刪除該檔案
- deleteOnExit():在JVM退出時刪除該檔案
目錄操作:
- String[] list():列出目錄下的檔案和子目錄名
- File[] listFiles():列出目錄下的檔案和子目錄名
- File[] listFiles(FileFilter filter)
- File[] listFiles(FilenameFilter filter)
- mkdir():建立該目錄
- mkdirs():建立該目錄,並在必要時將不存在的父目錄也創建出來
- delete():刪除該目錄
Input和Output
InputStream
java.io.InputStream是所有輸入流的超類
- int read()從輸入流中讀取資料的下一個位元組,返回0到255範圍內的int位元組值。如果因為已經到達流末尾而沒有可用的位元組,則返回-1。在輸入資料可用、檢測到流末尾或者丟擲異常前,此方法一直阻塞。
- int read(byte[])讀取若干位元組並填充到byte[]陣列,返回讀取的位元組數
- read()方法是阻塞(blocking)的
完整的讀取InputStream所有子節
改造後的程式碼,使用快取加快讀取的速度:package com.feiyangedu.sample;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
try (InputStream input = new FileInputStream("readme.txt")) {
int n;
byte[] buffer = new byte[1000];
while ((n = input.read(buffer)) != -1) {
System.out.println(n);
}
}
}
}
複製程式碼
上面程式碼存在一個問題,如果在讀取過程中發生io錯誤,InputStream就無法正確的關閉資源得不到釋放。改造後的程式碼如下:
或者(JDK1.7以後的寫法) 利用緩衝區一次讀取多個子節,提高效率: ByteArrayInputStream可以在記憶體中模擬一個InputStream:他的作用是將一個陣列變成一個InputStream,常常用於測試
OutputStream
OutputStream是所有輸出流的超類:
- write(int b)寫入一個位元組
- write(byte[])寫入byte[]陣列的所有位元組
- close():關閉輸出流,使用try(resource)可以保證OutputStream正確關閉
- flush()方法將緩衝器內容輸出,通常是自動呼叫不需要手動呼叫
package com.feiyangedu.sample;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) throws IOException {
try (OutputStream output = new FileOutputStream("output.txt")) {
byte[] b1 = "Hello".getBytes("UTF-8");
output.write(b1);
byte[] b2 = "你好".getBytes("UTF-8");
output.write(b2);
}
}
}
複製程式碼
輸入流、輸出流、位元組陣列緩衝區的綜合例項
InputStream輸入流、OutputStream輸出流、ByteArrayInputStream輸入緩衝區、ByteArrayOutStream輸出緩衝區的協調使用。
輸入流與輸出流可以比喻成水管的兩端,當水流通過入口(InputStream)流入通過出口(OutputStream)流出完成檔案或者字串從一個點到另一個點的移動或者轉換,在這個過程中似乎不需要緩衝區的介入。那麼什麼時候會用到緩衝區呢?當你想在輸入流和輸出流中間有很多操作,或者將輸入流“複製”一份去做另一個操作的時候就要用到緩衝區了。 比如我們上傳圖片,為了更加安全,通過檔案的前四個字串來判斷檔案的型別: (寫一個例項)
// 建立輸出流緩衝區
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = file.read(buffer)) > -1 ) {
baos.write(buffer,0,len);
}
// 將緩衝區資料寫入到陣列中
baos.flush();
// 關閉檔案
baos.close();
// 建立輸出流緩衝區,將輸入流緩衝區的資料寫入到輸入流緩衝區中,這樣就實現了一次輸入多次輸出的效果
InputStream getType = new ByteArrayInputStream(baos.toByteArray()); // 拿去檢測檔案型別
InputStream fileImg = new ByteArrayInputStream(baos.toByteArray()); // 如果檔案型別合法,拿去上傳檔案
複製程式碼
總結
- OutputStream是所有輸出流的超類
- FileOutputStream實現了檔案流輸出
- ByteArrayOutStream在記憶體中模擬一個位元組流輸出
- 使用try(resource)可以保證OutputStream正確關閉
Filter模式
已FileInputStream()為例,他從檔案中讀取資料,是最終資料來源。
在FileInputStream上增加功能的常規做法
如果我們要給FileInputStream新增緩衝功能:
BufferedFileInputStream extends FileInputStream
派生一個BufferedFileInputStream
如果給FileInputStream新增計算簽名的功能:
DigestFileInputStream extends FileInputStream
派生一個DigestFileInputStream
.....
如果要新增更多的功能就需要更多的子類去擴充套件,這樣的做的弊端是造成子類爆炸
JDK的Filter模式
JDK為瞭解決上面的問題把InputStream分為兩類:
- 直接提供資料的的InputStream:(資料真正的來源)
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
- ...
- 提供額外附加功能的InputStream:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
- ...
如何使用
組合:將一個物件複製給另一個物件的變數,使其持有另一個的物件的特性和方法 當我們使用InputStream的時候我們要根據實際情況組合使用:
上圖顯示FileInputStream提供資料後經過兩次包裝後,具有了更多的功能,但是無論經過多少次包裝他依然可以向上轉型為InputStream然後進行操作。Filter模式又稱Decorator模式,通過少量的類實現了各種功能的組合。
InputStream繼承樹
FilterInputStream 的作用是用來“封裝其它的輸入流,併為它們提供額外的功能”。它的常用的子類有BufferedInputStream和DataInputStream。 一邊是提供資料的實現類,一邊是提供附加功能的實現類。通過兩邊實現類的組合實現更多的功能。
個人理解:提供資料的實現類不能進行同類包裝,而功能類可以同類包裝。例項
package com.feiyangedu.sample;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
public class Main {
public static void main(String[] args) throws IOException {
try (InputStream input = new GZIPInputStream(new BufferedInputStream(new FileInputStream("test.txt.gz")))) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = input.read(buffer)) != -1) {
output.write(buffer,n);
}
byte[] data = output.toByteArray();
String text = new String(data,"UTF-8");
System.out.println(text);
}
}
}
複製程式碼
編寫自定義工具類計算讀取的位元組數
import java.io.*;
import java.util.zip.GZIPInputStream;
public class Main {
public static void main(String[] args) throws IOException {
try (InputStream input = new CountInputStream(new GZIPInputStream(new BufferedInputStream(new FileInputStream("test.txt.gz"))))) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = input.read(buffer)) != -1) {
output.write(buffer,"UTF-8");
// 列印內容
//System.out.println(text);
//由於我們要使用count方法,因此要將InputStream型別向下轉型為CountInputStream,因為我們明確的知道繼承關係這裡可以不判斷是否可以安全的向下轉型
System.out.println(InputStream.class.isAssignableFrom(CountInputStream.class));
System.out.println(((CountInputStream) input).count);
}
}
}
class CountInputStream extends FilterInputStream {
int count=0;
public CountInputStream(InputStream in) {
super(in);
}
@Override
public int read(byte[] b,int off,int len) throws IOException {
//通過呼叫父類的read方法獲取包裝的inputStream的位元組長度
int n = super.read(b,off,len);
count +=n;
return n;
}
}
複製程式碼
輸出
true
11356
複製程式碼
關於位元組(重要)
InputStream的read()與read(byte[] b)
在學習的過程中發現如果使用read()返回的值代表位元組值(0-255),而使用read(byte[] b)則返回的是讀取的位元組長度,而位元組值儲存在byte[] b
型別的b變數中為什麼會產生這樣的結果?
首先看JDK的解釋:InputStream的read()的方法從輸入流中讀取資料的下一個位元組,返回0到255範圍內的int位元組值。
那麼問題來了0到255範圍的位元組值是什麼?
簡單解釋:00000000
一個位元組包含8位二進位制值,這8位二進位制數相互變化,共有2^8 = 256種數字,0~255
那麼InputStream的read()每次返回的就是0~255的值,這個int型別10進位制的值可以轉換成一個8位的二進位制數
字元型別:char,因為java的char使用unicode編碼,所以可直接賦值給int型別檢視他的unicode編碼
反過來就是一個int型別(char)int就可以直接轉換成char字元
ab中國cdefj
複製程式碼
import java.io.*;
import java.util.zip.GZIPInputStream;
public class Main {
public static void main(String[] args) throws IOException {
InputStream in = null;
File f = new File("test.txt");
in = new FileInputStream(f);
int i = 0;
while ((i = in.read()) != -1) {
//String str = new String((char)i);
System.out.println((char)i);
}
}
}
複製程式碼
輸出
a
b
ä
¸
å
½
c
d
e
f
j
複製程式碼
上面的程式演示了read()
讀取單個位元組然後返回的int型別的值,通過(char)i
強制轉型為單個字元然後輸出,由於中文是佔3個字元的所以中文部分顯示亂碼
下面的程式碼演示byte[]讀取資料
import java.io.*;
import java.util.zip.GZIPInputStream;
public class Main {
public static void main(String[] args) throws IOException {
InputStream in = null;
File f = new File("test.txt");
//當一次讀取3個位元組的時候如果湊巧全是中文,或者從第四個位元組開始是中文,就不會出現亂碼,如果不是就會出現亂碼
byte[] b = new byte[3];
in = new FileInputStream(f);
int i = 0;
while ((i = in.read(b)) != -1) {
//String函式可以接收一個char陣列或者byte陣列然後轉換成字串
String str = new String(b);
System.out.println(str);
}
}
}
複製程式碼
下面是兩種演示資料: 不會出現亂碼,因為第三個位元組後是中文,每次都三個位元組正好讀取一箇中文字元
abc中國cdefj
複製程式碼
顯示效果
abc
中
國
cde
fje
複製程式碼
如果沒有前面的c
ab中國cdefj
複製程式碼
就會輸出亂碼
ab�
���
��c
def
jef
複製程式碼
操作Zip
ZipInputStream繼承自FlaterInputStream實現了ZipConstants介面,雖然ZipInputStream是FlaterInputStream子類但是有些方法是ZipInputStream獨有的,在使用這些方法的時候不能向上轉型,因此使用ZipInputStream的時候直接建立一個他的例項而且無需向上轉型,
ZipInputStream可以讀取Zip流。 JarInputStream提供了額外讀取jar包內容的能力。 ZipOutputStream可以寫入Zip流。 配合FileInputStream和FileOutputStream就可以讀寫Zip檔案。
package com.feiyangedu.sample;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class Main {
public static void main(String[] args) throws IOException {
try (ZipInputStream zip = new ZipInputStream(new BufferedInputStream(new FileInputStream("test.jar")))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory()) {
System.out.println("D " + entry.getName());
} else {
System.out.println("F " + entry.getName() + " " + entry.getSize());
printFileContent(zip);
}
}
}
}
static void printFileContent(ZipInputStream zip) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = zip.read(buffer)) != -1) {
output.write(buffer,n);
}
byte[] data = output.toByteArray();
System.out.println(" size: " + data.length);
}
}
複製程式碼
classpath資源 (linux與win的路徑有區別)
classpath中可以包含任意型別的檔案。 從classpath讀取檔案可以避免不同環境下檔案路徑不一致的問題。 讀取classpath資源:
try(InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// Read from classpath
}
}
複製程式碼
- 把資源儲存在classpath中可以避免檔案路徑依賴
- class物件的getResourceAsStream()可以從classpath讀取資源
- 需要檢查返回的InputStream是否為null
package com.feiyangedu.sample;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
public class Main {
public static void main(String[] args) throws IOException {
// 從classpath讀取配置檔案:
try (InputStream input = Main.class.getResourceAsStream("/conf.properties")) {
if (input != null) {
System.out.println("Read /conf.properties...");
Properties props = new Properties();
props.load(input);
System.out.println("name=" + props.getProperty("name"));
}
}
// 從classpath讀取txt檔案:
String data = "/com/feiyangedu/sample/data.txt";
try (InputStream input = Main.class.getResourceAsStream(data)) {
if (input != null) {
System.out.println("Read " + data + "...");
BufferedReader reader = new BufferedReader(new InputStreamReader(input,"UTF-8"));
System.out.println(reader.readLine());
} else {
System.out.println("Resource not found: " + data);
}
}
}
}
複製程式碼
conf.properties
name=Java IO
url=www.feiyangedu.com
複製程式碼
序列化
序列化是指把一個Java物件變成二進位制內容(byte[]) Java物件實現序列化必須實現Serializable介面(空介面) 反序列化是指把一個二進位制內容(byte[])變成Java物件 使用ObjectOutputStream和ObjectInputStream實現序列化和反序列化 readObject()可能丟擲的異常:
- ClassNotFoundException:沒有找到對應的Class
- InvalidClassException:Class不匹配
- 反序列化由JVM直接構造出Java物件,不呼叫構造方法
- 可設定serialVersionUID作為版本號(非必需)
Reader與InputStream
Reader與InputStream的區別:
Reader以字元為最小單位實現了字元流輸入:- int read() 讀取下一個字元並返回字元int值(0-65535)
- int read(char[]) 讀取若干字元並填充到char[]陣列 常用Reader類:
- FileReader:從檔案讀取
- CharArrayReader:從char[]陣列讀取
完整讀取檔案例項
利用緩衝區讀取檔案 FileReader可以從檔案中獲取Reader: 但是要注意這裡使用的是系統預設的編碼 CharArrayReader可以在記憶體中模擬一個Reader:Reader與InputStream的關係
Reader是基於InputStream構造的,任何InputStream都可指定編碼並通過InputStreamReader轉換為Reader:
Reader reader = new InputStreamReader(input,"UTF-8")
複製程式碼
FileReader內部持有一個FileInputStream
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
try (Reader reader = new InputStreamReader(new FileInputStream("readme.txt"),"UTF-8")) {
int n;
while ((n = reader.read()) != -1) {
System.out.println((char) n);
}
}
}
}
複製程式碼
Writer與OutputStream
Writer與OutputStream的區別
Writer以字元為最小單位實現了字元流輸出:- write(int c) 寫入下一個字元(0-65535)
- write(char[]) 寫入char[]陣列的所有字元
- write(char[] c,int off,int len) 寫入指定範圍的字元
- write(String s)寫入string表示的字元
常用Writer類:
- FileWriter:寫入檔案
- CharArrayWriter:寫入char[]陣列
向Writer寫入字元:
FileWriter可以從檔案中獲取Writer: CharArrayWriter可以在記憶體中模擬一個Writer:Writer與OutputStream的關係
Writer是基於OutputStream構造的,任何OutputStream都可指定編碼並通過OutputStreamWriter轉換為Writer:
Writer writer = new OutputStreamWriter(output,"UTF-8")
複製程式碼