簡介
IO 流的高級點的用法。
一、IO流的裝飾器模式
當我們需要給一個“基礎” InputStream 附加各種功能時,我們先確定這個能提供數據源的 InputStream 是什麼。
InputStream file = new FileInputStream("test.gz");
如果希望 FileInputStream 能提供緩衝的功能來提高讀取的效率,因此我們用BufferedInputStream 包裝這個 InputStream,得到的包裝類型是BufferedInputStream,但它仍然被視爲一個 InputStream:
InputStream buffered = new BufferedInputStream(file);
假設該文件已經用gzip壓縮了,我們希望直接讀取解壓縮的內容,就可以再包裝一個GZIPInputStream:
InputStream gzip = new GZIPInputStream(buffered);
- 無論我們包裝多少次,得到的對象始終是 InputStream,我們直接用 InputStream 來引用它,就可以正常讀取。
- 在疊加多個 FilterInputStream,我們只需要持有最外層的 InputStream,並且,當最外層的 InputStream關閉時(在try(resource)塊的結束處自動關閉),內層的InputStream 的
close()
方法也會被自動調用,並最終調用到最核心的“基礎” InputStream,因此不存在資源泄露。
二、操作Zip
ZipInputStream 是一種 FilterInputStream,它可以直接讀取zip包的內容。另一個JarInputStream 是從 ZipInputStream 派生,它增加的主要功能是直接讀取jar文件裏面的 MANIFEST.MF 文件。因爲本質上 jar 包就是 zip 包,只是額外附加了一些固定的描述文件。
1.讀取zip包
try (ZipInputStream zip = new ZipInputStream(new FileInputStream("test.zip"))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
2.寫入zip包
ZipOutputStream 是一種 FilterOutputStream,它可以直接寫入內容 zip 包。每寫入一個文件前,先調用 putNextEntry(),然後用 write()
寫入 byte[] 數據,寫入完畢後調用 closeEntry()
結束這個文件的打包。
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("out.zip"))) { //指定輸出文件
File[] files = {new File("MyFile.java"),new File("MyFilter.java")};
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(getFileDataAsBytes(file));
zip.closeEntry();
}
}
public static byte[] getFileDataAsBytes(File f){
byte[] data = new byte[(int)f.length()];
try(InputStream is =new FileInputStream(f)){
int n;
while((n=is.read(data))!=-1){
System.out.println("read" + n + "bytes");
}
}catch(IOException e){
e.printStackTrace();
}
return data;
}
三、讀取classpath資源
在 classpath 中的資源文件,路徑總是以/
開頭,我們先獲取當前的 Class 對象,然後調用 getResourceAsStream()
就可以直接從 classpath 讀取任意的資源文件,如果資源文件不存在,它將返回 null:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// 業務代碼。。。
}
}
四、序列化
序列化是指把一個 Java 對象變成二進制內容,本質上就是一個 byte[] 數組。一個 Java 對象要能序列化,必須實現一個特殊的 java.io.Serializable
接口。
1.序列化
把一個Java對象變爲byte[]數組,需要使用ObjectOutputStream。它負責把一個Java對象寫入一個字節流:
OutputStream outputStream = new FileOutputStream("out.txt");
try(ObjectOutputStream output = new ObjectOutputStream(outputStream)){
// 寫入int:
output.writeInt(12345);
// 寫入String:
output.writeUTF("Hello");
// 寫入Object:
output.writeObject(Double.valueOf(123.456));
output.flush();
}
ObjectOutputStream 既可以寫入基本類型,如 int,boolean,也可以寫入String(以UTF-8編碼),還可以寫入實現了 Serializable 接口的對象。因爲寫入Object時需要大量的類型信息,所以寫入的內容很大。
2.反序列化
和 ObjectOutputStream 相反,ObjectInputStream 負責從一個字節流讀取 Java 對象:
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream("out.txt"))) {
int n = input.readInt();
String s = input.readUTF();
Object d = input.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
readObject()可能拋出的異常有:
- ClassNotFoundException: 沒有找到對應的Class;
- InvalidClassException: Class不匹配。
對於 ClassNotFoundException,這種情況常見於一臺電腦上的Java程序把一個Java對象,例如,Person對象序列化以後,通過網絡傳給另一臺電腦上的另一個Java程序,但是這臺電腦的Java程序並沒有定義Person類,所以無法反序列化。
對於 InvalidClassException,這種情況常見於序列化的Person對象定義了一個int類型的age字段,但是反序列化時,Person類定義的age字段被改成了long類型,所以導致class不兼容。
爲了避免這種class定義變動導致的不兼容,Java 的序列化允許 class 定義一個特殊的 serialVersionUID 靜態變量,用於標識Java類的序列化“版本”,通常可以由IDE自動生成。如果增加或修改了字段,可以改變serialVersionUID的值,這樣就能自動阻止不匹配的class版本:
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
五、參考文檔
https://www.liaoxuefeng.com/wiki/1252599548343744/1255945227202752