超硬核详细学习系列第十天——深入浅出IO的知识点,值得你学习

“这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!

愿你在未来的日子,保持热爱,奔赴山海!

I/O高级流

昨天我们对高级流中的打印流学习,而在IO流的整个大体系中,他还有一些高级流等待着我们来解锁。

所以话不多说,今天我们先来学习其中一种高级流——序列化。

序列化

在上面的字节流中,我们不是有看到两个流:ObjectOutputStreamObjectInputStream吗,这个流主要用在对象序列化中。那我们来了解下序列化:

像之前的操作都是跟文件有关直接写入的,那我们学习的是面向对象,那可以把对象存入到文件保存起来吗。而接下来要学习的序列化流就是可以将保存在内存中的对象数据转化为二进制数据流进行传输,任何对象都可以序列化。

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。 注意一点就是你可能读不懂这个文件。这个文件不是给我们读取的,是保存对象的信息的。而我们就可以用该字节序列从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的属性信息,都可以用来在内存中创建对象。

序列化流 ObjectOutputStream

把对象按照流一样的方式存入文本文件或者在网络中传输。实现对象的持久存储。

1. 构造方法

  • public ObjectOutputStream(OutputStream out) : 创建一个指定字节输出流OutputStream的序列化流ObjectOutputStream。传入的参数是OutputStream字节输出流。

    构造方法演示:

    public class ObjectStreamDemo {
        public static void main(String[] args) throws IOException {
            //public ObjectOutputStream(OutputStream out)
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\demo\\oos.txt")); 
        }
    }
    复制代码

2. 序列化操作

我们要如何实现对象的序列化操作呢?

我们要实现对象的序列化操作,不得先创建一个对象出来:

public class Person {
    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;
    }

    public String toString() {
        return "Person{name = " + name + ", age = " + age + "}";
    }
}
复制代码

还需要知道一个方法如何写入序列化的方法对吧:

public final void writeObject(Object obj) : 将指定的对象写出。
复制代码

同时一个对象想要实现序列化操作需要满足以下条件:

  1. 这个对象类必须实现java.io.Serializable 接口,Serializable 可以看到里面没有任何方法,那有什么用呢,一般我们称之为一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

    具体来看看如果不实现,会出现什么状况:

    public class ObjectStreamDemo {
        public static void main(String[] args) throws IOException {
            //public ObjectOutputStream(OutputStream out)
            // 创建序列化流对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:\\demo\\oos.txt"));
    
            // 创建对象
            Person p = new Person("测试机器人1号", 01);
    
            //public final void writeObject(Object obj)
            //将对象写出
            oos.writeObject(p);
    
            // 释放资源 
            oos.close();
        }
    }
    复制代码

    此时对象并没有实现Serializable 接口,所以程序运行后:、

    Exception in thread "main" java.io.NotSerializableException: com.it.test11.Person
    	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    	at com.it.test11.ObjectStreamDemo.main(ObjectStreamDemo.java:18)
    复制代码

    表示你没有实现Serializable 接口。所以我们现在来实现这个接口看看

    public class Person implements Serializable {
        .....
    }
    复制代码

    现在再来执行看看,运行正常,没有报异常,我们再来看看文件信息:

    这...能看的懂吗,看不懂没关系,但是有东西能看懂就行了,我们能把这个文件读取就行了。那我们再来看看怎样读取?

反序列化流 ObjectInputStream

把文本文件中的流对象数据或者网络中的流对象数据还原成对象。

1. 构造方法

  • public ObjectInputStream(InputStream in) : 创建一个指定字节输入流InputStream 的反序列化流ObjectInputStream 。传入的参数是InputStream 字节输入流。

    构造方法演示如下:

    public class ObjectStreamDemo2 {
        public static void main(String[] args) throws IOException {
            //public ObjectInputStream(InputStream in)
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\demo\\oos.txt"));
        
        }
    }
    复制代码

2. 反序列化操作

既然之前我们已经做了把对象序列化到文件中,那我们如何把文件信息反序列化为对象呢?

那我们先得知道什么方法可以读取:

public final Object readObject () : 读取一个对象。
复制代码

按照我们之前读取文件的操作方式,可以同理得到对象吗,一起来试试呗。说白了,如果不会,就先尝试,在尝试中,不断改错对吧,才能变得更好。

public class ObjectStreamDemo2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //public ObjectInputStream(InputStream in)
       // 创建反序列化对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:\\demo\\oos.txt"));

        // 还原对象
        Object obj = ois.readObject();

        // 释放资源
        ois.close();

        // 输出对象
        System.out.println(obj);
    }
}

程序执行结果:
Person{name = 测试机器人1号, age = 1}
复制代码

3. 反序列化操作可能出现的问题

当我们已经把对象序列化到文件中后,现在我们第一次读取没什么问题对吧,但是如果我再把Person 类进行修改。然后在读取,会发生什么问题?

public class Person implements Serializable {
    private String name;
    private int age;
    private String address;//新增一个属性
   .....
}
复制代码

再读取结果:

Exception in thread "main" java.io.InvalidClassException: com.it.test11.Person; local class incompatible: stream classdesc serialVersionUID = 1228968110604265735, local class serialVersionUID = 3463310223685915264
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1829)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
	at com.it.test11.ObjectStreamDemo2.main(ObjectStreamDemo2.java:14)
复制代码

可以看到你的控制台一堆飘红的异常信息了。发生了InvalidClassException异常,为什么会发生这个异常呢。具体原因有可能如下:

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

我们再看下异常的具体原因:

com.it.test11.Person; local class incompatible: stream classdesc serialVersionUID = 1228968110604265735, local class serialVersionUID = 3463310223685915264
复制代码

可以发现使我们对Person 类修改后,造成了序列的版本号不一致了。因为我们修改后的序列号没有再次保存在文件中,所以最新的序列号和文件序列号不一致,导致异常了。那我们如何解决这个问题呢,有两个方法:

  1. 重新在写入文件:在我们修改Person 类后,我们再进行一次写入文件,可以发现这样再次读取后,没有任何问题了。这个方法比较麻烦吧,每次修改都要重新写入文件,才能再次读取,是不是相对比较麻烦,接下来了教你一个一劳永逸的方法。

  2. 写一个固定的版本号:既然每次修改都会造成文件的序列版本号的改变,那我们如果能设置一个固定的序列化版本号就可以解决这一问题了对吧。那如何设置呢?

    //加入序列版本号
    private static final long serialVersionUID = 2071565876962023344L; 
    复制代码

    Person 类中第一行加入这个即可,那我们进行重新写入,在读取都没有问题了。

transient关键字

一个类中可能会有很多的成员变量,但是有些变量我不想进行序列化该怎么办呢?

只需要在变量名前加上一个关键字transient

  • transient :短暂,瞬态的。表明这个变量不想被序列化。

    现在Person 类我只想保存姓名的变量,年龄的变量我不想被序列化。

    private transient int age;
    复制代码

    现在我们在重新写入,并读取后:

    Person{name = 测试机器人1号, age = 0}
    复制代码

    可以看到,年龄没有再序列化到文件了。

集合序列化案列

案例:将存有多个自定义教师对象的集合序列化操作,保存到arrayList.txt 文件中。反序列化arrayList.txt,并遍历集合,打印对象信息。

我们要怎么实现这个案例呢:

  1. 创建若干教师对象 ,并保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的教师信息。

OK,既然知道这个过程,现在直接来实现吧:

创建Teacher类,并实现 Serializable 接口。

package com.it.test12;

import java.io.Serializable;

public class Teacher implements Serializable {
    private String name;
    private int age;


    public Teacher() {
    }

    public Teacher(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 toString() {
        return "Teacher{name = " + name + ", age = " + age + "}";
    }
}
复制代码

在进行序列化操作测试。

package com.it.test12;

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

public class ObjectStreamTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建教师对象
        Teacher t1 = new Teacher("王老师", 40);
        Teacher t2 = new Teacher("李老师", 48);
        Teacher t3 = new Teacher("张老师", 46);

        //添加到集合中。
        ArrayList<Teacher> arrayList = new ArrayList<>();
        arrayList.add(t1);
        arrayList.add(t2);
        arrayList.add(t3);

        //序列化操作
        serializ(arrayList);

        //先进行序列化操作,然后再注释掉序列化操作。再执行反序列化
//        serializRead();
    }

     //反序列化操作
    private static void serializRead() throws IOException, ClassNotFoundException {
        ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
        // 读取对象,强转为ArrayList类型
        ArrayList<Teacher> list  = (ArrayList<Teacher>)ois.readObject();

        for (int i = 0; i < list.size(); i++ ){
            Teacher s = list.get(i);
            System.out.println(s.getName()+"--"+ s.getAge());
        }
    }

    //序列化操作
    private static void serializ(ArrayList<Teacher> arrayList) throws IOException {
        // 创建 序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
        // 写出对象
        oos.writeObject(arrayList);
        // 释放资源
        oos.close();
    }
}
复制代码

程序运行后结果:

王老师--40
李老师--48
张老师--46
复制代码

总结

相信各位看官都对IO流中高级流中的序列化有了一定了解,期待等待下一章的高级流——操作基本数据的流教学吧!

当然还有很多流等着下次一起看吧!欢迎期待下一章的到来!

学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!

感谢各位看到这里!愿你韶华不负,青春无悔!

注: 如果文章有任何错误和建议,请各位大佬尽情留言!如果这篇文章对你也有所帮助,希望可爱亲切的您给个三连关注下,非常感谢啦!