Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,当JVM停止运行时,这些对象就不复存在,但在实际应用中,我们需要持久化的保存指定的对象,并在特定的时间重新读取被保存的对象,Java对象序列化就能够帮助我们实现该功能。
在进行Java对象序列化的时候,会把其状态保存为一组字节,在对这些对象反序列化的时候再将这些字节组装成对象。但值得注意的事,对象序列化保存的是对象的”状态“,即它的成员变量,因此类中的静态变量不会被序列化。
除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化,下面我们分别通过文件和MySQL理解Java对象的序列化。
首先定义需要被序列化的类,通过实现Serializable接口:
class Student implements Serializable{
private static final long serialVersionUID = 1L;
//多种类型测试
private int id;
private String name;
private Address address;
private ArrayList<String> course;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public ArrayList<String> getCourse() {
return course;
}
public void setCourse(ArrayList<String> course) {
this.course = course;
}
}
//被引用的类也需要实现Serializable接口,否则会报错
class Address implements Serializable{
private static final long serialVersionUID = 1L;
private String street;
private String road;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getRoad() {
return road;
}
public void setRoad(String road) {
this.road = road;
}
}
接下来,先测试针对MySQL的对象序列化的测试:
先建立数据库表,字段对应为“blog”:
mysql> create table student(
-> id int auto_increment primary key, -> stu blob -> );
接下来,写测试用例:
public static void main(String[] args) throws IOException{
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String pass = "root";
Student student = new Student();
student.setId(2);
student.setName("gaoya");
Address address = new Address();
address.setStreet("aa");
address.setRoad("bb");
student.setAddress(address);
ArrayList<String> course = new ArrayList<String>();
course.add("math");
course.add("english");
student.setCourse(course);
try {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, pass);
String sql = "insert into student(stu) values(?)";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setObject(1, student);
pre.executeUpdate();
sql = "select stu from student";
pre = conn.prepareStatement(sql);
ResultSet rs = pre.executeQuery();
while(rs.next()){
Blob blob = rs.getBlob(1);
InputStream is = blob.getBinaryStream();
BufferedInputStream buff = new BufferedInputStream(is);
byte[] b = new byte[(int)blob.length()];
while(buff.read(b) != -1);
ObjectInputStream obj = new ObjectInputStream(new ByteArrayInputStream(b));
Student stu = (Student)obj.readObject();
System.out.println(stu.getName());
System.out.println(stu.getId());
System.out.println(stu.getAddress().getRoad() + " " + stu.getAddress().getStreet());
System.out.println(stu.getCourse());
;
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
再来针对文件的对象序列化
public static void main(String[] args){
Student student = new Student();
student.setId(2);
student.setName("gaoya");
Address address = new Address();
address.setStreet("aa");
address.setRoad("bb");
student.setAddress(address);
ArrayList<String> course = new ArrayList<String>();
course.add("math");
course.add("english");
student.setCourse(course);
try {
//将对象序列化到文件
File file = new File("d:\\out.txt");//输出目录
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(student);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Student stu = (Student)in.readObject();
System.out.println(stu.getId());
System.out.println(stu.getName());
System.out.println(stu.getAddress().getRoad() + " " + stu.getAddress().getStreet());
System.out.println(stu.getCourse());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
再提一下transient关键字,当某个字段被声明为transient后,默认序列化机制就会忽略该字段。此处将Student类中的id字段声明为transient:
private transient String name;
再运行得到的结果如下:
可见name字段未被序列化;如想再恢复该字段的序列化能力,除了删除transient关键字以外,还有另外一种方式:即在Student类中加两个方法,如下所示:
//需将这两个方法定义为private,序列化的时候会自动被调用
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();//执行默认化序列化机制
out.writeUTF(name);//将name字段写入ObjectOutputStream中
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();//执行默认反序列化机制
name = in.readUTF();//将name字段读入ObjectInputStream中
}
直接在此运行得到结果如图:
还有另一种序列化接口-Externalizable
将之前两个类修改如下:
class Student implements Externalizable{
private static final long serialVersionUID = 1L;
//多种类型测试
private int id;
private transient String name;
private Address address;
private ArrayList<String> course;
public Student(){
System.out.println("student");
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public ArrayList<String> getCourse() {
return course;
}
public void setCourse(ArrayList<String> course) {
this.course = course;
}
private void writeObject(ObjectOutputStream out) throws IOException{
out.defaultWriteObject();//执行默认化序列化机制
out.writeUTF(name);//将name字段写入ObjectOutputStream中
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();
name = in.readUTF();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
}
}
class Address implements Externalizable{
private static final long serialVersionUID = 1L;
private String street;
private String road;
public Address(){
System.out.println("address");
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getRoad() {
return road;
}
public void setRoad(String road) {
this.road = road;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
}
}
此时在此运行程序,得到结果如下:
从该结果可看出对象中任何一个字段都没有被序列化,但是可以看出调用了类的无参构造器。Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由通过实现writeExternal()与readExternal()方法。另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中类的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
对上述类作进一步的修改,使其能够对name与age字段进行序列化:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeInt(id);
out.writeObject(name);
out.writeObject(address);
out.writeObject(course);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
id = in.readInt();
name = (String)in.readObject();
address = (Address) in.readObject();
course = (ArrayList<String>) in.readObject();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
out.writeObject(street);
out.writeObject(road);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
street = (String) in.readObject();
road = (String) in.readObject();
}
在运行得到如下结果:
最后说一下serialVersionUID的作用:
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,如不同,就会出现序列化版本不一致的异常,如下图:
serialVersionUID有两种生成方式:一个是指定一个默认值,比如:private static final long serialVersionUID = 1L; 另一个是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = 8107332608515657739L。
在实现序列化的过程当中,如果我们没有显示的指定serialVersionUID,Java序列化机制会根据编译的class自动生成一个serialVersionUID(不同的编译器对于同一个类可能会产生不同的serialVersionUID),如果我们在序列化期间没有对类进行修改,那么无论编译多少次serialVersionUID都不会变,也就是说反序列化的时候不会出现问题,但是我们一旦修改了类的某一部分,反序列化就会出现如上所说的版本不一致的异常。
所以需要序列化的时候,我们最好显示的指定serialVersionUID(但要注意的是,如果使用第一种方式即指定默认值,需要对不同的类指定程不同的值,否则可能会出现把不同项目的相同类名的类当作一样的类去处理),这样当序列化一个类实例的时候,如添加修改删除一个字段,反序列化的时候对于添加和修改的字段会设定对于其类型的默认值,而删除的字段将不设置。