1、存储单元分配方式不同:
数组变量指向的是某一连续的内存的空间地址;
基本数据类型指向某一内存地址;基本数据类型变量获得存储单元的方式是静态的,声明了变量后系统就为变量分配了存储单元,就可以对变量赋值。
2、变量的赋值方式不同:
数组变量的引用赋值,数组变量保存的是数组的引用,(即数组占用的一片连续存储空间的首地址及长度特性)当声明一个数字变量而未申请空间时,变量是未初始化的,没有地址及特性值。只有申请了存储空间,才能以下标表示数组元素。
每篇文章纯属个人经验观点,如有错误疏漏欢迎指正
ECMAScript
变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是
简单的数据段,而引用类型值指那些可能由多个值构成的对象。在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值,因为两种数据类型不仅存放的位置不同,访问的方式也不同。
一、区分数据类型
基本数据类型(简单数据类型):
String,Number,Boolean,Undefined,Null,Symbol
;引用数据类型(复杂数据类型):
Object
;引用数据类型只有一种
Object
,但Object
是一个包罗万象的类型,像Array,Function
等等不在其它六种简单数据类型之外的数据,都属于Object
。二、栈和堆
我们简单理解一下栈和堆的区别:
栈:存放基本类型数据,系统会自动分配内存空间,由系统自动释放,占据固定大小的空间;
堆:存放引用类型数据,系统会动态分配内存空间,系统不会自动释放,且占据的空间大小不定;
在
JS
中,声明一个引用数据类型时,会在堆内存中保存一个对象,在栈中保存变量名和一个指针(地址),这个指针(地址)指向的是堆内存中对应的对象。指针(地址)指的是堆内存中的引用地址,通过这个引用地址可以快速查找到保存中堆内存中的对象。而由于
JS
不可以对堆内存进行直接访问和操作,又延伸出两种访问方式:
按值访问:对于基本类型,可以直接操作保存在栈中的实际值;
按引用访问:对于引用类型,通过栈中的引用地址连接到堆中对象,操作的是堆中的对象而不是栈中的地址。
三、基本数据类型(简单数据类型)
基本数据类型均为简单数据,这些原始类型的简单数据在内存中占据的空间是固定的,因此会被保存在栈中,这样存储便于迅速查寻变量的值,可以直接访问。
var a = 1;var b = a;b = 2;console.log(a); // 1console.log(b); // 2
在声明一个基本数据类型变量时,会直接在栈中的相应位置上创建一个值;如果从一个变量向另一个变量复制基本类型的值时,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。这表明两个变量的值是相互独立的,因此,这两个变量可以参与任何操作而不会相互影响。
下图演示了上述代码在栈内存中的演化过程:
因为基本数据类型都是简单数据段,它们的数据大小是固定的,所以会直接按值存放,并可以直接按值访问,而按值访问就代表着我们可以操作保存在变量中的实际的值。
四、引用数据类型(复杂数据类型)
引用数据类型都是
Object
,其中包含了Array,Function,Date
等,也就是说引用类型的内容实际上就是存放在堆内存中的对象,变量保存的是栈内存中一个指向堆内存中对应内容地址的指针。这是因为引用类型的数据大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。而引用地址的大小是固定的,所以把它存储在栈中对性能没有负面影响。
var obj1 = {a: 1};var obj2 = obj1;obj2.a = 2;console.log(obj1); // {a: 2}console.log(obj2); // {a: 2}
我们发现修改
arr2
后,arr1
发生了改变。这是因为这两个引用数据类型指向了同一个堆内存对象。obj1
复制给obj2
,实际上是将obj1
这个堆内存对象在栈内存的引用地址复制了一份给了obj2
,而他们的引用地址共同指向同一个堆内存对象,所以修改obj2
会导致obj1
也发生改变。也就是说,引用类型在保存时,会在堆内存中保存这个对象,在栈内存中保存变量名和一个指向这个对象的引用地址。在进行复制操作时,复制的是栈内的引用地址,而不是堆内的对象,这将导致两个变量实际上将指向同一个对象。因此,改变其中一个变量,就会影响另一个变量。
下图演示了上述代码在内存中的演化过程:
简单来说,引用类型的数据在复制时,复制的是栈中的引用地址,而非堆中储存的对象。而两者复制的引用地址指向的是同一个对象,这就导致了无论修改元数据还是复制后的数据,都会给二者造成同样的影响。
我们再来尝试一下,将复杂数据中的简单数据提取出来:
var arr1 = [1, 2, 3, 4, 5];var arr2 = arr1;var num1 = arr2[0];arr2[0] = 0;console.log(arr1); // [0, 2, 3, 4, 5]console.log(arr2); // [0, 2, 3, 4, 5]console.log(num1); // 1
我们修改
arr2
后,arr1
跟着发生了改变,但num1
并没有发生变化。这是因为arr2[0]
是一个基本数据类型,num1
复制的是arr2
堆中对象里的一个基本数据类型,会将这个简单值直接复制到栈内,此时,无论对堆内对象进行任何更改,都不会影响到栈内的值。
五、按值传递和按引用传递
按值传递:在调用函数时将参数的值复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数;
按引用传递:在调用函数时将参数的引用地址传递到函数中,操作的其实是堆内存中的对象,修改会影响到实际参数;
按值传递示例:
var num = 1;function fn(n){ n++; console.log(n)}fn(num); // 2console.log(num); // 1
我们在看一个例子:
var obj1 = { a: 1 };function fn(obj) { obj.b = 2; console.log(obj);}fn(obj1); // { a: 1, b: 2 };console.log(obj1); // { a: 1, b: 2 };
这里需要注意
JS
中没有按引用传递,所有的参数都是按值传递,但即使这个变量是按值传递的,引用数据类型的参数也会按引用来访问同一个对象。我们在上方示例中创建一个对象
obj1
并将其复制后的内容传递到函数fn
中,然后在函数中为其添加了一个新属性b
,在这个函数内部,obj
和obj1
引用的是同一个对象,于是,当在函数内部为obj
添加属性后,函数外部的obj1
也发生了改变,因为obj1
指向的对象在堆内存中只有一个,而且是全局对象。有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。为了证明对象是按值传递的,我们再看一看下面这个经过修改的例子:
var obj1 = { a: 1 };function fn(obj) { obj.b = 2; obj = { c: 3 }; console.log(obj);}fn(obj1) // { c: 3 }console.log(obj1); // { a: 1, b: 2 }
这个示例中,在把
obj1
传递给fn
后,函数为参数obj
添加了一个新属性b
,然后,又将一个全新的对象赋给变量obj
,同时赋予了它一个新属性c
。如果
obj1
是按引用传递的,那么obj1
就会指向这个新创建的包含属性c
的对象。但是,当接下来再打印obj1
时,显示的值仍然是具有a,b
属性的对象,这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写
obj
时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。六、总结
JavaScript
变量可以用来保存两种类型的值:基本数据类型和引用数据类型,除了Object
为引用数据类型外,其它数据均为基本数据类型。基本数据类型:
基本类型是储存在栈中的简单数据段,占据固定大小的空间,系统会自动释放;
基本类型的值可以直接访问到,也就是按值访问;
基本类型的变量在复制时,会将原变量的值复制给新变量,但两个变量是完全独立的,只是值相同;
基本类型作为参数时,会将原始值复制一份作为参数传递,参数和原始值互不影响,也就是按值传递;
区分是否为基本类型数据可以使用
typeof
运算符来判断是String,Number
等;引用数据类型:
引用类型是储存在堆中的对象,栈内存储的是指向堆中对象的指针,内存空间的大小由系统分配,系统不会自动释放;
引用类型的值需要根据栈中存储的路径去堆中查找相应的对象,也就是按引用访问;
引用类型的变量复制时,会将原变量的指针复制给新变量,两个变量指向的是堆中的同一个对象,修改时会互相影响;
引用类型作为参数时,会将引用路径复制一份作为参数传递,因此两个变量最终都指向同一个对象,但这是按值传递而非按引用传递;
区分引用数据的类型可以使用
instanceof
操作符来判断是Array,Function
等;
感谢大家的观看及支持,我们下篇博客再见!如果有问题需要老王帮忙或者想看关于某个主题的文章,也可以通过留言等方式来联系老王。
每篇文章纯属个人经验观点,如有错误疏漏欢迎指正。转载请附带作者信息及出处。您的评论和关注是我更新的动力!
点击查看原文跳转到 博客 点下再看,少个BUG
1、存储单元分配方式不同:
数组变量指向的是某一连续的内存的空间地址;
基本数据类型指向某一内存地址;基本数据类型变量获得存储单元的方式是静态的,声明了变量后系统就为变量分配了存储单元,就可以对变量赋值。
2、变量的赋值方式不同:
数组变量的引用赋值,数组变量保存的是数组的引用,(即数组占用的一片连续存储空间的首地址及长度特性)当声明一个数字变量而未申请空间时,变量是未初始化的,没有地址及特性值。只有申请了存储空间,才能以下标表示数组元素。
转载于:https://www.cnblogs.com/sxudk/p/3336714.html
在C#中值类型的变量直接存储数据,而引用类型的变量持有的是数据的引用,数据存储在数据堆中。
常见的值类型数据有:整值型(整形,浮点型,十进制型),布尔类型,枚举类型;
引用类型有:接口,数组,Object类型,类,委托,字符串,null类型。
在C#中每种类型的存储方式有两种:1)分配在托管栈中;2)分配在托管堆中;
内存的分配有CLR管理(即公共语言运行时),这两种方法的区别是:
1)分配在托管栈中的变量会在创建它们的方法返回时自动释放,例如在一个方法中声明Char型的变量UserInput=C,当实例化它的方法结束时,UserInput变量在栈上占用的内存就会自动释放;
2)分配在托管堆中的变量并不会在创建它们的方法结束时释放内存,它们所占用的内存会被CLR中的垃圾回收机制释放。
看下面的代码:
1 static void Main(string[] args)
2 {
3 //当nStudent声明并赋值是,这时在托管栈上就会开辟一块内存来存储nStudent的值,当实例化nStudent的Main()方法结束时,
4 //nStudent在托管栈上占用的内存会自动释放。
5 int nStudent = 0;
6 //当声明strStuName时,这个时候“小明”存储在托管堆中,而托管栈中存储的是strStuName指向的引用。
7 string strStuName = "小明";
8
9 Console.WriteLine("学生的总数是{0},五号的名字是{1}", nStudent, strStuName);
10 Console.ReadKey();
11 }装箱和拆箱
当值类型的数据转换成引用类型时,CLR会先在托管堆配置一块内存,将值类型的数据复制到这块内存,然后再让托管栈上的引用类型的变量指向这块内存,这样的过程称为装箱。相反的话,有引用类型转换成值类型的话就称为拆箱。
一般情况下,.NET会主动的帮我们完成装箱操作,但是拆箱并非主动,我们必须知道拆箱对象的实力类型,然后明确的去执行拆箱操作。
1 int BirthdayNum = 1989; 2 object BoxBirthdayNum = BirthdayNum;//系统自动装箱 3 int nBirthdayNum = (int)BoxBirthdayNum;//明确数据类型的拆箱因为花费了更多的时间,所以装箱和拆箱对程序的性能有一定的影响。
--------------------------------------------------------------------------------------------------------------------------------------
类型推断
在C#中有两种类型的数据,一种是值类型,另一种是引用类型。
值类型包括:内置值类型、用户自定义值类型、和枚举,如 int,float bool 等,以及struct等。
引用类型包括接口类型、用户自定义的类、委托等。如 string 、DateTime、数组等。
值类型是存储在堆栈中,而引用类型是存储在托管堆上,C#程序首先被编译成IL程序,然后在托管执行。值类型直接从堆栈中里面取值,而引用类型必须要先从堆栈里面取出它的地址,再根据这个地址在堆里找到对应的值。
值类型与饮用类型的本质区别有以下几点:
1.内存分配: 值类型是分配在栈中的;而引用类型是分配在堆中。
2.效率: 值类型效率高,不需要地址转换;引用类型效率较低,需要进行地址转换。
3.内存回收: 值类型使用完后立即回收;引用类型使用完后不立即回收,而是交给GC处理回收。
4.赋值操作: 值类型是创建一个新对象;引用类型创建一个引用。
5.类型扩展: 值类型不易扩展,所有值类型都是密封(seal)的,所以无法派生出新的值类型;引用类型具有多态的特性方便扩展。
这是别人的总结,我在这里拿来用下。
下面我在说说它们在用法上的区别了,C#之所以要分这两种数据类型的原因是达到更好的性能,把一些基本类型如int、bool规定为值类型,而把包含许多字段的较大类型规定为引用类型,如用户自定义的类。值类型主要是负责存储数据,引用类更多是用在代码的重用性上。
从C#3.0开始,C#引入了一个隐式类型推断的关键字var,编译器可以通过它的初始值来判断变量的具体类型。var只能用于局部变量的声明,不能用于字段级的变量声明。使用var关键字时,var必须得有初始值,这样编译器才能判断是否是真实变量。1 class Program
2 {
3 static void Main(string[] args)
4 {
5 var i = 10;//隐式类型
6 int m = 10;//显示类型
7
8 var Program=new Program();
9 Program.nAge = 20;
10 Program.SayHello();
11 }
12
13 private int nAge;
14 public void SayHello()
15 {
16 var message = "my age is {0}";
17 Console.WriteLine(message, nAge);
18 }
19 }message初始值的变量为字符串类型,因此编译器可以推断其类型为String类型。
转载链接:http://www.cr173.com/html/17724_1.html
深克隆和浅克隆有什么区别?它的实现方式有哪些?
浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的
简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象,如下图所示
深克隆(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象,如下图所示
在 Java 语言中要实现克隆则需要实现 Cloneable 接口,并重写 Object 类中的 clone() 方法,实现代码如下
public class CloneExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建被赋值对象 People p1 = new People(); p1.setId(1); p1.setName("Java"); // 克隆 p1 对象 People p2 = (People) p1.clone(); // 打印名称 System.out.println("p2:" + p2.getName()); } static class People implements Cloneable { // 属性 private Integer id; private String name; /** * 重写 clone 方法 * @throws CloneNotSupportedException */ @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } }
以上程序执行的结果为:p2:Java
clone() 源码分析
要想真正的了解克隆,首先要从它的源码入手,代码如下
/** * Creates and returns a copy of this object. The precise meaning * of "copy" may depend on the class of the object. The general * intent is that, for any object {@code x}, the expression: * <blockquote> * <pre> * x.clone() != x</pre></blockquote> * will be true, and that the expression: * <blockquote> * <pre> * x.clone().getClass() == x.getClass()</pre></blockquote> * will be {@code true}, but these are not absolute requirements. * While it is typically the case that: * <blockquote> * <pre> * x.clone().equals(x)</pre></blockquote> * will be {@code true}, this is not an absolute requirement. * <p> * By convention, the returned object should be obtained by calling * {@code super.clone}. If a class and all of its superclasses (except * {@code Object}) obey this convention, it will be the case that * {@code x.clone().getClass() == x.getClass()}. * <p> * By convention, the object returned by this method should be independent * of this object (which is being cloned). To achieve this independence, * it may be necessary to modify one or more fields of the object returned * by {@code super.clone} before returning it. Typically, this means * copying any mutable objects that comprise the internal "deep structure" * of the object being cloned and replacing the references to these * objects with references to the copies. If a class contains only * primitive fields or references to immutable objects, then it is usually * the case that no fields in the object returned by {@code super.clone} * need to be modified. * <p> * ...... */ protected native Object clone() throws CloneNotSupportedException;
从源码的注释信息中我们可以看出,Object 对 clone() 方法的约定有三条
对于所有对象来说,
x.clone() !=x
应当返回true
,因为克隆对象与原对象不是同一个对象;对于所有对象来说,
x.clone().getClass() == x.getClass()
应当返回true
,因为克隆对象与原对象的类型是一样的;对于所有对象来说,
x.clone().equals(x)
应当返回true
,因为使用 equals 比较时,它们的值都是相同的看
clone()
的实现方法,发现clone()
是使用native
修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行
Arrays.copyOf()分析
如果是数组类型,我们可以直接使用
Arrays.copyOf()
来实现克隆,实现代码如下People[] o1 = {new People(1, "Java")}; People[] o2 = Arrays.copyOf(o1, o1.length); // 修改原型对象的第一个元素的值 o1[0].setName("Jdk"); System.out.println("o1:" + o1[0].getName()); System.out.println("o2:" + o2[0].getName());
以上程序的执行结果为
o1:Jdk o2:Jdk
从结果可以看出,我们在修改克隆对象的第一个元素之后,原型对象的第一个元素也跟着被修改了,这说明
Arrays.copyOf()
其实是一个浅克隆因为数组比较特殊,数组本身就是引用类型,因此在使用
Arrays.copyOf()
其实只是把引用地址复制了一份给克隆对象,如果修改了它的引用对象,那么指向它的(引用地址)所有对象都会发生改变,因此看到的结果是,修改了克隆对象的第一个元素,原型对象也跟着被修改了
深克隆实现方式有几种?
深克隆的实现方式有很多种,大体可以分为以下几类
所有对象都实现克隆方法;
通过构造方法实现深克隆;
使用
JDK
自带的字节流实现深克隆;使用第三方工具实现深克隆,比如
Apache Commons Lang
;使用
JSON
工具类实现深克隆,比如Gson、FastJSON
等接下来分别来实现以上这些方式,在开始之前先定义一个公共的用户类,代码如下
/** * 用户类 */ public class People { private Integer id; private String name; private Address address; // 包含 Address 引用对象 // 忽略构造方法、set、get 方法 } /** * 地址类 */ public class Address { private Integer id; private String city; // 忽略构造方法、set、get 方法 }
1.所有对象都实现克隆
这种方式我们需要修改
People
和Address
类,让它们都实现Cloneable
的接口,让所有的引用对象都实现克隆,从而实现 People 类的深克隆,代码如下public class CloneExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建被赋值对象 Address address = new Address(110, "北京"); People p1 = new People(1, "Java", address); // 克隆 p1 对象 People p2 = p1.clone(); // 修改原型对象 p1.getAddress().setCity("西安"); // 输出 p1 和 p2 地址信息 System.out.println("p1:" + p1.getAddress().getCity() + " p2:" + p2.getAddress().getCity()); } /** * 用户类 */ static class People implements Cloneable { private Integer id; private String name; private Address address; /** * 重写 clone 方法 * @throws CloneNotSupportedException */ @Override protected People clone() throws CloneNotSupportedException { People people = (People) super.clone(); people.setAddress(this.address.clone()); // 引用类型克隆赋值 return people; } // 忽略构造方法、set、get 方法 } /** * 地址类 */ static class Address implements Cloneable { private Integer id; private String city; /** * 重写 clone 方法 * @throws CloneNotSupportedException */ @Override protected Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } // 忽略构造方法、set、get 方法 } }
以上程序的执行结果为
p1:西安 p2:北京
从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆
2.通过构造方法实现深克隆
《Effective Java》中推荐使用构造器(
Copy Constructor
)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新new
一个对象,实现代码如下public class SecondExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 Address address = new Address(110, "北京"); People p1 = new People(1, "Java", address); // 调用构造函数克隆对象 People p2 = new People(p1.getId(), p1.getName(), new Address(p1.getAddress().getId(), p1.getAddress().getCity())); // 修改原型对象 p1.getAddress().setCity("西安"); // 输出 p1 和 p2 地址信息 System.out.println("p1:" + p1.getAddress().getCity() + " p2:" + p2.getAddress().getCity()); } /** * 用户类 */ static class People { private Integer id; private String name; private Address address; // 忽略构造方法、set、get 方法 } /** * 地址类 */ static class Address { private Integer id; private String city; // 忽略构造方法、set、get 方法 } }
以上程序的执行结果为
p1:西安 p2:北京
从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆
3.通过字节流实现深克隆
通过 JDK 自带的字节流实现深克隆的方式,是先将要原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆,代码如下
import java.io.*; public class ThirdExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 Address address = new Address(110, "北京"); People p1 = new People(1, "Java", address); // 通过字节流实现克隆 People p2 = (People) StreamClone.clone(p1); // 修改原型对象 p1.getAddress().setCity("西安"); // 输出 p1 和 p2 地址信息 System.out.println("p1:" + p1.getAddress().getCity() + " p2:" + p2.getAddress().getCity()); } /** * 通过字节流实现克隆 */ static class StreamClone { public static <T extends Serializable> T clone(People obj) { T cloneObj = null; try { // 写入字节流 ByteArrayOutputStream bo = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bo); oos.writeObject(obj); oos.close(); // 分配内存,写入原始对象,生成新对象 ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流 ObjectInputStream oi = new ObjectInputStream(bi); // 返回生成的新对象 cloneObj = (T) oi.readObject(); oi.close(); } catch (Exception e) { e.printStackTrace(); } return cloneObj; } } /** * 用户类 */ static class People implements Serializable { private Integer id; private String name; private Address address; // 忽略构造方法、set、get 方法 } /** * 地址类 */ static class Address implements Serializable { private Integer id; private String city; // 忽略构造方法、set、get 方法 } }
以上程序的执行结果为
p1:西安 p2:北京
此方式需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须
实现 Serializable 接口
,标识自己可以被序列化,否则会抛出异常 (java.io.NotSerializableException
)4.通过第三方工具实现深克隆
使用 Apache Commons Lang 来实现深克隆,实现代码如下
import org.apache.commons.lang3.SerializationUtils; import java.io.Serializable; /** * 深克隆实现方式四:通过 apache.commons.lang 实现 */ public class FourthExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 Address address = new Address(110, "北京"); People p1 = new People(1, "Java", address); // 调用 apache.commons.lang 克隆对象 People p2 = (People) SerializationUtils.clone(p1); // 修改原型对象 p1.getAddress().setCity("西安"); // 输出 p1 和 p2 地址信息 System.out.println("p1:" + p1.getAddress().getCity() + " p2:" + p2.getAddress().getCity()); } /** * 用户类 */ static class People implements Serializable { private Integer id; private String name; private Address address; // 忽略构造方法、set、get 方法 } /** * 地址类 */ static class Address implements Serializable { private Integer id; private String city; // 忽略构造方法、set、get 方法 } }
以上程序的执行结果为
p1:西安 p2:北京
可以看出此方法和第三种实现方式类似,都需要
实现 Serializable 接口
,都是通过字节流
的方式实现的,只不过这种实现方式是第三方提供了现成的方法,让我们可以直接调用
5.通过 JSON 工具类实现深克隆
使用 Google 提供的 JSON 转化工具
Gson
来实现,其他 JSON 转化工具类也是类似的,实现代码如下import com.google.gson.Gson; /** * 深克隆实现方式五:通过 JSON 工具实现 */ public class FifthExample { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 Address address = new Address(110, "北京"); People p1 = new People(1, "Java", address); // 调用 Gson 克隆对象 Gson gson = new Gson(); People p2 = gson.fromJson(gson.toJson(p1), People.class); // 修改原型对象 p1.getAddress().setCity("西安"); // 输出 p1 和 p2 地址信息 System.out.println("p1:" + p1.getAddress().getCity() + " p2:" + p2.getAddress().getCity()); } /** * 用户类 */ static class People { private Integer id; private String name; private Address address; // 忽略构造方法、set、get 方法 } /** * 地址类 */ static class Address { private Integer id; private String city; // 忽略构造方法、set、get 方法 } }
以上程序的执行结果为
p1:西安 p2:北京
使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的
克隆设计理念猜想
对于克隆为什么要这样设计,官方没有直接给出答案,Java 中实现克隆需要两个主要的步骤,一是 实现
Cloneable
空接口,二是重写Object
的clone()
方法再调用父类的克隆方法(super.clone())
,那为什么要这么做?从源码中可以看出
Cloneable
接口诞生的比较早,JDK 1.0 就已经存在了,因此从那个时候就已经有克隆方法了,那我们怎么来标识一个类级别对象拥有克隆方法呢?克隆虽然重要,但我们不能给每个类都默认加上克隆,这显然是不合适的,那我们能使用的手段就只有这几个了
在类上新增标识,此标识用于声明某个类拥有克隆的功能,像 final 关键字一样;
使用 Java 中的注解;
实现某个接口;
继承某个类
以上方法对比分析:先说第一个,为了一个重要但不常用的克隆功能, 单独新增一个类标识,这显然不合适;再说第二个,因为克隆功能出现的比较早,那时候还没有注解功能,因此也不能使用;第三点基本满足我们的需求,第四点和第一点比较类似,为了一个克隆功能需要牺牲一个基类,并且 Java 只能单继承,因此这个方案也不合适。采用排除法,无疑使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口
因为 clone()
方法语义的特殊性,因此最好能有 JVM
的直接支持,既然要 JVM
直接支持,就要找一个 API
来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object
中,这样所有类就可以很方便地调用到了