IO流:物件流、序列化、隨機讀寫流與NIO
尚硅谷JavaSE筆記-27

標準輸出/入流

  • System.in:標準輸入字節流,預設是從鍵盤輸入,類型是InputStream
  • System.out:標準輸出流,預設輸出到控制台,類型是PrintStream,繼承自FilterOutputStream,繼承自OutputStream
重定向方法
  • public static void setIn(InputStream in)
  • public static void setOut(PrintStream out)
範例-讀取輸入

不使用scanner

public static void main(String[] args) {
    // 把標準輸入字節流先轉換成字符流
    BufferedReader bufferedReader = null;
    try {
        InputStreamReader isr = new InputStreamReader(System.in);
        // 把字符流用bufferedReader包起來,為了調用readline方法
        bufferedReader = new BufferedReader(isr);
        for (; true; ) {
            System.out.println("輸入要轉換成大寫的字串...");
            String str = bufferedReader.readLine();
            if ("exit".equalsIgnoreCase(str)) { // "exit"放前面防空指針
                System.out.println("離開");
                break;
            }
            System.out.println(str.toUpperCase());
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Print流

只有輸出,只會輸出

不是特別重要,認識一下有這2個PrintStreamPrintWriter

範例-寫log
public static void main(String[] args) throws FileNotFoundException {
    // 造一個輸出到檔案的流
    FileOutputStream fileOutputStream = new FileOutputStream("abc.txt");
    // 造一個Print輸出流
    PrintStream printStream = new PrintStream(fileOutputStream, true);// true表示自動flush
    // 將系統標準輸出重定向到Print輸出流
    System.setOut(printStream);
    System.out.println("原本想印在控制台,現在印到檔案abc.txt裡了!");
}

數據流

用於讀取或寫出String或基本數據類型,只有字節流

  • DataOutputStream:將記憶體中的數據寫到文件檔案中,這個文件基本不能給人看,因為可能有奇奇怪怪比如stringintboolean

  • DataInputStream:將文件檔案中的數據讀取記憶體中,寫的時候類型怎寫,讀的時候也必須照順序讀,否則報異常

  • 套在對應的in/outputStream字節流的子類使用,舉例:

    public static void main(String[] args) throws Exception {
        DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
        dataOutputStream.writeUTF("張三");
        dataOutputStream.flush(); // 刷新,確實寫到文件中
        dataOutputStream.writeBoolean(true);
        dataOutputStream.flush();
        dataOutputStream.writeFloat(654321.56465421f);
        dataOutputStream.flush();
    
        DataInputStream dataInputStream = new DataInputStream(new FileInputStream("data.txt"));
        System.out.println(dataInputStream.readUTF());
        System.out.println(dataInputStream.readBoolean());
        System.out.println(dataInputStream.readFloat());
    }
    

物件流

序列化

  • 定義:將一個Java物件轉換為二進制檔案,用以通信交換或保存

  • 條件:

    • 物件的類滿足Serializable接口或Externalizable接口
    • 在類聲明版本標識號:private static final long serialVersionUID,用以作為反序列化時的版本依據,
      • 比如第一代person類號碼是001,物件"小明“存成一個dat檔案
      • 後來person類更新了,多加了性別屬性,物件”阿花-女“存成的dat檔案與當初小明的顯然格式不同了,所以必須更改版本標示號
      • 如果不顯式聲明標示號,每次編譯都會自動給一個,反序列化時可能報錯
      • private是因為serialVersionUID顯然不該也不能被繼承
    • 類中所有的屬性也都必須是可序列化的(基本數據類型天生可以,要注意的是引用其他類的情況)
  • 避免序列化:

    • static:表示該屬性或方法是屬於類本身物件被序列化當然不關我的事;有時候可能會迷惑某屬性用static修飾了一樣能寫能讀,實際上那是JVM從對應類中拿到的值而非反序列化而來
    • transient:暫時的,發音"tran-洗-ent”,顧名思義只存在記憶體中不實際寫出;注意這邊講的是Serializable接口,此接口特性是會自動序列化
      • 如果是透過實現Externalizable接口者,需要在writeExternal方法手動指定要序列化的變量,該方法會蓋過transient的修飾
  • 序列化是RMI,Remote Method Invocation(遠端方法呼叫),是允許執行在一個Java虛擬機器的物件呼叫執行在另一個Java虛擬機器上的物件的方法,駭客常用反序列化的過程做壞事,要注意安全漏洞

  • 反序列化:相反的過程,舉例:

    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.dat"));
    oos.writeObject(new String("天線寶寶說你好"));
    oos.flush();
    System.out.println("將物件序列化為檔案成功");
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.dat"));
    Object obj = ois.readObject();
    System.out.println("反序列化,還原成物件...");
    String str = (String) obj;
    System.out.println(str);
    

RandomAccessFile類

隨機讀寫流,“隨機"其實"任意"的意思

特性

  • 聲明在java.io包下,直接繼承於java.io.Object
  • 實現了DataInputDataOutput接口,雙頭龍
  • 支持隨機訪問,想讀寫哪邊就讀寫哪邊
  • 其物件包含了一個紀錄指針,用以標示當前讀寫位置
  • 可以自由移動指針位置,透過方法:
    • long getFilePointer():獲取當前指針位置
    • void seek(long pos):將指針移到pos

實例化

構造器:

public RandomAccessFile(File file, String mode)
public RandomAccessFile(String name, String mode)

mode:參數,決定訪問模式

  • r:只讀,如果檔案不存在就報異常
  • rw:讀寫,如果檔案不存在就創建;用write()方法如果檔案存在會對"內容"進行覆蓋,且預設從頭開始覆蓋
  • rwd:讀寫並同步更新內容
  • rws:讀寫並同步更新內容及元數據

範例:

// 實例化
RandomAccessFile rw = new RandomAccessFile(new File("hello.txt"), "rw");

// 將string轉為byte編碼寫入
rw.write("12345abcde".getBytes(StandardCharsets.UTF_8));

// 在末尾追加,相當於append
rw.seek(new File("hello.txt").length());
rw.write("zzz".getBytes(StandardCharsets.UTF_8));

範例-插入字串

public void test1() throws Exception {
    RandomAccessFile rw = new RandomAccessFile(new File("hello.txt"), "rw");
    rw.seek(5); // 將指針移到5
    // 造一個StringBuilder用以保存5以後的資料
    StringBuilder builder = new StringBuilder(Math.toIntExact(new File("hello.txt").length()));
    byte[] buf = new byte[20];
    int len;
    while ((len = rw.read(buf)) != -1) { // 將讀取的"後面的"資料寫進buf
        builder.append(new String(buf, 0, len)); // 將buf寫入builder
    }
    rw.seek(5); // 將指針移再移回5,因為read也會動指針
    rw.write("***INSERT***".getBytes(StandardCharsets.UTF_8));
    // 寫回後面的部分
    rw.write(builder.toString().getBytes(StandardCharsets.UTF_8));
	rw.close()
    // 用中文可能會亂碼,因為中文字是3個Byte,read到buf寫入builder的時候被切壞了

改良-避免亂碼

使用ByteArrayOutputStream

rw.seek(6); // 將指針移
// 解決亂碼,用ByteArrayOutputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[32];
int len;
while ((len = rw.read(buf)) != -1) { // 將讀取的"後面的"資料寫進buf
    baos.write(buf, 0, len);
}
rw.seek(6); // 將指針移再移回,因為read也會動指針
rw.write("中文中文".getBytes(StandardCharsets.UTF_8));
// 寫回後面的部分
rw.write(baos.toString().getBytes(StandardCharsets.UTF_8));
// 還要注意一開始seek的位置,一開始就切在要害(例如奇數位)也會導致小部分亂碼

NIO

JDK4引入了NIO,(Non-blocking I/O),非阻塞式,加入觀察與通知的機制

JDK7強力更新成NIO.2版本,之後說的NIO基本都是指這個

  • IO主要是流本身,NIO是以緩衝區、管道為導向,讀寫效率更高
  • 位於java.nio.channels.Channel,其下有兩套NIO:
    • 處理本地文件FileChannel
    • 處理網路:SocketChannelServerSocketChannelDatagramChannel

Path接口

  • 用以取代File
  • 實例化:
    • static Path get(String first, String … more):用於將多個字串串連成路徑
    • static Path get(URI uri):返回指定uri對應的Path路徑
Paths類

Paths是工具類,除了上面兩種get靜態方法,還有許多常用方法:

  • String toString():返回調用Path 物件的字串表示形式
  • boolean startsWith(String path):判斷是否以path路徑開始
  • boolean endsWith(String path):判斷是否以path路徑結束
  • boolean isAbsolute():判斷是否是絕對路徑
  • Path getParent():返回Path物件包含整個路徑,不包含Path 物件指定的檔路徑
  • Path getRoot():返回調用Path 物件的根路徑
  • Path getFileName():返回與調用Path 物件關聯的檔案名
  • int getNameCount():返回Path 根目錄後面元素的數量
  • Path getName(int idx):返回指定索引位置idx 的路徑名稱
  • Path toAbsolutePath():作為絕對路徑返回調用Path 物件
  • Path resolve(Path p):合併兩個路徑,返回合併後的路徑對應的Path物件
  • File toFile():將Path轉化為File類的物件
Files類

位於java.nio.file.Files,是操作文件或目錄的工具類,常用方法:

  • Path copy(Path src, Path dest, CopyOption … how):檔的複製
  • Path createDirectory(Path path, FileAttribute<?> … attr):創建一個目錄
  • Path createFile(Path path, FileAttribute<?> … arr):創建一個檔
  • void delete(Path path):刪除一個檔/目錄,如果不存在,執行報錯
  • void deleteIfExists(Path path):Path對應的檔/目錄如果存在,執行刪除
  • Path move(Path src, Path dest, CopyOption…how):將src移動到dest位置
  • long size(Path path):返回path指定文件的大小

用於判斷

  • boolean exists(Path path, LinkOption … opts):判斷檔是否存在
  • boolean isDirectory(Path path, LinkOption … opts):判斷是否是目錄
  • boolean isRegularFile(Path path, LinkOption … opts):判斷是否是檔
  • boolean isHidden(Path path):判斷是否是隱藏檔
  • boolean isReadable(Path path):判斷檔是否可讀
  • boolean isWritable(Path path):判斷檔是否可寫
  • boolean notExists(Path path, LinkOption … opts):判斷檔是否不存在

用於操作內容

  • SeekableByteChannel newByteChannel(Path path, OpenOption…how):獲取與指定檔的連接,how 指定打開方式
  • DirectoryStream<Path> newDirectoryStream(Path path):打開path 指定的目錄
  • InputStream newInputStream(Path path, OpenOption…how):獲取InputStreamj物件
  • OutputStream newOutputStream(Path path, OpenOption…how):獲取OutputStreamj物件

小結

  • 序列化:實現Serializable接口、定義serialVersionUID、確認類中的屬性也都是可以序列化的
  • RandomAccessFile:雙向、可於任意位置存取,構造時用mode參數決定類型,使用seek(index)移動指針、getFilePointer()找指針
  • NIO:用Path取代File,緩衝區跟管道取代IO

上次修改於 2021-12-12