Java基础——IO流(3)——转换流、序列化流、随机存取文件流

1.转换流

待补充

 

2.序列化流

2.1 概述

(1)什么是序列化和反序列化?

  • 序列化:把对象转化为字节序列的过程
    • Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据 对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
  • 反序列化:把字节序列恢复为对象的过程
    • 反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据 、对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。

图解:

图1:

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第1张图片

图2:

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第2张图片

  • 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘中,或通过网络将这种二进制流传输到另一个网络节点,当其它程序获取了这种二进制流,就可以恢复成原来的Java对象
  • Java中使用ObjectOutputStream进行序列化,使用ObjectInputStream进行反序列化,
  • 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原

(2)为什么要序列化?

  • 1.持久存储对象数据的时候时需要序列化。通常当我们要把对象的数据保存在文件或者数据库中时,需要序列化
  • 2.在网络传输传输对象的时候需要序列化
  • 3.序列化是RMI(Remote Method Invoke,即远程方法调用)过程的参数和返回值都必须实现的机制,而RMI是JavaEE的基础。因此序列化机制是JavaEE平台的基础

2.2 ObjectOutputStream类(序列化流)

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第3张图片

java.io.ObjectOutputStream 类作用:把对象以流的方式写入到文件中实现保存

构造方法:

  • 参数OutputStream out:字节输出流

特有的成员方法:

使用步骤:

  • 1.创建一个ObjectOutputStream对象,构造方法中传递字节输出流
  • 2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
  • 3.释放资源

示例代码:

package Serializable;

import java.io.Serializable;

public class Person implements Serializable {

    private String name;
    private int age;

    public Person(){

    }

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

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

public class ObjectOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        //1.创建一个ObajectOutputStream对象,构造方法中传递字节输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\IDea_Project\\Demo01\\person.txt"));
        //2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
        oos.writeObject(new Person("小黑",12));
        //3.释放资源
        oos.close();
    }
}

2.3 ObjectInputStream类(反序列化流)

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第4张图片

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

构造方法:

  • 参数InputStream in:字节输入流

特有的成员方法:

说明:

  • readObject方法声明抛出了ClassNotFoundException(class文件找不到异常)
  • 当不存在对象的class文件时抛出此异常
  • 反序列化的前提:
    • 1.类必须实现Serializable
    • 2.必须存在类对应的class文件

使用步骤:

  • 1.创建一个ObjectInputStream对象,构造方法中传递字节输出流
  • 2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
  • 3.释放资源
  • 4.使用读取出来的对象(打印)

示例代码:

package Serializable;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectInputStreamDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //1.创建一个ObjectInputStream对象,构造方法中传递字节输出流
        ObjectInputStream oos = new ObjectInputStream(new FileInputStream("F:\\IDea_Project\\Demo01\\person.txt"));
        //2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
        Person person = (Person)oos.readObject();
        //3.释放资源
        oos.close();
        //4.使用
        System.out.println(person);
        System.out.println(person.getName()+person.getAge());
    }
}

2.4 不能对static与transient关键字修饰的成员变量序列化

(1)static

  • static关键字修饰的成员变量不能被序列化,因为static变量从属于类,而序列化是针对对象序列化

当我们把上述示例代码中的age设置为static变量:

package Serializable;

import java.io.Serializable;

public class Person implements Serializable {

    private String name;
    public static int age;

    public Person(){

    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package Serializable;

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

public class ObjectOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        //1.创建一个ObajectOutputStream对象,构造方法中传递字节输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\IDea_Project\\Demo01\\person01.txt"));
        //2.使用ObjectOutputStream对象中的方法writeObject,把对象写入到文件中
        Person person =new Person("小黑",12);
        oos.writeObject(person);
        System.out.println("写入成功,姓名:"+person.getName()+"\t年龄:"+Person.age);
        //3.释放资源
        oos.close();
    }
}

package Serializable;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectInputStreamDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //1.创建一个ObjectInputStream对象,构造方法中传递字节输出流
        ObjectInputStream oos = new ObjectInputStream(new FileInputStream("F:\\IDea_Project\\Demo01\\person01.txt"));
        //2.使用ObjectInputStream对象中的方法readObject读取保存对象的文件
        Person person = (Person)oos.readObject();
        //3.释放资源
        oos.close();
        //4.使用
        System.out.println(person);
        System.out.println(person.getName()+Person.age);
    }
}

(2)transient

把age用transient来修饰

序列化:

反序列化:

总结:

  • transient对于序列化和static差不多,只是没有静态的含义,被transient修饰的成员变量从属于对象
  • static和transient修饰的成员变量,在经过序列化和反序列化后,读到的对象的该成员变量会被赋成默认值

2.5 InvalidClassException异常原理和解决方案

JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操

作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。 serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的,类的实例变量做了修改,serialVersionUID可能发生变化,故建议,显式声明

图解:

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第5张图片

  • 问题分析:
    • 每次修改类的定义,都会给class文件生成一个新的序列号
  • 解决方案:
    • 无论是否对类的定义进行修改,都不重新生成新的序列号
    • 给类手动添加一个序列号
    • 格式在Serializable接口规定:
      • 可序列化类可以通过声明名为“serialVersionUID”的字段(该字段必须是static final long修饰的)显式声明自己的serialVersionUID
      • 如:static final long serialVersionUID = 42L;   //定义为常量保证了不能改变

2.6 实现序列化注意事项

  • transient或static修饰的属性,不会被序列化
  • 实现Serializable接口的时候,一定要给这个serialVersionUID赋值
  • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是序列化的(默认情况下基本类型是可序列化的),当属性是对象时,对象也要实现序列化接口,而为了让某个类是可序列化的,该类必须实现如下两个接口之一,否则会抛出NotSerializableException异常
    • Serializable
      • ​​​​​​Serializable接口也叫标记型接口
        • 可以看到这个接口什么都没有,就起到标记的作用
      • 要进行序列化和反序列化的类必须是实现Serializable接口,就会给类添加一个标记
      • 当我们进行序列化和反序列化的时候,就会检测类上是否有这个标记
        • 有:就可以序列化和反序列化
        • 没有:就会抛出NotSerializableException异常
    • Externalizable

2.7  案例:序列化集合

需求:

  • 当我们想在文件中保存多个对象的时候,可以把多个对象存储到一个集合中,对集合序列化和反序列化

代码:

package Serializable;

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

public class ListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ArrayList list  = new ArrayList<>();
        list.add(new Person("大黑",19));
        list.add(new Person("小黑",12));
        list.add(new Person("大白",13));
        list.add(new Person("小白",8));

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("F:\\IDea_Project\\Demo01\\list.txt"));

        oos.writeObject(list);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\IDea_Project\\Demo01\\list.txt"));

        ArrayList list2 = (ArrayList)ois.readObject();

        for(Person person : list2){
            System.out.println(person);
        }

        oos.close();
        ois.close();
    }
}

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第6张图片

2.8 Externalizable

待补充

3.随机存取文件流

  • RandomAccessFile声明在java.io包下,但直接继承于java.lang.Object类,并且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也可以写 
  • RandomAccessFile类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件
    • 支持只访问文件的部分内容
    • 可以向已存在的文件后追加内容
  • RandomAccessFile对象包含一个记录指针,用以标示当前读写处的位置,RandomAccessFile类对象可以自由移动记录指针

构造函数:

Java基础——IO流(3)——转换流、序列化流、随机存取文件流_第7张图片

创建RandomAccessFile类实例需要指定一个mode参数,该参数指定RandomAccessFile的访问模式:

  • r:以只读方式打开
  • rw:打开以便读取和写入
  • rwd:打开以便读取和写入;同步文件内容的更新
  • rws:打开以便读取和写入;同步文件内容和元数据的更新

如果模式为只读r,则不会创建文件,而是会读取一个已经存在的文件,如果读取的文件不存在则会出现异常,

如果模式为rw读写,如果文件不存在则会去创建文件,如果存在则不会创建文件,会对原有文件内容从头覆盖,能覆盖多少算多少

示例代码1:

package RandomAccessFile;

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

public class RandomAccessFileTest {
    public static void main(String[] args) {
        RandomAccessFile raf1 = null;
        RandomAccessFile raf2 = null;

        try{
            raf1 = new RandomAccessFile(new File("list.txt"),"r");
            raf2 = new RandomAccessFile(new File("list1.txt"),"rw");

            byte[] buffer = new byte[1024];
            int len;
            while((len = raf1.read(buffer)) != -1){
                raf2.write(buffer,0,len);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(raf1 != null){
                try {
                    raf1.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if(raf2 != null){
                try {
                    raf2.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

示例代码2:

package RandomAccessFile;

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

public class RandomAccessFileTest {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(new File("list1.txt"),"rw");

        raf.write("abcdefgh".getBytes());
        raf.close();
    }
}

  • 文件存在则不会创建新的文件,对原有文件内容从头覆盖,覆盖成了“abcdefgh”

示例代码3:

从第4个字符后开始覆盖

package RandomAccessFile;

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

public class RandomAccessFileTest {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(new File("list1.txt"),"rw");
        //将指针移动到第4个位置
        raf.seek(3);
        raf.write("xyz".getBytes());
        raf.close();
    }
}

示例4:

使用RandomAccessFile实现插入的效果

方式1:

package RandomAccessFile;

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

public class RandomAccessFileTest {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(new File("list1.txt"),"rw");
        //将指针移动到第4个位置
        raf.seek(3);

        //保存指针指向的第4个位置及以后的数据
        
        //此处如果使用String,我们每次都会在它上面追加,创建新的String,效率差,而StringBuilder底层是数组,直接一次性创建好,效率高一点
        //创建一个StringBuilder保存位置后的所有数据,传入文件的长度,提高了效率,不用再次扩容
        StringBuilder stringBuilder = new StringBuilder((int) new File("list1.txt").length());
        byte[] buffer = new byte[20];
        int len;
        while ((len = raf.read(buffer))!=-1){
            stringBuilder.append(new String(buffer,0,len));
        }
        //读取完以后,指针就到了最后一个位置,调回指针
        raf.seek(3);
        raf.write("uvw".getBytes());

        //写完之后指针会跟着移动到写入的字符序列的下一个位置
        //将stringBuilder写入到文件中
        raf.write(stringBuilder.toString().getBytes());

        raf.close();
    }
}

  • 从上可以看出插入的效率比较低,所以一般尽量做追加,减少插入

方式2:

package RandomAccessFile;

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

public class RandomAccessFileTest {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(new File("list1.txt"),"rw");
        //将指针移动到第4个位置
        raf.seek(3);

        //保存指针指向的第4个位置及以后的数据

        //创建一个ByteArrayOutputStream保存指针后的数据
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[20];
        int len;
        while ((len = raf.read(buffer))!=-1){
            baos.write(buffer,0,len);
        }
        //读取完以后,指针就到了最后一个位置,调回指针
        raf.seek(3);
        raf.write("uvw".getBytes());

        //写完之后指针会跟着移动到写入的字符序列的下一个位置
        //将stringBuilder写入到文件中
        raf.write(baos.toString().getBytes());


        raf.close();
    }
}

RandomAccessFile与其他流的对比:

  • 其他流要么是追加,要么是覆盖整个文件,而RandomAccessFile可以通过seek让指针指向一个位置,在该位置处开始做修改

应用举例:多线程断点下载

  • 我们下载一个文件的时候,一般被切分为几个部分,每个部分被用一个线程去下载,多线程保证下载速度,对于断点下载,用过下载工具就知道,下载前都会建立连个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上次的地方下载,从而实现断点下载或上传的功能

你可能感兴趣的:(#,Java基础)