java transient 方法_Java中序列化与transient的使用

一、序列化

1.1 什么是序列化,为什么要序列化?

我们在运行Java程序的时候,各个对象是有状态的。比如,我们创建了如下的一个Fruit对象,它的名字和重量是它当前的状态信息:

public static void main(String[] args) {

Fruit apple = new Fruit();

apple.setName("apple");

apple.setWeight(23);

}

在有的场景下,我们需要将该对象当前的状态信息持久化地保留下来,或者借助网络传输到别的地方,而这些场景是无法以对象的形态保留信息的,毕竟只有Java运行的时候才有对象这个概念。

因此,我们只能以文本或者字符的形式来表示当前Java中该对象的状态信息。那么,将Java中一个动态的对象表示成文本或者字符的过程,我们就称之为序列化。

1.2 如何在Java中使用序列化

常见的Java序列化方法有Java原生序列化、Hessian序列化、kryo序列化、Json序列化等,这里以介绍Java原生序列化为例。

默认声明的类是不能支持序列化的,只有当这个类实现了Serializable接口,才可以被序列化。而这个Serializable接口中不包含任何方法和属性,它仅仅是起到一个序列化标识的作用。下面是一个例子:

@Data

public class Fruit implements Serializable {

private String name;

private Integer weight;

// setters and getters...

}

public static void main(String[] args) {

Fruit apple = new Fruit();

apple.setName("apple");

apple.setWeight(23);

saveToFile(apple);

System.out.println("序列化结束");

}

private static void saveToFile(Fruit fruit){

try {

FileOutputStream fs = new FileOutputStream("fruit.txt");

ObjectOutputStream os = new ObjectOutputStream(fs);

os.writeObject(fruit);

os.flush();

os.close();

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

如果以上Fruit类没有实现Serializable接口,程序运行就会报错:

java.io.NotSerializableException: cn.zx.demo.Fruit

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

at cn.zx.demo.Main.saveToFile(Main.java:27)

at cn.zx.demo.Main.main(Main.java:19)

只有实现了该接口,才能正常运行并生成一个txt文件,这个就是我们apple对象序列化后的结果。

1.3 反序列化的使用

反序列化,顾名思义,就是要把以文本形式持久化了的对象信息再还原成Java运行时动态的对象的状态。

我们使用上面序列化中的例子,将持久化了的文本txt读入:

public static void main(String[] args) {

Fruit apple = readFromFile("fruit.txt");

// print apple

System.out.println(apple.getName());

// print 23

System.out.println(apple.getWeight());

}

private static Fruit readFromFile(String fileName){

Fruit fruit = null;

try{

FileInputStream fs = new FileInputStream(fileName);

ObjectInputStream os = new ObjectInputStream(fs);

fruit = (Fruit) os.readObject();

os.close();

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

return fruit;

}

反序列化成功,我们成功地从序列化了的文本信息中恢复了动态的Java对象及其序列化时的状态信息。

1.4 版本标识serialVersionUID

当一个对象被序列化后,接着需要被反序列化时,如何判断能否顺利地反序列化形成对象呢?因为原来的类可能被修改了,可能和序列化时的类是不一样的。

因此,需要使用serialVersionUID用来标识反序列化时类的版本和序列化时类的版本是否一致。当值是一致的,则表示可以序列化,反之则不能序列化,会报如下错误:

java.io.InvalidClassException: cn.zx.demo.code.Fruit; local class incompatible: stream classdesc serialVersionUID = -4079670274710263332, local class serialVersionUID = -1938591684852446153

at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)

at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)

在默认情况下,serialVersionUID的值不需要我们手动指定,系统会自动指定,只要我们确定序列化和反序列化前后的类没有发生变化,那么就不会出现如上的问题。

倘若我们将序列化之后的类增加了一个属性,或者加了一个别的逻辑,改变了类的程序结构,那么系统就会自动更改serialVersionUID的值,从而使得反序列化失败,报出如上的错误。比如我们加一个属性:

@Data

public class Fruit implements Serializable {

private String name;

private Integer weight;

private Double price;

// setters and getters...

}

但这是不合理的,因为我们只是加了一个属性,并不妨碍反序列化,新加的属性让其值为空不就行了,所以,让系统自动指定serialVersionUID的值在这种场景下就不是那么合理。我们需要自己指定它的值。

@Data

public class Fruit implements Serializable {

private static final long serialVersionUID = -1L;

private String name;

private Integer weight;

// setters and getters...

}

然后,随便你怎么改动类的结构,只要保证serialVersionUID不变,程序就不会比较这个值是否一致,从而尝试反序列化。

比如新加的一个属性反序列化后,内存中的它只不过是没有值而已,其它可以反序列化的属性不受影响。如果你改动的是原先序列化时就存在的属性,那么也没有关系,反序列化找不到对应的属性就会跳过,把能反序列化的都赋值,不能的全部为空。

1.5 序列化的使用注意事项

父类如果实现了序列化的接口,那么其子类自动拥有了序列化的能力;

一个已经实现了序列化的类引用了其它的类,那么其它的类也必须实现序列化的接口,否则序列化过程会报错java.io.NotSerializableException;

静态变量和transient修饰的变量不会被序列化;

1.6 序列化的常见使用场景

远程方法调用(RPC);

对象存储到文件或者数据库中;

实现对象的深拷贝;

二、transient

2.1 transient的作用是什么

在将对象序列化的时候,并不是所有的字段都需要保存起来。比如一些敏感信息,我们只要求在内存中使用就好,序列化的时候不要连同它们一起持久化。这时候,Java中的transient关键字就派上用场了,被它修饰了的字段就不会被序列化:

private transient Integer weight;

我们仍然使用上面的序列化和反序列化的例子,只不过给Fruit类中的weight属性加上transient关键字,希望它不要被序列化。

然后运行一次序列化和反序列的程序,得到的打印结果中weight为null。由此证明了transient生效了。

2.2 transient的使用注意事项

被transient修饰的变量无法序列化,随后反序列时便无法恢复该变量的值;

transient只能用来修饰变量,无法修饰类和方法;

类中的静态变量也是无法序列化的,因为类变量属于类,不属于对象。

关于如上第三点,给出实验进行证明:

public class Fruit implements Serializable {

private static final long serialVersionUID = 6950076107144781481L;

private String name;

private transient Integer weight;

private static String color;

public Fruit(){}

// getters and setters...

}

public static void main(String[] args) {

Fruit apple = new Fruit();

apple.setName("apple");

apple.setWeight(23);

Fruit.setColor("red");

saveToFile(apple);

System.out.println("序列化结束");

}

private static void saveToFile(Fruit fruit){

try {

FileOutputStream fs = new FileOutputStream("fruit.txt");

ObjectOutputStream os = new ObjectOutputStream(fs);

os.writeObject(fruit);

os.flush();

os.close();

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

先执行如上程序,将设置好静态变量color的apple对象进行序列化,然后再执行如下程序得出反序列化后的结果:

public static void main(String[] args) {

Fruit apple = readFromFile("fruit.txt");

// print apple

System.out.println(apple.getName());

// print null

System.out.println(apple.getWeight());

// print null

System.out.println(Fruit.getColor());

}

private static Fruit readFromFile(String fileName){

Fruit fruit = null;

try{

FileInputStream fs = new FileInputStream(fileName);

ObjectInputStream os = new ObjectInputStream(fs);

fruit = (Fruit) os.readObject();

os.close();

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

return fruit;

}

三、待解决的疑问

你最常用的序列化和反序列化的业务场景是什么?

答复:在1.6中已经列出场景。

日常开发中,最常使用的场景就是数据库数据的读写、关联方系统Json数据的传输,但是并没有留意到DTO和PO有实现Serializable接口,更没有serialVersionUID的声明,这是为什么呢?它们不算序列化的操作吗?

答复:Json是Java中的另一种序列化方法,不是原生的序列化方法,不需要实现Serializable接口。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值