約在7萬(wàn)多年前躲庄,我們的智人祖先經(jīng)歷了一場(chǎng)所謂的"認(rèn)知革命"。這場(chǎng)革命就像是一把鑰匙飒泻,打開(kāi)了潘多拉的魔盒鞭光,人類(lèi)的對(duì)于虛構(gòu)世界的腦洞從此一開(kāi)不可收拾。同人類(lèi)其他眾多的幻想一樣泞遗,對(duì)人事物的“復(fù)制“的這一虛構(gòu)臆想惰许,推進(jìn)了文明的演進(jìn),直接或間接地催促了藝術(shù)這種文化形態(tài)的繁榮史辙。
而現(xiàn)今汹买,隨著各種終端的普及佩伤,”復(fù)制“這個(gè)詞也隨著互聯(lián)網(wǎng)一起傳播出去。無(wú)論是你每天在電腦里使用ctrl
+c
和ctrl
+v
快捷鍵晦毙,還是各種網(wǎng)站對(duì)數(shù)字資源的二次分發(fā)生巡,都屬于“復(fù)制”這一范疇。而這一切的基礎(chǔ)见妒,無(wú)外乎計(jì)算機(jī)對(duì)信息載體的編碼和解碼障斋,然后就被電信號(hào)傳播。
你會(huì)不會(huì)和我一樣徐鹤,忍不住地要去幻想,若未來(lái)人類(lèi)復(fù)雜的思想也能被編碼成一串串字節(jié)碼邀层,那時(shí)候的世界又將會(huì)是怎樣呢返敬?
然而正文內(nèi)容和這個(gè)引子并沒(méi)太大的關(guān)系
JVM在等號(hào)賦值的時(shí)候都干了些什么?
定義一個(gè)Parent
類(lèi)和Child
類(lèi)
private class Parent {
public Parent() {
}
protected void test() {
// do sth ...
}
static {
// do sth ...
}
}
private class Child extends Parent {
public Child() {
// do sth ...
}
@Override
protected void test() {
super.test();
// do sth ...
}
static {
// do sth ,,,
}
}
靜被變量和常量先行
在類(lèi)在容器初始化時(shí)寥院,JVM會(huì)按照順序自上而下運(yùn)行類(lèi)中的靜態(tài)語(yǔ)句/塊或常量劲赠,如果有父類(lèi),則首先按照順序運(yùn)行靜態(tài)語(yǔ)句/塊或常量秸谢。初始化類(lèi)的行為有且僅有一次凛澎。
這一過(guò)程中,JVM會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)Class對(duì)象的實(shí)例估蹄,指向我們初始化后的這個(gè)類(lèi)塑煎。這個(gè)也被稱(chēng)作為方法區(qū)。
此時(shí)并沒(méi)有實(shí)例化該對(duì)象臭蚁。
在堆內(nèi)存創(chuàng)建實(shí)例
public static void main(String args[]) {
Child child = new Child();
}
main(String args[])標(biāo)志著這是一個(gè)主方法入口
main方法中最铁,類(lèi)又會(huì)按照這個(gè)順序執(zhí)行全局變量的賦值,然后執(zhí)行父類(lèi)的無(wú)參構(gòu)造函數(shù)和子類(lèi)的構(gòu)造函數(shù)垮兑。
在棧幀中冷尉,JVM會(huì)提前分配內(nèi)存地址用以?xún)?chǔ)存方法參數(shù)與局部變量。在這個(gè)例子中系枪,儲(chǔ)存的是args(如果有的話(huà))雀哨,和child在堆上的引用。
child對(duì)象會(huì)在堆內(nèi)存中被實(shí)例化私爷,其中包含它(及它父類(lèi))的成員變量(名稱(chēng)和具體值或指針)和方法(名稱(chēng)和具體實(shí)現(xiàn))的索引雾棺。
靜態(tài)成員變量會(huì)保存一個(gè)引用地址
入棧和出棧
public static void main(String args[]) {
Child child = new Child();
child.test();
}
執(zhí)行test()方法時(shí),會(huì)執(zhí)行父類(lèi)的同名方法衬浑,再執(zhí)行子類(lèi)的邏輯垢村。
因?yàn)榇朔椒▓?zhí)行了super.test(),而不是如隱形調(diào)用
而在內(nèi)存操作里嚎卫,此時(shí)會(huì)有一個(gè)新的棧幀被壓入棧中嘉栓,同樣的宏榕,該棧幀保存了方法中傳入的參數(shù)和局部變量。
由于該方法被其他方法調(diào)用(這里是main()方法)侵佃,棧幀中還有一個(gè)區(qū)域會(huì)保存main()方法的返回地址麻昼,這個(gè)區(qū)域被稱(chēng)作VM元數(shù)據(jù)區(qū)
。在test()方法結(jié)束時(shí)馋辈,它將被推出棧抚芦。并且根據(jù)元數(shù)據(jù)區(qū)的返回地址,正確地跳回到main()方法中迈螟。
在拋出異常時(shí)叉抡,可以看到一層層的Stack Trace
而如果該方法有一個(gè)返回值,這個(gè)又該如何傳遞給調(diào)用方呢答毫?
private class Parent {
...
protected String test() {
return "EvinK " + "is Awesome!";
}
...
}
private class Child extends Parent {
...
@Override
protected String test() {
String str = super.test();
return str;
}
...
}
操作數(shù)棧
在這個(gè)步驟中褥民,發(fā)揮了重要的作用。它屬于棧幀的一個(gè)組成部分洗搂,JVM臨時(shí)用它來(lái)存放需要計(jì)算的變量消返,然后將計(jì)算的結(jié)果推出到棧幀的局部變量區(qū)。
區(qū)域/棧幀 | return語(yǔ)句 | super.test() | str = super.test() | return語(yǔ)句 |
---|---|---|---|---|
局部變量區(qū) | str = "EvinK is Awesome!" | |||
操作數(shù)棧 | EvinK | EvinK is Awesome! | 指向局部變量str | |
- | is Awesome! |
使用等號(hào)復(fù)制時(shí)耘拇,發(fā)生了什么
private class Child extends Parent {
public String name;
public Child(String name) {
this.name = name;
}
...
}
public static void main(String args[]) {
Child child = new Child("小明");
Child child2 = child;
}
前面已經(jīng)說(shuō)了撵颊,使用new
關(guān)鍵字時(shí),會(huì)在堆內(nèi)存中存放該類(lèi)的實(shí)例惫叛。而棧中倡勇,會(huì)儲(chǔ)存這個(gè)在堆內(nèi)存中這個(gè)實(shí)例的引用。
而child2這個(gè)對(duì)象之間由child賦值嘉涌,也會(huì)在棧幀中的變量區(qū)译隘,創(chuàng)建一個(gè)指向這個(gè)實(shí)例在堆內(nèi)存地址的引用。
child2.name = "EvinK"; // -> child.name = "EvinK"
// == 比較的是對(duì)象間的引用
System.out.print(child2 == child); // always true
正是因?yàn)檫@兩個(gè)變量指向了同一個(gè)內(nèi)存地址洛心,所以只要修改這兩者中的任何一個(gè)引用固耘,都會(huì)導(dǎo)致另外一個(gè)局部變量被動(dòng)改變。
而作為程序開(kāi)發(fā)者的我們词身,對(duì)此居然一無(wú)所知厅目。
字符串也是對(duì)象
照這種說(shuō)法,字符串操作豈不是很危險(xiǎn)法严,稍不留神损敷,就會(huì)得出完全不一樣的結(jié)果。
String a = "a";
String b = a;
b = "b";
// a是什么深啤?
操作 | 常量池 | 指向地址 |
---|---|---|
a = "a" | "a" | a -> "a" |
b = a | "a" | b -> "a" |
b = "b" | "a", "b" | b -> "b" |
字符串也的確遵守這種“指向復(fù)制”規(guī)則拗馒。
b在重新被賦值后,并沒(méi)有在常量池中發(fā)現(xiàn)該字符串對(duì)象溯街,于是JVM在常量池中創(chuàng)建了新的字符串對(duì)象"b"诱桂。
讓情況再?gòu)?fù)雜點(diǎn)
String java1 = "java";
String java2 = "java";
String java3 = java;
String java4 = new String(java);
String jav = "jav";
String a = "a";
String java5 = jav + a;
System.out.println(java1 == java2);
System.out.println(java1 == java3);
System.out.println(java1 == java4);
System.out.println(java1 == java5);
字符串java1洋丐,java2和java3相等,因?yàn)樗鼈冎赶蛄送粔K內(nèi)存地址挥等。對(duì)于java2和java3而言友绝,它們聲明時(shí)內(nèi)存地址時(shí),發(fā)現(xiàn)了已存在的字符串對(duì)象"java",于是直接將引用指向這塊地址肝劲。
java4和java1的引用不相等迁客。使用new
關(guān)鍵字時(shí),會(huì)強(qiáng)制在常量池重新生成一個(gè)同值但不同地址的字符串對(duì)象辞槐。
java5和java1的引用不相等掷漱。java5的引用指向操作數(shù)幀的一個(gè)臨時(shí)地址,將在出棧時(shí)被銷(xiāo)毀榄檬。
復(fù)制
說(shuō)了這么多卜范,是不是有點(diǎn)跑題了?
太長(zhǎng)不看
Java里的所有類(lèi)都隱式地繼承了Object類(lèi)丙号,而在 Object 上,存在一個(gè) clone() 方法缰冤,它被聲明為了protected
犬缨,所以我們可以在其子類(lèi)中,使用它棉浸。
// Object Class
protected Object clone() throws CloneNotSupportedException {
if(!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class" + getClass().getName() +
" doesn`t implement Cloneable");
}
return internalClone();
}
private native Object internalClone();
可以看到怀薛,它的實(shí)現(xiàn)非常的簡(jiǎn)單,它限制所有調(diào)用 clone() 方法的對(duì)象迷郑,都必須實(shí)現(xiàn) Cloneable 接口枝恋,否者將拋出 CloneNotSupportedException 這個(gè)異常。最終會(huì)調(diào)用 internalClone() 方法來(lái)完成具體的操作嗡害。而 internalClone() 方法焚碌,實(shí)則是一個(gè) native 的方法。對(duì)此我們就沒(méi)必要深究了霸妹,只需要知道它可以 clone() 一個(gè)對(duì)象得到一個(gè)新的對(duì)象實(shí)例即可十电。
克隆
public class Person implements Cloneable {
public String name;
public Person(String name) {
this.name = name;
}
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
public static void main(String args[]) {
Person ming = new Person("小明");
Person evink = (Person) ming.clone();
evink.name = "EvinK";
}
當(dāng)一個(gè)類(lèi)的成員變量都是簡(jiǎn)單的基礎(chǔ)類(lèi)型時(shí),淺復(fù)制就可以解決我們的問(wèn)題叹螟。
讓情況變得復(fù)雜一點(diǎn)
public class Person implements Cloneable {
public String name;
public int[] scores;
...
}
public static void main(String args[]) {
Person ming = new Person("小明");
ming.scores = new int[]{
86
};
Person evink = (Person) ming.clone();
evink.name = "EvinK";
evink.scores[0] = 89; // -> ming.scores[0] = 89;
System.out.println(evink.scores); // [I@246b179d
System.out.println(ming.scores); // [I@246b179d
}
經(jīng)過(guò)了克隆( clone()
)方法的洗禮后鹃骂,我們聲明的兩個(gè)對(duì)象終于不再指向同一個(gè)內(nèi)存地址了“照溃可是畏线,為什么還會(huì)發(fā)生上面一段代碼的問(wèn)題。
簡(jiǎn)單描述一下就是良价,為什么復(fù)制這個(gè)行為寝殴,會(huì)和我們預(yù)期的不一致蒿叠?
在堆內(nèi)存中,進(jìn)行復(fù)制操作時(shí)杯矩,會(huì)再在堆內(nèi)分配一個(gè)地址用來(lái)存放Person對(duì)象栈虚,然后將原來(lái)Person中的成員變量的值或引用復(fù)制一份到新的對(duì)象中。而在棧幀中史隆,ming和evink指向的Person對(duì)象地址不同魂务,在代碼上表現(xiàn)為這兩者不相等。而由于其成員變量中可能含有其他對(duì)象的引用泌射,所以粘姜,即使經(jīng)過(guò)了復(fù)制操作,被克隆出的對(duì)象中的成員變量仍然指向相同的內(nèi)存地址熔酷。
使用淺復(fù)制時(shí)孤紧,會(huì)跳過(guò)構(gòu)造方法的實(shí)現(xiàn)。
深度復(fù)制
基于clone()方法的改進(jìn)方案
clone()方法的最大弊端是其無(wú)法復(fù)制對(duì)象內(nèi)部的對(duì)象拒秘,所以号显,只要使對(duì)象內(nèi)部的對(duì)象實(shí)現(xiàn)Cloneable接口,再在具體實(shí)現(xiàn)里使用構(gòu)造函數(shù)生成新的對(duì)象躺酒,這樣就能確保使用clone()方法生成的對(duì)象一定是全新的押蚤。
基于序列化(serialization)的改進(jìn)方案
public class Person implements Cloneable, Serializable {
public String name;
public int[] scores;
...
public Object deepCopy() {
Object obj = null;
try {
// 將對(duì)象寫(xiě)成 Byte Array
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(this);
out.flush();
out.close();
// 從流中讀出 byte array,調(diào)用readObject函數(shù)反序列化出對(duì)象
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
obj = in.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return obj;
}
}
public static void main(String args[]) {
Person ming = new Person("小明");
ming.scores = new int[]{
86
};
Person evink = (Person) ming.deepCopy();
evink.name = "EvinK";
evink.scores[0] = 89; // -> ming.scores = 86;
System.out.println(evink.scores); // [I@504bae78
System.out.println(ming.scores); // [I@246b179d
}