天天看點

java cloneable 用途_cloneable和Serializable的應用(java深複制、淺複制)

普通類

//普通類

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個資料。