天天看點

第15章:輸入輸出

15.1 File類:表示檔案或目錄

  1. File可以建立,删除,重命名檔案和目錄,不能通路檔案内容本身
  2. 目錄/檔案:File對象
  3. 目錄名/檔案名/路徑名:String對象
  4. 路徑與目錄、檔案差別:路徑是用來表示唯一的目錄、檔案的東西
例:路徑名:C:\\wusihan/liuxueting 目錄名:liuxueting
           
15.1.1 通路檔案和目錄
  1. 建立File執行個體:new File(String pathName)
    1. pathName既可以為絕對路徑,也可以為相對路徑,此相對路徑為相對"user.dir"的路徑。對于CMD,"user.dir"為使用者目前所在的路徑,對于eclipse和intellij,其為該項目路徑,E:\IdeaProjects\untitled,對于JAVA WEB項目,為eclipse的安裝目錄
    2. 系統屬性"java.class.path"
      1. 對于intellij:為E:\IdeaProjects\untitled\out\production\untitled
      2. 對于eclipse:bin目錄
      3. 所謂的在eclipse中添加jar包,其實就是添加classpath所包含的路徑,對于CMD,為目前目錄
  2. 通路檔案名相關方法
String getName():傳回File對象表示的檔案名、目錄名
String getPath():傳回File對象對應的路徑名
File getAbsoluteFile():傳回絕對路徑對應的File對象
String getAbsolutePath():傳回絕對路徑名
String getParent():隻是一個字元串截取,該File的上層
boolean renameTo(File newName):重命名此File對象所對應的檔案或目錄,成功傳回true,否則傳回false
           
  1. 檔案檢測的方法
boolean exists():判斷File對象對應的目錄或檔案是否存在
boolean canWrite():是否可寫
boolean canRead():
boolean isFile():判斷是否為檔案而不是目錄
boolean isDirectory():判斷是否為目錄而不是檔案
boolean isAbsolute():判斷是否為絕對路徑
           
  1. 擷取正常檔案資訊
long lastModified():最後修改時間
long length():傳回檔案内容的長度
           
  1. 檔案操作相關方法
boolean createNewFile():檔案不存在時建立該檔案,建立成功傳回true,注意不會建立目錄
boolean delete():删除File對象所對應的檔案或目錄
static File createTempFile(String prefix,String suffix):在預設的臨時檔案目錄中建立一個臨時的空檔案,使用指定字首prefix、系統随機生成的随機數和指定的字尾suffix作為檔案名。prefix至少3個位元組長,suffix預設為.tmp
static File createTempFile(String prefix,String suffix,File directory):在directory目錄中建立臨時檔案
void deleteOnExit():java虛拟機退出時,删除File對象所對應的檔案或目錄
           
  1. 目錄操作的相關方法
boolean mkdir():建立一個目錄,成功傳回true,File對象必須代表一個目錄
String[] list():列出File對象的所有子檔案名和目錄名,不遞歸
File[] listFiles():列出File對象的所有子檔案和目錄,不遞歸
static File[] listRoots():列出所有根路徑,靜态方法
           
15.1.2 檔案過濾器
File a = new File(".");
String[] nameList = a.list(new FilenameFilter() {
    dir表示對象a,name表示file中的目錄或檔案對象,依次指定a的所有子目錄或檔案進行疊代,如果該方法傳回true,list方法傳回的String[]中就會包括該目錄名、檔案名
    @Override
	public boolean accept(File dir, String name) {
		if(name.endsWith(".java")){
			return true;
		}
		return false;
	}
});
           

15.2 了解java的IO流

15.2.1 流的分類
  1. 輸入流和輸出流:按流的流向分,所謂的輸入輸出都是從程式所在記憶體角度來劃分的,将内容放入記憶體使用輸入流,将内容從記憶體放到外界使用輸出流
    1. 輸入流:隻能從中讀取資料,不能向其寫入資料。InputStream、Reader
    2. 輸出流:隻能向其寫入資料,不能從中讀取資料。OutputStream、Writer
  2. 位元組流和字元流:位元組流操作的資料單元為位元組(byte)=8bit,字元流操作的資料單元為字元(char)=16bit
    1. 位元組流:InputStream、OutputStream
    2. 字元流:Reader、Writer
  3. 節點流和處理流:按角色分
    1. 節點流:也稱低級流,可以從、向特定IO裝置讀寫資料的流
    2. 處理流:也稱進階流、包裝流,對已存在的節點流進行封裝後産生的流
      1. 處理流實際上就是裝飾者模式的一個應用,例如使用PrintStream或PrinWriter對OutputStream對象進行封裝,進而獲得新的功能
      2. 構造器中需要傳入一個節點流的流,都是處理流
      3. 關閉最上層的處理流後,系統自動關閉被該處理流包裝的節點流
      4. 優點
        1. 提高輸入輸出的效率
        2. 程式設計更加友善
功能 位元組輸入流 位元組輸出流 字元輸入流 字元輸出流 節點流/處理流
檔案轉為流 FileInputStream FileOutputStream Reader Writer 節點流
數組轉為流 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter 節點流
管道轉為流 PipedInputStream PipedOutputStream PipedReader PipedWriter 節點流
String轉為流 StringReader StringWriter 節點流
為流提供緩沖功能 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter 處理流
為位元組流提供轉為字元流的功能 InputStreamReader OutpuStreamWriter 處理流
為輸出流提供便利的列印功能 PrintStream PrintWriter 處理流
為輸入流提供推回重讀的功能 PushbackInputStream PushbackReader 處理流
為流提供寫入和讀取基本類型變量的功能 DataInputStream DataOutputStream 處理流
為流提供序列化與反序列化的功能 ObjectInputStream ObjectOutputStream 處理流
15.2.2 流的概念模型
  1. 二進制碼:水
  2. 流:水管,指針位于水管開頭,每次取水、裝水,都向後移動指針
  3. 輸入流:取水,取空為止
  4. 輸出流:裝水,裝滿為止

15.3 InputStream、Reader、OutputStream、Writer

15.3 InputStream、Reader
  1. InputStream和Reader都是抽象類,不允許直接建立對象
  2. InputStream方法
//read相當于讀取到數組中,讀到數組中,實際上也就讀到記憶體裡了
//讀取單個位元組(byte)(相當于取出一滴水),傳回讀取的位元組資料,傳回實際讀取到的位元組(byte可以轉int,是以該方法傳回值為int)
int read()
//取水放入b,從輸入流中一次讀取b.length個位元組,放入位元組數組b中,傳回實際讀取的位元組數
int read(byte[] b)
//一次讀取len個位元組,存放在數組b中,放入數組b時,不是從數組b的起點開始,而是從off位置開始放置,傳回實際讀取的位元組數
int read(byte[] b,int off,int len)
           
  1. Reader方法
int read()
int read(char[] cbuf)
int read(char[] b,int off,int len)
           
  1. InputStream、Reader移動記錄指針的方法
//在記錄指針目前位置記錄一個标記(mark),FileInputStream不支援mark
void mark(int readAheadLimit)
//判斷此輸入流是否支援mark操作,即是否支援記錄标記
boolean markSupported()
//将此流的記錄指針重新定位到上一次mark的位置
void reset()
//記錄指針向後移動n個字元或位元組
long skip(long n)
           
15.3.2 OutputStream、Writer
  1. OutputStream和Writer中的方法
//write相當于将數組寫出去,寫出去實際上也就寫到了目标位置
//将指定的位元組/字元c輸出到輸出流中
void write(int c)
//将位元組數組/字元數組中的的資料輸出到指定輸出流中
void write(byte[]/char[] buf)
//将位元組數組/字元數組從off
void write(byte[]/char[] buf,int off,int len)
           
  1. 由于Writer以字元作為操作機關,是以Writer可以用字元串來代替字元數組,即以String對象作為參數,是以Writer包含額外兩個方法
void write(String str)
//将str從off位置開始,長度為leng的字元輸出到指定輸出流
void write(String str,int off,int len)
           

15.4 流的常用實作類

15.4.1 FileInputStream、FileOutputStream、FileReader、FileWriter
  1. 将檔案轉為流
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileInputOutputStreamTest {
    public static void main(String[] args) throws IOException {
        String userDir = System.getProperty("user.dir");
        FileInputStream fis = new FileInputStream(userDir+"//src//FileOutputStreamTest.java");
        FileOutputStream fos = new FileOutputStream("C:\\Users\\含低調\\Desktop\\FileOutputStream.java");
        int readSize =  0;
        //1. GBK編碼中文字元占2位元組,如果read讀到半個中文字元會出現亂碼。即每次應至少取出2的倍數個位元組,才不會出現亂碼
        byte[] b = new byte[20];
        //2. read方法,如果全讀完了,會傳回-1
        while((readSize=fis.read(b))>0){
            //4. 如果直接fos.write(),最後一次可能隻讀取了3個位元組,那麼bbuf隻有前三個位元組是新的,後32-3個位元組還是倒數第二次讀出的,但是還是會将bbuf所有位元組都輸出,于是導緻倒數第二次的位元組有些會被再次輸出
            fos.write(b,0,readSize);
        }
        //3. 實體資源需要顯式回收
        fis.close();
        fos.close();
    }
}
           
15.4.2 ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
  1. 将位元組、字元數組轉為流
  2. 提供了以位元組、字元數組為參數的構造器
15.4.3 PipedInputStream、PipedOutputStream、PipedReader、PipedWriter
  1. 将管道轉為流
  2. 通常用于線程間通信,管道的兩邊連接配接這兩個不同的線程,參考高并發中的練習一章
15.4.4 StringReader、StringWriter
  1. 将字元串轉為流
//需為構造器傳入一個字元串
StringReader sr = new StringReader(src);
StringWriter sw = new StringWriter();
//toString方法可以直接傳回流内字元串的内容
sw.toString();
           
15.4.5 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
  1. 為流提供緩沖、mark、reset等功能
  2. 不帶緩沖區的流的無參的read()方法的問題
    1. 每從硬碟讀取一個位元組,就向記憶體放入一個位元組,記憶體處理速度快,硬碟處理速度慢,浪費了記憶體等硬碟的時間,也浪費了互動的時間
  3. 緩沖功能
    1. 所謂的緩沖功能,實際上隻是在其内部維護了一個byte[],稱為緩存
      1. 調用read()方法時,實際上是先判斷該數組裡是否還有值,如果有,就直接從該數組中傳回值,如果沒有,就調用其包裝的流的read(byte b[], int off, int len)方法,重新為這個byte[]指派,然後将byte[]中的一個元素傳回
      2. 調用write(int b)方法時,先将b放到這個數組中,然後判斷該數組是否滿了,如果滿了,調用其包裝的輸出流的write(byte b[], int off, int len),一次性将數組中所有内容寫出,如果沒滿,什麼操作也不做
    2. 隻能提升其包裝的流的一次讀一個位元組的read()、write(int b)方法的效率,不會提升read(byte b[], int off, int len)、write(byte b[], int off, int len)方法的效率
  4. BufferedReader新增了readLine方用于讀取一行并傳回,該方法如果讀不到換行,才會一直阻塞,是以讀檔案時不會阻塞,可能因為檔案為空時,預設會傳回換行
  5. BufferedOutputStream、BufferedWriter新增flush方法,可以直接将緩存的内容全部寫入磁盤的功能
15.4.6 InputStreamReader、OutputStreamWriter
  1. 為位元組流提供字元流的功能
  2. 可以叫做轉換流
  3. 将位元組流轉為字元流,因為字元流更友善,不存在将字元流轉為位元組流的轉換流,因為沒必要往麻煩了轉
15.4.7 PrintStream、PrintWriter
  1. 為輸出流提供輸出一行的功能,即額外提供了println方法,為處理流
  2. 内部使用了BufferedReader和BufferedWriter,隻不過println方法會自動幫助flush
package com.wsh.object;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;

public class PrintStreamTest {
	public static void main(String[] args) throws IOException {
		FileOutputStream fos = new FileOutputStream("D:/wusihan/newFile1.txt");
		//通常如果需要輸出文本内容,都應該将輸出流包裝為PrintStream進行輸出,注意PrintStream不能包裝字元流
		PrintStream ps = new PrintStream(fos);
		ps.println("普通字元串");
		ps.println(new PrintStreamTest());
		//關閉最上層的處理流後,系統自動關閉被該處理流包裝的節點流
		ps.close();
		//下面代碼會引發java.io.IOException,因為fos被自動關閉
		//fos.write(new byte[5]);
	}
}

           
15.4.8 PushbackInputStream、PushbackReader
  1. 為輸入流提供推回重讀的功能,新增方法unread
  2. 可以叫做推回輸入流
  3. 推回輸入流内部維護了一個byte[]或者char[],稱為推回緩沖區,可以通過unread方法向這個byte[]或char[]中放入值,而每次調用read方法時總是先從這個推回緩沖區讀取,隻有完全讀完了推回緩沖區的内容,但還沒裝滿read方法參數清單中的那個數組,才從原輸入流中繼續讀取,如果程式中放入推回緩沖區的内容超出其大小,引發Pushback buffer overflow的IOException異常
  4. PushbackInputStream、PushbackReader方法
//注意調用unread時,原輸入流的隐式指針位置是不變的,即下次調用read後,先從推回緩沖區中讀内容,之後,再接着原輸入流的原指針位置繼續讀
//将一個位元組/字元數組放入推回緩沖區
void unread(byte[]/char[] buf)
//将一個位元組/字元數組從off開始,長度為len,放入到推回緩沖區
void unread(byte[]/char[] buf,int off,int len)
void unread(int b)
           
  1. 示例
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;

//列印PushbackReaderTest.java類中"new PushbackReader"之前所有内容
public class PushbackReaderTest {
    public static void main(String[] args) throws IOException {
        String filePath = System.getProperty("user.dir")+"//src//PushbackReaderTest.java";
        //1. 建立一個PushbackReader對象,指定推回緩沖區的長度為兩個buf長度,20*2=40,如果不指定長度,預設1位,推回時會報錯
        PushbackReader pr = new PushbackReader(new FileReader(filePath),40);
        char[] buf = new char[20];
        int readSize = 0;
        String lastStr = "";
        String thisStr = "";
        int index = 0;
        while((readSize=pr.read(buf))>0){
            thisStr = new String(buf,0,readSize);
            if((index=(lastStr+thisStr).indexOf("new PushbackReader"))>0){
                pr.unread((lastStr+thisStr).toCharArray());
                char[] doubleByte = new char[index];
                pr.read(doubleByte);
                System.out.println(new String(doubleByte));
                break;
            }else{
                System.out.print(lastStr);
                lastStr = thisStr;
            }
        }
    }
}
           
15.4.9 DataInputStream、DataOutputStream
  1. 為流提供了寫入和讀取基本類型變量的功能,與ObjectInputStream、ObjectOutputStream使用方法很類似,一個是寫入讀取基本類型變量,一個是寫入讀取Object對象
import java.io.*;

public class DataInputOutputStreamTest {
    public static void main(String[] args) throws IOException {
        String file = "C:\\Users\\含低調\\Desktop\\test.txt";
        OutputStream os = new FileOutputStream(file);
        DataOutputStream dos = new DataOutputStream(os);
        dos.writeBoolean(true);
        dos.writeBytes("123334");
        dos.writeFloat(3.13f);
        InputStream is = new FileInputStream(file);
        DataInputStream dis = new DataInputStream(is);
        System.out.println(dis.readBoolean());
        byte[] a = new byte["123334".length()];
        dis.read(a);
        System.out.println(new String(a));
        System.out.println(dis.readFloat());
    }
}

           

15.5 重定向标準輸入/輸出

  1. java中預設的标準輸入裝置為鍵盤,标準輸出裝置為顯示器,标準顯示錯誤的裝置也是顯示器
  2. 标準輸入、輸出流,指的就是将标準裝置與記憶體連接配接的這個輸入、輸出流
  3. System.in/out/err方法,分别可以擷取标準的輸入流、輸出流、錯誤流
15.5.1 System類提供的三個重定向方法
  1. 所謂重定向就是将一個自定義的流對象,替代預設的标準的輸入、輸出流對象
//重定向标準錯誤的輸出流
static void setErr(PrintStream err)
//重定向标準輸入的輸入流
static void setIn(InputStream in)
//重定向标準輸出的輸出流
static void setOut(PrintStream err)
           
15.5.2 示例
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"));
System.setOut(ps);
//此時不再在顯示器輸出"吳思含"三個字,而是在out.txt中輸出
System.out.println("吳思含");

FileInputStream fis = new FileInputStream("RedirectIn.java");
System.setIn(fis);
//System.in表示标準輸入的輸入流
Scanner sc = new Scanner(System.in);
sc.userDelimiter("\n");
while(sc.hasNext()){
    //sc.next()取的不再是鍵盤輸入的内容,而是RedirectIn.java中檔案的内容
    System.out.println("鍵盤輸入為:"+sc.next());
}
           
System.setErr(new PrintStream("C:\\Users\\含低調\\Desktop\\err.log"));
System.setOut(new PrintStream("C:\\Users\\含低調\\Desktop\\out.log"));
System.out.println("wusihan測試");
int i = 1/0;

//運作程式後,再err.log列印如下
//Exception in thread "main" java.lang.ArithmeticException: / by zero
//	at test.wsh.Chongdingxiang.main(Chongdingxiang.java:11)

//out.log列印如下
//wusihan測試

//控制台并沒有任何輸出結果
           
15.6 java 虛拟機讀寫其他程序的資料
package test.wsh;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ReadFromProcess {
	public static void main(String[] args) throws IOException {
	    //Runtime的exec方法可以運作平台上的其他程式,Process對象代表由java程式啟動的子程序
		Process p = Runtime.getRuntime().exec("javac");
		//Process提供三個方法
		//InputStream getErrorStream():将javac指令的錯誤結果,轉為一個輸入流,以便傳入記憶體
		//InputStream getInputStream():将javac指令的正确結果,轉為一個輸入流,以便傳入記憶體
		//OutputStream getOutputStream():擷取javac程序的輸出流,以便将記憶體中(在這個程式中定義點什麼)内容傳輸給該程序
		//例如子程序啟動後,需要從控制台輸出讀取内容再進行處理,此時就可以用該流代替控制台的輸入
		BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
		String buff = null;
		while ((buff = br.readLine()) != null) {
			System.out.println(buff);
		}
	}
}
           

15.7 RandomAccessFile

支援跳轉到檔案的任意檔案進行讀寫資料,且可以向檔案中追加内容,而不是覆寫(FileInputStream)

15.7.1 構造方法
//mode為RandomAccessFile的通路模式
//r:隻讀方式打開檔案,如果試圖對RandomAccessFile執行寫入方法抛出IOException
//rw:讀寫,檔案不存在就建立。
//rws: 相對于rw還要求對檔案内容或中繼資料的每個更新同步都寫入到底層儲存設備
//rwd:相對于rw還要求對檔案内容的每個更新同步都寫入到底層儲存設備
RandomAccessFile(String name, String mode):
           
15.7.2 移動記錄指針的方法
//檔案指針初始位置為0
long getFilePointer():傳回檔案記錄指針的目前位置
void seek(long pos):将檔案記錄指針移動到pos位置,pos值對應位元組
           
15.7.3 其他
  1. RandomAccessFile無法在指定位置插入内容,因為新輸出内容會覆寫檔案中原有内容,如果需要插入内容,程式需要先把插入點後内容讀入緩沖區,等把需要插入的資料寫入檔案後,再将緩沖區中内容追加到檔案後面
  2. 通常用RandomAccessFile實作多線程斷點下載下傳

15.8 對象序列化

15.8.1 序列化的含義與意義
  1. 序列化:序列化是将對象的狀态資訊轉換為二進制流(對象輸出流)的過程,用以存儲或傳輸。實際上将對象p,通過"對象輸出流"ObjectOutputStream的writeObject§,就完成了序列化。而通過對象輸入流ObjectInputStream的readObject()方法,來擷取對象p,就完成了反序列化
  2. 反序列化:從IO流中恢複java對象
  3. java對象如果想支援序列化機制,需要實作Serializable或Externalizable
  4. 通常建議JavaBean(滿足特定規則的java對象,以便一些工具識别)都應實作Serializable
15.8.2 使用對象流進行序列化
  1. 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(p);
           
  1. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
//1. 反序列化恢複java對象時,如果沒有該java對象對應的類,會引發ClassNotFoundException
//2. Person類隻有有參構造器,其内列印一條語句,但反序列化時,未看到該語句,即反序列化時,無需通過構造器來初始化java對象
//3. 序列化時如果寫入多個java對象,反序列化時必須按實際寫入順序讀取
//4. 反序列化的java對象的父類必須具有"無參構造器","可序列化"二者之一,否則反序列化時會引發InvalidClassException
//5. 如果父類隻有無參構造器,但不可序列化,那麼序列化時會成功,但父類中定義的成員變量的值不會序列化到二進制流中
Person p1 = (Person)ois.readObject();
           
15.8.3 對象引用的序列化
  1. 如果某個類成員變量的類型不是基本類型或String,而是另一個引用類型,那麼這個引用類型必須是可序列化的,否則擁有該類型成員變量的類也是不可序列化的
  2. 對類A的對象a進行序列化時,其内的static成員變量,是不會被序列化的,static成員變量永遠是最新的值
  3. 序列化算法
    1. 所有儲存到磁盤中的對象都有一個序列化編号
    2. 當序列化一個對象時,程式先檢查該對象是否已經被序列化過,隻有該對象從未被序列化過(本次虛拟機中),系統才将該對象轉換成位元組序列并輸出
    3. 如果已經序列化過,程式隻是直接輸出一個序列化編号,而不是再次重新序列化該對象
  4. 示例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"));
Person per = new Person("孫悟空",500);
Teacher t1 = new Teacher("唐僧",per);
Teacher t2 = new Teacher("菩提祖師",per);
oos.writeObject(t1);
oos.writeObject(t2);
//将對象轉換為位元組序列,并寫入
oos.writeObject(per);
per.setName("含");
oos.writeObject(t2);
//系統發現per已經被序列化過,是以隻是輸出序列化編号,是以改變後的name值不會被序列化
oos.writeObject(per);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"));
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
Teacher t4 = (Person)ois.readObject();
Person per = (Person)ois.readObject();
//都傳回true
System.out.println(t1.getStudent()==p);
System.out.println(t2.getStudent()==p);
System.out.println(t2==t3);
//還是輸出孫悟空
System.out.println(per.getName());
           
15.8.4 自定義序列化
  1. transient關鍵字
    1. 對某個對象序列化時,系統會把該對象的所有執行個體變量依次序列化,稱為遞歸序列化
    2. 使用場景:某些執行個體變量為敏感資訊(不想被序列化),或不可序列化(被序列化時引發NotSerializableException),可使用transient保證該執行個體變量不被序列化
    3. transient修飾的成員變量也稱為瞬态(不能序列化,也就沒法儲存在硬碟,是以叫瞬态)成員變量
  2. 自定義序列化機制:可以讓程式控制如何序列化各執行個體變量,甚至完全不序列化某些執行個體變量(類似transient功能)
    1. 序列化和反序列化中需要特殊處理的類,可以提供以下方法完成特殊處理
    //實際上ObjectOutputStream調用writeObject和readObject時,會利用反射,發現其要處理的對象含有private的writeObject和readObject方法,就會調用該方法,修改序列化得到的對象的屬性值,或者修改反序列化得到的對象的屬性值,優點類似構造器的功能,不建立對象,但初始化成員變量
    //1. 将某成員變量反轉再序列化
    private void writeObject(ObjectOutputStream out) throws IOException{
        //就算黑客截獲了Person對象流,看到的name也是加密後的,提高安全性
    	out.writeObject(new StringBuffer(name).reverse());
    	out.writeInt(age);
    }
    private void readObject(ObjectInputStream in)throws IOException, ClassNotFoundException{
        //恢複對象的順序需要與writeObject寫入對象的順序一緻
        //其實很好了解,這跟之前介紹的輸入流和輸出流的特性有關
    	this.name = ((StringBuffer)in.readObject()).reverse().toString();
    	this.age = in.readInt();
    }
    //當序列化流不完整(接收方使用的反序列化類與發送方的版本不一緻,或序列化流被篡改)時,系統調用該方法初始化反序列化的對象
    private void readObjectNoData() throws ObjectStreamException{
        
    }
    
    //2. 用新對象替換要被序列化的對象
    //在寫入Person對象時将該對象替換成ArrayList,即之後readObject時傳回一個ArrayList對象。因為writeObject方法,在序列化某對象前,先調用該對象的writeReplace方法,如果該方法傳回另一個對象,系統再調用另一個對象的writeReplace直到該方法不再傳回另一個對象為止,最後再調用該對象的writeObject,對該對象狀态進行序列化
    任意修飾符 Object writeReplace() throws ObjectStreamException{
    	ArrayList<Object> list = new ArrayList<Object>();
    	list.add(name);
    	list.add(age);
    	return list;
    }
    
    //3. 用新對象替換被反序列化傳回的對象
    //一般用于單例類或枚舉類,由于writeObject與readObject傳回的對象不是同一個對象,但單例類和枚舉類又不希望這種情況發生。readResolve的傳回值會代替原反序列化對象,原反序列化對象會被丢棄
    任意修飾符 Object readResolve() throws ObjectStreamException{
        if(value == 1){
            return HORIZONTAL;
        }
        if(value == 2){
            return VERTICAL;
        }
        return null;
    }
               
15.8.5 另一種自定義序列化的機制
  1. 需要序列化的類實作Externalizable接口,并實作其void writeExternal(ObjectOutput out)和void readExternal(ObjectInput in)方法
package test.wsh;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{
	
	//省略成員變量,構造器,get/set方法等
	...

	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
		
	}

}

           
  1. Externalizable性能比Serializable好,但程式設計比較複雜
15.8.6 版本
  1. 定義版本
public class Test{
    //1. 為序列化類提供serialVersionUID值,用于辨別java類的序列化版本
    //2. serialVersionUID值不同時,反序列化會失敗
    //3. 如果不人為設定,該值由JVM根據類的相關資訊自動計算,是以往往修改後的類反序列化會失敗,因為serialVersionUID值與修改前的類不同
    //4. 是以如果類的修改确實會導緻反序列化失敗,則應重新為該類的serialVersionUID成員變量重新配置設定值
    private static final long serialVersionUID=512L;
}
           
  1. 假設目前系統中類為A2,要反序列化的對象類型為A1,A2為在A1基礎上做了修改
    1. UID不一緻,反序列化報錯
    2. A1比A2多成員變量,反序列得到的A2類型的對象的成員變量被忽略
    3. A1比A2少字段,反序列化得到的A2類型的對象的成員變量為null或0
    4. A1與A2成員變量名一緻,但成員類型改變,反序列化失敗
    5. A1與A2方法,靜态成員變量,瞬态執行個體變量不同,反序列化無影響

15.9 NIO

15.9.1 java新IO概述
  1. 傳統的輸入/輸出流每次隻能處理處理一個位元組,效率低。新IO采用記憶體映射檔案的方式處理輸入/輸出流,新IO将檔案或檔案的一段區域映射到記憶體中(Buffer對象),這樣就可以像通路記憶體一樣通路檔案了,效率高
  2. Channel:可看作裝水的流,與類似于傳統的InputStream、OutputStream,差別為其提供map方法,将一塊資料映射到記憶體中,傳統IO是面向流的處理,新IO是面向塊的
  3. Buffer:可看作取水的竹筒,本質是一個數組,發送到Channel或從Channel中讀取的所有對象必須先放入Buffer中
  4. Charset類:用于位元組字元轉換,可以将Unicode字元串映射成位元組序列及逆映射
  5. Selector類:支援非阻塞式輸入輸出
15.9.2 使用Buffer
  1. Buffer為抽象類,對應于除了boolean外的基本類型都有對應的Buffer子類,例如:CharBuffer、ShortBuffer等
  2. 擷取Buffer對象
//XxxBuffer類的靜态方法
//建立一個容量為capacity的XxxBuffer對象
static XxxBuffer allocate(int capacity)
//建立一個建立成本高,讀取效率也高的Buffer,叫直接Buffer。隻有ByteBuffer提供該方法, 如果想使用其他類型,可以用該Buffer進行轉換
static XxxBuffer allocateDirect(int capacity)
           
  1. Buffer中的重要概念:
    1. capacity:容量,表示該Buffer最大資料容量,即最多可以存儲多少資料,不能為負,建立後不可改變
    2. limit:界限,第一個不應該被讀出或寫入的緩沖區位置索引。即limit後的資料既不能被讀,也不能被寫
    3. position:位置,指明下一個可以被讀出或寫入的緩沖區的位置索引,類似于IO中的記錄指針
  2. Buffer對象的初始狀态:position為0,limit與capacity相等
  3. Buffer中的方法
//put(),get()為相對,即position跟着改變
//put(int index,Xxx c),get(int index)為絕對,position位置不變
put():向Buffer中放入資料,放入多少資料,position跟着向後移多少
put(int index,Xxx c):直接向索引處放入資料,不會改變position位置
get():從Buffer中取出資料,取出多少,position跟着向後移多少
get(int index):從Buffer中指定索引處取出資料,不會改變position位置
flip():為get做準備,向Buffer中put資料後調用,将limit設為目前position的位置,将position設為0,即此時Buffer對象不再可以寫入,同時為get資料做好準備
clear():為put做準備,Buffer中get資料後調用,将position設為0,limit設定為capacity,此時Buffer對象又可以從頭寫入,即為put做好準備,此時其實仍可以get到值
int capacity():傳回Buffer的capacity大小
boolean hasRemaining():判斷目前position和limit之間是否還有元素可供處理,即寫入時,判斷是否還能寫入内容,讀取時判斷是否還有内容未被讀取
int limit():傳回limit位置
Buffer limit(int newLt):重新設定limit值,并傳回一個新的Buffer對象
Buffer mark():設定Buffer的mark位置,隻能在0和position之間做mark
int position():傳回position值
Buffer position(int newPs):重新設定position,并傳回一個新的Buffer對象
int remaining():傳回目前position和limit之間元素個數
Buffer reset():将position轉到mark所在位置
Buffer rewind():将position設為0,并取消mark
           
15.9.3 使用Channel
  1. Channel可以将指定檔案的部分或全部直接映射成Buffer
  2. 程式不能直接讀寫Channel中資料,Channel隻能與Buffer互動
  3. Channel對象的建立:通過傳統的節點流的getChannel建立,不同的節點流傳回的Channel不同。
    1. FileInputStream、FileOutputStream、RandomAccessFile會傳回FileChannel
    2. InputStream擷取的Channel隻能讀
    3. OutputStream擷取的隻能寫
    4. RandomAccessFile擷取的Channel是隻讀還是讀寫,取決于RandomAccessFile打開檔案的模式,如果是讀寫,那麼既可以讀又可以寫
  4. Channel中方法
//FileChannel.MapMode為執行映射的模式,有隻讀、讀寫等,position與size控制将Channel中哪些資料映射成ByteBuffer
MappedByteBufer map(FileChannel.MapMode mode,long position,long size)
//将src中内容寫到某位置
write(ByteBuffer src)
//讀取内容放入src中
read(ByteBuffer src)
           
  1. RandomAccessFile中方法
position(int position):将Channel的記錄指針移動到最後
           
15.9.4 字元集和Charset
  1. 計算機裡的檔案、資料、圖檔都隻是一種表面現象,所有檔案在底層都是二進制檔案,即全部是位元組碼,對于文本,之是以可以看到一個個字元,是因為系統将底層的二進制序列轉換成字元的緣故
  2. 編碼(Encode):将字元轉為二進制序列,解碼(Decode):将二進制序列轉換成普通人能看懂的明文字元串
  3. java預設使用Unicode字元集,但很多作業系統不使用Unicode字元集,那麼當從作業系統中讀取資料到Java程式中時,就可能出現亂碼等問題
package test.wsh;

import java.nio.charset.Charset;
import java.util.SortedMap;

public class CharsetTest {
	public static void main(String[] args) {
		//1. 傳回目前Java支援的全部字元集Charset.availableCharsets擷取目前JDK所支援所有字元集
		SortedMap<String,Charset> map = Charset.availableCharsets();
		for(String alias:map.keySet()) {
			System.out.println(alias+"-------->"+map.get(alias));
		}
		//2. 列印目前作業系統所使用字元集
		System.out.println(
				System.getProperty("file.encoding"));
	}
}

           
  1. String類中提供的字元轉位元組方法
  1. 示例:
package test.wsh;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

public class CharsetTransform {
	public static void main(String[] args) throws CharacterCodingException {
		//1. 通過字元集建立Charset對象,這個GBK就是之前通過CharsetTest擷取到的目前Java支援的字元集的一種
		Charset cn = Charset.forName("GBK");
		//2. 擷取cn對象的編碼器和解碼器
		CharsetEncoder cnEncoder = cn.newEncoder();
		CharsetDecoder cnDecoder = cn.newDecoder();
		CharBuffer cbuff = CharBuffer.allocate(8);
		cbuff.put('孫');
		cbuff.put('悟');
		cbuff.put('空');
		cbuff.flip();
		//3. 将字元序列CharBuffer按字元集GBK轉換為位元組序列ByteBuffer
		ByteBuffer bbuff = cnEncoder.encode(cbuff);
		//4. 循環通路ByteBuffer中每個位元組
		//列印:-53 -17 -50 -14 -65 -43 
		for(int i = 0;i<bbuff.capacity();i++) {
			System.out.print(bbuff.get(i)+" ");
		}
		//5. 重新解碼成字元序列,由于CharBuffer重寫了toString,是以直接可以列印其内内容
		//列印:孫悟空
		System.out.println("\n"+cnDecoder.decode(bbuff));
		//6. 可以簡單調用Charset對象的encode、decode方法,使用預設字元集進行編碼與解碼
		//列印:java.nio.HeapByteBuffer[pos=0 lim=0 cap=0]
		System.out.println(cn.encode(cbuff));
		//由于之前bbuff已全部被讀取,是以如果想重新讀一遍,需要調用其flip方法重置指針位置
		bbuff.flip();
		System.out.println(cn.decode(bbuff));
	}
}

           
15.9.5 檔案鎖
  1. 檔案鎖目的是組織多個程序并發修改同一個檔案
  2. 共享鎖(S鎖):又稱讀鎖,若事務T對資料對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務隻能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改
  3. 排它鎖(X鎖):又稱寫鎖。若事務T對資料對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A
  4. 示例
package test.wsh;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class FileLockTest {
	public static void main(String[] args) throws IOException, InterruptedException {
		FileChannel channel = new FileOutputStream("a.txt").getChannel();
		//tryLock():非阻塞,嘗試失敗時傳回null,排他鎖
		//lock():阻塞,排他鎖
		//tryLock(long position,long size,boolean shared):shared為true表示共享鎖,支援鎖定檔案中一部分内容 
		//lock(long position,long size,boolean shared):shared為true表示共享鎖,支援鎖定檔案中一部分内容 
		//1. 鎖定的10s内,其他程序無法讀寫a.txt
		FileLock lock = channel.tryLock();
		Thread.sleep(10000);
		lock.release();
	}
}

           
  1. 注意事項
    1. 某些平台,檔案鎖是建議性的,即使一個程式不能獲得檔案鎖,它也可以對該檔案進行讀寫
    2. 某些平台,不能同時鎖定一個檔案并把它映射進記憶體
    3. 檔案所由Java虛拟機持有,即如果兩個Java類在不同Java虛拟機上,鎖無效
    4. 某些平台,關閉FileChannel時,會釋放該檔案上所有鎖,是以應避免在一個檔案上打開多個FileChannel。

15.10 Java7的NIO.2

  1. Files:用于簡化IO
  2. Path、Paths:彌補File性能問題,以及其大多數方法出錯時隻傳回失敗,不抛異常
15.10.1 Path、Paths、Files核心API
  1. Path、Paths
package test.wsh;

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathTest {
	public static void main(String[] args) {
		//1. 構造Path對象
		Path path = Paths.get(".");
		//2. 以多個String構造Path對象
		Path path2 = Paths.get("E:", "Program Files","Flexcube_dev","Workspace","AllInOneTest");
		//3. 擷取path中包含的路徑數量,例如g:\publish\codes調用該方法傳回3
		//列印:1
		System.out.println(path.getNameCount());
		//4. 擷取根路徑
		//列印:null
		System.out.println(path.getRoot());
		//5. 擷取絕對路徑對應的Path
		Path absolutePath = path.toAbsolutePath();
		//列印:E:\Program Files\Flexcube_dev\Workspace\AllInOneTest\.
		System.out.println(absolutePath);
		//列印:E:\
		System.out.println(absolutePath.getRoot());
		//列印:5
		System.out.println(absolutePath.getNameCount());
		
	}
}

           
  1. Files
package test.wsh;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class FileTest {
	public static void main(String[] args) throws FileNotFoundException, IOException {
		Path pp = Paths.get(".\\src\\test\\wsh\\FileTest.java");
		//1. 複制檔案
		Files.copy(pp, new FileOutputStream("a.txt"));
		//2 判斷是否為隐藏檔案
		//列印:false
		System.out.println(Files.isHidden(pp));
		//3 一次性将FileTest.java檔案所有行放入集合中,字元集不正确會報錯。相當于輸入流功能
		List<String> lines = Files.readAllLines(pp,Charset.forName("UTF-8"));
		//列印:用一行字列印整個類中内容
		System.out.println(lines);
		//4 判斷指定檔案大小,機關為位元組
		//列印:1426
		System.out.println(Files.size(pp));
		//5 将集合寫入指定檔案中,相當于輸出流功能
		List<String> poem = new ArrayList<>();
		poem.add("低調處理");
		poem.add("不願意寫了");
		Files.write(Paths.get("poem.txt"), poem, Charset.forName("UTF-8"));
		//6 擷取C槽總空間與可用空間
		FileStore cStore = Files.getFileStore(Paths.get("C:"));
		//列印:106571706368
		System.out.println(cStore.getTotalSpace());
		//列印:64986263552
		System.out.println(cStore.getUsableSpace());
	}
}

           
15.10.2 FileVisitor周遊目錄
//周遊E:\\Program Files\\Flexcube_dev\\Workspace\\AllInOneTest下所有内容,并找到FileVisitorTest.java所在位置
package test.wsh;

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

public class FileVisitorTest {
	public static void main(String[] args) throws IOException {
		Path pp = Paths.get("E:\\Program Files\\Flexcube_dev\\Workspace\\AllInOneTest");
		//1. walkFileTree方法可以周遊pp路徑下所有檔案和子目錄
		//2. 與下面方法類似,最多周遊maxDepth深度的檔案:public static Path walkFileTree(Path start,Set<FileVisitOption> options,int maxDepth,FileVisitor<? super Path> visitor)
		Files.walkFileTree(pp, new SimpleFileVisitor<Path>() {
			//FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs):通路子目錄前觸發
			//FileVisitResult postVisitDirectory(T dir, IOException exc):通路子目錄後觸發
			//FileVisitResult visitFile(T file, BasicFileAttributes attrs):通路檔案後觸發
			//FileVisitResult visitFileFailed(T file, IOException exc):通路檔案失敗後觸發
			@Override
		    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
		        throws IOException
		    {	
				System.out.println("正通路"+file+"檔案");
				if(file.endsWith("FileVisitorTest.java")) {
		        	System.out.println("已找到該檔案,檔案路徑為:"+file);
		        	//CONTINUE:繼續通路
		        	//TERMINATE:停止通路
		        	//SKIP_SUBTREE:繼續通路,但不通路該檔案或目錄的子目錄樹
		        	//SKIP_SIBLINGS:繼續通路,但不通路該檔案或目錄的兄弟檔案或目錄
		        	return FileVisitResult.TERMINATE;
		        }else {
		        	
		        }
		        return FileVisitResult.CONTINUE;
		    }
			@Override
		    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
		        throws IOException
		    {
		        System.out.println("正通路"+dir+"路徑");
		        return FileVisitResult.CONTINUE;
		    }
			
		});
	}
}	

           
15.10.3 WatchService監控檔案變化

以前Java版本中,如果需要監控檔案變化,需要啟動一個背景線程,每隔一段時間掃描要監控的檔案夾,看檔案是否變化,十分繁瑣,性能較差

  1. WatchService:監聽服務
  2. WatchKey:擷取到的事件的key
  3. WatchEvent:真正的事件
//持續監控E:下的檔案變化,并列印
package test.wsh;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

public class WatchServiceTest {
	public static void main(String[] args) throws IOException, InterruptedException {
		WatchService watchService = FileSystems.getDefault().newWatchService();
		//1. 将Path注冊到WatchService中:用watcher監聽Path對應的路徑下的檔案變化,events表示監聽哪些類型的事件
		//WatchKey register(WatchService watcher,WatchEvent.Kind<?>... events)
		Paths.get("E:/").register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
				StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
		while(true) {
			//2. 注冊後就可以使用WatchService的方法來擷取被監聽目錄的檔案變化事件
			//WatchKey take():擷取下一個WatchKey,如果沒發生就繼續等待
			//WatchKey poll():嘗試等待timeout時間來擷取下一個WatchKey
			//WatchKey poll(long timeout,TimeUnit unit):擷取下一個WatchKey,如果沒發生就繼續等待
			WatchKey key = watchService.take();
			for(WatchEvent event:key.pollEvents()) {
				System.out.println(event.context()+" 檔案發生了 "+event.kind()+"事件!");
			}
			boolean valid = key.reset();
			if(!valid) {
				break;
			}
		}
	}
}

           
15.10.4 通路檔案屬性
//通路與設定檔案的某些屬性
package test.wsh;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.util.Date;
import java.util.List;

public class AttributeViewTest {
	public static void main(String[] args) throws IOException {
		Path testPath = Paths.get(".\\src\\test\\wsh\\FileTest.java");
		BasicFileAttributeView basicView = Files.getFileAttributeView(testPath, BasicFileAttributeView.class);
		BasicFileAttributes basicAttribs = basicView.readAttributes();
		System.out.println("建立時間:"+new Date(basicAttribs.creationTime().toMillis()));
		System.out.println("最後通路時間:"+new Date(basicAttribs.lastAccessTime().toMillis()));
		System.out.println("最後修改時間:"+new Date(basicAttribs.lastModifiedTime().toMillis()));
		System.out.println("檔案大小:"+basicAttribs.size());
		
		FileOwnerAttributeView ownerView = Files.getFileAttributeView(testPath, FileOwnerAttributeView.class);
		System.out.println("該檔案所屬使用者:"+ownerView.getOwner());
		UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest");
		//修改使用者,使用者不存在會報錯
//		ownerView.setOwner(user);
		UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath, UserDefinedFileAttributeView.class);
		//為檔案寫入屬性,不寫的話,這個java檔案自定義屬性是空的
		userView.write("發行者", Charset.defaultCharset().encode("Java瘋狂講義"));
		List<String> attrNames = userView.list();
		for(String name :attrNames) {
			ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
			//将屬性名為name的屬性值,讀入到buf中
			userView.read(name, buf);
			buf.flip();
			String value = Charset.defaultCharset().decode(buf).toString();
			System.out.println(name+"---->"+value);
		}
		DosFileAttributeView dosView = Files.getFileAttributeView(testPath, DosFileAttributeView.class);
		//設定隐藏與隻讀
		dosView.setHidden(true);
		dosView.setReadOnly(true);
	}
}