欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于Java中的对象浅克隆和深入理解protected关键字

程序员文章站 2024-03-12 16:16:50
...

为什么要出现克隆

  • 要想了解克隆的含义,先来看一个包含对象引用的变量建立副本时会发生什么。
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();
    }
}

关于Java中的对象浅克隆和深入理解protected关键字

关于浅克隆

  • 如上所见,克隆可以简单地理解为:如果希望被赋值的对象是一个新对象,它的初始状态与赋值对象相同,但是之后它们各自会有自己不同的状态,这种情况下就使用 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成员来自何方,其可见性范围是什么,然后就可以判断出当前用法是否可行了;

相关标签: JavaSE面试题整理