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位的哈希字段)。这样,某个对象被序列化之后,即使它所对应的类被修改了,该对象也依然可以被正确地反序列化。

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