文章目录
- 数据流与对象流说明
- 对象流API
- 认识对象序列化机制
- 如何实现序列化机制
- 反序列化失败问题
数据流与对象流说明
如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文件中,那怎么办呢?
int age = 300;
char gender = '男';
int energy = 5000;
double price = 75.5;
boolean relive = true;String name = "巫师";
Student stu = new Student("张三",23,89);
Java提供了数据流和对象流来处理这些类型的数据:
- 数据流:DataOutputStream、DataInputStream
-
DataOutputStream:允许应用程序将基本数据类型、String类型的变量写入输出流中
-
DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取基本数据类型、String类型的变量。
-
- 对象流DataInputStream中的方法:
byte readByte() short readShort()int readInt() long readLong()float readFloat() double readDouble()char readChar() boolean readBoolean() String readUTF() void readFully(byte[] b)
-
对象流DataOutputStream中的方法:将上述的方法的read改为相应的write即可。
-
数据流的弊端:只支持Java基本数据类型和字符串的读写,而不支持其它Java对象的类型。而ObjectOutputStream和ObjectInputStream既支持Java基本数据类型的数据读写,又支持Java对象的读写,所以重点介绍对象流ObjectOutputStream和ObjectInputStream。
-
对象流:ObjectOutputStream、ObjectInputStream
- ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。通过在流中使用文件可以实现Java各种基本数据类型的数据以及对象的持久存储。
- ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写出的基本数据类型的数据和对象进行读入操作,保存在内存中。
说明:对象流的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
对象流API
ObjectOutputStream中的构造器:
public ObjectOutputStream(OutputStream out)
: 创建一个指定的ObjectOutputStream。
FileOutputStream fos = new FileOutputStream("game.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
ObjectOutputStream中的方法:
- public void writeBoolean(boolean val):写出一个 boolean 值。
- public void writeByte(int val):写出一个8位字节
- public void writeShort(int val):写出一个16位的 short 值
- public void writeChar(int val):写出一个16位的 char 值
- public void writeInt(int val):写出一个32位的 int 值
- public void writeLong(long val):写出一个64位的 long 值
- public void writeFloat(float val):写出一个32位的 float 值。
- public void writeDouble(double val):写出一个64位的 double 值
- public void writeUTF(String str):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。根据字符的值,将字符串 s 中每个字符转换成一个字节、两个字节或三个字节的字节组。注意,将 String 作为基本数据写入流中与将它作为 Object 写入流中明显不同。 如果 s 为 null,则抛出 NullPointerException。
- `public void writeObject(Object obj)`:写出一个obj对象
- public void close() :关闭此输出流并释放与此流相关联的任何系统资源
ObjectInputStream中的构造器:
public ObjectInputStream(InputStream in)
: 创建一个指定的ObjectInputStream。
FileInputStream fis = new FileInputStream("game.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
ObjectInputStream中的方法:
- public boolean readBoolean():读取一个 boolean 值
- public byte readByte():读取一个 8 位的字节
- public short readShort():读取一个 16 位的 short 值
- public char readChar():读取一个 16 位的 char 值
- public int readInt():读取一个 32 位的 int 值
- public long readLong():读取一个 64 位的 long 值
- public float readFloat():读取一个 32 位的 float 值
- public double readDouble():读取一个 64 位的 double 值
- public String readUTF():读取 UTF-8 修改版格式的 String
- `public void readObject(Object obj)`:读入一个obj对象
- public void close() :关闭此输入流并释放与此流相关联的任何系统资源
认识对象序列化机制
1、何为对象序列化机制?
对象序列化机制
允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
-
序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该
对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存
了一个对象的信息。 -
反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行
反序列化
。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。
2、序列化机制的重要性
序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
序列化的好处,在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
3、实现原理
-
序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。方法为:
public final void writeObject (Object obj)
: 将指定的对象写出。
-
反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。方法为:
public final Object readObject ()
: 读取一个对象。
如何实现序列化机制
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现java.io.Serializable
接口。Serializable
是一个标记接口
,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。
- 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现
Serializable
接口 - 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰。 静态(static)变量
的值不会序列化。因为静态变量的值不属于某个对象。
举例1:
import org.junit.Test;import java.io.*;public class ReadWriteDataOfAnyType {@Testpublic void save() throws IOException {String name = "巫师";int age = 300;char gender = '男';int energy = 5000;double price = 75.5;boolean relive = true;ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("game.dat"));oos.writeUTF(name);oos.writeInt(age);oos.writeChar(gender);oos.writeInt(energy);oos.writeDouble(price);oos.writeBoolean(relive);oos.close();}@Testpublic void reload()throws IOException{ObjectInputStream ois = new ObjectInputStream(new FileInputStream("game.dat"));String name = ois.readUTF();int age = ois.readInt();char gender = ois.readChar();int energy = ois.readInt();double price = ois.readDouble();boolean relive = ois.readBoolean();System.out.println(name+"," + age + "," + gender + "," + energy + "," + price + "," + relive);ois.close();}
}
举例2:
import java.io.Serializable;public class Employee implements Serializable {//static final long serialVersionUID = 23234234234L;public static String company; //static修饰的类变量,不会被序列化public String name;public String address;public transient int age; // transient瞬态修饰成员,不会被序列化public Employee(String name, String address, int age) {this.name = name;this.address = address;this.age = age;}public static String getCompany() {return company;}public static void setCompany(String company) {Employee.company = company;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", address='" + address + '\'' +", age=" + age +", company=" + company +'}';}
}
import org.junit.Test;
import java.io.*;public class ReadWriteObject {@Testpublic void save() throws IOException {Employee.setCompany("小和尚");Employee e = new Employee("小姐姐", "美女", 23);// 创建序列化流对象ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.dat"));// 写出对象oos.writeObject(e);// 释放资源oos.close();System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。}@Testpublic void reload() throws IOException, ClassNotFoundException {// 创建反序列化流FileInputStream fis = new FileInputStream("employee.dat");ObjectInputStream ois = new ObjectInputStream(fis);// 读取一个对象Employee e = (Employee) ois.readObject();// 释放资源ois.close();fis.close();System.out.println(e);}
}
举例3:如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集合对象即可。
import org.junit.Test;import java.io.*;
import java.util.ArrayList;public class ReadWriteCollection {@Testpublic void save() throws IOException {ArrayList<Employee> list = new ArrayList<>();list.add(new Employee("张三", "宏福苑", 23));list.add(new Employee("李四", "白庙", 24));list.add(new Employee("王五", "平西府", 25));// 创建序列化流对象ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employees.dat"));// 写出对象oos.writeObject(list);// 释放资源oos.close();}@Testpublic void reload() throws IOException, ClassNotFoundException {// 创建反序列化流FileInputStream fis = new FileInputStream("employees.dat");ObjectInputStream ois = new ObjectInputStream(fis);// 读取一个对象ArrayList<Employee> list = (ArrayList<Employee>) ois.readObject();// 释放资源ois.close();fis.close();System.out.println(list);}
}
反序列化失败问题
问题1:
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException
异常。
问题2:
当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 该类包含未知数据类型
解决办法:
Serializable
接口给需要序列化的类,提供了一个序列版本号:serialVersionUID
。凡是实现 Serializable接口的类都应该有一个表示序列化版本标识符的静态变量:
static final long serialVersionUID = 234242343243L; //它的值由程序员随意指定即可。
- serialVersionUID用来表明类的不同版本间的兼容性。简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
- 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节
自动生成
的。若类的实例变量做了修改,serialVersionUID可能发生变化
。因此,建议显式声明。 - 如果声明了serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。
import java.io.Serializable;public class Employee implements Serializable {private static final long serialVersionUID = 1324234L; //增加serialVersionUID//其它结构:略
}