在介绍transient关键字之前首先得了解对象的序列化:一个类实现了 Serializable接口, 我们就可以把它往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象.
-
transient的作用及使用方法
transient关键字就是用来修饰成员变量(不能是方法 ),防止该变量被序列化,比如一些需要隐藏的信息(如密码等)为了安全考虑就要避免被序列化、持久化,此时就需要用transient关键字修饰。 总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
package blog;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
public class user implements Serializable{
private String username;
transient private String password;
public user(String username,String password) {
this.username=username;
this.password=password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class serilizable {
public static void main(String[] args) {
user U=new user("蔡徐坤","caixukun");
try {
ObjectOutputStream out= new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\user.txt"));
out.writeObject(U);
out.flush();
out.close();
ObjectInputStream in=new ObjectInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\user.txt"));
user o=(user)in.readObject();
in.close();
System.out.println(o.getUsername());//输出:蔡徐坤
System.out.println(o.getPassword());//输出:null
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
可以从上面的结果看出,经过transient关键字修饰的变量在反序列化的时候是无法被读取到的,这样保证了一些信息的安全性。
-
transient使用小结
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)一个静态(static)变量不管是否被transient修饰,均不能被序列化。(结合静态变量的实际存储原理)
-
transient使用细节——被transient关键字修饰的变量真的不能被序列化吗?
事实上,这句话说的有点片面,前提条件是该类实现了Serilizable接口(所有的序列化将会自动进行)。
java中实现序列化的方法不止一种,还有一个接口是Externalizable接口,该接口并未实现任何变量的自动序列化,所有的变量序列化都需要手动在对应的writeExternal方法中进行指定,这与是否被transient修饰无关。
示例:
import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
/**
* @descripiton Externalizable接口的使用
*
* @author Alexia
* @date 2013-10-15
*
*/
public class ExternalizableTest implements Externalizable {
private transient String content = "是的,我将会被序列化,不管我是否被transient关键字修饰";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);//指定要序列化的变量
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();//相应的反序列
}
public static void main(String[] args) throws Exception {
ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);
ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);
out.close();
in.close();
}
}
序列化知识拓展
readResolve()方法与Serilizable接口
思考一个问题,序列化和反序列化会不会破坏单例模式?
单例是要求一个JVM中只有一个类对象的, 而现在通过反序列化,一个新的对象克隆了出来.
public class mysingleton implements Serializable {
private String message;
private static final mysingleton instance=new mysingleton("我是蔡徐坤");
public mysingleton(String string) {
// TODO 自动生成的构造函数存根
this.message=string;
}
public static mysingleton getInstance() {
return instance;
}
当把 mysingleton 对象(通过getInstance方法获得的那个单例对象)序列化后再从内存中读出时, 就有一个全新但跟原来一样的mysingleton 对象存在了. 那怎么来维护单例模式呢?这就要用到readResolve方法了. 如下所示:
public final class MySingleton implements Serializable{
private MySingleton() { }
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance() { return INSTANCE; }
private Object readResolve() throws ObjectStreamException {
// instead of the object we're on,
// return the class variable INSTANCE
return INSTANCE;
}
}
jdk中的serilizable接口描述
在将对象写入到流中时需要指定要使用的替代对象的可序列化类应实现具有确切签名的特殊方法:
Object writeReplace() throws ObjectStreamException;
如果该方法存在并且可以通过在被序列化的对象的类中定义的方法来访问,则该writeReplace方法通过序列化来调用。 因此,该方法可以具有私有,受保护和包私有访问。 子类访问此方法遵循java可访问性规则。
当从流中读取实例时需要指定替换的类应实现具有确切签名的特殊方法。
Object readResolve() throws ObjectStreamException;
这个readResolve方法遵循与writeReplace相同的调用规则和可访问性规则。
序列化运行时将每个可序列化的类与称为serialVersionUID的版本号相关联,该序列号在反序列化期间用于验证序列化对象的发送者和接收者是否已加载与该序列化兼容的对象的类。 如果接收方加载了一个具有不同于相应发件人类的serialVersionUID的对象的类,则反序列化将导致InvalidClassException
。 一个可序列化的类可以通过声明一个名为"serialVersionUID"
的字段来显式地声明它自己的serialVersionUID,该字段必须是静态的,最终的,类型是long
:
static final long serialVersionUID = 42L;
如果可序列化类没有显式声明serialVersionUID,则序列化运行时将根据Java(TM)对象序列化规范中所述的类的各个方面计算该类的默认serialVersionUID值。 但是, 强烈建议所有可序列化的类都明确声明serialVersionUID值,因为默认的serialVersionUID计算对类详细信息非常敏感,这可能会因编译器实现而异,因此可能会在反InvalidClassException
化期间导致InvalidClassException
的InvalidClassException。 因此,为了保证不同Java编译器实现之间的一致的serialVersionUID值,一个可序列化的类必须声明一个显式的serialVersionUID值。 还强烈建议,显式的serialVersionUID声明在可能的情况下使用private
修饰符,因为这种声明仅适用于立即声明的类 - serialVersionUID字段作为继承成员无效。 数组类不能声明一个显式的serialVersionUID,所以它们总是具有默认的计算值,但是对于数组类,放弃了匹配serialVersionUID值的要求。
结尾
java序列化存在严重的安全性问题,在实际开发过程中优先考虑替代方案,比如跨平台结构化数据(JSON和Protocol Buffers)。
非常谨慎的使用serilizable接口
实现Serializable接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。
实现Serializable接口的第二个代价是,增加了出现bug和安全漏洞的可能性。
实现Serializable接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。
内部类不应该实现Serializable。