"萬字" Java I/O 詳解

Sea發表於2023-02-11

Java I/O流講解

每博一文案

誰讓你讀了這麼多書,又知道了雙水村以外還有一個大世界,如果從小你就在這個天地裡,日出而作,日落而息。
那你現在就會和眾鄉親抱同一理想:經過幾年的辛勞,像大哥一樣娶個滿意的媳婦,生個胖兒子,加上你的體魄,
會成為一名出色的莊稼人。不幸的是,你知道的太多了,思考的太多了,因此才有了,這種不能為周圍人所理解的苦惱。
                                          —————— 《平凡的世界》
人生是這樣的不可預測,沒有永恆的痛苦,也沒有永恆的幸福,生活就像流水一般,
有時是那麼平展,有時又是那麼曲折。
世界上有些人因為忙而感到生活的沉重,也有些人因為閒而活得壓抑,人啊,都有自己一本難唸的經;
可是不同處境的人又很難理解別人的苦處。
細想過來,每個人的生活也同樣是一個世界,即使是最平方的人,也要為他那個世界的存在而戰鬥。
                                          ——————  《平凡的世界》

@

1. File 類

java.io.File 類:檔案和檔案目錄路徑的抽象表示形式,與平臺無關

File 能新建,刪除,重新命名檔案和目錄,但File 不能訪問檔案內容本身。如果需要訪問檔案內容本身,則需要使用 輸入/輸出 流。

想要在Java 程式中表示一個真實存在的檔案或目錄,那麼必須有一個 File 物件,但是 Java 程式中的一個 File 物件,可能沒有一個真實存在的檔案或目錄。

File 物件可以作為引數傳遞給流的構造器。

1.1 File 類中:構造器

public File(String pathname);  // 過將給定路徑名字串轉換為抽象路徑名來建立一個新 File 例項。如果給定字串是空字串,那麼結果是空抽象路徑名
public File(String parent,String child);  // 根據 parent 路徑名字串和 child 路徑名字串建立一個新 File 例項。以parent為父路徑,child為子路徑建立File物件
public File(File parent, String child); // 根據 parent 抽象路徑名和 child 路徑名字串建立一個新 File 例項。根據一個父File物件和子檔案路徑建立File物件
// 路徑可以是絕對路徑,也可以是相對路徑
  • 絕對路徑: 是一個固定的路徑,從磁碟機代號開始。
  • 相對路徑: 是相對於某個位置開始。IDEA中預設相對路徑是從**Project**專案(路徑)下和同級的 **src** 的路徑開始的,注意不是模組開始的**Module** 。如下圖所示:src 和 Module 模組是同級的。

1.2 File 類中:路徑分隔符

路徑中的每級目錄之間用一個路徑分隔符隔開。

路徑分隔符和系統有關:

  • windows和DOS系統預設使用“\”來表示,需要注意的是在 java 中 **"\"** 具有轉義的意思,所以想要表示真正的 “\” 需要兩個 **"\\"** 來轉義回來表示一個斜杆。
  • UNIX和URL使用“/”來表示。

Java程式支援跨平臺執行,因此路徑分隔符要慎用。

為了解決這個隱患,File類提供了一個常量:

public static final String separator。// 根據作業系統,動態的提供分隔符。
File file1 = new File("E:\\Test\\info.txt");

File file2 = new File("E:" + File.separator + "Test" + File.separator + "info.txt");

File file3 = new File("E:/Test");

// 這三者表示的路徑是一樣的。只是表示方式不同而已。

舉例:

package blogs.blog9;


import java.io.File;

public class FileTest {

    /**
     * File 構造器的使用
     */
    public static void main(String[] args) {
        // 構造器一:
        // 絕對路徑: 帶磁碟機代號
        File file = new File("E:\\Java\\JavaRebuilt\\src\\blogs\\blog9"); // 雙右斜杆表示一個 \ (轉義)
        // 相對路徑: IDEA預設是Project的根目錄,不是模組Module的根目錄,
        // 也可以使用:左斜杆表示路徑分隔符
        System.out.println(file);
        File file2 = new File("src/blogs/blog9"); // 這裡是在src的包下(src 和 Module 模組是同級的)
        System.out.println(file2);

        // 構造器二:
        // 第一個引數是第二個引數的父路徑,第二個引數是子路徑
        File file3 = new File("E:\\Java\\JavaRebuilt\\src\\blogs","blog9");
        System.out.println(file3);

        // 構造器三:
        // 第一個引數是 File 類物件(這裡是第二個引數的父路徑的File物件),第二個引數是子路徑
        File file4 = new File(file3,"blog9");
        System.out.println(file4);

    }
}

1.3 File 類中:常用方法

獲取檔案屬性的資訊的方法:

  • getAbsoluteFile() : 返回此File物件中路徑的絕對路徑。返回的是 File 物件
public File getAbsoluteFile(); // 返回此抽象路徑名的絕對路徑名形式。
  • getAbsolutePath() : 返回此抽象路徑名的絕對路徑名字串
public String getAbsolutePath(); // 返回此抽象路徑名的絕對路徑名字串
  • getPath() : 返回此抽象路徑名以字串的形式。
public String getPath(); // 返回此抽象路徑名
  • getName() : 獲取名稱
public String getName(); // 返回由此抽象路徑名錶示的檔案或目錄的名稱。該名稱是路徑名名稱序列中的最後一個名稱。如果路徑名名稱序列為空,則返回空字串
  • getParent() : 獲取上層檔案目錄路徑(也就是父路徑)。若無,返回null
public String getParent();  // 返回此抽象路徑名父目錄的路徑名字串;如果此路徑名沒有指定父目錄,則返回 null。
  • length() : 返回檔案長度即(位元組數)
public long length();  // 獲取檔案長度(即:位元組數)。不能獲取目錄的長度
  • lastModified() : 獲取最後一次的修改時間,毫秒值。
public long lastModified();  // 獲取最後一次的修改時間,毫秒值
  • list() : 獲取指定目錄下的所有檔案或者檔案目錄的名稱陣列
public String[] list();  // 返回一個字串陣列,這些字串指定此抽象路徑名錶示的目錄中的檔案和目錄。
  • listFiles() : 獲取指定目錄下的所有檔案或者檔案目錄的 File 陣列
public File[] listFiles();  // 返回一個抽象路徑名陣列,這些路徑名錶示此抽象路徑名錶示的目錄中的檔案。

舉例:

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src\\blog9\\hello.txt"); // 注意轉義以及檔案字尾
        File file2 = new File("src/blog9/hello3.txt"); // 左斜杆也是可以的

        String absolutePath = file.getAbsolutePath();  // 返回絕對路徑,以String的形式返回
        System.out.println(absolutePath);
        File absoluteFile = file.getAbsoluteFile();  // 返回絕對路徑,以File 物件的形式返回
        System.out.println();
        System.out.println(file.getPath());  // 返回此路徑名/目錄名
        System.out.println(file.getName()); // 返回該檔名/目錄名
        System.out.println(file.getParent()); // 返回該上層檔案/目錄名稱
        System.out.println(file.length());  // 返回檔案長度即檔案的大小(位元組)
        long l = file.lastModified();  // 返回該檔案的最後一次修改的時間值(毫秒值)時間戳
        // 將時間戳轉換為Date,再轉換為 指定格式的字串
        Date date = new Date(l);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:ss:mm SSS");
        String format = simpleDateFormat.format(date);
        System.out.println(format);

    }
}

舉例:

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileTest {

    /**
     * File 檔案目錄
     */
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9"); // 也可以使用 左斜杆
        String[] list = file.list();  // 返回獲取指定目錄下的所有檔案或者檔案目錄的名稱陣列
        for (String s : list) {
            System.out.println(s);
        }

        File[] files = file.listFiles();
        for(File f : files) {
            System.out.println(f);
        }
    }
}


File類的重新命名功能

  • renameTo(File dest) : 把檔案重新命名為指定的檔案路徑。換句話說:就是剪下附加對檔案的重新命名 的意思。
public boolean renameTo(File dest);  // 重新命名此抽象路徑名錶示的檔案。

注意: 這裡的剪下效果,有一定的要求:就是比如:file.renameTo(dest) 想要將 file 剪下到 dest 位置路徑上。要保證 file 剪下的檔案實際在硬碟中存在,並且 dest 不能在硬碟檔案中存在(僅僅當一個路徑)。如果不滿足會失敗,返回 false

舉例:

import java.io.File;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src\\blogs\\blog9\\hello.txt");
        File dest = new File("E:\\臨時檔案\\temp\\test.txt");
        boolean b = file.renameTo(dest);  // 將file檔案剪下到 dest 中並重新命名
        System.out.println(b);

    }
}

失敗:原因是:dest 中的 test.txt 是在硬碟中實際存在的。

將test.txt去了就沒事了 ,再重新剪下,就可以了。

File 類的判斷功能

  • isDirectory() : 判斷是否是檔案目錄
public boolean isDirectory(); // 測試此抽象路徑名錶示的檔案是否是一個目錄。
  • isFile()  :判斷是否是檔案
public boolean isFile();  // 當且僅當此抽象路徑名錶示的檔案存在且 是一個標準檔案時,返回 true;否則返回 false
  • exists() : 判斷該檔案/目錄是否存在
public boolean exists();  // 測試此抽象路徑名錶示的檔案或目錄是否存在
  • canRead() : 判斷該檔案是否可讀的
public boolean canRead(); // 當且僅當此抽象路徑名指定的檔案存在且 可被應用程式讀取時,返回 true;否則返回 false
  • canWrite() : 判斷該檔案是否可寫的
public boolean canWrite(); // 當且僅當檔案系統實際包含此抽象路徑名錶示的檔案且 允許應用程式對該檔案進行寫入時,返回 true;否則返回 false.
  • isHidden() :  判斷該檔案是否隱藏的
public boolean isHidden(); // 當且僅當此抽象路徑名錶示的檔案根據底層平臺約定是隱藏檔案時,返回 true

舉例:

import java.io.File;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src\\blogs\\blog9\\hello.txt"); // 注意轉義以及檔案字尾(該檔案實際存在的)
        File file2 = new File("src/blogs/blog9/hello3.txt"); // 左斜杆也是可以的 (該檔案不存在的)

        System.out.println(file.isDirectory()); // 判斷是否是檔案目錄
        System.out.println(file.isFile());  // 判斷是否為檔案
        System.out.println(file.exists()); // 判斷該檔案/目錄是否實際存在
        System.out.println(file.canRead());  // 判斷該我呢見是否可讀的
        System.out.println(file.canWrite()); // 判斷該檔案是否是可寫的
        System.out.println(file.isHidden()); // 判斷該檔案是否隱藏的

        System.out.println("*************************** file2 *************************");
        System.out.println(file2.isDirectory()); // 判斷是否是檔案目錄
        System.out.println(file2.isFile());  // 判斷是否為檔案
        System.out.println(file2.exists()); // 判斷該檔案/目錄是否實際存在
        System.out.println(file2.canRead());  // 判斷該我呢見是否可讀的
        System.out.println(file2.canWrite()); // 判斷該檔案是否是可寫的
        System.out.println(file2.isHidden()); // 判斷該檔案是否隱藏的
    }
}

File 類的建立功能

  • **createNewFile() ** : 建立檔案。若檔案存在,則不建立,返回false
public boolean createNewFile() throws IOException // 如果指定的檔案不存在併成功地建立,則返回 true;如果指定的檔案已經存在,則返回 false
  • mkdirs() : 建立檔案目錄。建立檔案目錄。如果上層檔案目錄不存在,一併建立
public boolean mkdirs(); // 建立此抽象路徑名指定的目錄,包括所有必需但不存在的父目錄。注意,此操作失敗時也可能已經成功地建立了一部分必需的父目錄。
  • mkdir() : 建立檔案目錄。如果此檔案目錄存在,就不建立了。如果此檔案目錄的上層目錄不存在,也不建立。
public boolean mkdir(); // 建立此抽象路徑名指定的目錄。

注意事項:如果你建立檔案或者檔案目錄沒有寫磁碟機代號路徑,那麼,預設在專案路徑下。

舉例:

import java.io.File;
import java.io.IOException;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9/hello.txt");

        boolean b = false;
        try {
            b = file.createNewFile();  // 檔案存在不建立,不存在檔案建立
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(b);
    }
}

建立目錄: 使用 mkdir

import java.io.File;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9/test/test2/");
        boolean b = file.mkdir();  // 如果對應的 test2目錄的上級目錄test不存在,則目錄都不建立
        System.out.println(b);
    }
}

使用 mkdirs()

import java.io.File;
import java.io.IOException;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9/test/test2/");
        boolean b = file.mkdirs();  // 如果對應的 test2目錄的上級目錄test不存在,則目錄一併都建立
        System.out.println(b);
    }
}

File類的刪除功能:

  • delete()  : 刪除檔案或者資料夾
public boolean delete(); // 刪除此抽象路徑名錶示的檔案或目錄。如果此路徑名錶示一個目錄,則該目錄必須為空才能刪除。

刪除注意事項:Java中的刪除不走回收站。要刪除一個檔案目錄,請注意該檔案目錄內不能包含檔案或者檔案目錄。如果含有無法刪除的。

舉例:

import java.io.File;

public class FileTest {
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9/test");
        boolean b = file.delete();  // test 目錄下不能有檔案/目錄,有的話無法刪除
        System.out.println(b);
    }
}

小結

1.4 實用案例:

判斷指定目錄下是否有字尾名為.jpg的檔案,如果有,就輸出該檔名稱

package com.atguigu.exer2;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;

import org.junit.Test;
/**
判斷指定目錄下是否有字尾名為.jpg的檔案,如果有,就輸出該檔名稱
 */
public class FindJPGFileTest {

	@Test
	public void test1(){
		File srcFile = new File("d:\\code");
		
		String[] fileNames = srcFile.list();
		for(String fileName : fileNames){
			if(fileName.endsWith(".jpg")){
				System.out.println(fileName);
			}
		}
	}
    
	@Test
	public void test2(){
		File srcFile = new File("d:\\code");
		
		File[] listFiles = srcFile.listFiles();
		for(File file : listFiles){
			if(file.getName().endsWith(".jpg")){
				System.out.println(file.getAbsolutePath());
			}
		}
	}
    
	/*
	 * File類提供了兩個檔案過濾器方法
	 * public String[] list(FilenameFilter filter)
	 * public File[] listFiles(FileFilter filter)
	 */
	@Test
	public void test3(){
		File srcFile = new File("d:\\code");
		
		File[] subFiles = srcFile.listFiles(new FilenameFilter() {
			
			@Override
			public boolean accept(File dir, String name) {
				return name.endsWith(".jpg");
			}
		});
		
		for(File file : subFiles){
			System.out.println(file.getAbsolutePath());
		}
	}
	
}

遍歷指定目錄所有檔名稱,包括子檔案目錄中的檔案。
擴充1:並計算指定目錄佔用空間的大小
擴充2:刪除指定檔案目錄及其下的所有檔案

package com.atguigu.exer2;

import java.io.File;
/**
 * 3. 遍歷指定目錄所有檔名稱,包括子檔案目錄中的檔案。
	擴充1:並計算指定目錄佔用空間的大小
	擴充2:刪除指定檔案目錄及其下的所有檔案

 * @author shkstart 郵箱:shkstart@126.com
 * @version  建立時間:2019年2月23日  上午1:55:31
 *
 */
public class ListFilesTest {

	public static void main(String[] args) {
		// 遞迴:檔案目錄
		/** 列印出指定目錄所有檔名稱,包括子檔案目錄中的檔案 */

		// 1.建立目錄物件
		File dir = new File("E:\\teach\\01_javaSE\\_尚矽谷Java程式語言\\3_軟體");

		// 2.列印目錄的子檔案
		printSubFile(dir);
	}

	public static void printSubFile(File dir) {
		// 列印目錄的子檔案
		File[] subfiles = dir.listFiles();

		for (File f : subfiles) {
			if (f.isDirectory()) {// 檔案目錄
				printSubFile(f);
			} else {// 檔案
				System.out.println(f.getAbsolutePath());
			}

		}
	}

	// 方式二:迴圈實現
	// 列出file目錄的下級內容,僅列出一級的話
	// 使用File類的String[] list()比較簡單
	public void listSubFiles(File file) {
		if (file.isDirectory()) {
			String[] all = file.list();
			for (String s : all) {
				System.out.println(s);
			}
		} else {
			System.out.println(file + "是檔案!");
		}
	}

	// 列出file目錄的下級,如果它的下級還是目錄,接著列出下級的下級,依次類推
	// 建議使用File類的File[] listFiles()
	public void listAllSubFiles(File file) {
		if (file.isFile()) {
			System.out.println(file);
		} else {
			File[] all = file.listFiles();
			// 如果all[i]是檔案,直接列印
			// 如果all[i]是目錄,接著再獲取它的下一級
			for (File f : all) {
				listAllSubFiles(f);// 遞迴呼叫:自己呼叫自己就叫遞迴
			}
		}
	}

	// 擴充1:求指定目錄所在空間的大小
	// 求任意一個目錄的總大小
	public long getDirectorySize(File file) {
		// file是檔案,那麼直接返回file.length()
		// file是目錄,把它的下一級的所有大小加起來就是它的總大小
		long size = 0;
		if (file.isFile()) {
			size += file.length();
		} else {
			File[] all = file.listFiles();// 獲取file的下一級
			// 累加all[i]的大小
			for (File f : all) {
				size += getDirectorySize(f);// f的大小;
			}
		}
		return size;
	}

	// 擴充2:刪除指定的目錄
	public void deleteDirectory(File file) {
		// 如果file是檔案,直接delete
		// 如果file是目錄,先把它的下一級幹掉,然後刪除自己
		if (file.isDirectory()) {
			File[] all = file.listFiles();
			// 迴圈刪除的是file的下一級
			for (File f : all) {// f代表file的每一個下級
				deleteDirectory(f);
			}
		}
		// 刪除自己
		file.delete();
	}

}

2. I/O 流的概述

一個 I / O流 代表輸入源或輸出目的地。流可以表示許多不同種類的源和目的地,包括磁碟檔案,裝置,其他程式和儲存器陣列。

流支援許多不同型別的資料,包括簡單位元組,原始資料型別,本地化字元和物件。一些流簡單地傳遞資料; 其他人以有用的方式操縱和轉換資料。

I/O  其中的 IInput 的縮寫,OOutput 的縮寫。I/O 技術是非常實用的技術,用於處理裝置之間的資料傳輸。如讀/寫檔案,網路通訊等。

Java 程式中,對於資料的輸入/輸出操作以 流(stream) 的方式進行。

java.io 包下提供了各種 “流”類和介面,用以獲取不同種類的資料,並透過方法輸入或輸出資料。

無論內部工作如何,所有流都會使用與使用它們的程式相同的簡單模型:

流是一系列資料。程式使用 輸入流 從源中讀取資料:**input** 輸入流以記憶體為參考物件(將檔案中的資料內容寫入到記憶體當中) 以及 **Read** (以檔案為參考物件,將讀取檔案中的資料到記憶體當中)。這兩個都是將意思都是一樣的將檔案中的資料讀取出來寫入到記憶體當中。

程式使用 輸出流 將資料寫入目的地。**Output** 輸出流以記憶體為參考物件(將記憶體中的資料內容輸出到硬碟檔案當中) 以及 **Write** (以檔案為參考物件,將記憶體中的資料到寫入到硬碟檔案當中)。這兩個都是將意思都是一樣的:將記憶體中的資料輸出到硬碟檔案當中。

2.1 I/O的分類和體系結構

按照不同的分類方式, 可以將流分為不同的型別。

2.1.1 輸入流 和 輸出流

按照流的流向來分, 可以分為輸入流和輸出流:

輸入流: 只能從中讀取資料, 而不能向其寫入資料。
輸出流: 只能向其寫入資料, 而不能從中讀取資料。
此處的輸入、 輸出涉及一個方向問題, 對於如圖 1 所示的資料流向, 資料從記憶體到硬碟, 通常稱為輸出流——也就是說, 這裡的輸入、 輸出都是從程式執行所在記憶體的角度來劃分的。

對於如圖 2 所示的資料流向, 資料從伺服器透過網路流向客戶端, 在這種情況下, Server 端的記憶體負責將資料輸出到網路裡, 因此 Server 端的程式使用輸出流; Client 端的記憶體負責從網路裡讀取資料, 因此 Client 端的程式應該使用輸入流。

2.1.2 位元組流 和 字元流

按運算元據單位不同分為:位元組流(8 bit),字元流(16 bit)。

位元組流和字元流的用法幾乎完全一樣, 區別在於位元組流和字元流操作的資料單元的不同:位元組流是 8 位的位元組, 而字元流操作的資料單元是 16 位的字元。其中還有一點不同的就是:

  • 字元流:只能讀取操作文字檔案,因為字元流讀取的是檔案中的 char 字元資訊。 .c,.java,.c++,.txt 等等這些都是文字檔案不僅僅只是 txt檔案,而特別注意的是 :.wrod 不是文字檔案,wrod中的文字是經過特殊處理的存在一定的規範格式,不是純文字檔案。
  • 位元組流:可以操作任何的檔案,因為位元組流讀取的是二進位制資訊,讀取1個位元組byte,等同於一次讀取8個二進位制,這種流是萬能的,什麼型別的檔案都可以讀取到,因為檔案都是有二進位制組成的。包括: 文字檔案,圖片,聲音檔案。

2.1.3 節點流 和 處理流(包裝流)

按流的角色的不同分為:節點流,處理流。

  • 節點流: 所謂的節點流:就是最基本的一個流到底目的的流向,其中的流沒有被其它的流所包含住。直接從資料來源或目的地讀寫資料。如下圖所示

  • 處理流: 處理流又稱為包裝流 ,處理流對一個己存在的流進行連線或封裝/包裝, 透過封裝後的流來實現資料讀/寫功能。不直接連線到資料來源或目的地,而是“連線”在已存在的流(節點流或處理流)之上,透過對資料的處理為程式提 供更為強大的讀寫功能。
  • 如下圖所示:

注意: 節點流和包裝流是相對的,有時候,相對於不同的流的,一個節點流變成了是另一個流的包裝流。一個包裝流變成了另一個流的節點流 。如下圖所示:

區分一個流是節點流還是包裝流:大家可以:以誰包裝誰作為參考,被包裝的流就是節點流,包裝了其它的流的就是包裝流,當然注意這是相對的。

2.1.4 流的概念模型

Java 把所有裝置裡的有序資料抽象成流模型, 簡化了輸入/輸出處理, 理解了流的概念模型也就瞭解了Java IO。

透過使用處理流, Java 程式無須理會輸入/輸出節點是磁碟、 網路還是其他的輸入/輸出裝置, 程式只要將這些節點流包裝成處理流, 就可以使用相同的輸入/輸出程式碼來讀寫不同的輸入/輸出裝置的資料。

2.1.5 I/O 的體系結構

  • Java的IO流共涉及40多個類,實際上非常規則,都是從如下 Java Io 流四大家族  個 抽象基類派生的。
  • 由以下這四個類派生出來的子類。其名稱都是以其父類名作為子類名字尾。這樣用於我們辨認。
  • Java IO流四大家族:
    • java.io.InputStream  位元組輸入流,類名是以  "stream" 結尾的。
public abstract class InputStream implements Closeable {}
  • java.io.OutputStream 位元組輸出流,類名是以  "stream" 結尾的。
public abstract class OutputStream implements Closeable, Flushable {}
  • java.io.Reader 字元輸入流,類名是以 "Reader/Writer"結尾的。
public abstract class Reader implements Readable, Closeable {}
  • java.io.Writer 字元輸出流,類名是以以 "Reader/Writer"結尾的。
public abstract class Writer implements Appendable, Closeable, Flushable {}
  • 上述四大家族的首領都是抽象類 abstract class。這四個類都實現了 java.io.Closeable 介面,都是可以關閉的,都有 close() 方法。流畢竟是一個管道,這個記憶體和硬碟之間的通道,用完之後一定要關閉。不然會耗費很多資源(因為Java開啟的資源是有限的,當你開啟過多的資源超過限制時就無法開啟其它的資源了)。養成好習慣,用完之後一定要關閉(必須關閉)。

  • 這四個類都實現了java.io.Flushable 介面,都是可重新整理 的,都是有 flush() 方法的,養成一個好習慣,輸出流(將記憶體當中的資料輸出到硬碟檔案當中)在最終(輸出完)之後,一定要記得 flush() ,重新整理一下,這個重新整理的作用就是清空管道(強制將記憶體當中的資料輸出到檔案中)。為什麼要清空管道呢:因為:如果沒有 flush() 可以會導致記憶體中一部分的資料並沒有全部輸出到硬碟當中,從而導致一部分的資料丟失。
  • 注意:在Java中只要 類名是以 **"stream"** 結尾的都是位元組流,以 **"Reader/Writer"**結尾的都是字元流。
java.io包下需要掌握的流有 16個

檔案專屬:
java.io.FileInputStream
java.io.FileOutputStream    位元組流無法讀取到: 檔案中的空格的
java.io.FileReader
java.io.FileWriter          字元流可以讀取到:檔案中的空格的

轉換流: (將位元組流轉換字元流)
java.io.InputStreamReader
java.io.OutputStreamWriter

緩衝流專屬:
java.io.BufferedReader
java.io.BufferedWriter
java.io.BufferedInputStream
java.io.BufferedOutputStream
java.io.BufferedOutputStream

資料流專屬:
java.io.DataInputStream
java.io.DataOutputStream

標準輸出流:
java.io.PrintWtiter
java.io.PrinStream

物件專屬流:
java.io.ObjectInputStream
java.io.ObjectOutputStream

2.2 字元流

2.2.1 java.io.FileReader 字元輸入流

關於字元輸入流的類都是繼承了:java.io.Writer(字元輸出流)/ java.io.Reader (字元輸入流) 來使用的,但是這個兩個類是抽象類,是無法 new 物件來使用的。所以我們就需要使用其實現的子類:對於檔案字元輸入流比較常用的就是: java.io.FileReader 這個子類了。

字元流: 只能讀取文字檔案,不能讀取其它格式的檔案,文字檔案不僅僅是 .txt 字尾的檔案,.c,.java,.c++ 都是文字檔案,注意 : word 不是文字檔案,因為 word 中的文字字元是有一個規範格式設定的。不是純的文字檔案。字元流操作字元,只能操作普通文字檔案。最常見的文字檔案:.txt,.java,.c,.cpp 等語言的原始碼。尤其注意.doc,excel,ppt這些不是文字檔案。

在讀取檔案時,必須保證該檔案已存在,否則報異常。

其中繼承的 InputStreamReader 是個轉換流,繼承了  Reader 抽象類的。

FileReader的構造器

public FileReader(File file) throws FileNotFoundException; // 在給定從中讀取資料的 File 的情況下建立一個新 FileReader物件
public FileReader(String fileName) throws FileNotFoundException; // 根據檔案的相對路徑名/絕對路徑建立一個新 FileReader物件

舉例:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;

public class FileReaderTest {
    public static void main(String[] args) {
        File file = new File("E:\\Java\\JavaRebuilt\\src\\blogs\\blog9\\hello.txt");  // 絕對路徑
        try {
            FileReader fileReader = new FileReader(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        try {
            FileReader fileReader2 = new FileReader("src/blogs/blog9/hello.txt"); // 相對路徑
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

如下是 FileReader  繼承 java.io.InputStreamReader** 繼承的方法**

  • read() : 讀取單個字元。作為整數讀取的字元,範圍在 0 到 65535 之間 (0x00-0xffff)(2個位元組的Unicode碼),如果已到達流的末尾,則返回 -1。
public int read() throws IOException;  // 讀取單個字元。
  • int read(char[] cbuf) : 將字元讀入陣列。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數。
public int read(char[] cbuf) throws IOException; // 將檔案中的資料讀取到char[] cbuf的字元陣列當中,返回讀取到的個數。
  • int read(char[] cbuf,int off,int len) : 將字元讀入陣列的某一部分。存到陣列cbuf中,從off處開始儲存,最多讀len個字 符。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數。
public int read(char[] cbuf,int offset, int length) throws IOException; // 將字元讀入陣列中的某一部分.
  • public void close() throws IOException :關閉此輸入流並釋放與該流關聯的所有系統資源。
public void close() throws IOException;  // 關閉此輸入流並釋放與該流關聯的所有系統資源。

需要明白:對於 read() 讀取檔案內容的方式是,一個一個字元的讀取的,每呼叫一次 read()對於的檔案中的游標就會往後移動一下。對於特殊的 read(char[ ] cbuf) 讀取的個數是 char[] 陣列的長度,往後移動游標的位置也是  char[] 陣列的長度。以及返回的是對於字元的編碼值。

 設檔案 file1.txt ,採用字元流的話是這樣讀的:
  a中國bo張三
  第一次讀: ‘a’字元
  第二次讀: ‘中’字元

舉例:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fileReader = null; // 相對路徑
        try {
            fileReader = new FileReader("src/blogs/blog9/hello.txt");
            int read = fileReader.read();  // 返回的是編碼值
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);

            read = fileReader.read();
            System.out.println(read);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // fileReader 防止 null引用
            if(fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

舉例: 使用 while() 迴圈處理

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fileReader = null; // 相對路徑
        try {
            fileReader = new FileReader("src/blogs/blog9/hello.txt");
            int len = 0;

            // 當read()讀取到 檔案末尾返回 -1,跳出迴圈
            while((len = fileReader.read()) != -1) {
                System.out.println(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // fileReader 防止 null引用
            if(fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

舉例: 使用 int read(char[] cbuf) : 將字元讀入陣列。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fileReader = null; // 相對路徑
        try {
            fileReader = new FileReader("src/blogs/blog9/hello.txt");
            int len = 0;
            char [] chars = new char[4];
            // read(chars) 一次性讀取陣列長度個字元,返回讀取到的字元個數。到達檔案末尾返回-1
            while((len = fileReader.read(chars)) != -1) {
                // 將char[] 陣列轉換為字串
                System.out.println(new String(chars));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // fileReader 防止 null引用
            if(fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

read(char[]) 讀取資料時的覆蓋效果的講解

舉例: 去除 read(char[] cduf) 的覆蓋效果,我們讀取多到了多少個字元,就 new String 轉換多少個字元

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fileReader = null; // 相對路徑
        try {
            fileReader = new FileReader("src/blogs/blog9/hello.txt");
            int len = 0;
            char [] chars = new char[4];
            // read(chars) 一次性讀取陣列長度個字元,返回讀取到的字元個數。到達檔案末尾返回-1
            while((len = fileReader.read(chars)) != -1) {
                // 將char[] 陣列轉換為字串
                // 這裡我們 讀取到了多少個字元,就將 chars陣列中的前多少個轉換為字串
                System.out.println(new String(chars,0,len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // fileReader 防止 null引用
            if(fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.2.2 java.io.FileWriter 字元輸出流

於字元流輸出的類都是繼承了:java.io.Writer(字元輸出流)/ java.io.Reader (字元輸入流) 來使用的,但是這個兩個類是抽象類,是無法 new 物件來使用的。所以我們就需要使用其實現的子類:對於檔案字元輸出流比較常用的就是: java.io.FileWriter 這個子類了。

其中繼承的 OutputStreamWriter 是個轉換流,繼承了  Writer 抽象類的。

OutputStreamWriter 的構造器:

public FileWriter(File file) throws IOException;  // 根據給定的 File 物件構造一個 FileWriter 物件
public FileWriter(String fileName) throws IOException; // 根據給定的檔名構造一個 FileWriter 物件。
public FileWriter(File file,boolean append) throws IOException; // 根據給定的 File 物件構造一個 FileWriter 物件。如果第二個引數為 true,則將位元組寫入檔案末尾處,而不是寫入檔案開始處。
//  file - 要寫入資料的 File 物件
// append - 如果為 true,則將位元組寫入檔案末尾處,而不是寫入檔案開始處 ,預設是 false,不寫的話也是 false

舉例:

package blogs.blog9;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTest {
    public static void main(String[] args) {
        File file = new File("E:\\Java\\JavaRebuilt\\src\\blogs\\blog9\\hello.txt"); // 絕對路徑
        try {
            FileWriter fileWriter = new FileWriter(file);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileWriter fileWriter2 = new FileWriter("src/blogs/blog9/hello.txt"); // 相對路徑
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileWriter fileWriter3 = new FileWriter("src/blogs/blog9/hello.txt",true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

如下是 FileWriter  繼承 java.io.OutputStreamWriter繼承的方法

  • void write(int c) : 寫入單個字元。要寫入的字元包含在給定整數值的 16 個低位中,16 高位被忽略。 即寫入0 到 65535 之間的Unicode碼。
public void write(int c) throws IOException;  // 寫入單個字元。
  • void write(char[] cbuf) : 將字元陣列的內容,寫入到對應的硬碟檔案中去
public void write(char[] cbuf) throws IOException; // 將字元陣列的內容,寫到檔案中
  • void write(char[] cbuf,int off,int len) : 寫入字元陣列的某一部分。從off開始,寫入len個字元。
public void write(char[] cbuf,int off,int len) throws IOException // 寫入字元陣列的某一部分。
  • void write(String str) :將字串的內容,寫入到對應的硬碟檔案中去
public void write(Stirng str) throws IOException
  • void write(String str,int off,int len) :寫入字串的某一部分。從off開始,寫入len個結束
public void write(String str,int off,int len) throws IOException
  • void flush() :重新整理該流的緩衝,立即記憶體中的資料寫入預期目標硬碟檔案中去。
public void flush() throws IOException;  // 重新整理該流的緩衝。
  • public void close() :關閉此輸出流並釋放與該流關聯的所有系統資源
public void close() throws IOException; // 關閉此流,但要先重新整理它。在關閉該流之後,再呼叫 write() 或 flush() 將導致丟擲 IOException。關閉以前關閉的流無效。

注意: 如果寫入到的檔案不存在,是會自動建立的。如果檔案已經存在了,在建立FileWriter 物件時沒有設定為 true 的話,是會將原本檔案中已經存在的內容覆蓋的,寫入新的內容。

舉例: 檔案不存在,自動建立。

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fileReader = null; // 相對路徑
        try {
            fileReader = new FileReader("src/blogs/blog9/hello.txt");
            int len = 0;
            char [] chars = new char[4];
            // read(chars) 一次性讀取陣列長度個字元,返回讀取到的字元個數。到達檔案末尾返回-1
            while((len = fileReader.read(chars)) != -1) {
                // 將char[] 陣列轉換為字串
                // 這裡我們 讀取到了多少個字元,就將 chars陣列中的前多少個轉換為字串
                System.out.println(new String(chars,0,len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // fileReader 防止 null引用
            if(fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

舉例: 檔案已經存在,寫入的資訊覆蓋原本檔案的全部內容

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTest {
    public static void main(String[] args) {
        FileWriter fileWriter = null; // 相對路徑
        try {
            // 1. 建立輸出流物件: FileWriter
            fileWriter = new FileWriter("src/blogs/blog9/hello2.txt");

            // 2. 將記憶體當中的內容寫入到檔案中
            fileWriter.write("你好世界");
            // 3. 重新整理:將記憶體中沒有輸出到檔案中的內容,強制全部寫入到檔案中
            fileWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 防止 null 引用
            if(fileWriter != null) {
                // 4. 關閉IO資源
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

舉例: 建立 **FileWriter ** 物件時,設定 true ,將記憶體當中的資訊寫入到檔案的末尾去,不會覆蓋原本檔案中的內容

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTest {
    public static void main(String[] args) {
        FileWriter fileWriter = null; // 相對路徑
        try {
            // 1. 建立輸出流物件: FileWriter,並設定 true 將寫入的內容追加到檔案的末尾中去
            fileWriter = new FileWriter("src/blogs/blog9/hello2.txt",true);

            // 2. 將記憶體當中的內容寫入到檔案中
            fileWriter.write("\n");  // 換行
            fileWriter.write("Hello World");
            // 3. 重新整理:將記憶體中沒有輸出到檔案中的內容,強制全部寫入到檔案中
            fileWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 防止 null 引用
            if(fileWriter != null) {
                // 4. 關閉IO資源
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.2.3 例項文字檔案的複製

將同目錄中的 hello.txt 檔案的內容複製到 同目錄中的 hello2.txt 中去

思路:

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterTest {
    public static void main(String[] args) {
        FileWriter descFile = null;
        FileReader srcFile = null; // 注意檔案字尾
        try {
            // 1. 建立hello.txt 檔案的字元輸入流物件,以及 hello2.txt檔案的字元輸出流物件
            descFile = new FileWriter("src/blogs/blog9/hello2.txt");
            srcFile = new FileReader("src/blogs/blog9/hello.txt");

            // 2. 一邊讀,一邊寫
            int len = 0;
            char[] chars = new char[10];
            // 讀取hello.txt的資料資訊
            while((len = srcFile.read(chars)) != -1) {
                // 將讀取到的內容寫入到hello2.txt檔案中
                descFile.write(chars,0,len);
            }

            // 3. 重新整理:將記憶體中遺留沒有寫入到檔案中的資訊,全部強制寫入到檔案中去
            descFile.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 關閉IO資源
            // 分開 try,如果兩個一起try的話其中一個出現異常了,後面的一個IO就無法關閉了
            if(srcFile != null) {  // 防止 null引用
                try {
                    srcFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if(descFile != null) {  // 防止 null引用
                try {
                    descFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }
}

2.3 位元組流

2.3.1 FileInputStream 位元組輸入流

關於位元組輸入流的類都是繼承了:InputStream(位元組輸入流) 來使用的,但是個類是抽象類,是無法 new 物件來使用的。所以我們就需要使用其實現的子類:對於檔案字元輸入流比較常用的就是: **java.io.FileInputStream ** 這個子類了。

位元組流: 可以操作任何的檔案,因為位元組流讀取的是二進位制資訊,讀取1個位元組byte,等同於一次讀取8個二進位制,這種流是萬能的,什麼型別的檔案都可以讀取到,因為檔案都是有二進位制組成的。包括: 文字檔案,圖片,聲音檔案。再比如:比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt等檔案。

FileInputStream的構造器

public FileInputStream(File file) throws FileNotFoundException; // 透過開啟一個到實際檔案的連線來建立一個 FileInputStream,該檔案透過檔案系統中的 File 物件 file 指定。
public FileInputStream(String name) throws FileNotFoundException;  // 透過開啟一個到實際檔案的連線來建立一個 FileInputStream,該檔案透過檔案系統中的路徑名 name 指定。

和 字元流中的 FileReader 基本上是一樣的。

舉例:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class FileInputStreamTest {
    public static void main(String[] args) {
        File file = new File("E:\\Java\\JavaRebuilt\\src\\blogs\\blog9\\test.png"); // 絕對路徑
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream fileInputStream2 = new FileInputStream("src/blogs/blog9/test.png"); // 相對路徑
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }
}

如下是  FileInputStream  繼承 java.io.InputStream 繼承的方法

位元組流和字元流的用法幾乎完全一樣, 區別在於位元組流和字元流操作的資料單元的不同:位元組流是 8 位的位元組, 而字元流操作的資料單元是 16 位的字元。方法是上的使用也是一樣的。

  • read() :讀取檔案的一個位元組,返回值是: 讀取到"位元組"本身,到達檔案末尾返回 -1。
public int read() throws IOException ; // 從此輸入流中讀取一個資料位元組。如果沒有輸入可用,則此方法將阻塞。
  • read(byte[] b) :  讀取檔案中 byte[] 陣列長度的位元組個數,返回讀取的的位元組個數,到位檔案末尾返回 -1。
public int read(byte[] b) throws IOException; // 從此輸入流中將最多 b.length 個位元組的資料讀入一個 byte 陣列中。在某些輸入可用之前,此方法將阻塞。
  • read(byte[] b int off, int len) : 將位元組讀入陣列的某一部分。存到陣列b中,從off處開始儲存,最多讀len個字 符。如果已到達流的末尾,則返回 -1。否則返回本次讀取的字元數。
public int read(byte[] b, int off, int len) throws IOException; // 從此輸入流中將最多 len 個位元組的資料讀入一個 byte 陣列中。如果 len 不為 0,則在輸入可用之前,該方法將阻塞;否則,不讀取任何位元組並返回 0。
  • public void close() throws IOException :關閉此輸入流並釋放與該流關聯的所有系統資源。
public void close() throws IOException;  // 關閉此輸入流並釋放與該流關聯的所有系統資源。

舉例:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamTest {
    public static void main(String[] args) {
         FileInputStream fileInputStream = null;
        try {
            // 1. 建立位元組流物件
            fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt");

            int len = 0;
            // 2.讀取檔案資訊
            while((len = fileInputStream.read()) != -1) {
                System.out.println(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉資源
            // 防止null引用
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

舉例: 使用 byte[] 位元組陣列


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamTest {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            // 1. 建立位元組流物件
            fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt");

            // 2.讀取檔案資訊
            int len = 0;
            byte[] bytes = new byte[1024];  // 1KB
            // read(bytes) 一次性讀取byte[]陣列大小的位元組個數,並儲存到 bytes 陣列中,返回讀取的位元組個數
            while((len = fileInputStream.read(bytes)) != -1) {
                // 將 bytes 陣列轉換為字串,讀取多少,轉換多少
                String s = new String(bytes,0,len);
                System.out.println(s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

2.3.2 FileOutputStream 位元組輸出流

關於位元組輸出流的類都是繼承了:OutputStream(位元組輸出流) 來使用的,但是個類是抽象類,是無法 new 物件來使用的。所以我們就需要使用其實現的子類:對於檔案字元輸出流比較常用的就是: java.io.FileOutputStream 這個子類了。

FileOutputStream的構造器

public FileOutputStream(File file) throws FileNotFoundException // 建立一個向指定 File 物件表示的檔案中寫入資料的檔案輸出流。建立一個新 FileDescriptor 物件來表示此檔案連線。 
public FileOutputStream(String name) throws FileNotFoundException // 建立一個向具有指定名稱的檔案中寫入資料的輸出檔案流。建立一個新 FileDescriptor 物件來表示此檔案連線
public FileOutputStream(File file,boolean append)throws FileNotFoundException // 建立一個向指定 File 物件表示的檔案中寫入資料的檔案輸出流。如果第二個引數為 true,則將位元組寫入檔案末尾處,而不是寫入檔案開始處。建立一個新 FileDescriptor 物件來表示此檔案連線。 
// file - 為了進行寫入而開啟的檔案。
// append - 如果為 true,則將位元組寫入檔案末尾處,而不是寫入檔案開始處

舉例:


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class FileOutputStreamTest {
    public static void main(String[] args) {
        File file = new File("E:\\Java\\JavaRebuilt\\src\\blogs\\blog9\\test.png"); // 絕對路徑
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        try {
            FileOutputStream fileOutputStream2 = new FileOutputStream("src/blogs/blog9/test.png"); // 相對路徑
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }

}

如下是 FileOutputStream 繼承 java.io.OutputStream繼承的方法

  • void write(int c) : 寫入單個字元。要寫入的字元包含在給定整數值的 16 個低位中,16 高位被忽略。 即寫入0 到 65535 之間的Unicode碼。
public void write(int b) throws IOException;  // 寫入單個字元。
  • void write(byte[] b) : 將字元陣列的內容,寫入到對應的硬碟檔案中去
public void write(byte[] cbuf) throws IOException; // 將字元陣列的內容,寫到檔案中
  • void write(byte[] b,int off,int len) : 寫入字元陣列的某一部分。從off開始,寫入len個字元。
public void write(byte[] b,int off,int len) throws IOException // 寫入字元陣列的某一部分。
  • void flush() :重新整理該流的緩衝,立即記憶體中的資料寫入預期目標硬碟檔案中去。
public void flush() throws IOException;  // 重新整理該流的緩衝。
  • public void close() :關閉此輸出流並釋放與該流關聯的所有系統資源
public void close() throws IOException; // 關閉此流,但要先重新整理它。在關閉該流之後,再呼叫 write() 或 flush() 將導致丟擲 IOException。關閉以前關閉的流無效。

注意: 如果寫入到的檔案不存在,是會自動建立的。如果檔案已經存在了,在建立 FileOutputStream 物件時沒有設定為 true 的話,是會將原本檔案中已經存在的內容覆蓋的,寫入新的內容。

舉例: 建立 FileOutputStream 物件時沒有設定為 true 的,在檔案的末尾新增資訊,不會覆蓋原來檔案的資訊。


import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamTest {
    public static void main(String[] args) {
        File file = new File("src/blogs/blog9/hello.txt");
        FileOutputStream fileOutputStream = null;
        try {
            // 1. 建立FileOutputStream物件
            fileOutputStream = new FileOutputStream(file,true);

            // 2. 寫入資訊到檔案中
            byte[] bytes = new byte[]{'A','B'} ;
            fileOutputStream.write(bytes);

            // 3. 重新整理:將遺留在記憶體當中沒有寫入到檔案的資訊,強制全部寫入到檔案中
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 關閉IO資源
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

2.3.3 例項圖片檔案的複製

思路:


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamTest {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        FileInputStream fileInputStream = null;
        try {
            // 1.建立 test2.png 檔案的輸入位元組流物件
            fileOutputStream = new FileOutputStream("src/blogs/blog9/test2.png");
            // 2.建立 test.png 檔案的輸出位元組流物件
            fileInputStream = new FileInputStream("src/blogs/blog9/test.png");

            // 2. 一邊讀,一邊寫
            int len = 0;
            byte[] bytes = new byte[1024 * 1024]; // 1MB
            // 讀
            while ((len = fileInputStream.read(bytes)) != -1) {
                // 讀取多少寫入多少
                fileOutputStream.write(bytes, 0, len);
            }

            // 3. 重新整理:
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉IO資源
            // 分開 try 防止,如果一起try的話,其中一個出現了異常,後面的IO資源就無法關閉了。
            if (fileInputStream != null) {  // 防止null引用
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

2.3.4 例項對圖片的簡單加密

思路:

加密:這裡我們透過位元組流,獲取到圖片中的每個 byte 位元組資訊,再對獲取到的每個 byte 位元組資訊進行 ^ 5 運算加密,新生成一個加密後的圖片(這個圖片是加密了的,是無法開啟的)。

解密:同樣獲取到圖片中每個 byte 位元組資訊,再對獲取到的每個 byte 位元組資訊進行 ^ 5 運算解密。新生成一個解密後的圖片(這個圖片就可以正常開啟了)

核心 : 就是利用 對於一個數^ 兩個異或同一個數值,返回原來的數值,例如:6 ^ 2 = 4;4 ^ 2 = 6;

舉例:


import org.junit.Test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 圖片的加密解密操作
 */
public class ImageEncryDecry {


    /**
     * 對圖片中的每個畫素點中的 byte 進行 ^ 5 的加密
     */
    @Test
    public void test() {
        FileInputStream fileInputStream = null; // 注意檔案字尾
        FileOutputStream fileOutputStream = null;  //
        try {
            fileInputStream = new FileInputStream("src/day27/test2.jpg");
            fileOutputStream = new FileOutputStream("src/day27/test3.jpg");

            // 一邊讀,一邊加密,一邊寫
            byte[] bytes = new byte[20];
            int len = 0;

            // 讀取
            while((len = fileInputStream.read(bytes)) != -1) {
                // 讀取多少,加密多少,注意了不是 bytes陣列的長度,因為存在重複的覆蓋效果
                for (int i = 0; i < len; i++) {
                    bytes[i] = (byte)(bytes[i] ^ 5);  // 加密
                }

                // 加密完後,寫入到檔案中:讀取多少,寫入多少
                fileOutputStream.write(bytes,0,len);

            }

            // 重新整理
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        // 關閉:
            if(fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

package day27;


import org.junit.Test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 圖片的加密解密操作
 */
public class ImageEncryDecry {
    /**
     * 對加密 ^ 5 的檔案解密
     */
    @Test
    public void test2() {
        FileInputStream fileInputStream = null; // 注意檔案字尾
        FileOutputStream fileOutputStream = null;  //
        try {
            fileInputStream = new FileInputStream("src/day27/test3.jpg");
            fileOutputStream = new FileOutputStream("src/day27/test4.jpg");

            // 一邊讀,一邊加密,一邊寫
            byte[] bytes = new byte[20];
            int len = 0;

            // 讀取
            while((len = fileInputStream.read(bytes)) != -1) {
                // 讀取多少,加密多少,注意了不是 bytes陣列的長度,因為存在重複的覆蓋效果
                for (int i = 0; i < len; i++) {
                    bytes[i] = (byte)(bytes[i] ^ 5);  // 加密
                }

                // 加密完後,寫入到檔案中:讀取多少,寫入多少
                fileOutputStream.write(bytes,0,len);

            }

            // 重新整理
            fileOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉:
            if(fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

3.1 緩衝流

為了提高資料讀寫的速度,Java API 提供了帶緩衝區功能的流類。在使用這些流類時,會建立一個內部緩衝區陣列,預設使用 8192個位元組(8kb)的緩衝區

  • 緩衝流要 ”套接“ 在相應的節點流之上,根據資料操作單位可以把緩衝流分為 :
  • 位元組緩衝流
    • java.io.BufferedInputStream : 位元組輸入緩衝流

  • java.io.BufferedOutputStream :位元組輸出緩衝流

  • 字元緩衝流
    • java.io.BufferedReader : 字元輸入緩衝流
    • java.io.BufferedWriter : 字元輸出緩衝流

  • 緩衝流的使用:當讀取資料時,資料按塊讀入緩衝區 ,其和的讀操作則直接訪問緩衝區。
  • BufferedInputStream 讀取位元組檔案時,BufferedInputStream 會一次性從檔案中讀取 8192 個 ( 1024 * 8 = 8KB) ,存在緩衝區中,直到緩衝區裝滿了,才重新從檔案中讀取下一個 8192 個位元組陣列。
  • 向流中寫入位元組時,不會直接寫到檔案中,先寫道緩衝區中直到緩衝區寫滿,BufferedOutputStream 才會把緩衝區中的資料一次性寫到檔案中裡。使用方法 flush()  可以強制將緩衝區的內容全部寫入輸出流。
  • 關閉流的順序和開啟流的順序相反。只要關閉最外層流即可,關閉最外層流也會相應關閉內層節點流。因為從原始碼中可以看出。
    BufferedOutputStream.close() 關閉的同時,會將其中對應的節點流關閉。如下原始碼:

3.1.1 BufferedReader (字元輸入緩衝流) / BufferedWriter (字元輸出緩衝流)

BufferedReader的構造器

public BufferedReader(Reader in);  // 建立一個使用預設大小輸入緩衝區的緩衝字元輸入流。
public BufferedReader(Reader in,int sz); // 建立一個使用指定大小輸入緩衝區的緩衝字元輸入流。

BufferedReader 中的方法和FileReader是一樣的因為都是繼承了 Reader的抽象類的方法的 所以這裡就不多介紹說明了。

舉例: 使用字元緩衝區讀取檔案資訊


import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReaderWriterTest {
    public static void main(String[] args) {
        BufferedReader bufferedReader = null;
        try {
            FileReader fileReader = new FileReader("src/blogs/blog9/hello.txt");
            // 1. 建立字元輸入緩衝區流物件
            // 引數是:Reader 抽象類,這裡我們使用 FileReader 同樣也是 Reader 的子類作為引數
            bufferedReader = new BufferedReader(fileReader);

            // 2. 讀取檔案資訊
            int len = 0;
            char [] chars = new char[3];
            while((len = bufferedReader.read(chars))!= -1) {
                // 將 char 轉換為字串
                // 讀多少轉換多少
                String s = new String(chars,0,len);
                System.out.println(s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (bufferedReader != null) {
                try {
                    // 只需要關閉外層緩衝區的資源就可以,內層的會自動一起關閉
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

BufferedWriter的構造器

public BufferedWriter(Writer out);  // 建立一個使用預設大小輸出緩衝區的緩衝字元輸出流。
public BufferedWriter(Writer out,int sz); // 建立一個使用給定大小輸出緩衝區的新緩衝字元輸出流。

BufferedWriter 中的方法和FileReader是一樣的因為都是繼承了 Writer的抽象類的方法的 所以這裡就不多介紹說明了。

舉例: 使用字元緩衝區寫入檔案資訊


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedReaderWriterTest {
    public static void main(String[] args) {
        BufferedWriter bufferedWriter = null;
        try {
            FileWriter fileWriter = new FileWriter("src/blogs/blog9/hello.txt",true);
            // 1. 建立字元輸出緩衝流物件
            // 引數是:Writer 抽象類,這裡我們使用 BufferedWriter 同樣也是 Writer 的子類作為引數
            bufferedWriter = new BufferedWriter(fileWriter);
            // 2. 寫入檔案資訊
            char[] chars = new char[]{'H','H'};
            bufferedWriter.write("\n"); // 換行
            bufferedWriter.write(chars);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源這裡不用flush()因為緩衝流會自動重新整理
            if (bufferedWriter != null) {
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

3.1.2 BufferedInputStream(位元組輸入緩衝流) / BufferedOutputStream (位元組輸出緩衝流)

這裡位元組緩衝流和上面的字元緩衝流是一樣的這裡就不多說明了。

BufferedOutputStream / BufferedInputStream 位元組的構造器

public BufferedInputStream(InputStream in); //建立一個 BufferedInputStream 並儲存其引數,即輸入流 in,以便將來使用。建立一個內部緩衝區陣列並將其儲存在 buf 中。
public BufferedInputStream(InputStream in,int size); // 建立具有指定緩衝區大小的 BufferedInputStream 並儲存其引數,即輸入流 in,以便將來使用。建立一個長度為 size 的內部緩衝區陣列並將其儲存在 buf 中。
public BufferedOutputStream(OutputStream out); // 建立一個新的緩衝輸出流,以將資料寫入指定的底層輸出流。
public BufferedOutputStream(OutputStream out, int size); // 建立一個新的緩衝輸出流,以將具有指定緩衝區大小的資料寫入指定的底層輸出流。

舉例: 使用BufferedInputStream 輸入緩衝流讀取檔案的資訊


import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedReaderWriterTest {
    public static void main(String[] args) {
        BufferedInputStream bufferedInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt");

            // 1. 建立位元組輸入緩衝流物件
            bufferedInputStream = new BufferedInputStream(fileInputStream);
            // 2. 讀取檔案資訊
            int len = 0;
            byte[] bytes = new byte[3];
            while ((len = bufferedInputStream.read(bytes)) != -1) {
                // 將 bytes 轉換為字串,讀取了多少轉換為多少
                String s = new String(bytes,0,len);
                System.out.println(s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            // 關閉IO資源這裡只需要關閉外層緩衝區的資源就可以,內層的會自動一起關閉
            if (bufferedInputStream != null) {
                try {
                    bufferedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


    }
}

舉例: 使用BufferedOutputStream 輸出緩衝流。


import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedReaderWriterTest {
    public static void main(String[] args) {
        BufferedOutputStream bufferedOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt",true);
            // 1.建立位元組輸出緩衝流物件
            bufferedOutputStream = new BufferedOutputStream(fileOutputStream);

            // 2. 寫入資訊
            byte[] bytes = new byte[]{'K','K'};
            bufferedOutputStream.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源這裡不用flush()因為緩衝流會自動重新整理
            if (bufferedOutputStream != null) {
                try {
                    bufferedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

4.1 轉換流

  • 轉換流提供了在位元組流和字元流之間的轉換
  • Java API 提供了兩個轉換流:
    • java.io.InputStreamReader :將 InputStream 轉換為 Reader

  • Java.io.OutputStreamWriter : 將 OutputStream 轉換為 Writer

  • 位元組流中的資料都是字元時,轉成字元流操作更高效。
  • 很多時候我們使用轉換流來處理檔案亂碼問題。實現編碼和 解碼的功能。

InputStreamReader

實現將位元組的輸入流按指定字符集轉換為字元的輸入流。

需要和 InputStream ”套接“

InputStreamReader 構造器:

public InputStreamReader(InputStream in); // 建立一個使用預設字符集的 InputStreamReader。 
public InputStreamReader(InputStream in,String charsetName) throws UnsupportedEncodingException; //建立使用指定字符集的 InputStreamReader。

舉例:

import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.FileNotFoundException;
public class StreamWriterTest {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        // 將 FileInputStream 位元組輸入流轉換為 InputStreamReader 字元輸入流
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
    }
}

OutputStreamWriter

  • 實現將字元的輸出流按指定字符集轉換為位元組的輸出流。
  • 需要和OutputStream“套接”。

OutputStreamWriter的構造器

public OutputStreamWriter(OutputStream out); // 建立使用預設字元編碼的 OutputStreamWriter
public OutputStreamWriter(OutputStream out,String charsetName) throws UnsupportedEncodingException; // 建立使用指定字符集的 OutputStreamWriter。

舉例:


import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;

public class StreamWriterTest {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        // 將 FileOutputStream 位元組輸出流轉換為 OutputStreamWriter 字元輸出流
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);

    }

4.2 標準輸入\輸出流

  • System.in  和 System.out 分佈代表了系統標準的輸入和輸出裝置。
  • 預設輸入裝置時:鍵盤,輸出裝置時:顯示器
  • System.in 實際上是一個 InputStream 位元組輸入流。將控制檯的資料讀取到。一個System 類中的靜態屬性

  • System.out 的型別實際上是 PrintStream 位元組輸出流,其是OutputSteam 的子類,FilterOutputStream 的子類。
  • 重定向:透過 System 類的 setIn ,setOut 方法對預設裝置進行改變。
public static void setIn(InputStream in); //重新分配“標準”輸入流。 首先,如果有安全管理器,則透過 RuntimePermission("setIO") 許可權呼叫其 checkPermission 方法,檢視是否可以重新分配“標準”輸入流。 
public static void setOut(PrintStream out); //重新分配“標準”輸出流。 首先,如果有安全管理器,則透過 RuntimePermission("setIO") 許可權呼叫其 checkPermission 方法,檢視是否可以重新分配“標準”輸出流

舉例: 將 System.out 輸出的內容,不顯示在控制檯中,而是寫入到檔案中,製作一個日誌檔案資訊

package blogs.blog9;


import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class PrintWriterStreamTest {
    public static void main(String[] args) {
        log("呼叫了System類的gc()方法,建議啟動垃圾回收");
        log("呼叫了UserService的doSome()方法");
        log("使用者嘗試進行登入,驗證失敗");

    }

    public static void log(String msg) {
        // 1. 建立一個輸出的位元組的檔案物件,用於 System的重定向
        PrintStream printStream = null;
        try {
            printStream = new PrintStream(new FileOutputStream("src/blogs/blog9/log.txt",true));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        // 2.改變 System.out()的輸出方向使用:SetOut()方法改為重定向到 printSteam的檔案中
        System.setOut(printStream);

        // 3. 設定輸入的日期時間
        Date date = new Date();  // 獲取當前系統的時間(毫秒值)時間戳
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");
        // 將 Date 轉換為規定格式的字串
        String strTime = simpleDateFormat.format(date);
        System.out.println(strTime +": " + msg);

        // 4. 關閉IO資源
        System.out.close();
    }
}

舉例:

從鍵盤輸入字串,要求將讀取到的整行字串轉成大寫輸出,然後繼續進行輸入操作。

  • 直至當輸入 "e" 或者 "exit" 時,退出程式,
  • 方法一: 使用Scanner 實現,呼叫next()返回一個字串
  • 方法二:使用System.in 實現,System.in ---> 轉換流 ---> BufferedReader 的readLine

這裡我們使用方法二的方式:


import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class PrintWriterStreamTest {
    public static void main(String[] args) {
        // System.in 就是 public final static InputStream in = null;
        InputStreamReader isr = new InputStreamReader(System.in);  // System.in 控制檯
        // 轉換為該字元輸出流,可以讀取一行的資訊
        BufferedReader bufferedReader = new BufferedReader(isr);

        while(true) {
            String data = null;
            try {
                data = bufferedReader.readLine();
                if("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
                    System.out.println("程式結束");
                    break;
                }

                // 將字元轉換為大寫字元
                String upperCase = data.toUpperCase();
                System.out.println(upperCase);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        if(bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.3 資料流

資料流支援原始資料型別值的二進位制 I / O 操作,booleancharbyteshortintlongfloat and double 以及字串值。所有資料流都實現 DataInput 介面或 DataOutput 介面。

為了方便地操作Java語言的基本資料型別和String的資料,可以使用資料流。

  • 資料流有兩個類:(用於讀取和寫出基本資料型別、String類的資料)
  • DataInputStreamDataOutputStream
  • 分別“套接”在 InputStream OutputStream 子類的流上
  • DataInputStream中的方法
boolean readBoolean()
byte readByte()
char readChar() 
float readFloat() 
double readDouble() 
short readShort() 
long readLong() 
int readInt() 
String readUTF() 
void readFully(byte[] b)
  • DataOutputStream 中的方法 將上述的方法的read改為相應的即可。
boolean writeBoolean()
byte writeByte()
char writeChar()
float writeFloat()
double writeDouble()
short writeShort()
long writeLong()
int writeInt()
String writeUTF()
void writeFully(byte[] b)

需要特別注意的是:

  • 輸入流判斷是否結束,不是按普通流那樣判斷一個返回值,而是透過:EOFException 異常。
    此異常主要被資料輸入流用來表明到達流的末尾。注意,其他許多輸入操作返回一個特殊值表示到達流的末尾,而不是丟擲異常。
  • write 和 read 的取值順序一定要匹配。
    輸入流由簡單的二進位制資料組成,沒有指示單個值的型別,或者它們在流中開始的位置。
    該示例使用一個非常糟糕的程式設計技術:它使用浮點數來表示貨幣值。一般來說,浮點對精確值是不利的。它對於小數分數特別不利,因為常用值(如 0.1)不具有二進位制表示形式。

舉例: DataOutputStream的使用


import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataInputStreamWriterTest {
    public static void main(String[] args) {
        DataOutputStream dataOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/hello.txt");
            // 1.建立對應的 DataOutputStream物件
            dataOutputStream = new DataOutputStream(fileOutputStream);

            byte b = 100;
            short s = 200;
            int i = 300;
            long l = 400;
            float f = 2.0f;
            double d = 3.14;
            boolean bool = true;
            char c = 'A';
            String str = "hello";

            // 2. 寫資料,將 記憶體當中的資料寫入到檔案中
            dataOutputStream.writeByte(b);
            dataOutputStream.writeShort(s);
            dataOutputStream.writeInt(i);
            dataOutputStream.writeLong(l);
            dataOutputStream.writeFloat(f);
            dataOutputStream.writeDouble(d);
            dataOutputStream.writeBoolean(bool);
            dataOutputStream.writeChar(c);
            dataOutputStream.writeChars(str);
            dataOutputStream.writeChars(str);

            // 3.重新整理
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉IO資源
            if (dataOutputStream != null) {
                try {
                    dataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

舉例:  DataInputStream 讀取被 DataOutputStream 寫入到檔案中的資訊

注意: write 和 read 的取值順序一定要匹配。 DataOutputStream  依次寫入到檔案的 型別是什麼順序,後面的 DataInputStream  讀取檔案的型別的順序就是什麼要的,必須保持一致性,不然 讀取獲取到的資料是錯誤的。


import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataInputStreamWriterTest {
    public static void main(String[] args) {
        DataInputStream dataInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/hello.txt");
            // 1.建立對應的DataInputStream 物件
            dataInputStream = new DataInputStream(fileInputStream);

            // 2. 讀取其中檔案的型別資訊:注意:write 和 read 的取值順序一定要匹配
            byte b = dataInputStream.readByte();
            System.out.println(b);

            short s = dataInputStream.readShort();
            System.out.println(s);

            int i = dataInputStream.readInt();
            System.out.println(i);

            long l = dataInputStream.readLong();
            System.out.println(l);

            float f = dataInputStream.readFloat();
            System.out.println(f);

            double d = dataInputStream.readDouble();
            System.out.println(d);

            boolean bool = dataInputStream.readBoolean();
            System.out.println(bool);

            char c = dataInputStream.readChar();
            System.out.println(c);

            String str = dataInputStream.readLine();
            System.out.println(str);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

4.4 物件流 (序列化 ObjectOutputStream,反序列化 ObjectInputStream)

就像資料流支援原始資料型別的 I / O,物件流支援 I / O 的物件。大部分,但不是全部,標準類支援物件的序列化。 都實現了標記介面 Serializable

物件流類是

  • java.io.ObjectInputStream

  • java.io.OjbectOutputSteam

  • 用於儲存和讀取基本資料型別資料物件的處理流。它的強大之處就是可以把 Java中的物件寫入到硬碟當中,也能把物件從硬碟當中還原回來。
  • 序列化:  用 ObjectOutputStream 類將基本資料型別或物件型別儲存儲存到硬碟檔案的當中。
  • 反序列化:ObjectInputStream類 讀取儲存到硬碟檔案當中的基本資料型別或物件型別還原回來。儲存到記憶體當中。
  • 物件序列化機制 :允許把記憶體中的Java物件轉換成平臺無關的二進位制流,從而允許把這種二進位制流持久地儲存在磁碟上,或透過網路將這種二進位制流傳輸到另一個網路節點。當其它程式獲取了這種二進位制流,就可以恢復成原來的 Java物件
  • 序列化是 RMI(Remote Method Invoke – 遠端方法呼叫)過程的引數和返回值都必須實現的機制,而 RMI 是 JavaEE 的基礎。因此序列化機制是 JavaEE 平臺的基礎。
  • Java的序列化與反序列化的圖示如下:

什麼時候需要用到序列化和反序列化呢?

在本地 JVM 裡執行下 Java 例項,這個時候是不需要什麼序列化和反序列化的

但當我們需要將記憶體中的物件持久化到磁碟,資料庫中時, 當我們需要與瀏覽器進行互動時,或者當我們需要實現 RPC 時, 這個時候就需要序列化和反序列化了。

只要我們對 JVM 堆記憶體中的物件進行持久化或網路傳輸, 這個時候都需要序列化和反序列化。

序列化的好處: 在於可將任何實現了 Serializable 介面的物件轉化為 位元組資料 ,使其在儲存和傳輸時可被還原。

JSON 格式實際上就是將一個物件轉化為字串, 所以伺服器與瀏覽器互動時的資料格式其實是字串,我們來看來 String 型別的原始碼:

String 型別實現了 Serializable 介面,並顯示指定 serialVersionUID 的值。也就是說我們使用Json進行傳輸字串資料的時候, JVM已經將字串資料序列化了

4.4.1 物件的序列化

如果需要讓某個物件支援序列化機制,則必須讓物件所屬的類及其屬性是可序列化的,為了讓某個類是可序列化的,該類必須實現如下兩個介面之一。 否則,會丟擲NotSerializableException異常。

  • java.io.Serializable 介面一般使用這個介面,序列化

  • java.io.Externalizable

凡是實現了 Serializable 介面的類都有一個表示序列化版本識別符號的靜態變數:

private static final long serialVersionUID;
  • Java當中所有的包裝類以及 String 都實現了 java.io.Serializable 介面。所以一般要實現該介面的都是我們自定義的類。

  • serialVersionUID用來表明類的不同版本間的相容性。簡言之,其目的是以序列化物件 進行版本控制,有關各版本反序列化時是否相容。在 Java 中實現了 Serializable 介面後, JVM 在類載入的時候就會發現我們實現了這個介面, 然後在初始化例項物件的時候就會在底層幫我們實現序列化和反序列化
  • 如果一個類實現了了 java.io.Serializable 介面,但是卻沒有定義這個 serialVersionUID 靜態變數,以及也沒有從父類中繼承這個靜態變數。那麼它的值會由 Java執行時環境根據類的內部細節自動生成。這個自動生成的值,我們是看不到的,導致我們無法手動修改,而且每次生成的都不太一樣。若類中的例項變數做了修改,那麼 **serialVersionUID ** 可能會發生變化,就不是原來的了。故建議,手動顯式定義該靜態變數,不要讓Java自動生成。
  • 簡單來說,Java的序列化機制是透過在執行時,判斷類的 serialVersionUID 來驗證版本是否一致性的,在進行反序列化時,JVM會把傳來的位元組流中的 serialVersionUID 與本地相應實體類的 serialVersionUID 進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。(InvalidCastException)
  • Java 序列化機制採用了一種特殊的序列化演算法, 其演算法內容如下:

所有儲存到磁碟中的物件都有一個序列化編號。
當程式試圖序列化一個物件時, 程式將先檢查該物件是否己經被序列化過, 只有該物件從未(在本次虛擬機器中) 被序列化過, 系統才會將該物件轉換成位元組序列並輸出。
如果某個物件已經序列化過, 程式將只是直接輸出一個序列化編號, 而不是再次重新序列化該物件。

舉例:使用:ObjectOutputStream 類 將自定義類序列化(將類物件型別儲存到硬碟檔案中),該類沒有實現  java.io.Serializable這個 介面的錯誤無法序列化的錯誤演示:

package blogs.blog9;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class ObjectOutputStreamTest {
    public static void main(String[] args) {

        ObjectOutputStream objectOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp");
            // 1. 建立 ObjectOutputStream 序列化輸出流物件
            // 這裡的引數是 OutputStream ,而 FileOutputSteam 實現了該介面
            objectOutputStream = new ObjectOutputStream(fileOutputStream);

            Person person = new Person("Tom",99);

            // 2. 將自定義的Person 物件序列化:儲存到硬碟檔案當中去.
            objectOutputStream.writeObject(person);

            // 3. 重新整理
            objectOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4 關閉IO資源
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


// 該自定義類沒有實現 java.io.Serializable 介面,無法序列化
class Person {
    String name;
    int age;

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

舉例: 自定義類 Person 實現了 Serailaizable 介面,但是沒有顯式定義 serialVersionUID 靜態屬性,而是由Java自動生成的。

package blogs.blog9;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {
    public static void main(String[] args) {

        ObjectOutputStream objectOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp");
            // 1. 建立 ObjectOutputStream 序列化輸出流物件
            // 這裡的引數是 OutputStream ,而 FileOutputSteam 實現了該介面
            objectOutputStream = new ObjectOutputStream(fileOutputStream);

            Person person = new Person("Tom",99);

            // 2. 將自定義的Person 物件序列化:儲存到硬碟檔案當中去.
            objectOutputStream.writeObject(person);

            // 3. 重新整理
            objectOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4 關閉IO資源
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


// 該自定義類沒有實現 java.io.Serializable 介面,無法序列化
class Person implements Serializable {
    String name;
    int age;

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

4.4.2 物件的反序列化

舉例: 使用 ObjectInputStream 類讀取儲存到物件的序列化檔案 temp 。


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {
    public static void main(String[] args) {
        ObjectInputStream objectInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意檔案字尾

            // 1. 建立反序列化輸入物件
            // 注意: 這裡的引數是: InputSteam ,而FileInputStream 實現了該介面
            objectInputStream = new ObjectInputStream(fileInputStream);

            // 2.讀取其中序列化物件資訊
            // Object object = objectInputStream.readObject();
            // 這裡因為我們知道該檔案中序列化的物件是 Person型別的,所以可以直接進行強制轉化
            Person person = (Person) objectInputStream.readObject();
            System.out.println(person);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    
}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    int age;

    public Person() {

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

4.4.3 不顯式定義 serialVersionUID 值的問題

不指定serialVersionUID出現的問題

  1. 如果我們自定義的類想要序列化,實現了  java.io.Serializable 介面,但是卻沒有顯式定義 serialVersionUID 靜態屬性,而是由Java自動 生成的。但是該自動生成的 serialVersionUID,我們無法修改,當我們對應的類中是屬性發生了改變時,由於我們沒有自行顯式定義 serialVersionUID靜態屬性,導致Java會重新生成一個新的 serialVersionUID 屬性值,該屬性值與原先就的屬性值的版本不一致,導致再次想序列化物件時,會出錯。
  2. 如果我們在不同的電腦上都有一個Person類, 我們想透過網路進行傳輸, 那就必須現在A電腦實現序列化, 在B電腦實現反序列化, 那麼如果我們不指定serialVersionUID就就有可能反序列化失敗
  3. 在例項開發過程中, 我們的類會經常改變, 如果我們使用JVM幫我們自動生成的serialVersionUID, 那麼如果這個類已經有一些序列化物件, 那我們一旦修改了這個類,這些物件反序列化的時候就都會報錯。

為什麼要顯式定義serialVersionUID的值?

如果不顯示指定 serialVersionUID, JVM 在序列化時會根據屬性自動生成一個 serialVersionUID, 然後與屬性一起序列化,再進行持久化或網路傳輸。

在反序列化時,JVM 會再根據屬性自動生成一個新版 serialVersionUID,然後將這個新版 serialVersionUID 與序列化時生成的舊版 serialVersionUID 進行比較,如果相同則反序列化成功, 否則報錯.

如果顯示指定了 serialVersionUID, JVM 在序列化和反序列化時仍然都會生成一個 serialVersionUID, 但值為我們顯示指定的值, 就會進行serialVersionUID值的覆蓋,這樣在反序列化時新舊版本的 serialVersionUID 就一致了。

舉例: 在沒有自定義 serialVersionUID 的值的前提:這裡我們修改了Person類中是屬性,多加一個 int id 屬性後,反序列化,讀取儲存序列化檔案時的報錯提示:java.io.InvalidClassException:

package blogs.blog9;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {

    public static void main(String[] args) {
        ObjectInputStream objectInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意檔案字尾

            // 1. 建立反序列化輸入物件
            // 注意: 這裡的引數是: InputSteam ,而FileInputStream 實現了該介面
            objectInputStream = new ObjectInputStream(fileInputStream);

            // 2.讀取其中序列化物件資訊
            // Object object = objectInputStream.readObject();
            // 這裡因為我們知道該檔案中序列化的物件是 Person型別的,所以可以直接進行強制轉化
            Person person = (Person) objectInputStream.readObject();
            System.out.println(person);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    int age;

    // 多加一個屬性
    int id;

    public Person() {

    }

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }
}


顯式定義serialVersionUID 的值:

  1. 定義的 serialVersionUID 的值是 private 私有化的,static 靜態的所以物件共用,final 常量不可修改的,值是 Long 型別的
private static final long serialVersionUID = -6849794470754667710L;
  1. Java比較判斷版本號時,是在同一個專案中,先判斷類名是否相等,類名相等的前提下,再比較判斷對應的類名的版本號。所以版本號可以不用設定的太大 ,1L 也是可以的。
  2. 注意靜態屬性名是:serialVersionUID 這是固定的。不要修改了。
private static final long serialVersionUID = 1L;

舉例: 在顯式定義 serialVersionUID 的值的前提:這裡我們修改了Person類中是屬性,多加一個 int id 屬性後,反序列化,讀取儲存序列化檔案時的。讀取正常。注意: 要先將我們已經顯式定義的 serialVersionUID 的值,先序列化一下,再新增 int id 屬性,後在反序列化。不然你就是還是用的是 serialVersionUID 由Java自行生成的版本號。還是會報錯的。

package blogs.blog9;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {

    public static void main(String[] args) {
        ObjectInputStream objectInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意檔案字尾

            // 1. 建立反序列化輸入物件
            // 注意: 這裡的引數是: InputSteam ,而FileInputStream 實現了該介面
            objectInputStream = new ObjectInputStream(fileInputStream);

            // 2.讀取其中序列化物件資訊
            // Object object = objectInputStream.readObject();
            // 這裡因為我們知道該檔案中序列化的物件是 Person型別的,所以可以直接進行強制轉化
            Person person = (Person) objectInputStream.readObject();
            System.out.println(person);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    int age;

    // 多加一個屬性
    int id;

    // 顯式定義了 serialVersionUID 的版本號值
    private static final long serialVersionUID = 1L;


    public Person() {

    }


     public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }


}

4.4.3.1  設定不被序列化的型別

在一些特殊的場景下, 如果一個類裡包含的某些例項變數是敏感資訊, 例如銀行賬戶資訊等, 這時不希望系統將該例項變數值進行序列化; 或者某個例項變數的型別是不可序列化的, 因此不希望對該例項變數進行遞迴序列化, 以避免引java.io.NotSerializableException 異常。

透過在例項變數前面使用 transient 關鍵字修飾, 可以指定 Java 序列化時無須理會該例項變數。 如下 Person 類與前面的 Person 類幾乎完全一樣, 只是它的 age 使用了 transient 關鍵字修飾。

簡單的說就是:被 transient 關鍵字修飾的屬性不會被序列化到檔案中,更不會被反序列化讀取到

舉例: 將 Person 類中的 age 被 transizent 修飾,不會被序列化。

transient int age;

先序列化一下:將物件儲存到檔案中

package blogs.blog9;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {

    public static void main(String[] args) {

        ObjectOutputStream objectOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp");
            // 1. 建立 ObjectOutputStream 序列化輸出流物件
            objectOutputStream = new ObjectOutputStream(fileOutputStream);

             Person person = new Person("Tom", 99,001);

             //2. 將自定義的Person 物件序列化:儲存到硬碟檔案當中去.
            objectOutputStream.writeObject(person);

            // 3. 重新整理
            objectOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4 關閉IO資源
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    transient int age;  // 被transient 修飾不會序列化

    // 多加一個屬性
    int id;

    // 顯式定義了 serialVersionUID 的版本號值
    private static final long serialVersionUID = 1L;


    public Person() {

    }


     public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }


}

再反序化檢視效果:

package blogs.blog9;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class ObjectOutputStreamTest {

    public static void main(String[] args) {
        ObjectInputStream objectInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp"); // 注意檔案字尾

            // 1. 建立反序列化輸入物件
            // 注意: 這裡的引數是: InputSteam ,而FileInputStream 實現了該介面
            objectInputStream = new ObjectInputStream(fileInputStream);

            // 2.讀取其中序列化物件資訊
            // Object object = objectInputStream.readObject();
            // 這裡因為我們知道該檔案中序列化的物件是 Person型別的,所以可以直接進行強制轉化
            Person person = (Person) objectInputStream.readObject();
            System.out.println(person);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    transient int age;

    // 多加一個屬性
    int id;

    // 顯式定義了 serialVersionUID 的版本號值
    private static final long serialVersionUID = 1L;


    public Person() {

    }


     public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }


}


注意:同樣的被 **static** 修飾為靜態的屬性也不會被序列化

static 屬性為什麼不會被序列化?

因為序列化是針對例項物件而言的,而 static 屬性優先於物件存在, 隨著類的載入而載入, 所以不會被序列化.

是不是有人會問, serialVersionUID 也被 static 修飾, 為什麼 serialVersionUID 會被序列化?

其實 serialVersionUID 屬性並沒有被序列化, JVM 在序列化物件時會自動生成一個 serialVersionUID, 然後將我們顯示指定的 serialVersionUID 屬性值賦給自動生成的 serialVersionUID。

補充:

一次性序列化多個物件:可以,可以將物件放到集合當中,序列化集合。如下

建立多個 Person 物件,並儲存到 List 集合中並,反序列化到 temp 檔案中。



import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class ObjectOutputStreamTest {
    public static void main(String[] args) {
        ObjectOutputStream objectOutputStream = null;
        try {
            FileOutputStream fileOutputStream = new FileOutputStream("src/blogs/blog9/temp");

            // 1. 建立 ObjectOutputStream 序列化輸出流: 注意構造器的引數是 OutputStream
            objectOutputStream = new ObjectOutputStream(fileOutputStream);

            // 2. 建立多個 Person 物件型別,儲存到 List 集合中
            Person person1 = new Person("Tom",18,001);
            Person person2 = new Person("zhangsan",28,002);
            Person person3 = new Person("lisi",20,003);

            // <Person>泛型限定儲存物件型別
            List<Person> list = new ArrayList<Person>();
            // 新增元素
            list.add(person1);
            list.add(person2);
            list.add(person3);

            // 3. 序列化:將儲存到 List集合中的元素,寫入到 temp 硬碟檔案中
            objectOutputStream.writeObject(list);

            // 4. 重新整理:將遺留在記憶體中沒有寫入到檔案的資訊,強制全部寫入到檔案中,防止資料丟失
            objectOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 關閉IO資源
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    transient int age;

    // 多加一個屬性
    int id;

    // 顯式定義了 serialVersionUID 的版本號值
    private static final long serialVersionUID = 1L;


    public Person() {

    }


     public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }


}

讀取檔案中的多個序列化物件 :將我儲存到 List 集合中的多個物件,從檔案中讀取到記憶體當中

package blogs.blog9;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class ObjectOutputStreamTest {
    public static void main(String[] args) {
        ObjectInputStream objectInputStream = null;
        try {
            FileInputStream fileInputStream = new FileInputStream("src/blogs/blog9/temp");

            // 1.建立 ObjectInputStream 反序列化輸入流物件,構造器引數是 InputStream
            objectInputStream = new ObjectInputStream(fileInputStream);

            // 2. 讀取儲存序列資訊的檔案到記憶體當中
            // 因為這裡我們知道該檔案中儲存的是 List<Person> 集合型別的所以可以直接強制轉化
            List<Person> list = (List<Person>)objectInputStream.readObject();

            // 遍歷集合
            for (Person person : list) {
                System.out.println(person);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            // 3. 關閉IO資源
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}


// 該自定義類實現 java.io.Serializable 介面
class Person implements Serializable {
    String name;
    transient int age;

    // 多加一個屬性
    int id;

    // 顯式定義了 serialVersionUID 的版本號值
    private static final long serialVersionUID = 1L;


    public Person() {

    }


     public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                '}';
    }


}

5. 實用案例:

複製目錄以及目錄下的所有檔案

package day26;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 複製目錄以及目錄下的所有檔案
 */
public class CopyAll {


    /**
     * 複製目錄下的所有檔案
     * @param srcFile
     * @param destFile
     */
    private static void copyDir(File srcFile, File destFile) {
        // 遞迴結束條件。(是檔案是最後一層了,不用再遞迴下去了)
        // 判斷該複製物件是否是檔案,
        if(srcFile.isFile()) {
            // srcFile如果是檔案的話,將檔案複製完,就返回了遞迴結束,因為檔案都是最後的東西的
            // 是檔案複製,一邊讀一邊寫
            FileInputStream in = null;
            FileOutputStream out = null;

            try {
                in = new FileInputStream(srcFile);

                String path = destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() :
                        "\\" + srcFile.getAbsolutePath().substring(3);
                out = new FileOutputStream(destFile);
                // 一邊讀,一邊寫
                byte[] bytes = new byte[1024*1024];  // 一次複製1mb
                int readCout = 0;

                while((readCout = in.read(bytes)) != -1) {
                    out.write(bytes,0,readCout);  // 讀到多少返回多少
                }

                // 重新整理
                out.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }

            return;
        }

        // 獲取被複製的目錄下面的子目錄:該目錄下的目錄和檔案
        File[] files = srcFile.listFiles();

        for (File file : files) {

            // 判斷該檔案是否在同一個目錄下
            if(file.isDirectory()) {
                // D:/curse/02-javaSe/ 被複製的目錄
                // C:/curse/02-javaSe/ 到的目錄  兩者之間的目錄盤必須是一樣的

                // 獲取所有檔案的(包括目錄和檔案)絕對路徑
                String srcDir = file.getAbsolutePath();
                String destDir = file.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() :
                        "\\" + srcDir.substring(3);

                File newFile = new File(destDir);
                // 判斷該 目錄是否存在,不存在建立
                if(!newFile.exists()) {
                    newFile.mkdirs();  // 多重目錄的建立
                }

            }
            // 遞迴呼叫
            copyDir(file,destFile);  // 將其中的檔案/目錄,複製到 destFile目錄中
        }
    }
}

6. 檔案 I/O(nio.2)

關於這部分內容,大家可以移步至:???  https://zq99299.github.io/java-tutorial/essential/io/fileio.html 觀看學習。

7. 總結:

  1. File 類中的方法的使用。
  2. I/O 流的分類,輸入流:將硬碟檔案的資料資訊寫入到記憶體當中;輸出流:將記憶體當中的資訊寫入到硬碟檔案中。
  3. Java的IO流共涉及40多個類,實際上非常規則,都是從如下 Java Io 流四大家族  個 抽象基類派生的。
  4. I/O流的四大首領:java.io.InputStream ,java.io.OutputStream,java.io.Reade,java.io.Writer
  5. 在Java中只要 類名是以 "stream" 結尾的都是位元組流,以 "Reader/Writer"結尾的都是字元流。
  6. 位元組流和字元流的用法幾乎完全一樣, 區別在於位元組流和字元流操作的資料單元的不同:位元組流是 8 位的位元組, 而字元流操作的資料單元是 16 位的字元。其中還有一點不同的就是:
  • 字元流:只能讀取操作文字檔案,因為字元流讀取的是檔案中的 char 字元資訊。 .c,.java,.c++,.txt 等等這些都是文字檔案不僅僅只是 txt檔案,而特別注意的是 :.wrod 不是文字檔案,wrod中的文字是經過特殊處理的存在一定的規範格式,不是純文字檔案。
  • 位元組流:可以操作任何的檔案,因為位元組流讀取的是二進位制資訊,讀取1個位元組byte,等同於一次讀取8個二進位制,這種流是萬能的,什麼型別的檔案都可以讀取到,因為檔案都是有二進位制組成的。包括: 文字檔案,圖片,聲音檔案。
  1. 靈活使用:位元組流,字元流,緩衝流,轉換流,標準輸入、輸出流,資料流
  2. 序列化:把物件轉換為位元組序列的過程稱為物件的序列化。將物件寫入到硬碟檔案中
    反序列化:把位元組序列恢復為物件的過程稱為物件的反序列化。將儲存到硬碟檔案中的物件,讀取到記憶體當中。
  3. 如果某個類的屬性不是基本資料型別或 String 型別,而是另一個 引用型別,那麼這個引用型別必須是可序列化的,否則擁有該型別的Field 的類也不能序列化。
  4. 如果需要讓某個物件支援序列化機制,則必須讓物件所屬的類及其屬性是可序列化的,為了讓某個類是可序列化的,該類必須實現如下兩個介面(java.io.Serializable(常用),java.io.Externalizable)之一。 否則,會丟擲NotSerializableException異常。
  5. 建議顯式定義 serialVersionUID 版本值。
private static final long serialVersionUID = -6849794470754667710L;
  1. transient 關鍵字修飾的屬性不會被序列化, static 屬性也不會被序列化.

10. 最後:

??? ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️   感謝以下大佬提供的參考資料    ✏️✏️✏️✏️✏️✏️✏️✏️✏️✏️ ???

【1】: https://fighter3.blog.csdn.net/article/details/103554407

【2】:https://zq99299.github.io/java-tutorial/essential/io/streams.html

【3】:https://blog.csdn.net/Shangxingya/article/details/113744323

限於自身水平,其中存在的錯誤,希望大家給予指教,韓信點兵——多多益善,謝謝大家,後會有期,江湖再見 !!!

相關文章