Java I/O流輸入輸出,序列化,NIO,NIO.2

Java IO流

 

File類:

File類是java.io包下表明和平臺無關的文件和目錄,File不能訪問文件內容自己。java

File類基本操做:程序員

        System.out.println("判斷文件是否存在:"+file.exists());//判斷文件是否存在,返回Boolean值
        System.out.println("建立文件夾:"+file.mkdir());//建立文件夾,只能建立一層,返回Boolean值
        System.out.println("文件目錄:"+file.getParent());//返回文件最後一級子目錄
        System.out.println("建立文件夾:"+file2.mkdirs());//建立文件夾,建立多層,返回Boolean值
        System.out.println("建立新文件:"+file3.createNewFile());//建立新文件,此處需處理異常,返回Boolean值
        System.out.println("刪除文件:"+file3.delete());//刪除文件,返回Boolean值
        System.out.println("文件更名:"+file.renameTo(file4));//文件更名,傳入另外一個文件
        System.out.println("文件名:"+file.getName());//返回名
        System.out.println("文件路徑:"+file.getPath());//返回文件路徑
        System.out.println("絕對路徑:"+file.getAbsolutePath());//返回絕對路徑
        System.out.println("文件夾:"+file.isDirectory());//返回是否文件夾
        System.out.println("是否文件:"+file.isFile());//返回是否文件
        System.out.println("是否文件夾:"+file.isDirectory());//返回是否文件夾
        System.out.println("是否絕對路徑:"+file.isAbsolute());//返回是否絕對路徑
        System.out.println("文件長度:"+file.length());//返回文件長度
        System.out.println("最後修改時間:"+file.lastModified());//返回最後修改時間

        // 以當前路徑來建立一個File對象
        File file = new File(".");
        // 直接獲取文件名,輸出一點
        System.out.println(file.getName());
        // 獲取相對路徑的父路徑可能出錯,下面代碼輸出null
        System.out.println(file.getParent());
        // 獲取絕對路徑
        System.out.println(file.getAbsoluteFile());
        // 獲取上一級路徑
        System.out.println(file.getAbsoluteFile().getParent());
        // 在當前路徑下建立一個臨時文件
        File tmpFile = File.createTempFile("aaa", ".txt", file);
        // 指定當JVM退出時刪除該文件
        tmpFile.deleteOnExit();
        // 以系統當前時間做爲新文件名來建立新文件
        File newFile = new File(System.currentTimeMillis() + "");
        System.out.println("newFile對象是否存在:" + newFile.exists());
        // 以指定newFile對象來建立一個文件
        newFile.createNewFile();
        // 以newFile對象來建立一個目錄,由於newFile已經存在,
        // 因此下面方法返回false,即沒法建立該目錄
        newFile.mkdir();
        // 使用list()方法來列出當前路徑下的全部文件和路徑
        String[] fileList = file.list();
        System.out.println("====當前路徑下全部文件和路徑以下====");
        for (String fileName : fileList)
        {
            System.out.println(fileName);
        }
        // listRoots()靜態方法列出全部的磁盤根路徑。
        File[] roots = File.listRoots();
        System.out.println("====系統全部根路徑以下====");
        for (File root : roots)
        {
            System.out.println(root);
        }

文件過濾器:算法

File類的list()方法中能夠接受一個FilenameFilter參數,經過該參數能夠只列出符合條件的文件。FilenameFilter接口裏包含了一個accept(**)方法,該方法將一次對指定File的全部子目錄或者文件進行迭代,若是該方法返回true,則list()方法會列出該子目錄或者文件。數組

     File file = new File(".");
        // 使用Lambda表達式(目標類型爲FilenameFilter)實現文件過濾器。
        // 若是文件名以.java結尾,或者文件對應一個路徑,返回true
        String[] nameList = file.list((dir, name) -> name.endsWith(".java")
            || new File(name).isDirectory());
        for(String name : nameList)
        {
            System.out.println(name);
        }

FilenameFilter接口內只有一個抽象方法,所以改接口也是一個函數式接口,可以使用Lambda表達式建立實現該接口的對象。安全

Java的IO流概念

Java的IO流是實現輸入輸出的基礎,在Java中把不一樣的輸入輸出源抽象表述爲流,經過流的方式容許Java使用相同的方式來訪問不一樣的輸入輸出源。網絡

stream是從起源(source)到接收(sink)的有序數據。併發

IO(in / out)流的分類

  流向:app

    輸入流  讀取數據dom

    輸出流  寫出數據異步

  數據類型:

    字節流

    一個字節佔8位, 以一個字節爲單位讀數據  

    八大數據類型所佔字節數:
    byte(1), short(2), int(4), long(8),float(4), double(8),boolean(1),char(2)

      字節輸入流  讀取數據  InputStream

                     

      字節輸出流  寫出數據  OutputStream

                     

    字符流

    一個字符佔兩個字節, 以一個字符爲一個單位

      字符輸入流  讀取數據  Reader

                    

      字符輸出流  寫出數據  Writer

                    

 

字節流的基本抽象類

 InputStream    OutputStream

字符流的基本抽象類

Reader   Writer

  功能:
    節點流: 只有一個根管道套在文件上進行傳輸
    處理流: 將節點流處理一下, 加強管道的功能, 至關於在管道上套一層

 字節流和字符流:

InputStream和Reader是全部輸入流的抽象基類,自己並不能建立實例來執行輸入,但它們將成爲全部輸入流的模板,他們的方法是有輸入流均可以使用的方法。

這兩個基類的功能基本是同樣的。

他們分別有一個用於讀取文件的輸入流:FileInputStream和FileReader,他們都是節點流,會直接和指定文件關聯。

使用FileInputStream讀取文件自身:

    public static void main(String[] args) throws IOException
    {
        // 建立字節輸入流
        FileInputStream fis = new FileInputStream(
                "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\FileInputStreamTest.java");
        // 建立一個長度爲1024的「竹筒」
        byte[] bbuf = new byte[1024];
        // 用於保存實際讀取的字節數
        int hasRead = 0;
        // 使用循環來重複「取水」過程
        while ((hasRead = fis.read(bbuf)) > 0 )
        {
            // 取出「竹筒」中水滴(字節),將字節數組轉換成字符串輸入!
            System.out.print(new String(bbuf , 0 , hasRead ));
        }
        // 關閉文件輸入流,放在finally塊裏更安全
        fis.close();
    }

須要注意,若是bbuf字節數組的長度較小,遇到中文時可能會亂碼,由於若是文件自己保存時採用GBK編碼方式,在這種方式下,每一箇中文字符佔兩個字節,若是read方法讀取時只讀取到了半個中文就會亂碼。

程序裏打開的文件IO資源不屬於內存裏的資源,垃圾回收機制沒法回收該資源,因此要顯示關閉文件IO資源。

使用FileReader:

    public static void main(String[] args)
    {
        try(
                // 建立字符輸入流
                FileReader fr = new FileReader("D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\FileReaderTest.java"))
        {
            // 建立一個長度爲32的「竹筒」
            char[] cbuf = new char[32];
            // 用於保存實際讀取的字符數
            int hasRead = 0;
            // 使用循環來重複「取水」過程
            while ((hasRead = fr.read(cbuf)) > 0 )
            {
                // 取出「竹筒」中水滴(字符),將字符數組轉換成字符串輸入!
                System.out.print(new String(cbuf , 0 , hasRead));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

InputStream和Reader還支持以下幾個方法來移動記錄指針:

方法 釋義
void mark(int readlimit) Marks the current position in this input stream.
boolean markSupported() Tests if this input stream supports the mark and reset methods.
void reset() Repositions this stream to the position at the time the mark method was last called on this input stream.
long skip(long n) Skips over and discards n bytes of data from this input stream.

OuputStream和Writer:

    public static void main(String[] args)
    {
        try(
                // 建立字節輸入流
                FileInputStream fis = new FileInputStream(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\FileOutputStreamTest.java");
                // 建立字節輸出流
                FileOutputStream fos = new FileOutputStream("newFile.txt"))//文件在項目根
        {
            byte[] bbuf = new byte[32];
            int hasRead = 0;
            // 循環從輸入流中取出數據
            while ((hasRead = fis.read(bbuf)) > 0 )
            {
                // 每讀取一次,即寫入文件輸出流,讀了多少,就寫多少。
                fos.write(bbuf , 0 , hasRead);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

注意:使用IO流執行輸出時,不要忘記關閉輸出流,關閉輸出流處能夠保證流的物流自願被回收,還能夠將輸出流緩衝區的數據flush到物理節點裏。

若是但願直接輸出字符串內容,使用Writer更好:

    public static void main(String[] args)
    {
        try(
                FileWriter fw = new FileWriter("poem.txt"))
        {
            fw.write("錦瑟 - 李商隱\r\n");
            fw.write("錦瑟無故五十弦,一弦一柱思華年。\r\n");
            fw.write("莊生曉夢迷蝴蝶,望帝春心託杜鵑。\r\n");
            fw.write("滄海月明珠有淚,藍田日暖玉生煙。\r\n");
            fw.write("此情可待成追憶,只是當時已惘然。\r\n");
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

輸入輸出流體系:

使用處理流的典型思路是,使用處理流來包裝節點流,程序經過處理流來執行輸入輸入,讓節點流與底層設備、文件交互。

使用PrintStream處理流包裝OutStream:

    public static void main(String[] args)
    {
        try(
                FileOutputStream fos = new FileOutputStream("test.txt");
                PrintStream ps = new PrintStream(fos))
        {
            // 使用PrintStream執行輸出
            ps.println("普通字符串");
            // 直接使用PrintStream輸出對象
            ps.println(new PrintStreamTest());
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

一般若是要輸出文本內容,都應將輸出流包裝成PrintStream後輸出。

注意,在使用處理流包裝了底層節點以後,關閉輸入輸出流資源是,只要關閉最上層的處理流便可,關閉最上層處理流時,系統會自動關閉被該處理流包裝的節點流。

Java輸入輸出流體系經常使用流分類:

流分類 使用分類 字節輸入流 字節輸出流 字符輸入流 字符輸出流
  抽象基類 InputStream

OutputStream

Reader Writer
節點流 訪問文件 FileInputStream FileOutStream FileReader FileWriter
訪問數值 ByteArrayInputStream ByteArrayOutStream CharArrayReader CharArrayWriter
訪問管道 PipedInputStream PipedOutStream PipedReader PipedWriter
訪問字符串     StringReader StringWriter
處理流 緩衝流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
轉換流     InputStreamReader OutputStreamWriter
對象流 ObjectInputStream ObjectOutputStream    
抽象基類(過濾) FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流   PrintStream   PrintWriter
推回輸入流 PushbackInputStream   PushbackReader  
特殊流 DataInputStream DataOutputStream    

一般來講,字節流的功能比字符流強大,由於計算機全部數據都是二進制的,字節流能夠處理全部的二進制文件,可是須要使用合適的方式把這些字節轉換成字符,一般:若是進行輸入輸出的內容是文本內容,則應該考慮使用字符流,若是是二進制內容,則使用字節流。

使用字符串做爲物理節點:

    public static void main(String[] args)
    {
        String src = "從明天起,作一個幸福的人\n"
                + "餵馬,劈柴,周遊世界\n"
                + "從明天起,關心糧食和蔬菜\n"
                + "我有一所房子,面朝大海,春暖花開\n"
                + "從明天起,和每個親人通訊\n"
                + "告訴他們個人幸福\n";
        char[] buffer = new char[32];
        int hasRead = 0;
        try(
                StringReader sr = new StringReader(src))
        {
            // 採用循環讀取的訪問讀取字符串
            while((hasRead = sr.read(buffer)) > 0)
            {
                System.out.print(new String(buffer ,0 , hasRead));
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
        try(
                // 建立StringWriter時,實際上以一個StringBuffer做爲輸出節點
                // 下面指定的20就是StringBuffer的初始長度
                StringWriter sw = new StringWriter())
        {
            // 調用StringWriter的方法執行輸出
            sw.write("有一個美麗的新世界,\n");
            sw.write("她在遠方等我,\n");
            sw.write("哪裏有天真的孩子,\n");
            sw.write("還有姑娘的酒窩\n");
            System.out.println("----下面是sw的字符串節點裏的內容----");
            // 使用toString()方法返回StringWriter的字符串節點的內容
            System.out.println(sw.toString());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

和前面使用FileReader和FileWriter類似,只是建立對象時傳入的是字符串節點,用於String是不可變得字符串對象,因此StringWriter使用StringBuffer做爲輸出節點。

轉換流

InputStreamReader將字節輸入流轉換成字符輸入流,OutputStreamWriter將字節輸出流轉換成字符輸出流。

以鍵盤輸入爲例,java使用System.in表明標準輸入也就是鍵盤輸入,但這個標準輸入流是InputStream類的實例,可使用InputStreamReader將其轉換成字符輸入流,再講普通的Reader再次包裝成BufferedReader:

    public static void main(String[] args)
    {
        try(
                // 將Sytem.in對象轉換成Reader對象
                InputStreamReader reader = new InputStreamReader(System.in);
                // 將普通Reader包裝成BufferedReader
                BufferedReader br = new BufferedReader(reader))
        {
            String line = null;
            // 採用循環方式來一行一行的讀取
            while ((line = br.readLine()) != null)
            {
                // 若是讀取的字符串爲"exit",程序退出
                if (line.equals("exit"))
                {
                    System.exit(1);
                }
                // 打印讀取的內容
                System.out.println("輸入內容爲:" + line);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

推回輸入流:

PushbackInputStream和PushbackReader

推回輸入流都帶有一個推回緩衝區,當程序調用推回輸入流的unread()方法,系統將會把指定數組內容推回到該緩衝區,而推回輸入流每次調用read()方法老是先從推回緩衝區讀取,只有徹底讀取了推回緩衝區的內容後,但尚未裝滿read()所需的數組時纔會從原輸入流讀取。

    public static void main(String[] args)
    {
        try(
                // 建立一個PushbackReader對象,指定推回緩衝區的長度爲64
                PushbackReader pr = new PushbackReader(new FileReader(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\PushbackTest.java") , 64))
        {
            char[] buf = new char[32];
            // 用以保存上次讀取的字符串內容
            String lastContent = "";
            int hasRead = 0;
            // 循環讀取文件內容
            while ((hasRead = pr.read(buf)) > 0)
            {
                // 將讀取的內容轉換成字符串
                String content = new String(buf , 0 , hasRead);
                int targetIndex = 0;
                // 將上次讀取的字符串和本次讀取的字符串拼起來,
                // 查看是否包含目標字符串, 若是包含目標字符串
                if ((targetIndex = (lastContent + content)
                        .indexOf("new PushbackReader")) > 0)
                {
                    // 將本次內容和上次內容一塊兒推回緩衝區
                    pr.unread((lastContent + content).toCharArray());// 從新定義一個長度爲targetIndex的char數組
                    if(targetIndex > 32)
                    {
                        buf = new char[targetIndex];
                    }
                    // 再次讀取指定長度的內容(就是目標字符串以前的內容)
                    pr.read(buf , 0 , targetIndex); // 打印讀取的內容
                    System.out.print(new String(buf , 0 ,targetIndex));
                    System.exit(0);
                }
                else
                {
                    // 打印上次讀取的內容
                    System.out.print(lastContent);
                    // 將本次內容設爲上次讀取的內容
                    lastContent = content;
                }
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

粗體下劃線部分實現了將制定內容推回到推回緩衝區,因而程序再次調用read()方法時,實際上只是讀取了推回緩衝區的部份內容,從而實現了只打印目標字符串前面內容的功能。

重定向標準輸入輸出:

        通常狀況下,System.in表明的是鍵盤、System.out是表明的控制檯(顯示器)。當程序經過System.in來獲取輸入的時候,默認狀況下,是從鍵盤讀取輸入;當程序試圖經過System.out執行輸出時,程序老是輸出到顯示器。若是咱們想對這樣的狀況作一個改變,例如獲取輸入時,不是來自鍵盤,而是來自文件或其餘的位置;輸出的時候,不是輸出到顯示器上顯示,而是輸出到文件或其餘位置,怎麼實現?因而,java重定標準輸入輸出應運而生。

  static void setErr(PrintStream err)、重定向標準錯誤輸出流

  static void setIn(InputStream in)、重定向標準輸入流

  static void setOut(PrintStream out) 重定向標準輸出流

 

    public static void main(String[] args)
    {
        try(
                // 一次性建立PrintStream輸出流
                PrintStream ps = new PrintStream(new FileOutputStream("out.txt")))
        {
            // 將標準輸出重定向到ps輸出流
            System.setOut(ps);
            // 向標準輸出輸出一個字符串
            System.out.println("普通字符串");
            // 向標準輸出輸出一個對象
            System.out.println(new RedirectOut());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

 

    public static void main(String[] args)
    {
        try(
                FileInputStream fis = new FileInputStream("RedirectIn.java"))
        {
            // 將標準輸入重定向到fis輸入流
            System.setIn(fis);
            // 使用System.in建立Scanner對象,用於獲取標準輸入
            Scanner sc = new Scanner(System.in);
            // 增長下面一行將只把回車做爲分隔符
            sc.useDelimiter("\n");
            // 判斷是否還有下一個輸入項
            while(sc.hasNext())
            {
                // 輸出輸入項
                System.out.println("鍵盤輸入的內容是:" + sc.next());
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

Java虛擬機讀寫其餘進程數據

 讀取其餘進程的輸出信息:

 

    public static void main(String[] args)
            throws IOException
    {
        // 運行javac命令,返回運行該命令的子進程
        Process p = Runtime.getRuntime().exec("javac");
        try(
                // 以p進程的錯誤流建立BufferedReader對象
                // 這個錯誤流對本程序是輸入流,對p進程則是輸出流
                BufferedReader br = new BufferedReader(new
                        InputStreamReader(p.getErrorStream())))
        {
            String buff = null;
            // 採起循環方式來讀取p進程的錯誤輸出
            while((buff = br.readLine()) != null)
            {
                System.out.println(buff);
            }
        }
    }

在Java程序中啓動Java虛擬機運行另外一個Java程序,並像另外一個Java程序中輸入數據:

package com.jiangwenzhang.mybootbill.learn.FileIO;

import java.io.*;
import java.util.*;

public class WriteToProcess
{
    public static void main(String[] args)
            throws IOException
    {
        // 運行java ReadStandard命令,返回運行該命令的子進程
        Process p = Runtime.getRuntime().exec("java ReadStandard");
        try(
                // 以p進程的輸出流建立PrintStream對象
                // 這個輸出流對本程序是輸出流,對p進程則是輸入流
                PrintStream ps = new PrintStream(p.getOutputStream()))
        {
            // 向ReadStandard程序寫入內容,這些內容將被ReadStandard讀取
            ps.println("普通字符串");
            ps.println(new WriteToProcess());
        }
    }
}
// 定義一個ReadStandard類,該類能夠接受標準輸入,
// 並將標準輸入寫入out.txt文件。
class ReadStandard
{
    public static void main(String[] args)
    {
        try(
                // 使用System.in建立Scanner對象,用於獲取標準輸入
                Scanner sc = new Scanner(System.in);
                PrintStream ps = new PrintStream(
                        new FileOutputStream("out.txt")))
        {
            // 增長下面一行將只把回車做爲分隔符
            sc.useDelimiter("\n");
            // 判斷是否還有下一個輸入項
            while(sc.hasNext())
            {
                // 輸出輸入項
                ps.println("鍵盤輸入的內容是:" + sc.next());
            }
        }
        catch(IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

RandomAccessFile

 隨機流(RandomAccessFile)不屬於IO流,支持對文件的讀取和寫入隨機訪問。

RandomAccessFile容許自由定位文件記錄指針,RandomAccessFile能夠不從開始的地方開始輸出,由於RandomAccessFile能夠向已存在的文件後追加內容。

RandomAccessFile最大的侷限就是隻能讀寫文件,不能讀寫其餘IO節點。

RandomAccessFile對象包含了一個記錄指針,用以標識當前讀寫位置。RandomAccessFile能夠自由移動該記錄指針。

使用RandomAccessFile來訪問指定的中間部分數據:

    public static void main(String[] args)
    {
        try(
                RandomAccessFile raf =  new RandomAccessFile(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\RandomAccessFileTest.java" , "r"))
        {
            // 獲取RandomAccessFile對象文件指針的位置,初始位置是0
            System.out.println("RandomAccessFile的文件指針的初始位置:"
                    + raf.getFilePointer());
            // 移動raf的文件記錄指針的位置
            raf.seek(300);
            byte[] bbuf = new byte[1024];
            // 用於保存實際讀取的字節數
            int hasRead = 0;
            // 使用循環來重複「取水」過程
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 取出「竹筒」中水滴(字節),將字節數組轉換成字符串輸入!
                System.out.print(new String(bbuf , 0 , hasRead ));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

該方法以只讀方式打開文件,從300字節處開始讀取。

像文件中追加內容,爲了追加內容,程序應該先將記錄指針移動到文件最後,而後項文件中輸出內容。

    public static void main(String[] args)
    {
        try(
                //以讀、寫方式打開一個RandomAccessFile對象
                RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
        {
            //將記錄指針移動到out.txt文件的最後
            raf.seek(raf.length());
            raf.write("追加的內容!\r\n".getBytes());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

指定文件,指定位置插入內容:

package com.jiangwenzhang.mybootbill.learn.FileIO;

import java.io.*;


public class InsertContent
{
    public static void insert(String fileName , long pos
            , String insertContent) throws IOException
    {
        File tmp = File.createTempFile("tmp" , null);
        tmp.deleteOnExit();
        try(
                RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
                // 使用臨時文件來保存插入點後的數據
                FileOutputStream tmpOut = new FileOutputStream(tmp);
                FileInputStream tmpIn = new FileInputStream(tmp))
        {
            raf.seek(pos);
            // ------下面代碼將插入點後的內容讀入臨時文件中保存------
            byte[] bbuf = new byte[64];
            // 用於保存實際讀取的字節數
            int hasRead = 0;
            // 使用循環方式讀取插入點後的數據
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 將讀取的數據寫入臨時文件
                tmpOut.write(bbuf , 0 , hasRead);
            }
            // ----------下面代碼插入內容----------
            // 把文件記錄指針從新定位到pos位置
            raf.seek(pos);
            // 追加須要插入的內容
            raf.write(insertContent.getBytes());
            // 追加臨時文件中的內容
            while ((hasRead = tmpIn.read(bbuf)) > 0 )
            {
                raf.write(bbuf , 0 , hasRead);
            }
        }
    }
    public static void main(String[] args)
            throws IOException
    {
        insert("InsertContent.java" , 45 , "插入的內容\r\n");
    }
}

Java9改進的對象序列化

對象序列化的目標是將對象保存到磁盤中,或容許在網絡中直接傳輸對象。全部分佈式應用經常須要跨平臺,跨網絡,所以要求全部傳的參數、返回值都必須實現序列化。

序列化:把Java對象轉換爲字節序列的過程。 

反序列化:把字節序列恢復爲Java對象的過程。

對象的序列化是指將一個Java對象寫入IO流中,對象的反序列化則是是指從IO流中恢復該Java對象。

Java9加強了對象序列化機制,他容許對讀入的序列化數據進行過濾,這種過濾能夠在反序列化以前對數據執行校驗,從而提升安全性和健壯性。

讓某個類可序列化,必須實現以下兩個接口之一:

Serializable

Externailzable

Java的不少類已經實現了Serializable,這是一個標記接口,無需實現任何方法,只是代表該類的實例是能夠序列化的。

全部可能在網絡上傳輸的對象的類都應該是可序列化的。

使用對象流實現序列化:

public class Person
        implements java.io.Serializable
{
    private String name;
    private int age;
    // 注意此處沒有提供無參數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }
}
public class WriteObject
{
    public static void main(String[] args)
    {
        try(
                // 建立一個ObjectOutputStream輸出流
                ObjectOutputStream oos = new ObjectOutputStream(
                        new FileOutputStream("object.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 將per對象寫入輸出流
            oos.writeObject(per);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

從而二進制流中恢復Java對象:

public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
            Person p = (Person)ois.readObject();
            System.out.println("名字爲:" + p.getName()
                + "\n年齡爲:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

注意:反序列化讀取的僅僅是Java對象的數據,而不是Java類,所以採用反序列化恢復Java對象必須提供Java對象所屬類的class文件。

反序列化機制無需經過構造器來初始化Java對象。

若是使用序列化機制向文件中寫入了多個Java對象,使用反序列化機制恢復對象必須按實際寫入的順序讀取。

對象引用的序列化

若是某個類的成員變量的類型不是基本類型或String而是引用類型,那麼這個引用類必須是可序列化的,不然擁有該類型成員變量的的類也是不可序列化的。

public class Teacher
    implements java.io.Serializable
{
    private String name;
    private Person student;
    public Teacher(String name , Person student)
    {
        this.name = name;
        this.student = student;
    }
    // 此處省略了name和student的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // student的setter和getter方法
    public void setStudent(Person student)
    {
        this.student = student;
    }
    public Person getStudent()
    {
        return this.student;
    }
}

特殊狀況:

            Person per = new Person("孫悟空", 500);
            Teacher t1 = new Teacher("唐僧" , per);
            Teacher t2 = new Teacher("菩提祖師" , per);

兩種對象互相引用,這樣若是先序列化t1,系統將t1對象引用的Person對象一塊兒序列化,在序列化t2,程序將同樣會序列化該t2對象,而且再次序列化Person對象,若是程序在顯示序列化per對象,系統又一次序列化person對象。這個過程向輸出流中輸出三個Person對象。

這樣程序從輸入流中反序列化這些對象,將會獲得三個person對象,從而引發t1和t2所引用的Person對象不是同一個對象。

Java序列化機制採用了一種特殊的序列化算法:

一、全部保存到磁盤中的對象都有一個序列化編號。

二、當程序試圖序列化一個對象時,會先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化,系統纔會將該對象轉換成字節序列並輸出。

三、若是對象已經被序列化,程序將直接輸出一個序列化編號,而不是從新序列化。

經過以上算法,當第二次第三次序列化,程序不會再次將Person對象轉換成字節序列並輸出,而是僅僅輸出一個序列化編號。

當屢次調用wirteObject()方法輸出同一個對象時,只有第一次調用wirteObject()方法纔會將該對象轉換成字節序列並輸出。

    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            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);
            oos.writeObject(t2);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸出流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("teacher.txt")))
        {
            // 依次讀取ObjectInputStream輸入流中的四個對象
            Teacher t1 = (Teacher)ois.readObject();
            Teacher t2 = (Teacher)ois.readObject();
            Person p = (Person)ois.readObject();
            Teacher t3 = (Teacher)ois.readObject();
            // 輸出true
            System.out.println("t1的student引用和p是否相同:"
                + (t1.getStudent() == p));
            // 輸出true
            System.out.println("t2的student引用和p是否相同:"
                + (t2.getStudent() == p));
            // 輸出true
            System.out.println("t2和t3是不是同一個對象:"
                + (t2 == t3));
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

上面代碼依次讀取了序列化文件中的4個Java對象,經過比較能夠看出t2和t3是同一個對象。

注意:

因爲Java序列化機制使然,屢次序列化同一個Java對象時,只有第一次序列化該對象纔會把該Java對象轉換成字節序列並輸出,所以程序序列化一個可變對象以後,後面改變了對象的實例變量值,再次序列化也只是輸出前面的序列化編號,改變的實例變量值也不會輸出。

    public static void main(String[] args)
    {

        try(
            // 建立一個ObjectOutputStream輸入流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("mutable.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("mutable.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 系統會per對象轉換字節序列並輸出
            oos.writeObject(per);
            // 改變per對象的name實例變量
            per.setName("豬八戒");
            // 系統只是輸出序列化編號,因此改變後的name不會被序列化
            oos.writeObject(per);
            Person p1 = (Person)ois.readObject();    //
            Person p2 = (Person)ois.readObject();    //// 下面輸出true,即反序列化後p1等於p2
            System.out.println(p1 == p2);
            // 下面依然看到輸出"孫悟空",即改變後的實例變量沒有被序列化
            System.out.println(p2.getName());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

Java9增長的過濾功能:

Java9爲ObjectInputStream增長了setObjectInputFilter()和getObjectInputFilter()兩個方法,其中第一個方法用於爲對象輸入流設置過濾器。當程序經過ObjectInputStream反序列化時,過濾器的checkInput()方法會被自動激發,用於檢查序列化數據是否有效。

    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            ois.setObjectInputFilter((info) -> {
                System.out.println("===執行數據過濾===");
                ObjectInputFilter serialFilter = ObjectInputFilter.Config.getSerialFilter();
                    if (serialFilter != null) {
                        // 首先使用ObjectInputFilter執行默認的檢查
                        ObjectInputFilter.Status status = serialFilter.checkInput(info);
                        // 若是默認檢查的結果不是Status.UNDECIDED
                        if (status != ObjectInputFilter.Status.UNDECIDED) {
                            // 直接返回檢查結果
                            return status;
                        }
                    }
                    // 若是要恢復的對象不是1個
                    if(info.references() != 1)
                    {
                        // 不容許恢復對象
                        return ObjectInputFilter.Status.REJECTED;
                    }
                    if (info.serialClass() != null &&
                        // 若是恢復的不是Person類
                        info.serialClass() != Person.class)
                    {
                        // 不容許恢復對象
                        return ObjectInputFilter.Status.REJECTED;
                    }
                    return ObjectInputFilter.Status.UNDECIDED;
                });
            // 從輸入流中讀取一個Java對象,並將其強制類型轉換爲Person類
            Person p = (Person)ois.readObject();
            System.out.println("名字爲:" + p.getName()
                + "\n年齡爲:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

自定義序列化

經過在實例變量前使用transient關鍵字修飾,能夠指定Java序列化時無需理會該實例變量。

public class Person
    implements java.io.Serializable
{
    private String name;
    private transient int age;
    // 注意此處沒有提供無參數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }
}
public class TransientTest
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("transient.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("transient.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 系統會per對象轉換字節序列並輸出
            oos.writeObject(per);
            Person p = (Person)ois.readObject();
            System.out.println(p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

程序將輸出0

被transient修飾的的實例變量被徹底隔離在序列化機制以外,這致使在反序列化恢復Java對象時沒法取得該實例變量值。

Java提供了一種自定義序列化機制:

public class Person
    implements java.io.Serializable
{
    private String name;
    private int age;
    // 注意此處沒有提供無參數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    private void writeObject(java.io.ObjectOutputStream out)
        throws IOException
    {
        // 將name實例變量的值反轉後寫入二進制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        // 將讀取的字符串反轉後賦給name實例變量
        this.name = ((StringBuffer)in.readObject()).reverse()
            .toString();
        this.age = in.readInt();
    }
}

還有一種更完全的自定義機制,他能夠在序列化對象時將該對象替換成其餘對象。

public class Person
    implements java.io.Serializable
{
    private String name;
    private int age;
    // 注意此處沒有提供無參數的構造器!
    public Person(String name , int age)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    //    重寫writeReplace方法,程序在序列化該對象以前,先調用該方法
    private Object writeReplace()throws ObjectStreamException
    {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

Java的序列化機制保證在序列化某個對象以前,先調用該對象的writeReplace()方法,若是方法返回的是另外一個對象,則系統轉爲序列化另外一個對象,

public class ReplaceTest
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("replace.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("replace.txt")))
        {
            Person per = new Person("孫悟空", 500);
            // 系統將per對象轉換字節序列並輸出
            oos.writeObject(per);
            // 反序列化讀取獲得的是ArrayList
            ArrayList list = (ArrayList)ois.readObject();
            System.out.println(list);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

序列化機制裏還有一個特殊的方法,他能夠實現保護整個對象,

public class Orientation
    implements java.io.Serializable
{
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
    private int value;
    private Orientation(int value)
    {
        this.value = value;
    }
    // 爲枚舉類增長readResolve()方法
    private Object readResolve()throws ObjectStreamException
    {
        if (value == 1)
        {
            return HORIZONTAL;
        }
        if (value == 2)
        {
            return VERTICAL;
        }
        return null;
    }
}

Java的另外一種自定義序列化機制:

Java還提供了另外一種自定義序列化機制,這種序列化機制徹底由程序員存儲和恢復對象數據。須要實現Externalizable接口。

public class Person
    implements java.io.Externalizable
{
    private String name;
    private int age;
    // 注意必須提供無參數的構造器,不然反序列化時會失敗。
    public Person(){}
    public Person(String name , int age)
    {
        System.out.println("有參數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略name與age的setter和getter方法

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    public void writeExternal(java.io.ObjectOutput out)
        throws IOException
    {
        // 將name實例變量的值反轉後寫入二進制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }
    public void readExternal(java.io.ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        // 將讀取的字符串反轉後賦給name實例變量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

NIO

NIO(新IO)和傳統的IO具備相同的目的,都用於進行輸入輸出,但新IO採用了不一樣的方式來處理,NIO採用內存映射文件的方式來處理,NIO將文件或文件的一段區域映射到內存中,從而像訪問內存同樣訪問文件。就像操做系統的虛擬內存概念。

Channel(通道)和Buffer(緩衝)是NIO的兩個核心對象,Channel是對傳統輸入輸出系統的模擬,在NIO中全部的數據都須要經過通道傳輸,Channel和傳統的輸入輸出最大的區別在於它提供了一個map()方法,該方法能夠直接將一塊數據映射到內存中。

IO是面向流的,NIO是面向快(緩衝區)的。

Buffer能夠被理解成一個容器,他的本質是數組,發送到Channel中的全部對象都必須首相放到Buffer中,從Channel中取出的數據也必須先放到Buffer中,Buffer能夠一次次去Channel中取數據,也容許用Channel將文件的某塊數據映射成Buffer。

NIO還提供了用於將Unicode字符串映射成字節序列以及逆映射操做的Charset累和用於支持非阻塞式輸入輸出的Selector類。

Buffer

Buffer就像一個數組能夠保存多個類型相同的數據,Buffer是一個抽象類,最經常使用的子類是ByteBuffer,他能夠在底層字節數組上進行getset操做。其餘基本數據類型除了boolean都有相應的Buffer類。

在Buffer中有幾個重要概念

屬性 描述
Capacity 容量,便可以容納的最大數據量;在緩衝區建立時被設定而且不能改變
Limit 上界,緩衝區中當前數據量
Position 位置,下一個要被讀或寫的元素的索引
Mark 標記,調用mark()來設置mark=position,再調用reset()可讓position恢復到標記的位置即position=mark

並遵循:capacity>=limit>=position>=mark>=0

Buffer的主要做用是裝入數據而後輸出數據,開始時Buffer的postiton爲0,limit爲capatity,程序經過put()方法像Buffer裝入數據,或者從Channel中獲取數據,Buffer的postion相應的後移。

Buffer裝入數據後,調用Buffer的flip()方法,將limit位置設爲postiton所在位置,並將postiton設爲0,是的Buffer的讀寫指針移動到了開始的位置,爲輸出數據作好準備,當BUffer輸出數據結束後,Buffer調用clear方法,不是清空數據,僅僅將postiton設置爲0,兩limit設置爲capatity,爲再次向Buffer中裝入數據作好了準備。

Buffer常規操做:

ByteBuffer buffer=ByteBuffer.allocate(1024);       //非直接    大小爲1024個字節  此時它的position=0 limit=1024 capacity=1024

ByteBuffer buf =ByteBuffer.allocateDirect(1024);     //直接    大小爲1024個字節

buffer.put("abcde".getBytes());                       //將一個字節數組寫入緩衝區 此時它的position=5 limit=1024 capacity=1024

buffer.put("abcde".getBytes()); //該數組會在以前position開始寫 寫完後 position=10 limit=1024 capacity=1024

buffer.flip();                                                   //這一步的做用是 使position=0 limit爲能夠操做的最大字節數 這裏limit=10 capacity不變 仍是1024

System.out.println(new String(byteBuffer.array(),0,2));          //它的結果在這裏是:ab

這個方法的做用是什麼呢?回到屬性,執行下面語句:

System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());

輸出的結果是(以空格代替換行): 0      1024       1024

也就是說,如今這個緩衝區如今能夠從索引0位置開始操做了。那就put試一試:

byteBuffer.put("hhh".getBytes());                               //注意這裏只put3個字節

那麼若是put以後的position、limit、capacity又是多少呢?

此時position=3    limit=1024  capacity=1024

對於上面的結果?也許會有疑問?先保留吧,接下來咱們讀取該buffer的內容:

byteBuffer.flip();         //讀以前先翻轉    翻轉後position=3    limit=3  capacity=1024

System.out.println(new String(buffer.array(),0,buffer.limit()));

結果是:hhh

不知道小夥伴有沒有疑問,不是說以前的數據還在嗎?它去哪了呢?

被遺忘了,這裏仍是要把clear()方法再次提出來,以前說過,它並不會將緩衝區中的數據清空,也就是說緩衝區中以前的數據還在。執行clear後咱們能夠像操做一個空的緩衝區

同樣從索引0位置開始來操做這個緩衝區。可是以前的數據還存在,只是被遺忘了。若是上面咱們沒有執行byteBuffer.flip(); 那麼,結果就會是:hhhdeworld

因此啊,flip()必定不要忘了。

mark() :標記當前position

reset() :恢復position到mark標記的位置

hasRemaining :判斷緩衝區中是否含有元素

get() :從緩衝區中讀取單個字節

get(byte[] dst) :批量讀取多個字節到dst數組

get(int index) : 讀取指定位置的字節(position不會改變)

put(byte b) :將單個字節寫入緩衝區position位置

put(byte[] dst) :將多個字節從緩衝區position位置開始寫入

put(int index,byte b) : 將指定字節寫入緩衝區索引位置,不會移動position

public class BufferTest
{
    public static void main(String[] args)
    {
        // 建立Buffer
        CharBuffer buff = CharBuffer.allocate(8);    //
        System.out.println("capacity: "    + buff.capacity());
        System.out.println("limit: " + buff.limit());
        System.out.println("position: " + buff.position());
        // 放入元素
        buff.put('a');
        buff.put('b');
        buff.put('c');      //
        System.out.println("加入三個元素後,position = "
            + buff.position());
        // 調用flip()方法
        buff.flip();      //
        System.out.println("執行flip()後,limit = " + buff.limit());
        System.out.println("position = " + buff.position());
        // 取出第一個元素
        System.out.println("第一個元素(position=0):" + buff.get());  //
        System.out.println("取出一個元素後,position = "
            + buff.position());
        // 調用clear方法
        buff.clear();     //
        System.out.println("執行clear()後,limit = " + buff.limit());
        System.out.println("執行clear()後,position = "
            + buff.position());
        System.out.println("執行clear()後,緩衝區內容並無被清除:"
            + "第三個元素爲:" +  buff.get(2));    //
        System.out.println("執行絕對讀取後,position = "
            + buff.position());
    }
}

Buffer的建立成本很高,因此直接Buffer適用於長期生存的Buffer。

只有ByteBuffer提供allocateDirect()方法,因此只能在ByteBuffer級別上建立直接ByteBuffer,若是但願使用其餘類型,將該Buffer轉換成其餘類型Buffer。

Channel

channel相似於傳統流對象,區別:

channel能夠直接將指定文件的部分或者所有映射成Buffer。

程序不能直接訪問channel中的數據,channel只能和Buffer進行交互。

全部的channel都不該該經過構造器直接建立,而是經過傳統節點InputStream、outPutstream的getChannel()方法返回對應的channel,不一樣的節點流得到的channel也不同。

channel中最經常使用的三類方法map() , read() , write() 。map()用於將數據映射成ByteBuffer,另外兩個有一系列重載,用於對Buffer讀寫數據。

將FileChannel的所有數據映射成ByteBuffer:

public class FileChannelTest
{
    public static void main(String[] args)
    {
        File f = new File("FileChannelTest.java");
        try(
            // 建立FileInputStream,以該文件輸入流建立FileChannel
            FileChannel inChannel = new FileInputStream(f).getChannel();
            // 以文件輸出流建立FileBuffer,用以控制輸出
            FileChannel outChannel = new FileOutputStream("a.txt")
                .getChannel())
        {
            // 將FileChannel裏的所有數據映射成ByteBuffer
            MappedByteBuffer buffer = inChannel.map(FileChannel
                .MapMode.READ_ONLY , 0 , f.length());   //// 使用GBK的字符集來建立解碼器
            Charset charset = Charset.forName("GBK");
            // 直接將buffer裏的數據所有輸出
            outChannel.write(buffer);     //// 再次調用buffer的clear()方法,復原limit、position的位置
            buffer.clear();
            // 建立解碼器(CharsetDecoder)對象
            CharsetDecoder decoder = charset.newDecoder();
            // 使用解碼器將ByteBuffer轉換成CharBuffer
            CharBuffer charBuffer =  decoder.decode(buffer);
            // CharBuffer的toString方法能夠獲取對應的字符串
            System.out.println(charBuffer);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}
FileInputStream獲取的 channel只能讀,FileOutputStream獲取的 channel只能寫。RandomAccessFile中也包含getChannel方法,他是隻讀的仍是讀寫的,取決於RandomAccessFile打開文件的模式。
public class RandomFileChannelTest
{
    public static void main(String[] args)
        throws IOException
    {
        File f = new File("a.txt");
        try(
            // 建立一個RandomAccessFile對象
            RandomAccessFile raf = new RandomAccessFile(f, "rw");
            // 獲取RandomAccessFile對應的Channel
            FileChannel randomChannel = raf.getChannel())
        {
            // 將Channel中全部數據映射成ByteBuffer
            ByteBuffer buffer = randomChannel.map(FileChannel
                .MapMode.READ_ONLY, 0 , f.length());
            // 把Channel的記錄指針移動到最後
            randomChannel.position(f.length());
            // 將buffer中全部數據輸出
            randomChannel.write(buffer);
        }
    }
}

使用Channel和Buffer使用傳統的IO的屢次獲取數據的方式:

public class ReadFile
{
    public static void main(String[] args)
        throws IOException
    {
        try(
            // 建立文件輸入流
            FileInputStream fis = new FileInputStream("ReadFile.java");
            // 建立一個FileChannel
            FileChannel fcin = fis.getChannel())
        {
            // 定義一個ByteBuffer對象,用於重複取水
            ByteBuffer bbuff = ByteBuffer.allocate(256);
            // 將FileChannel中數據放入ByteBuffer中
            while( fcin.read(bbuff) != -1 )
            {
                // 鎖定Buffer的空白區
                bbuff.flip();
                // 建立Charset對象
                Charset charset = Charset.forName("GBK");
                // 建立解碼器(CharsetDecoder)對象
                CharsetDecoder decoder = charset.newDecoder();
                // 將ByteBuffer的內容轉碼
                CharBuffer cbuff = decoder.decode(bbuff);
                System.out.print(cbuff);
                // 將Buffer初始化,爲下一次讀取數據作準備
                bbuff.clear();
            }
        }
    }
}

字符集和Charset

Java默認採用Unicode字符集。JDK1.4提供了Charset來處理字節序列和字符序列之間的轉換關係。該類提供了建立解碼器和編碼器的方法。

獲取JDK支持的所有字符集:

public class CharsetTest
{
    public static void main(String[] args)
    {
        // 獲取Java支持的所有字符集
        SortedMap<String,Charset>  map = Charset.availableCharsets();
        for (String alias : map.keySet())
        {
            // 輸出字符集的別名和對應的Charset對象
            System.out.println(alias + "----->"
                + map.get(alias));
        }
    }
}

知道了字符集別名,能夠調用Charset的forName方法建立對應的Charset對象,forName方法的參數是字符集別名。

得到了Charset對象以後,能夠得到Charset的編碼器和解碼器,而後能夠實現字節序列和字符序列的轉換。

public class CharsetTransform
{
    public static void main(String[] args)
        throws Exception
    {
        // 建立簡體中文對應的Charset
        Charset cn = Charset.forName("GBK");
        // 獲取cn對象對應的編碼器和解碼器
        CharsetEncoder cnEncoder = cn.newEncoder();
        CharsetDecoder cnDecoder = cn.newDecoder();
        // 建立一個CharBuffer對象
        CharBuffer cbuff = CharBuffer.allocate(8);
        cbuff.put('孫');
        cbuff.put('悟');
        cbuff.put('空');
        cbuff.flip();
        // 將CharBuffer中的字符序列轉換成字節序列
        ByteBuffer bbuff = cnEncoder.encode(cbuff);
        // 循環訪問ByteBuffer中的每一個字節
        for (int i = 0; i < bbuff.capacity() ; i++)
        {
            System.out.print(bbuff.get(i) + " ");
        }
        // 將ByteBuffer的數據解碼成字符序列
        System.out.println("\n" + cnDecoder.decode(bbuff));
    }
}

Charset自己也提供了編碼解碼方法,若是僅需編碼解碼操做,能夠直接使用,沒必要建立編碼器和解碼器對象。

String的getBytes方法也是使用指定字符集將字符串轉換成字節序列。

文件鎖

若是多個程序須要併發修改同一個文件,程序須要某種機制來進行通訊,使用文件鎖能夠有效地阻止多個進程併發修改同一個文件。

文件鎖控制文件的所有或部分字節的訪問。在NIO中Java提供FileLock來支持文件鎖定功能。

lock():對文件從position開始,長度爲size的內容加鎖,阻塞。

tryLock():非阻塞。

當穿的shared參數是true時,表示這是共享鎖,容許多個進程讀取文件,但阻止其餘進程得到對該文件的排它鎖。

直接使用lock() tryLock()獲取文件鎖就是排它鎖。

public class FileLockTest
{
    public static void main(String[] args)
        throws Exception
    {

        try(
            // 使用FileOutputStream獲取FileChannel
            FileChannel channel = new FileOutputStream("a.txt")
                .getChannel())
        {
            // 使用非阻塞式方式對指定文件加鎖
            FileLock lock = channel.tryLock();
            // 程序暫停10s
            Thread.sleep(10000);
            // 釋放鎖
            lock.release();
        }
    }
}

NIO.2

Java7 NIO.2對NIO進行了重大改進,主要包括:

提供了全文見IO和文件系統訪問支持。

基於異步的Channel的IO。

Path、Paths、Files

Path接口表明和平臺無關的平臺路徑。

public class PathTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 以當前路徑來建立Path對象
        Path path = Paths.get(".");
        System.out.println("path裏包含的路徑數量:"
            + path.getNameCount());
        System.out.println("path的根路徑:" + path.getRoot());
        // 獲取path對應的絕對路徑。
        Path absolutePath = path.toAbsolutePath();
        System.out.println(absolutePath);
        // 獲取絕對路徑的根路徑
        System.out.println("absolutePath的根路徑:"
            + absolutePath.getRoot());
        // 獲取絕對路徑所包含的路徑數量
        System.out.println("absolutePath裏包含的路徑數量:"
            + absolutePath.getNameCount());
        System.out.println(absolutePath.getName(3));
        // 以多個String來構建Path對象
        Path path2 = Paths.get("g:" , "publish" , "codes");
        System.out.println(path2);
    }
}

Files是一個操做文件的工具類:

public class FilesTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 複製文件
        Files.copy(Paths.get("FilesTest.java")
            , new FileOutputStream("a.txt"));
        // 判斷FilesTest.java文件是否爲隱藏文件
        System.out.println("FilesTest.java是否爲隱藏文件:"
            + Files.isHidden(Paths.get("FilesTest.java")));
        // 一次性讀取FilesTest.java文件的全部行
        List<String> lines = Files.readAllLines(Paths
            .get("FilesTest.java"), Charset.forName("gbk"));
        System.out.println(lines);
        // 判斷指定文件的大小
        System.out.println("FilesTest.java的大小爲:"
            + Files.size(Paths.get("FilesTest.java")));
        List<String> poem = new ArrayList<>();
        poem.add("水晶潭底銀魚躍");
        poem.add("清徐風中碧竿橫");
        // 直接將多個字符串內容寫入指定文件中
        Files.write(Paths.get("pome.txt") , poem
            , Charset.forName("gbk"));
        // 使用Java 8新增的Stream API列出當前目錄下全部文件和子目錄
        Files.list(Paths.get(".")).forEach(path -> System.out.println(path));
        // 使用Java 8新增的Stream API讀取文件內容
        Files.lines(Paths.get("FilesTest.java") , Charset.forName("gbk"))
            .forEach(line -> System.out.println(line));
        FileStore cStore = Files.getFileStore(Paths.get("C:"));
        // 判斷C盤的總空間,可用空間
        System.out.println("C:共有空間:" + cStore.getTotalSpace());
        System.out.println("C:可用空間:" + cStore.getUsableSpace());
    }
}

使用FileVisitor遍歷文件和目錄:

FileVisitor表明一個文件訪問器,

public class FileVisitorTest {

    public static void main(String[] args)
            throws Exception {
        // 遍歷g:\publish\codes\15目錄下的全部文件和子目錄
        Files.walkFileTree(Paths.get("D:", "idea_project", "mybootbill" /*, "15"*/)
                , new SimpleFileVisitor<Path>() {
                    // 訪問文件時候觸發該方法
                    @Override
                    public FileVisitResult visitFile(Path file
                            , BasicFileAttributes attrs) throws IOException {
                        System.out.println("正在訪問" + file + "文件");
                        // 找到了FileInputStreamTest.java文件
                        if (file.endsWith("D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\NIO\\FileVisitorTest.java")) {
                            System.out.println("--已經找到目標文件--");
                            return FileVisitResult.TERMINATE;
                        }
                        return FileVisitResult.CONTINUE;
                    }

                    // 開始訪問目錄時觸發該方法
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir
                            , BasicFileAttributes attrs) throws IOException {
                        System.out.println("正在訪問:" + dir + " 路徑");
                        return FileVisitResult.CONTINUE;
                    }
                });
    }
}

使用WatchService監控文件變化

之前的Java中,須要監控文件變化,能夠考慮啓動一個後臺線程,每隔一段時間遍歷一次指定目錄,若是發現遍歷結果和上次不一樣則認爲文件發生了變化。

WatchService

public class WatchServiceTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 獲取文件系統的WatchService對象
        WatchService watchService = FileSystems.getDefault()
            .newWatchService();
        // 爲C:盤根路徑註冊監聽
        Paths.get("C:/").register(watchService
            , StandardWatchEventKinds.ENTRY_CREATE
            , StandardWatchEventKinds.ENTRY_MODIFY
            , StandardWatchEventKinds.ENTRY_DELETE);
        while(true)
        {
            // 獲取下一個文件改動事件
            WatchKey key = watchService.take();    //
            for (WatchEvent<?> event : key.pollEvents())
            {
                System.out.println(event.context() +" 文件發生了 "
                    + event.kind()+ "事件!");
            }
            // 重設WatchKey
            boolean valid = key.reset();
            // 若是重設失敗,退出監聽
            if (!valid)
            {
                break;
            }
        }
    }
}

啓動項目後,在C盤根目錄新建文件夾,而後刪除文件夾

訪問文件屬性

NIO.2中提供了大量工具類,能夠簡單的讀取修改文件屬性

 

public class AttributeViewTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 獲取將要操做的文件
        Path testPath = Paths.get("AttributeViewTest.java");
        // 獲取訪問基本屬性的BasicFileAttributeView
        BasicFileAttributeView basicView = Files.getFileAttributeView(
            testPath , BasicFileAttributeView.class);
        // 獲取訪問基本屬性的BasicFileAttributes
        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
        FileOwnerAttributeView ownerView = Files.getFileAttributeView(
            testPath, FileOwnerAttributeView.class);
        // 獲取該文件所屬的用戶
        System.out.println(ownerView.getOwner());
        // 獲取系統中guest對應的用戶
        UserPrincipal user = FileSystems.getDefault()
            .getUserPrincipalLookupService()
            .lookupPrincipalByName("guest");
        // 修改用戶
        ownerView.setOwner(user);
        // 獲取訪問自定義屬性的FileOwnerAttributeView
        UserDefinedFileAttributeView userView = Files.getFileAttributeView(
            testPath, UserDefinedFileAttributeView.class);
        List<String> attrNames = userView.list();
        // 遍歷全部的自定義屬性
        for (String name : attrNames)
        {
            ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
            userView.read(name, buf);
            buf.flip();
            String value = Charset.defaultCharset().decode(buf).toString();
            System.out.println(name + "--->" + value) ;
        }
        // 添加一個自定義屬性
        userView.write("發行者", Charset.defaultCharset()
            .encode("瘋狂Java聯盟"));
        // 獲取訪問DOS屬性的DosFileAttributeView
        DosFileAttributeView dosView = Files.getFileAttributeView(testPath
            , DosFileAttributeView.class);
        // 將文件設置隱藏、只讀
        dosView.setHidden(true);
        dosView.setReadOnly(true);
    }
}
相關文章
相關標籤/搜索