Java講課筆記27:RandomAccessFile與對象序列化

零、本講學習目標

1、掌握RandomAccessFile類的基本使用

2、熟悉Java中的對象序列化

一、RandomAccessFile

(一)RandomAccessFile概述

在IO包中,提供了一個RandomAccesseFile類,它不屬於流類,但具有讀寫文件數據的功能,可以隨機從文件的任何位置開始並以指定的操作權限(如只讀、可讀寫等)執行讀寫數據的操作。

(二)RandomAccessFile構造方法

方法聲明 功能描述
RandomAccessFile(File file, String mode) 使用參數file指定被訪問的文件,並使用mode來指定訪問模式
RandomAccessFile(String name, String mode) 使用參數name指定被訪問文件的路徑,並使用mode來指定訪問模式

參數mode用於指定訪問文件的模式,也就是文件的操作權限。

參數mode 文件操作權限
r 以只讀的方式打開文件。如果執行寫操作,會報IOException異常。
rw 以“讀寫”的方式打開文件。如果文件不存在,會自動創建該文件。
rws 以“讀寫”方式打開文件。與“rw”相比,它要求對文件的內容或元數據的每個更新都同步寫入到底層的存儲設備。
rwd 以“讀寫”方式打開文件。與“rw”相比,它要求對文件的內容的每個更新都同步寫入到底層的存儲設備。

(三)RandomAccessFile操作原理

RandomAccessFile對象包含了一個“記錄指針”來標識當前讀寫處的位置。

  • 當新建RandomAccessFile對象時,該對象的文件記錄指針會在文件開始處(即標識爲0的位置);
  • 當讀寫了n個字節後,文件記錄指針會向後移動n個字節。
  • 除了按順序讀寫外,RandomAccessFile對象還可以自由的移動記錄指針,既可以向前移動,也可以向後移動。

(四)RandomAccessFile常用方法

方法聲明 功能描述
long getFilePointer() 返回當前讀寫指針所處的位置
void seek(long pos) 設定讀寫指針的位置,與文件開頭相隔pos個字節數
int skipBytes(int n) 使讀寫指針從當前位置開始,跳過n個字節
void write(byte[] b) 將指定的字節數組寫入到這個文件,並從當前文件指針開始
void setLength(long newLength) 設置此文件的長度
final String readLine() 從指定文件當前指針讀取下一行內容
  • 說明:seek(long pos)方法可以使RandomAccessFile對象中的記錄指針向前、向後自由移動,通過getFilePointer()方法,便可獲取文件當前記錄指針的位置。

(五)案例演示

1、利用隨機存取文件類讀寫文件

  • 創建Example2701
    在這裏插入圖片描述
package net.hw.lesson27;

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

/**
 * 功能:利用隨機存取文件類讀寫文件
 * 作者:華衛
 * 日期:2020年05月28日
 */
public class Example2701 {
    public static void main(String[] args) throws IOException {
        // 創建file對象(邏輯)
        File file = new File("src/net/hw/lesson27/rain.txt");
        // 創建file對象指向的文件(物理)
        file.createNewFile();
        // 創建一個可讀也可寫的隨機存取文件對象
        RandomAccessFile raf = new RandomAccessFile(file, "rw");

        /* 利用隨機存取文件對象寫文件 */
        byte[] b = {'I', 't', ' ', 'i', 's', ' ', 'r', 'a', 'i', 'n', 'i', 'n', 'g', '.', '\n'};
        raf.write(b);
        raf.write("Let's enjoy walking in the rain.\n".getBytes());
        raf.writeUTF("We have a good time today.\n");

        /* 利用隨機存取文件對象讀文件 */
        // 把指針移到文件頭
        raf.seek(0);
        // 利用行讀取方法讀取文件內容
        System.out.println(raf.readLine());
        System.out.println(raf.readLine());
        System.out.println(raf.readLine());

        System.out.println();

        // 把指針移到文件頭
        raf.seek(0);
        // 利用循環讀取文件內容
        for (int i = 0; i < raf.length(); i++) {
            System.out.print((char) raf.read());
        }

        System.out.println();

        raf.seek(0);
        int len = 0;
        while ((len = raf.read()) != -1) {
            System.out.print((char) len);
        }
    }
}
  • 運行程序,查看結果
    在這裏插入圖片描述
  • 說明:利用隨機存取文件對象的writeUTF()方法寫入字符串,在最開頭會有一個亂碼。如果將字符串轉化成字節數組寫入,就沒有任何問題。
  • 查看生成的輸出文件rain.txt
    在這裏插入圖片描述

2、利用隨機存取文件實現文件拷貝

  • 將圖片文件拷貝到當前包裏
    在這裏插入圖片描述
  • 創建Example2702
    在這裏插入圖片描述
package net.hw.lesson27;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 功能:利用隨機存取文件實現文件拷貝
 * 作者:華衛
 * 日期:2020年05月28日
 */
public class Example2702 {
    public static void main(String[] args) throws IOException {
        // 創建隨機存取文件對象
        RandomAccessFile raf1 = new RandomAccessFile("src/net/hw/lesson27/source.png", "r");
        RandomAccessFile raf2 = new RandomAccessFile("src/net/hw/lesson27/target.png", "rw");

        // 獲取開始拷貝文件的系統時間
        long startTime = System.currentTimeMillis();

        // 定義字節數組作爲緩衝區
        byte[] buffer = new byte[1024];
        // 讀取源文件,寫入目標文件
        int len = 0;
        while ((len = raf1.read(buffer)) != -1) {
            raf2.write(buffer, 0, len);
        }

        // 獲取結束拷貝文件的系統時間
        long endTime = System.currentTimeMillis();

        // 輸出拷貝文件耗費時間
        System.out.println("拷貝文件耗費時間:" + (endTime - startTime) + "毫秒");
        // 輸出拷貝文件字節數
        System.out.println("文件拷貝完畢,共拷貝" + raf1.length() + "字節。");
    }
}
  • 運行程序,查看結果
    在這裏插入圖片描述
  • 可以對比一下其它四種方式拷貝文件的耗時
序號 拷貝方式 耗時
1 採用文件字節流,不採用字節流的緩衝區 62296毫秒
2 採用文件字節流,採用字節流的緩衝區 100毫秒
3 採用緩衝字節流,不採用字節流的緩衝區 359毫秒
4 採用緩衝字節流,採用字節流的緩衝區 57毫秒

——參看《Java案例:幾種方式拷貝文件的耗時比較

二、對象序列化

(一)對象序列化概述

1、對象序列化作用

程序在運行過程中,可能需要將一些數據永久的保存到磁盤上,而數據在Java中都是保存在對象當中的。那麼我們要怎樣將對象中的數據保存到磁盤上呢?這時就需要使用Java中的對象序列化。

2、對象序列化定義

對象的序列化(Serialization)是指將一個Java對象轉換成一個I/O流中字節序列的過程。目的是爲了將對象保存到磁盤中,或允許在網絡中直接傳輸對象。

3、對象序列化說明

  • 對象序列化可以使內存中的Java對象轉換成與平臺無關的二進制流
  • 既可以將這種二進制流持久地保存在磁盤上,又可以通過網絡將這種二進制流傳輸到另一個網絡節點
  • 其他程序在獲得了這種二進制流後,還可以將它恢復成原來的Java對象
  • 將I/O流中的字節序列恢復爲Java對象的過程被稱之爲反序列化(Deserialization)

4、實現對象序列化的兩種方法

(1)實現Serializable接口

  • 系統自動存儲必要信息
  • Java內部支持,易實現,只需實現該接口即可,不需要其他代碼支持
  • 性能較差
  • 容易實現,實際開發使用較多

(2)實現Externalizable接口

  • 由程序員決定存儲的信息
  • 接口中提供了兩個空方法,實現該接口必須爲兩個方法提供實現
  • 性能較好
  • 編程複雜度大

(二)對象序列化案例演示

1、創建Student類,實現Serializable接口

在這裏插入圖片描述

package net.hw.lesson27;

import java.io.Serializable;

/**
 * 功能:學生類,實現序列化接口
 * 作者:華衛
 * 日期:2020年05月28日
 */
public class Student implements Serializable {
    private String name;
    private int age;

    public Student() {
    }

    public Student(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 "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2、創建TestStudent類

在這裏插入圖片描述

3、創建testWrite()方法,測試序列化

  • 使用了單元測試包JUnit4的註解符@Test,要將JUnit4添加到類路徑
    在這裏插入圖片描述
package net.hw.lesson27;

import org.junit.Test;

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

/**
 * 功能:測試學生類
 * 作者:華衛
 * 日期:2020年05月28日
 */
public class TestStudent {
    /**
     * 序列化過程
     */
    @Test
    public void testWrite() throws Exception {
        // 創建學生對象
        Student student = new Student("howard", 18);
        // 創建文件輸出流
        FileOutputStream fos = new FileOutputStream("src/net/hw/lesson27/test1.txt");
        // 創建對象輸出流
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        // 寫入學生對象數據
        oos.writeObject(student);
        // 關閉輸出流
        fos.close();
        oos.close();
    }
}
  • 運行testWrite()方法,查看結果
    在這裏插入圖片描述
  • 查看生成的輸出文件test1.txt
    在這裏插入圖片描述
  • 說明:本來寫入的對象只包含兩項數據:howard,18,可以看到序列化後的數據明顯增多了,這是Java原生序列化的一個侷限性。還有一點,看到的是亂碼。
  • 修改Student類,不實現Serializable接口在這裏插入圖片描述
  • 運行TestStudent類的testWrite()方法就會拋出異常
    在這裏插入圖片描述
  • 修改Student類,實現Serializable接口
    在這裏插入圖片描述
  • 序列化id: 每個對象序列化時都會生成一個序列化id,假如不手動設置,那麼會自動根據當前這個類生成一個序列化id。當反序列化時,要根據序列化id來操作,如果序列化id和反序列化id不同,那麼反序列化就會失敗。

4、編寫testRead()方法,測試反序列化

在這裏插入圖片描述

@Test                                                                                 
public void testRead() throws Exception {                                             
    // 創建文件輸入流                                                                        
    FileInputStream fis = new FileInputStream("src/net/hw/lesson27/test1.txt");       
    // 創建對象輸入流                                                                        
    ObjectInputStream ois = new ObjectInputStream(fis);                               
    // 讀取學生對象數據                                                                       
    Student student = (Student) ois.readObject();                                     
    // 輸出學生信息                                                                         
    System.out.println(student);                                                      
    // 關閉輸入流                                                                          
    fis.close();                                                                      
    ois.close();                                                                      
}                                                                                     
  • 運行testRead()方法,查看結果
    在這裏插入圖片描述
  • 確實將先前序列化保存在文件中的數據讀取出來,然後反序列化成Java對象輸出。
  • 修改Student類,增加一個字段gender,添加對應的getter和setter,修改toString()方法
package net.hw.lesson27;

import java.io.Serializable;

/**
 * 功能:學生類,實現序列化接口
 * 作者:華衛
 * 日期:2020年05月28日
 */
public class Student implements Serializable {
    private String name;
    private int age;
    private String gender;

    public Student() {
    }

    public Student(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;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                '}';
    }
}
  • 運行testRead()方法,會拋出異常
    在這裏插入圖片描述

  • 異常:java.io.InvalidClassException: net.hw.lesson27.Student; local class incompatible: stream classdesc serialVersionUID = 1647147692337296220, local class serialVersionUID = 6941659366870658235

  • 反序列化id:stream classdesc serialVersionUID = 1647147692337296220

  • 序列化id:local class serialVersionUID = 6941659366870658235

  • 用錯誤提示信息中的反序列化id(1647147692337296220)去給Student類設置序列化id
    在這裏插入圖片描述

  • 再次運行testRead()方法,查看結果
    在這裏插入圖片描述

5、serialVersionUID作用

serialVersionUID適用於Java的序列化機制。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就可以進行反序列化,否則就會出現異常。因此,爲了在反序列化時確保序列化版本的兼容性,最好在每一個要序列化的類中加入private static final long serialVersionUID的變量值,具體數值可自定義(默認是1L,系統還可以根據類名、接口名、成員方法及屬性等生成的一個64位的哈希字段)。這樣,某個對象被序列化之後,即使它所對應的類被修改了,該對象也依然可以被正確地反序列化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章