普通類
//普通類
class Clazz {
int myId;
...
}
//測試
public static void main(String[] args) throws Exception {
Clazz zz = new Clazz();
zz.setMyId(1);
System.out.println("原始id: " + zz.getMyId());
//添加新引用
Clazz clazz = zz;
System.out.println("新引用的id: " + clazz.getMyId());
//改變原類的屬性
zz.setMyId(2);
System.out.println("原始id: " + zz.getMyId() + "; 新引用的id: " + clazz.getMyId());
}
想必大家都很熟悉,我們擁有指向同一個對象的兩個引用,通過任何一個引用改變對象的内容,對于另外的引用都即時可見。
但是當我們想要複制一份該怎麼辦呢?于是就有了下邊的接口
Cloneable類
//克隆類
class CloneClass implements Cloneable {
int myId;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//測試
public static void main(String[] args) throws Exception {
CloneClass cc = new CloneClass();
cc.setMyId(1);
System.out.println("原始id: " + cc.getMyId());
//克隆
CloneClass cloneClass = (CloneClass) cc.clone();
System.out.println("克隆後的id: " + cloneClass.getMyId());
//改變原類的屬性
cc.setMyId(2);
System.out.println("原始id: " + cc.getMyId() + "; 克隆後的id: " + cloneClass.getMyId());
}
//結果
原始id: 1
克隆後的id: 1
原始id: 2; 克隆後的id: 1
改變原始類的id後,複制的類的資訊保持不變,說明拷貝的類是一個不同的類,也就達到了複制一份的目的。
我要想克隆類,就要調用clone方法,由于Object類的clone方法是protect的,是以要想我們的類在所有包中都能夠克隆,就要将其實作為public的。有clone方法後,如果我們不實作Cloneable接口,編譯就會報不能克隆的錯。是以重寫方法和實作接口必須共存。
關于protect修飾符,我們知道可以子類調用,但同時還有一個條件是,隻能在子類所在的包中調用,在其他地方調不到。
但是如果類裡面有對象的話,就存在問題了
//類内引用的類
class User {
int id;
String name;
...
}
//克隆類
class CloneClass implements Cloneable {
int myId;
User user;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//測試
public static void main(String[] args) throws Exception {
CloneClass cc = new CloneClass();
User user = new User(100, "張三");
cc.setUser(user);
System.out.println("原始User: " + cc.getUser());
//克隆
CloneClass cloneClass = (CloneClass) cc.clone();
System.out.println("克隆後的User: " + cloneClass.getUser());
//改變原類引用的類的屬性
cc.getUser().setName("趙二麻子");
System.out.println("原始User: " + cc.getUser() + "; 克隆後的User: " + cloneClass.getUser());
}
//結果
原始User: 張三
克隆後的User: 張三
原始User: 趙二麻子; 克隆後的User: 趙二麻子
通過clone一個類,類内有引用類的時候,并不會重新拷貝一份(僅僅是複制了一個引用),指向的還是原類内引用指向的對象。是以改變原類的屬性,對于克隆的類的引用即時可見。這就是所謂的淺拷貝。
可以用如下的方法進行克服。
//類内引用的類, 讓其實作Cloneable
class User implements Cloneable {
int id;
String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
//改變克隆類的clone方法
class CloneClass implements Cloneable {
User user;
@Override
protected Object clone() throws CloneNotSupportedException {
CloneClass cloneClass = (CloneClass) super.clone();
cloneClass.user = (User) cloneClass.user.clone();
return cloneClass;
}
...
}
//測試,同上
//結果
原始User: 張三
克隆後的User: 張三
原始User: 趙二麻子; 克隆後的User: 張三
内部引用對象的複制問題是解決了,但是類裡每次新引用一個類,就得在clone裡增加一段代碼,是不是很繁瑣,假如這個類很多類裡都用到了呢,是不是一場災難。
Serializable接口将其操作簡化了。
Serializable類
先引入一個工具類,不用看具體内容,作用是給一個類(Serializable類),傳回這個類的副本
abstract class BeanUtil {
@SuppressWarnings("unchecked")
public static T cloneTo(T src) throws RuntimeException {
ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
ObjectOutputStream out = null;
ObjectInputStream in = null;
T dist = null;
try {
out = new ObjectOutputStream(memoryBuffer);
out.writeObject(src);
out.flush();
in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
dist = (T) in.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (out != null)
try {
out.close();
out = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
if (in != null)
try {
in.close();
in = null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return dist;
}
}
Serializable類
//類内引用的類, 讓其實作Serializable類
class User implements Serializable {
int id;
String name;
...
}
//序列化類
class SerializClass implements Serializable {
User user;
...
}
//測試
public static void main(String[] args) throws Exception {
//複制類
SerializClass sc = new SerializClass();
User user = new User(100, "張三");
sc.setUser(user);
System.out.println("原始User: " + sc.getUser());
SerializClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆後的User: " + serializClass.getUser());
sc.getUser().setName("趙二麻子");
System.out.println("原始User: " + sc.getUser() + "; 克隆後的User: " + serializClass.getUser());
}
//結果
原始User: 張三
克隆後的User: 張三
原始User: 趙二麻子; 克隆後的User: 張三
達到了複制一份的目的,同時代碼是不是也清爽了很多(工具類是複雜了點,但是隻寫一份就夠了)。
這種複制的方法,連同上邊比較複雜的一種clone,都成為深拷貝。
但這麼好的方法也有失效的時候。
//序列化類,有transient修飾符的字段
class SerializClass implements Serializable {
transient int i;
...
}
//測試
public static void main(String[] args) throws Exception {
//複制類
SerializClass sc = new SerializClass();
sc.setI(1);
System.out.println("原始i: " + sc.getI());
SerializClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆後的i: " + serializClass.getI());
sc.setI(2);
System.out.println("原始i: " + sc.getI() + "; 克隆後的i: " + serializClass.getI());
}
//結果
原始i: 1
克隆後的i: 0
原始i: 2; 克隆後的i: 0
也就是這種序列化會跳過帶有transient修飾符的字段(該修飾符隻能修飾字段,不能修飾方法),那麼還能有什麼方法可以複制嗎?答案是有
//序列化類
class SerializPlusClass implements Serializable {
transient int i;
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(i);
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
i = s.readInt();
}
...
}
//測試
public static void main(String[] args) throws Exception {
//複制類
SerializPlusClass sc = new SerializPlusClass();
sc.setI(1);
System.out.println("原始i: " + sc.getI());
SerializPlusClass serializClass = BeanUtil.cloneTo(sc);
System.out.println("克隆後的i: " + serializClass.getI());
sc.setI(2);
System.out.println("原始i: " + sc.getI() + "; 克隆後的i: " + serializClass.getI());
}
//結果
原始i: 1
克隆後的i: 1
原始i: 2; 克隆後的i: 1
可以看到我們實作了writeObject和readObject方法,但這個方法既不是Object的方法,也不是Serializable的方法,那為什麼這樣就可以呢?
我們可以回頭看看那個工具類,在序列化的時候我們用到了ObjectInputStream和ObjectOutputStream類,就是說在序列化和反序列化的時候,這兩個類調用了我們寫的方法。進而用這種方法也可以自定序列化内容,包括transient修飾的字段。
由于序列化對static修飾的字段也無效,是以也可以在這裡實作。由于實驗相對麻煩,這裡沒有給出。如果有興趣可以序列化包含靜态變量的類到檔案,然後重新開機虛拟機(重新運作),執行反序列化。
因為靜态變量屬于類,是以同一個虛拟機反序列化後還是原來的值,并不是反序列化出來的。
後話
我們知道transient修飾符,就是為了辨別出不需要反序列化的字段,那為什麼我們還要費盡心思來序列化它呢?
考慮這樣一種情況,有一個長度為100的list,裡面隻有一個有值,其他的都是null,如果全序列化出來,豈不是很浪費空間。是以通過這種方法有選擇地序列化。具體執行個體參見ArrayList的實作,隻序列化elementData的size個資料。