转载

浅谈Java序列化机制

简单的讲,序列化就是将java对象转化成二进制保存到磁盘中去,反序列化就是从磁盘中读取文件流然后转成java对象。

2、使用场景

  • 1、网络通讯传输java对象数据
  • 2、永久保存java对象

二、实现序列化的方式有哪些?

JDK提供了下面两种方式实现序列化:

Serializable
Externalizable

下面分别实例演示两种实现方式: 假设本文所有的序列化对象为 User ,其拥有下面属性:

/**
 * 序列化對象
 * 
 * @Author jiawei huang
 * @Since 2020年1月2日
 * @Version 1.0
 */
public class User {
    
    private String userName;
    
    private String address;
    // ....setter/getter
}
复制代码

1、基于Serializable接口

我们在上面 User 对象的基础上实现 Serializable 接口,代码如下:

public class User implements Serializable {
    // 序列化ID
    private static final long serialVersionUID = 1L;
复制代码

序列化和反序列化代码为:

// 序列化方法
public static void serialize(User user) {
    ObjectOutputStream outputStream = null;
    try {
    	outputStream = new ObjectOutputStream(new FileOutputStream("C://Users//Administrator//Desktop//data.txt"));
    	outputStream.writeObject(user);
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
        // close stream
        if (outputStream != null) {
        	try {
        		outputStream.close();
        	} catch (IOException e) {
        		e.printStackTrace();
        	}
        }
    }
}

// 反序列化方法
public static void deserialize() {
    ObjectInputStream objectInputStream = null;
    try {
    	objectInputStream = new ObjectInputStream(
    			new FileInputStream("C://Users//Administrator//Desktop//data.txt"));
    	User user = (User) objectInputStream.readObject();
    	System.out.println(user.getAddress());
    	System.out.println(user.getUserName());
    } catch (Exception e) {
    	// error
    	e.printStackTrace();
    } finally {
    	// close stream
    	if (objectInputStream != null) {
    		try {
    			objectInputStream.close();
    		} catch (IOException e) {
    			// error
    			e.printStackTrace();
    		}
    	}
    }
}
复制代码

测试代码如下:

public static void main(String[] args) {
    User user = new User();
    user.setAddress("广东深圳");
    user.setUserName("hjw");
    
    serialize(user);
    deserialize();
}
复制代码

输出如下:

广东深圳
hjw
复制代码
  • Q1、 User 实现 Serializable 接口是必须的吗?

是的,是必须的,否则报下面异常:

java.io.NotSerializableException: ex.serializable.User
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at ex.serializable.Main.serialize(Main.java:41)
    at ex.serializable.Main.main(Main.java:33)
复制代码

因为在 ObjectOutputStream 中执行了如下代码限制:

浅谈Java序列化机制

这也说明,jdk并没有默认支持对象的序列化,为什么默认不支持呢?因为java的安全机制限制,我们设想一下,假设对象都默认支持序列化,那么就像上面那个 User 对象,其私有 private 修饰属性也被序列化了,那么不符合 private 的设计语义

2、基于Externalizable接口

  • 1、 Externalizable 接口继承自 Serializable 接口
  • 2、 Externalizable 接口允许我们自定义对象属性的序列化
  • 3、实现 Externalizable 接口必须重写 writeExternalreadExternal 方法

我们重新新建一个 User1 对象如下:

public class User1 implements Externalizable {

	private String userName;

	private String address;

    // setter、getter
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeObject(userName);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		System.out.println(in.readObject());
	}

}

复制代码

测试代码如下:

User1 user1 = new User1();
user1.setAddress("广东深圳");
user1.setUserName("hjw");
user1.writeExternal(
	new ObjectOutputStream(new FileOutputStream("C://Users//Administrator//Desktop//data.txt")));
user1.readExternal(new ObjectInputStream(new FileInputStream("C://Users//Administrator//Desktop//data.txt")));
复制代码

输出如下:

hjw
复制代码

两种实现方式的区别在于 Externalizable 允许我们自定义序列化规则, Externalizable 接口会初始化一次无参构造器,而 Serializable 不会

三、transient关键字和static成员变量

通过实现 Externalizable 接口我们可以自定义序列化的属性,同样的道理,关键字 transient 也可以达到相同的效果,但是两者之间还是有一些区别的。

  • 1、 transient 修饰的变量,即使使用 private 修饰也会被序列化
  • 2、如果我们想 private 属性不被序列化,则可以使用 Externalizable

static 修饰的成员变量属于类全局属性,其值在JDK1.8存储于元数据区(1.8以前叫方法区),不属于对象范围,将不会被序列化。

下面验证 static 不会被序列化: 修改 User 对象 userName 属性为 static 修饰

User user = new User();
user.setAddress("广东深圳");
user.setUserName("hjw");
serialize(user);
user.setUserName("mike");
deserialize();
复制代码

输出如下:

广东深圳
mike
复制代码

我们将 hjw 序列化到了磁盘文件,结果反序列化之后得到的值却是 mike

四、关于serialVersionUID

java能否反序列化成功,取决于 serialVersionUID 是否一致,我们可以把它理解成为版本号,一旦版本号不一致,将会报序列化出错。我们可以为对象声明一个自定义 serialVersionUID ,也可以使用默认的 1L

总结就是:

当我们新增一个类实现 Serializable 接口时,建议我们为其新增一个 serialVersionUID ,因为假设我们没有声明 serialVersionUID ,那么后面假设我们修改了该类的接口(新增字段)时,当我们再次反序列化时,就会报错。因为java会拿编译器根据类信息自动生成一个id1和反序列化得到的id2进行比较,如果类有改动,id2和id1肯定不一致啦。

Q1:既然 static 修饰的不会被序列化,而java又是如何通过 serialVersionUID 进行比较的呢?

serialVersionUID 应该是一个特殊字段可以被序列化,应该可以从源码中找到答案,小编没找到,欢迎评论区留言。

五、序列化、反序列化实现深度克隆

深度克隆即把引用类型的成员也给克隆了,基于上面例子,我们新增一个类 Car ,并且 User 拥有一个 Car 类型对象。

public class Car implements Serializable {
	private static final long serialVersionUID = 1L;

	private String carName;

	private int price;

    // ......
    
}
复制代码
public class User implements Serializable {

	private static final long serialVersionUID = 1L;

	private String userName;
	//
	private Car car;
	private String address;
    @Override
    public String toString() {
        return "User [userName=" + userName + ", carName=[" + car.getCarName() + "],price=[" + car.getPrice() + "]"
        		+ ", address=" + address + "]";
    }
    // ......
}
复制代码

克隆方法

public static <T extends Serializable> T cloneObject(T obj) throws IOException {
    ObjectOutputStream oos = null;
    ByteArrayOutputStream baos = null;
    byte[] bytes = null;
    try {
    	// 序列化
    	baos = new ByteArrayOutputStream();
    	oos = new ObjectOutputStream(baos);
    	oos.writeObject(obj);
    	bytes = baos.toByteArray();
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	if (baos != null) {
    		baos.close();
    	}
    	if (oos != null) {
    		oos.close();
    	}
    }
    
    ByteArrayInputStream bais = null;
    ObjectInputStream ois = null;
    try {
    	// 反序列化
    	bais = new ByteArrayInputStream(bytes);
    	ois = new ObjectInputStream(bais);
    	return (T) ois.readObject();
    } catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	if (bais != null) {
    		baos.close();
    	}
    	if (oos != null) {
    		ois.close();
    	}
    }
    return null;
}
复制代码

测试代码及输出如下:

User user = new User();
user.setAddress("广东深圳");
user.setUserName("hjw");
Car car = new Car();
car.setCarName("单车");
car.setPrice(300);
user.setCar(car);
User clonedUser = cloneObject(user);
System.out.println(clonedUser);
复制代码
User [userName=hjw, carName=[单车],price=[300], address=广东深圳]
复制代码

六、总结

  • 1、新增序列化类时,建议新增一个自定义id,防止后续版本之间不兼容
  • 2、static、transient修饰的字段不会被序列化,序列化同样会序列化private属性
  • 3、序列化可以实现深度克隆
  • 4、实现序列化有两种方式,一种是直接实现 Serializable 接口,另一种是实现 Externalizable 接口,后者允许我们自定义序列化规则
原文  https://juejin.im/post/5e0d86e05188253a907112e1
正文到此结束
Loading...