概述
在Java中,一个类只要实现Serializable接口,这个类的对象就可以被序列化,这种序列化模式为开发者提供了很多便利,我们可以不必关心具体序列化的过程,只要这个类实现了Serializable接口,这个类的所有属性都会自动序列化。但是有时我们需要让类的某些属性不被序列化,如密码这类信息,为了安全起见,不希望在网络操作中被传输或者持久化到本地。只要在相应的属性前加上transient关键字,就可以实现部分属性不被序列化,该属性的生命周期仅存于调用者的内存中而不会写入到磁盘持久化。
transient的使用
public class TransientTest {
public static void main(String[] args) {
User user = new User();
user.setUsername("Github");
user.setPassword("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPassword());
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPassword());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public class User implements Serializable {
private static final long serialVersionUID = 1234567890L;
private String username;
private transient String 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;
}
}
transient修饰静态变量
public class TransientTest {
public static void main(String[] args) {
User user = new User();
user.setUsername("Github");
user.setPassword("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPassword());
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
// 在反序列化前盖板username的值
user.setUsername("Tom");
ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPassword());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public class User implements Serializable {
private static final long serialVersionUID = 1234567890L;
private static String username;
private transient String 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;
}
}
案例研究:HashMap如何使用transient关键字?
到目前为止,我们一直在讨论与transient关键字相关的概念,这些概念基本上都是理论性的。让我们了解一下在HashMap类中逻辑地使用transient的正确用法。它将使您更好地了解java中transient关键字的实际用法。
在理解使用transient创建的解决方案之前,让我们先确定问题本身。
HashMap用于存储键-值对,这一点我们都知道。我们还知道HashMap中键的位置是根据键实例的哈希码计算的。现在,当我们序列化一个HashMap时,这意味着HashMap中的所有键以及与键相关的所有值也将被序列化。序列化之后,当我们反序列化HashMap实例时,所有关键实例也将被反序列化。我们知道在这个序列化/反序列化过程中,可能会丢失信息(用于计算hashcode),最重要的是它本身是一个新实例。
在java中,任何两个实例(甚至是相同类的实例)都不能有相同的hashcode。这是一个大问题,因为应该根据新的hashcode放置键的位置不正确。当检索键的值时,您将在这个新的HashMap中引用错误的索引。
因此,当一个哈希表被序列化时,它意味着哈希索引,和表的顺序不再有效,不应该被保留。这是问题陈述。
现在看看如何在HashMap类中解决这个问题。如果通过HashMap.java的源代码。你会发现下面的声明:
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
所有重要字段都标记为transient(所有字段实际上都是在运行时计算/更改的),因此它们不是序列化HashMap实例的一部分。为了再次填充这个重要的信息,HashMap类使用writeObject()和readObject()方法,如下所示:
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (Node<K,V> e : tab) {
for (; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
使用上面的代码,HashMap仍然允许像通常那样处理非transient字段,但是它们在字节数组的末尾一个接一个地写存储的键-值对。在反序列化时,它允许默认情况下处理的非transient变量,然后逐个读取键-值对。对于每个键,哈希值和索引将被再次计算,并被插入到表中的正确位置,以便再次检索时不会出现任何错误。
上面使用transient关键字就是一个很好的例子。您应该记住它,并在下一次java面试问题中提到它。
总结
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
- transient关键字只能修饰变量,而不能修饰方法和类。本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
- 一个静态变量不管是否被transient修饰,均不能被序列化。
- 无论何时将任何final字段/引用计算为“常量表达式”,JVM都会对其进行序列化,忽略transient关键字的存在。
参考链接
https://zhuanlan.zhihu.com/p/51980884
https://www.jianshu.com/p/2911e5946d5c