1.從Serilizable說到transient
我們知道,如果一個對象需要序列化,那么需要實現Serilizable
接口,那么這個類的所有非靜態屬性,都會被序列化。
注意:上面說的是非靜態屬性,因為靜態屬性是屬於類的,而不是屬於類對象的,而序列化是針對類對象的操作,所以這個根本不會序列化。下面我們可以實驗一下:
實體類Teacher.class
:
import java.io.Serializable;
class Teacher implements Serializable {
public int age;
public static String schoolName;
public Teacher(int age) {
this.age = age;
}
@Override
public String toString() {
return "Teacher{" +
"age=" + age +
'}';
}
}
測試代碼SerialTest.java
,基本思路就是初始化的時候,靜態屬性schoolName
為"東方小學",序列化對象之后,將靜態屬性修改,然后,反序列化,發現其實靜態變量還是修改之后的,說明靜態變量並沒有被序列化。
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
Teacher.schoolName = "東方小學";
serial();
Teacher.schoolName = "西方小學";
deserial();
System.out.println(Teacher.schoolName);
}
// 序列化
private static void serial(){
try {
Teacher teacher = new Teacher(9);
FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(teacher);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
// 反序列化
private static void deserial() {
try {
FileInputStream fis = new FileInputStream("Teacher.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Teacher teacher = (Teacher) ois.readObject();
ois.close();
System.out.println(teacher.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
輸出的結果,證明靜態變量沒有被序列化!!!
Teacher{age=9}
西方小學
2.序列化屬性對象的類需要實現Serilizable接口?
突然想到一個問題,如果有些屬性是對象,而不是基本類型,需不需要改屬性的類型也實現Serilizable
呢?
問題的答案是:需要!!!
下面是實驗過程:
首先,有一個Teacher.java
,實現了Serializable
,里面有一個屬性是School
類型:
import java.io.Serializable;
class Teacher implements Serializable {
public int age;
public School school;
public Teacher(int age) {
this.age = age;
}
@Override
public String toString() {
return "Teacher{" +
"age=" + age +
'}';
}
}
School
類型,不實現Serializable
:
public class School {
public String name;
public School(String name) {
this.name = name;
}
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
'}';
}
}
測試代碼,我們只測試序列化:
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
serial();
}
private static void serial(){
try {
Teacher teacher = new Teacher(9);
teacher.school = new School("東方小學");
FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(teacher);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
會發現報錯了,報錯的原因是:School不能被序列化,也就是沒有實現序列化接口,所以如果我們想序列化一個對象,那么這個對象的屬性也必須是可序列化的,或者它是transient
修飾的。
java.io.NotSerializableException: com.aphysia.transienttest.School
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.aphysia.transienttest.SerialTest.serial(SerialTest.java:18)
at com.aphysia.transienttest.SerialTest.main(SerialTest.java:9)
當我們將School
實現序列化接口的時候,發現一切就正常了...問題完美解決
3.不想被序列化的字段怎么辦?
但是如果有一個變量不是靜態變量,但是我們也不想序列化它,因為它可能是一些密碼等敏感的字段,或者它是不那么重要的字段,我們不希望增加報文大小,所以想在序列化報文中排除該字段。或者改字段存的是引用地址,不是真正重要的數據,比如ArrayList
里面的elementData
。
這個時候就需要使用transient
關鍵字,將改字段屏蔽。
當我們用transient
修飾School
的時候:
import java.io.Serializable;
class Teacher implements Serializable {
public int age;
public transient School school;
public Teacher(int age) {
this.age = age;
}
@Override
public String toString() {
return "Teacher{" +
"age=" + age +
", school=" + school +
'}';
}
}
import java.io.Serializable;
public class School implements Serializable {
public String name;
public School(String name) {
this.name = name;
}
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
'}';
}
}
執行下面序列化和反序列化的代碼:
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
serial();
deserial();
}
private static void serial(){
try {
Teacher teacher = new Teacher(9);
teacher.school = new School("東方小學");
FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(teacher);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
private static void deserial() {
try {
FileInputStream fis = new FileInputStream("Teacher.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Teacher teacher = (Teacher) ois.readObject();
ois.close();
System.out.println(teacher.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
執行結果如下,可以看到School
字段反序列化出來,其實是null,這也是transient
起作用了。
但是注意,transient
只能修飾變量,但是不能修飾類和方法,
Teacher{age=9,school=null}
4.ArrayList里面的elementData都被transient 關鍵字修飾了,為什么ArrayList還可以序列化呢?
這里提一下,既然transient
修飾了ArrayList
的數據節點,那么為什么序列化的時候我們還是可以看到ArrayList
的數據節點呢?
這是因為序列化的時候:
如果僅僅實現了
Serializable
接口,那么序列化的時候,肯定是調用java.io.ObjectOutputStream.defaultWriteObject()
方法,將對象序列化。然后如果是transient
修飾了該屬性,肯定該屬性就不能序列化。
但是,如果我們雖然實現了Serializable
接口,也transient
修飾了該屬性,該屬性確實不會在默認的java.io.ObjectOutputStream.defaultWriteObject()
方法里面被序列化了,但是ArrayList重寫一個writeObject()
方法,這樣一來,序列化的時候調用的就是ArrayList
重寫的writeObject()
方法,而不是java.io.ObjectOutputStream.defaultWriteObject()
的方法了。
下面的源碼是ObjectOutputStream.writeObject(Object obj)
,里面底層其實會有反射的方式調用到重寫的對象的writeObject()
方法,這里不做展開。
public final void writeObject(Object obj) throws IOException {
// 如果可以被重寫,那么就會調用重寫的方法
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
ArrayList
重寫的writeOject()
方法如下:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
// 調用默認的序列化對象的方法,實現常規的序列化操作
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// 此時elementData在默認序列化方法中沒有被序列化,單獨對elementData中的元素進行writeObject操作
for (int i=0; i<size; i++) {
// 序列化對象的值
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
我們可以看到,writeOject()
里面其實在里面調用了默認的方法defaultWriteObject()
,defaultWriteObject()
底層其實是調用改了writeObject0()
方法。ArrayList
重寫的writeOject()
的思路主要是先序列化默認的,然后序列化數組大小,再序列化數組elementData
里面真實的元素。這就達到了序列化元素真實內容的目的。
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList
的反序列化操作入上面代碼所示。先調用defaultReadObject()進行默認的反序列化操作,對於之前writeOject()寫入的elementData中的真實元素,在for循環中挨個讀取出來,實現了elementData的反序列化,因此elementData雖然使用了transient關鍵字修飾,但是因為重寫了writeOject()和readObject()方法,所以最終還是實現了elementData屬性的序列化和反序列化。
同理,借用ArrayList
的實現邏輯,可以實現帶有transient關鍵字的屬性的序列化和反序列化,下面構造一個簡單的實體類
class TransientTest implements Serializable {
private static final long serialVersionUID = 233858934995755239L;
private String name1;
private transient String name2;
public TransientTest(String name1, String name2) {
this.name1 = name1;
this.name2 = name2;
}
public String toString() {
return String.format("TransientTest.toString(): name1=%s,name2=%s", name1, name2);
}
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
s.writeObject(name2);
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
name2 = String.valueOf(s.readObject());
}
private static void serial() {
try {
TransientTest teacher = new TransientTest("常規屬性", "transient修飾的屬性");
System.out.println("序列化前:" + teacher.toString());
FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(teacher);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
private static void deserial() {
try {
FileInputStream fis = new FileInputStream("Teacher.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
TransientTest teacher = (TransientTest) ois.readObject();
ois.close();
System.out.println("反序列化:" + teacher.toString());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
serial();
deserial();
}
}
結果如下:
序列化前:TransientTest.toString(): name1=常規屬性,name2=transient修飾的屬性
反序列化:TransientTest.toString(): name1=常規屬性,name2=transient修飾的屬性
5.除了transient,有沒有其他的方式,可以屏蔽屬性的反序列化?
且慢,問出這個問題,答案肯定是有的!!!那就是Externalizable接口。
具體情況:Externalizable
意思就是,類里面有很多很多屬性,但是我只想要一部分,要屏蔽大部分,那么我不想在大部分的屬性前面加關鍵字transient
,我只想標識一下自己序列化的字段,這個時候就需要使用Externalizable
接口。
首先定義一個Person.java
,里面有三個屬性
- age:年齡
- name:名字(被
transient
修飾) - score:分數
實現了Externalizable
接口,就必須實現writeExternal()
和readExternal()
方法。
- writeExternal:將需要序列化的屬性進行自定義序列化
- readExternal:將需要反序列化的屬性進行自定義反序列化
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable {
public int age;
public transient String name;
public int score;
// 必須實現無參構造器
public Person() {
}
public Person(int age, String name, int score) {
this.age = age;
this.name = name;
this.score = score;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
/*
* 指定序列化時候寫入的屬性。這里不寫入score
*/
out.writeObject(age);
out.writeObject(name);
out.writeObject(score);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
/*
* 指定序列化時候寫入的屬性。這里仍然不寫入年齡
*/
this.age = (int)in.readObject();
this.name = (String)in.readObject();
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
", score='" + score + '\'' +
'}';
}
}
上面的代碼,我們可以看出,序列化的時候,將三個屬性都寫進去了,但是反序列化的時候,我們僅僅還原了兩個,那么我們來看看測試的代碼:
import java.io.*;
public class ExternalizableTest {
public static void main(String[] args) {
serial();
deserial();
}
private static void serial(){
try {
Person person = new Person(9,"Sam",98);
FileOutputStream fileOutputStream = new FileOutputStream("person.txt");
ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(person);
objectOutputStream.flush();
} catch (Exception exception) {
exception.printStackTrace();
}
}
private static void deserial() {
try {
FileInputStream fis = new FileInputStream("person.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Person person = (Person) ois.readObject();
ois.close();
System.out.println(person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
測試結果如下,就可以發現其實前面兩個都反序列化成功了,后面那個是因為我們重寫的時候,沒有自定義該屬性的反序列化,所以沒有是正常的啦...
Person{age=9, name='Sam', score='0'}
如果細心點,可以發現,有一個字段是transient
修飾的,不是說修飾了,就不會被序列化么,怎么序列化出來了。
沒錯,只要實現了Externalizable
接口,其實就不會被transient
左右了,只會按照我們自定義的字段進行序列化和反序列化,這里的transient
是無效的...