关于Java中的对象浅克隆和深入理解protected关键字
为什么要出现克隆
- 要想了解克隆的含义,先来看一个包含对象引用的变量建立副本时会发生什么。
public class MyTest1 {
public static void main(String[] args) {
Student s1 = new Student("张三", 20);
System.out.println(s1);
//Student{name='张三', age=20}
Student s2 = s1;
s2.setAge(21);
//s1 also changed age
System.out.println(s1);
//Student{name='张三', age=21}
System.out.println(s2);
//Student{name='张三', age=21}
//Student类重写了toString()
}
}
- 原变量和副本都是同一个对象的引用。这说明,
任何一个变量的改变都会影响另一个变量
。 - 如果希望s2是一个新对象,它的初始状态与s1相同,但是之后它们各自会有自己不同的状态,这种情况下就使用 clone();
public class MyTest2 {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("张三", 20);
System.out.println(s1);
//Student{name='张三', age=20}
Student s2 = (Student) s1.clone();
s2.setAge(22);
System.out.println(s1);
//Student{name='张三', age=20}
System.out.println(s2);
//Student{name='张三', age=22}
}
}
自定义类Student:
要想让对象使用clone(),必须使得该对象所在的类实现Cloneable接口,并重写父类Object类中的clone();
public class Student implements Cloneable{
private String name;
private int age;
………
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
关于浅克隆
-
如上所见,克隆可以简单地理解为:如果希望被赋值的对象是一个新对象,它的初始状态与赋值对象相同,但是之后它们各自会有自己不同的状态,这种情况下就使用 clone()而不是直接赋值;
-
APII是这样描述Object类中的clone():
protected Object clone() throws CloneNotSupportedException
创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。这样做的目的是,对于任何对象 x:
表达式:x.clone() != x为 true
,
表达式:x.clone().getClass() == x.getClass()也为 true
,但这些并非必须要满足的要求。
一般情况下:x.clone().equals(x)为 true
,但这并非必须要满足的要求。
按照惯例,返回的对象应该通过调用 super.clone 获得。如果一个类及其所有的超类(Object 除外)都遵守此约定,则x.clone().getClass() == x.getClass()
。
- 不过并没有这样简单,clone()是Object类中的一个protected方法,这说明你的代码不能够直接调用这个方法。只有Student类可以克隆Student类的对象。这个限制是有原因的,想想看Object类中如何实现clone。它对这个对象一无所知,所以只能逐个域地进行拷贝,如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域,没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会存在一些共享信息。
现象说明:
-----测试类:
public class MyTest1 {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("张三", 23);
Person p2 = (Person) p1.clone();
p2.name="张三";
p2.age=30;
System.out.println(p1);
//Person{name='张三', age=23, address=null}
System.out.println(p2);
//Person{name='张三', age=30, address=null}
System.out.println("--------------------------------------------");
Address address = new Address("北京");
Person p3 = new Person("王五", 25, address);
Person p4 = (Person) p3.clone();
p4.name="赵六";
p4.age=36;
p4.address.city="上海";
System.out.println(p3);
//Person{name='王五', age=25, address=Address{city='上海'}}
System.out.println(p4);
//Person{name='赵六', age=36, address=Address{city='上海'}}
}
}
------自定义Person类:
public class Person implements Cloneable{
public String name;
public int age;
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
------自定义Adress类:
public class Address {
public String city;
public Address(String city) {
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
'}';
}
}
- 根据上面Adress对象打印的结果就可以看出,两个对象共享了这个域;默认的克隆操作是
"浅拷贝"
,并没有克隆对象中引用的其他对象,直接复制了一份地址;
没拷贝有什么影响吗?和要看具体情况,如果原对象和浅克隆对象共享的子对象是不可变的,那么共享就是安全的,例如子对象属于一个String类,就是这种情况;或者在对象的生命周期中,子对象一直包含不变的常量,没有更改器的方法会改变它,也没有方法生成它的引用,这种情况同样是安全的;
如何解决上面的问题
- 不过,通常子对象都是可变的,
必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象
;在这个例子中,Adress域就是一个可变的,所以它也需要克隆;
代码实现:
------测试类:
public class MyTest2 {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("北京");
Person p3 = new Person("王五", 25, address);
Person p4 = (Person) p3.clone();
p4.name="赵六";
p4.age=36;
p4.address.city="上海";
System.out.println(p3);
//Person{name='王五', age=25, address=Address{city='北京'}}
System.out.println(p4);
//Person{name='赵六', age=36, address=Address{city='上海'}}
}
}
------自定义Person类:
public class Person implements Cloneable{
public String name;
public int age;
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone();
return cloned;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
-----自定义可变Adress类:
public class Address implements Cloneable{
public String city;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Address(String city) {
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
'}';
}
}
这个代码与上面的浅克隆进行对比,它的可变Adress类也实现了Cloneable接口,并重写了clone()方法;Person类中,返回对象的时候,将该对象Adress域进行了克隆处理;最终结果是克隆成功,两个对象的Adress域也是不同的;
浅克隆需要注意的事项
- 对于每一个类,需要确定:
1、默认的clone()是否满足需要;
2、是否可以在可变的子对象上调用clone来修补默认的clone();
3、是否不该使用clone();
实际上,第3点是默认选项,如果选择第1项或第2项,类必须:
1、实现Cloneable接口;
2、重新定义clone(),并指定public访问修饰符;
- 对象浅克隆要注意的细节:
1、如果一个对象需要调用clone的方法克隆,那么该对象所属的类必须要实现Cloneable接口;
2、Cloneable接口只不过是一个标识接口而已,没有任何方法;
3、对象的浅克隆就是克隆一个对象的时候,如果被克隆的对象中维护了另外一个类的对象,这时候只是克隆另外一个对象的地址,而没有把另外一个对象也克隆一份;
4、对象的浅克隆也不会调用到构造方法的;
5、对象的深克隆(后面讲):采用IO流来实现 使用 ObjectOutputStream 将对象写入文件中,然后再用ObjectInputStream读取回来;
protected关键字
- Object类中的clone()是protected,为什么任意类不能访问到它?
我们会不会有这样一个考虑,不是所有的子类都能访问受保护方法吗?不是所有类都是Object的子类吗?
- 首先我们来看下为什么会存在protected这个关键字?
有些时候人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,为此,需要将这些方法或域声明为protected,而不是私有的;- 很多介绍Java语言的书籍都对protected介绍的比较的简单,基本都是一句话,就是:
被protected修饰的成员对于本包和其子类可见
。这种说法有点太过含糊,常常会对大家造成误解。实际上,protected的可见性在于两点:
- 基类的protected成员是包内可见的,并且对子类可见;
- 若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法;
【说明】关于protected关键字,我学习了以下博客:该博客的链接;
- 上面关于protected关键字可见性的两点,理解起来很晦涩,学习了该作者的博客之后,有一些案实例来分析帮助理解:
【实例1】
-----Father类:
package www.baidu.demo1;
public class Father {
// 父类Father1中的protected方法
protected void f() {}
}
-----Son1类:
package www.baidu.demo1;
public class Son1 extends Father {}
-----Son2类:
package www.baidu.demo2;
import www.baidu.demo1.Father;
public class Son2 extends Father {}
------测试类:
package www.baidu.demo1;
import www.baidu.demo2.Son2;
public class MyTest1 {
public static void main(String[] args) {
Son1 son1 = new Son1();
son1.f(); // Compile OK-----1
son1.clone(); // Compile Error-----2
Son2 son2 = new Son2();
son2.f(); // Compile OK-----3
son2.clone(); // Compile Error -----4
}
}
我们来分析为什么同样的protected方法,有的能调到,有的调不到;
首先看1、3,其中的f()方法从类Father继承而来,其可见性是包demo1及其子类Son1和Son2,而由于调用f()方法的类MyTest1所在的包也是demo1,因此1、3处编译通过;
其次看2、4,因为son2调用的clone()方法的可见性是java.lang包及其所有子类,对于语句son1.clone()
和son2.clone()
,二者的clone()在类Son1、Son2中是可见的,但对MyTest1是不可见的,因此2、4处编译不通过;
【实例2】
------MyObject类:
package www.baidu.demo2;
public class MyObject {
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
------测试类:
package www.baidu.demo1;
import www.baidu.demo2.MyObject;
public class MyTest extends MyObject {
public static void main(String[] args) throws CloneNotSupportedException {
MyObject object = new MyObject();
object.clone(); // Compile Error -----1
MyTest test = new MyTest();
test.clone(); // Compile OK-----2
}
}
对于1而言,clone()方法来自于类MyObject本身,因此其可见性为包demo2及MyObject的子类,虽然MyTest是MyObject的子类,但在MyTest中不能访问基类MyObject的protected方法clone(),因此编译不通过;
对于2而言,由于在MyTest中访问的是其本身实例的从基类MyObject继承来的的clone(),因此编译通过;
【实例3】
--------测试类:
package www.baidu.demo1;
import www.baidu.demo2.MyObject;
public class MyTest1 {
public static void main(String[] args) throws CloneNotSupportedException {
MyObject object = new MyObject();
object.clone(); // Compile OK-----1
}
}
---------MyObject类:
package www.baidu.demo2;
import www.baidu.demo1.MyTest1;
public class MyObject extends MyTest1 {}
对于1而言,clone()方法来自于类MyTest1,因此其可见性为包demo1及其子类MyObject,而1正是在demo1的类MyTest1中调用,属于同一包,编译通过;
【实例4】
-------测试类:
package www.baidu.demo1;
import www.baidu.demo2.MyObject;
public class MyTest {
public static void main(String[] args) throws CloneNotSupportedException {
MyObject object = new MyObject();
object.clone(); // Compile Error-----1
}
}
------自定义MyObject类:
package www.baidu.demo2;
import www.baidu.demo1.MyTest;
public class MyObject extends MyTest {
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
对于1而言,clone()方法来自于类MyObject,因此其可见性为包demo2及其子类(这里没有子类),而1是在demo1包下调用,不属于同一包,编译未通过;
【实例5】
------测试类:
package www.baidu.demo1;
public class MyTest {
public static void main(String[] args) throws CloneNotSupportedException {
MyObject obj = new MyObject();
obj.clone(); // Compile OK-------1
}
}
-----自定义类MyObject:
package www.baidu.demo1;
public class MyObject{
protected Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
这里的1中clone()来自于MyObject,它的可见性是demo1包以及它的子类,显然这是一个包下,编译通过;
【实例6】
------测试类:
package www.baidu.demo1;
public class MyObject extends MyTest{
public static void main(String[] args) throws CloneNotSupportedException {
MyObject obj = new MyObject();
obj.clone();// Compile OK -------1
}
}
-----自定义类MyTest:
package www.baidu.demo1;
public class MyTest {}
这里的1中clone()来自于MyTest,它的可见性是demo1包以及它的子类,显然这是它的子类,编译通过;
【实例7】
------测试类:
package www.baidu.demo1;
public class MyObject extends MyTest{
public static void main(String[] args) {
MyTest test = new MyTest();
test.clone(); // Compile Error ----- 1
}
}
-----自定义类MyTest:
package www.baidu.demo1;
public class MyTest {}
这里的1中clone()来自于类Object,它的可见性是java.lang包以及它的子类MyTest 中,显然这个范围不在其中,编译未通过;
- 总结:
在碰到涉及protected成员的调用时,首先要确定出该protected成员来自何方,其可见性范围是什么,然后就可以判断出当前用法是否可行了;
下一篇: php实现查询功能(数据访问)