约在7万多年前,我们的智人祖先经历了一场所谓的”认知革命”。这场革命就像是一把钥匙,打开了潘多拉的魔盒,人类的对于虚构世界的脑洞从此一开不可收拾。同人类其他众多的幻想一样,对人事物的“复制“的这一虚构臆想,推进了文明的演进,直接或间接地催促了艺术这种文化形态的繁荣。
而现今,随着各种终端的普及,”复制“这个词也随着互联网一起传播出去。无论是你每天在电脑里使用
你会不会和我一样,忍不住地要去幻想,若未来人类复杂的思想也能被编码成一串串字节码,那时候的世界又将会是怎样呢?
JVM在等号赋值的时候都干了些什么
定义一个
1 |
|
静被变量和常量先行
在类在容器初始化时,JVM会按照顺序自上而下运行类中的静态语句/块或常量,如果有父类,则首先按照顺序运行静态语句/块或常量。初始化类的行为有且仅有一次。
这一过程中,JVM会在堆内存中创建一个Class对象的实例,指向我们初始化后的这个类。这个也被称作为方法区。
在堆内存创建实例
1 |
|
main方法中,类又会按照这个顺序执行全局变量的赋值,然后执行父类的无参构造函数和子类的构造函数。
在栈帧中,JVM会提前分配内存地址用以储存方法参数与局部变量。在这个例子中,储存的是args(如果有的话),和child在堆上的引用。
child对象会在堆内存中被实例化,其中包含它(及它父类)的成员变量(名称和具体值或指针)和方法(名称和具体实现)的索引。
入栈和出栈
1 |
|
执行test()方法时,会执行父类的同名方法,再执行子类的逻辑。
而在内存操作里,此时会有一个新的栈帧被压入栈中,同样的,该栈帧保存了方法中传入的参数和局部变量。
由于该方法被其他方法调用(这里是main()方法),栈帧中还有一个区域会保存main()方法的返回地址,这个区域被称作
而如果该方法有一个返回值,这个又该如何传递给调用方呢?
1 |
|
区域/栈帧 | return语句 | super.test() | str = super.test() | return语句 |
---|---|---|---|---|
局部变量区 | str = “EvinK is Awesome!” | |||
操作数栈 | EvinK | EvinK is Awesome! | 指向局部变量str | |
- | is Awesome! |
使用等号复制时,发生了什么
1 |
|
前面已经说了,使用
而child2这个对象之间由child赋值,也会在栈帧中的变量区,创建一个指向这个实例在堆内存地址的引用。
1 |
|
正是因为这两个变量指向了同一个内存地址,所以只要修改这两者中的任何一个引用,都会导致另外一个局部变量被动改变。
而作为程序开发者的我们,对此居然一无所知。
字符串也是对象
照这种说法,字符串操作岂不是很危险,稍不留神,就会得出完全不一样的结果。
1 |
|
操作 | 常量池 | 指向地址 |
---|---|---|
a = “a” | “a” | a -> “a” |
b = a | “a” | b -> “a” |
b = “b” | “a”, “b” | b -> “b” |
字符串也的确遵守这种“指向复制”规则。
b在重新被赋值后,并没有在常量池中发现该字符串对象,于是JVM在常量池中创建了新的字符串对象”b”。
让情况再复杂点
1 |
|
字符串java1,java2和java3相等,因为它们指向了同一块内存地址。对于java2和java3而言,它们声明时内存地址时,发现了已存在的字符串对象”java”,于是直接将引用指向这块地址。
java4和java1的引用不相等。使用
java5和java1的引用不相等。java5的引用指向操作数帧的一个临时地址,将在出栈时被销毁。
复制
说了这么多,是不是有点跑题了?
1 | 太长不看 |
Java里的所有类都隐式地继承了Object类,而在 Object 上,存在一个 clone() 方法,它被声明为了
1 |
|
可以看到,它的实现非常的简单,它限制所有调用 clone() 方法的对象,都必须实现 Cloneable 接口,否者将抛出 CloneNotSupportedException 这个异常。最终会调用 internalClone() 方法来完成具体的操作。而 internalClone() 方法,实则是一个 native 的方法。对此我们就没必要深究了,只需要知道它可以 clone() 一个对象得到一个新的对象实例即可。
克隆
1 |
|
当一个类的成员变量都是简单的基础类型时,浅复制就可以解决我们的问题。
让情况变得复杂一点
1 |
|
经过了克隆(
简单描述一下就是,为什么复制这个行为,会和我们预期的不一致?
在堆内存中,进行复制操作时,会再在堆内分配一个地址用来存放Person对象,然后将原来Person中的成员变量的值或引用复制一份到新的对象中。而在栈帧中,ming和evink指向的Person对象地址不同,在代码上表现为这两者不相等。而由于其成员变量中可能含有其他对象的引用,所以,即使经过了复制操作,被克隆出的对象中的成员变量仍然指向相同的内存地址。
深度复制
基于clone()方法的改进方案
clone()方法的最大弊端是其无法复制对象内部的对象,所以,只要使对象内部的对象实现Cloneable接口,再在具体实现里使用构造函数生成新的对象,这样就能确保使用clone()方法生成的对象一定是全新的。
基于序列化(serialization)的改进方案
1 |
|