由于指针不仅会给开发人员带来使用上的不便,而且也是造成程序不稳定的根源,为了消除C/C++语言的这些缺点,Java语言取消了指针的概念,但这只是在Java语言中没有明确提供指针的概念与用法,而实质上每个new语言返回的都是一个指针的引用,只不过在大部分情况下开发人员不需要关心如何去操作这个指针而已。
由于Java取消了指针的概念,因此开发人员在编程中往往忽略了对象和引用的区别,示例如下:
import java.util.Arrays;
class Obj{
private String string="默认值";
public void setString(String string){
this.string=string;
}
public String toString(){
return string;
}
}
public class TestRef {
private Obj aObj=new Obj();
private int aInt=0;
public Obj getaObj() {
return aObj;
}
public void changeObj(Obj aObj) {
aObj.setString("改变后的值");
}
public int getaInt() {
return aInt;
}
public void changeInt(int aInt) {
aInt = 1;
}
public static void main(String[] args) {
TestRef testRef=new TestRef();
System.out.println("*****************引用类型*******************");
System.out.println("调用changeObj前:"+testRef.getaObj());
testRef.changeObj(testRef.aObj);
System.out.println("调用changeObj后:"+testRef.getaObj());
System.out.println("*****************基本数据类型*******************");
System.out.println("调用changeInt前:"+testRef.getaInt());
testRef.changeInt(testRef.getaInt());
System.out.println("调用changeInt后:"+testRef.getaInt());
}
}
程序运行结果:
*****************引用类型*******************
调用changeObj前:默认值
调用changeObj后:改变后的值
*****************基本数据类型*******************
调用changeInt前:0
调用changeInt后:0
上面两个看似类似的方法却有着不同的运行结果,主要原因是java在处理基本数据类型(8种:int、short、long、byte、float、double、char、boolean)时,都是采用按值传递(传递的是输入参数的复制)的方式执行,除此之外的其他类型都是按引用传递(传递的是对象的引用)的方式执行。对象除了在函数调用时是引用传递,在使用“=”赋值时也是采用引用传递,示例代码如下:
class Obj2{
private int aInt=0;
public int getaInt() {
return aInt;
}
public void setaInt(int aInt1) {
aInt = aInt1;
}
public void changeInt() {
this.aInt = 1;
}
}
public class TestRef2 {
public static void main(String[] args) {
Obj2 a=new Obj2();
Obj2 b=a;//此处b也指向了a的内存地址,则共同使用了一个存储空间
//原书中没有输出a、b的值,此处输出a、b的值会更直观些
System.out.println(a);
System.out.println(b);
b.changeInt();
System.out.println("a:"+a.getaInt());
System.out.println("b:"+b.getaInt());
}
}
程序运行结果:
cloneDemo.Obj2@1b6d3586
cloneDemo.Obj2@1b6d3586
a:1
b:1
在实际编程中,经常会遇到从某个已有的对象A创建出另一个与A具有相同状态的对象B,并且对B的修改不会影响到A的状态,例如,例如,Prototype(原型)模式中,就需要clone一个对象实例。在Java语言中,仅仅通过简单的赋值操作显然无法达到这个目的,而Java提供了一个简单有效的clone()方法来满足这个需求。
Java中的所有类默认都继承自Object类,而Object类中提供了一个clone()方法。这个方法的作用时返回一个Object对象的复制。这个复制函数返回的是一个新的对象而不是一个引用。(可以理解为指向了新开辟的存储空间,但是两个存储空间的存储内容一样)
使用clone()方法的步骤:
1. 实现clone的类首先需要继承Cloneable接口。Cloneable接口实质上是一个标识接口,没有任何接口方法。(如果没有继承,会报java.lang.CloneNotSupportedException:异常以指示无法复制某个实例。)
2. 在类中重写Object类中的clone()方法。
3. 在clone方法中调用super.clone()。无论clone类的继承结构是什么,super.clone()都会直接或间接调用java.lang.Object类的clone()方法。
4. 把浅复制的引用指向原型对象新的克隆体。
对上面的例子引入clone方法如下:
class Obj2 implements Cloneable{
private int aInt=0;
public int getaInt() {
return aInt;
}
public void setaInt(int aInt1) {
aInt = aInt1;
}
public void changeInt() {
this.aInt = 1;
}
public Object clone(){
Object o=null;
try {
o=(Obj2)super.clone();
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class TestRef2 {
public static void main(String[] args) {
Obj2 a=new Obj2();
Obj2 b= (Obj2) a.clone();
//原书中没有输出a、b的值,此处输出a、b的值会更直观些
//由于clone是返回的是一个新的对象而不是一个引用,所以不是指向同一片内存地址,两个互不影响
System.out.println(a);
System.out.println(b);
b.changeInt();
System.out.println("a:"+a.getaInt());
System.out.println("b:"+b.getaInt());
}
}
程序运行结果:
cloneDemo.Obj2@1b6d3586
cloneDemo.Obj2@4554617c
a:0
b:1
在C++语言中,当开发人员自定义复制函数时,会存在浅复制与深复制之分。Java语言在重载clone()方法时也存在统一同样的问题,当类中只有一些基本数据类型时,采用上述方法就可以,但是当类中包含一些对象时,就需要用到深复制,实现方法是在对对象调用clone()方法完成复制后,接着对对象中的非基本类型的属性也调用clone()方法完成深复制,示例如下:
class Obj3 implements Cloneable{
private Date birth=new Date();//这里实际是个地址
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public void changeDate(){
this.birth.setMonth(4);
}
public Object clone(){
Obj3 o=null;
try {
o=(Obj3)super.clone(); //实际开辟了新空间,由于内容是地址,所以是保存了相同的地址
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
//实现深复制
o.birth= (Date) this.getBirth().clone();
return o;
}
}
public class TestRef3 {
public static void main(String[] args) {
Obj3 a=new Obj3();
Obj3 b= (Obj3) a.clone();
b.changeDate();
System.out.println("a="+a.getBirth());
System.out.println("b="+b.getBirth());
}
}
运行结果:
a=Sat Apr 18 15:38:15 CST 2020
b=Mon May 18 15:38:15 CST 2020
如何选择使用哪种复制方式呢?
首先,检查类有无非基本类型(对象)的数据成员。若没有,则返回super.clone()即可;若有,确保类中包含的所有非基本类型的成员变量都可以实现了深复制。
需要注意的是,clone()方法的保护机制在Object中的clone()是被声明为protected的。以User类为例,通过声明为protected,就可以保证只有User类里面才能“克隆”User对象,原理可以参考前面关于public、protected、private的讲解。
引申:浅复制和深复制有什么区别?
浅复制(Shallow Clone):被复制对象的所有变量都含有与原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
深复制(Deep Clone):被复制对象的所有变量都含有与原来对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制的新对象,而不在是原有的那些被引用的对象。换言之,深复制把复制的对象所引用的对象都复制了一遍。