最全面的Java面向对象讲解(七)_内部类、深拷贝和浅拷贝

一、内部类

        内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。

        从种类上说,内部类可以分为四类:成员内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:
                非静态成员内部类(普通内部类/成员内部类)
                静态成员内部类
                局部内部类
                匿名内部类---在JDK8新特性中可以使用Lambda表达式替代。(当该数据类型只使用一次/临时存在)

        内部类可以访问外部类的一切资源(重要)。

成员内部类

        成员内部类是最普通的内部类,它的定义位于另一个类的内部,形如下面的形式:

class class Circle{
    // 定义半径
    private double r;

    public Circle(){}

    public Circle(double r) {
        this.r = r;
    }

    /*
    * 定义成员内部类: 位于另一个类的内部
    *   Draw类位于Circle类内部,那么Draw类就是成员内部类
    * */
    class Draw{
        public void drawCircle(){
            System.out.println("绘制圆形");
        }
    }

}

        这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员变量、成员方法、静态变量、静态方法;

public class Circle {
	//成员变量
	double radius = 0;
	
	//定义四种访问权限的成员变量
	public int x = 1;
	int y = 2;
	protected int z = 3;
	private int k = 4;
	
	//定义一个静态变量
	public static int j = 5;

	//以后,只要定义了有参构造函数,那么必须把无参构造函数添加上去
	public Circle() {}
	
	//有参构造函数
	public Circle(double radius) {
		this.radius = radius;
	}
	
	public void method1() {
		System.out.println("Circle的method1方法");
	}
	
	private void method2(){
		System.out.println("Circle的私有method02方法");
	}
	
	//在Circle类的内部定义了Draw
	class Draw { // 内部类
		int y = 11;
		
		public void drawSahpe() {
			//内部类中可以访问任意的成员变量和静态变量
			System.out.println("外部类的成员变量:"+x+"\t"+z+"\t"+k);
			System.out.println("外部类的静态变量:"+j);
			System.out.println("外部类有一个成员变量y,内部类也有一个成员变量y:");
			System.out.println("内部类的y:"+this.y);
			System.out.println("外部类的y:"+Circle.this.y);
			
			this.method1();
			Circle.this.method1();
			method2();
			System.out.println("drawshape");
		}
		
		public void method1() {
			System.out.println("我是内部类的method1");
		}
	}
}

        不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
        外部类.this.成员变量
        外部类.this.成员方法

        虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

class Circle {
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
}

        成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
         
        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}
 
class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    class Inner {
        public Inner() {
             System.out.println("创建成员内部类对象 - Inner");
        }
    }
}

        内部类可以拥有 private 访问权限、protected 访问权限、public 访问权限及包访问权限。比如上面的例子,如果成员内部类 Inner 用 private 修饰,则只能在外部类的内部访问,如果用 public 修饰,则任何地方都能访问;如果用 protected 修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被 public 和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

总结:
        成员内部类可以使用private、default、protected、public任意进行修饰。
        成员内部类必须寄存在一个外部类对象里。因此,如果有一个成员内部类对象那么一定存在对应的外部类对象。成员内部类对象单独属于外部类的某个对象。
        成员内部类可以直接访问外部类的成员,但是外部类不能直接访问成员内部类成员。
        成员内部类不能有静态方法、静态属性和静态初始化块。
        外部类的静态方法、静态代码块不能访问成员内部类,包括不能使用成员内部类定义变量、创建实例。

静态内部类

        静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。

        静态内部类是不需要依赖于外部类对象的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {

    }

    static class Inner {
        public Inner() {
            System.out.println("创建静态内部类对象 - Inner");
        }
    }
}

public class Outter {
    private int x = 1;
    int y = 2;
    protected int z = 3;
    public int k = 4;
    static int j = 5;

    public Outter() {

    }

    public void outterMethod() {
        System.out.println("外部类的成员方法");
    }

    public static void outterStaticMethod() {
        System.out.println("外部类的静态方法");
    }

    public static Inner getInnerInstance() {
        return new Inner();
    }

    static class Inner {
        // String name;
        // static int age;

        // static {}
        // public static void gg() {}

        public Inner() {
            System.out.println("创建静态内部类对象 - Inner");
        }
        //Cannot make a static reference to the non-static field x
        public void innerMethod() {
            //所有的外部类成员变量都不可访问,因为静态内部类不依赖于外部类对象
            //System.out.println("外部类的成员变量:"+x+"\t"+y+"\t"+z+"\t"+k+"\t");
            System.out.println("外部类的静态变量:"+j);

            //不能访问外部类的成员方法
            //outterMethod();

            outterStaticMethod();
        }
    }

}


public class Test_Outter {
    public static void main(String[] args) {
        //静态内部类的创建不依赖于外部类
        //1、通过外部类实例化静态内部类对象
        //在这里实际上是new的  Outter里面的静态类
        Outter.Inner inner = new Outter.Inner();
        inner.innerMethod();

        //2、通过调用外部类的静态方法
        Inner innerInstance = Outter.getInnerInstance();
        innerInstance.innerMethod();
    }
}

总结:
        静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。
        静态内部类不依赖于外部类对象。
        静态内部类可以访问外部类的静态变量和静态方法;但是对于非静态成员变量和成员方法不能访问;
        静态内部类可以定义成员变量、成员方法甚至于静态变量、静态方法和静态代码块;

局部内部类

        局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

/*
 * 不是一个外部类,只是给内部类的父类
 */
public class People {
    public People() {
        System.out.println("Person的无参构造函数");
    }
    public void method() {

    }
    public static void main(String[] args) {
        Man man = new Man();

        //上溯造型:父类有的可以调用,子类扩展的不能调用
        People woman = man.getWoman();

        /*
		 * 怎么调用woman里面的方法呢?
		 * 	1.强转,但是Woman虽说是People的子类,但是只能在方法里面看到这个类,JVM找不到这个类,失败
		 *  2.让getWoman这个方法返回一个Woman类型,但是Woman类型在方法内部可见,不能作为返回值类型,失败
		 *  3.最终,让People类也定义method方法,那么子类的就可以被调用到了
		 */

        woman.method();
    }
}

class Man{
    private int x = 1;
    static int j = 5;

    public Man(){
        System.out.println("Man的无参构造函数");
    }

    //Man的成员方法
    public People getWoman(){
        //在成员方法里面定义一个内部类,那么这个内部类我们称之为局部内部类
        class Woman extends People{
            int age =0;

            public void method() {
                System.out.println("外部类的成员变量:"+x);
                System.out.println("外部类的静态变量:"+j);
            }
        }
        return new Woman();
    }
}

        注意: 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。

匿名内部类

        匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:

public class InnerClassTest {
    //各种修饰符的成员变量
    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;
    static int x = 10;

    //无参构造函数
    public InnerClassTest() {
        System.out.println("创建 InnerClassTest 对象");
    }

    // 自定义接口
    interface OnClickListener {
        void onClick(Object obj);
    }

    private void anonymousClassTest() {
        // 在这个过程中会新建一个匿名内部类对象,
        // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法
        /*
    	 * 当我们去new 接口/类(),在后面加上语句块,其实相当于创建了一个匿名内部类对象
    	 * 并且这个你们内部类对象是实现了该接口,并且要重写接口里面的方法
    	 * clickListener就是一个匿名内部类对象
    	 */
        OnClickListener clickListener = new OnClickListener() { 
            // 可以在内部类中定义属性,但是只能在当前内部类中使用,
            // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,
            // 也就无法创建匿名内部类的对象
            int field = 1;

            @Override
            public void onClick(Object obj) {
                System.out.println("对象 " + obj + " 被点击");
                System.out.println("其外部类的 field1 字段的值为: " + field1);
                System.out.println("其外部类的 field2 字段的值为: " + field2);
                System.out.println("其外部类的 field3 字段的值为: " + field3);
                System.out.println("其外部类的 field4 字段的值为: " + field4);
            }

        };
        // new Object() 过程会新建一个匿名内部类,继承于 Object 类,
        // 并重写了 toString() 方法
        clickListener.onClick(new Object() {
            @Override
            public String toString() {
                return "obj1";
            }
        });
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.anonymousClassTest();
    }
}

运行结果:

创建 InnerClassTest 对象
对象 obj1 被点击
其外部类的 field1 字段的值为: 1
其外部类的 field2 字段的值为: 2
其外部类的 field3 字段的值为: 3
其外部类的 field4 字段的值为: 4

上面的代码中展示了常见的两种使用匿名内部类的情况:
        1、直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;
        2、new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。

    同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。

内部类作用

        1、内部类可以很好的实现隐藏,一般的非内部类,是不允许有 private 与protected权限的,但内部类可以

        2、内部类拥有外围类的所有元素的访问权限

        3、可以实现多重继承

        4、可以避免修改接口而实现同一个类中两种同名方法的调用。

作用一:实现隐藏

        平时我们对类的访问权限,都是通过类前面的访问修饰符来限制的,一般的非内部类,是不允许有 private 与protected权限的,但内部类可以,所以我们能通过内部类来隐藏我们的信息。可以看下面的例子

public interface InterfaceTest {
    void increment();
}

public class Example {
    //使用private修饰内部类
    private class InsideClass implements InterfaceTest {
        public void test() {
            System.out.println("这是一个测试");
        }

        @Override
        public void increment() {
			System.out.println("increment...");
        }
    }

    public InterfaceTest getIn() {
        return new InsideClass();
    }
}

public class TestExample {
    public static void main(String args[]) {
        Example a=new Example();
        InterfaceTest a1=a.getIn();
        a1.increment();
    }
}

        从这段代码里面我只知道Example的getIn()方法能返回一个InterfaceTest 实例但我并不知道这个实例是这么实现的。而且由于InsideClass 是private的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。

作用二:可以无条件地访问外围类的所有元素

public class TagBean {
    private String name = "Jimbo";

    private class InTest {
        public InTest() {
            System.out.println(name);
        }
    }

    public void test() {
        new InTest();
    }

    public static void main(String args[]) {
        TagBean bb = new TagBean();
        bb.test();
    }
}

        name这个变量是在TagBean里面定义的私有变量。这个变量在内部类中可以无条件地访问System.out.println(name);

作用三:可以实现多重继承

        这个特点非常重要,个人认为它是内部类存在的最大理由之一。正是由于他的存在使得Java的继承机制更加完善。大家都知道Java只能继承一个类,它的多重继承在我们没有学习内部类之前是用接口来实现的。但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法。而有了内部类就不一样了。它可以使我们的类继承多个具体类或抽象类。大家看下面的例子。

public class Example1 {
    public String name() {
        return "liutao";
    }
}

public class Example2 {
    public int age() {
        return 25;
    }
}

public class MainExample {
    private class test1 extends Example1 {
        public String name() {
            return super.name();
        }
    }

    private class test2 extends Example2 {
        public int age() {
            return super.age();
        }
    }

    public String name() {
        return new test1().name(); 
    }

    public int age() {
        return new test2().age();
    }

    public static void main(String args[]) {

        MainExample mi=new MainExample();

        System.out.println("姓名:"+mi.name());

        System.out.println("年龄:"+mi.age());

    }
}

        大家注意看类三,里面分别实现了两个内部类 test1,和test2 ,test1类又继承了Example1,test2继承了Example2,这样我们的类三MainExample就拥有了Example1和Example2的方法和属性,也就间接地实现了多继承。

作用四:避免修改接口而实现同一个类中两种同名方法的调用

        大家假想一下如果,你的类要继承一个类,还要实现一个接口,可是你发觉你继承的类和接口里面有两个同名的方法怎么办?你怎么区分它们??这就需要我们的内部类了。看下面的代码

public interface Incrementable {
    void increment();
}

public class MyIncrement {
    public void increment() {
        System.out.println("Other increment()");
    }

    static void f(MyIncrement f)  {
        f.increment();
    }
}

        大家看上面,两个方法都是一样的。在看下面这个类要继承这个类和实现这个接口;

如果不用内部类:

public class Callee2 extends MyIncrement implements Incrementable {
 
    public void increment() {
        //代码
    }
}

        想问一下大家increment()这个方法是属于覆盖MyIncrement这里的方法呢?还是Incrementable这里的方法。我怎么能调到MyIncrement这里的方法?显然这是不好区分的。而我们如果用内部类就很好解决这一问题了。

        看下面代码:

public class Callee2 extends MyIncrement {

    private int i=0;

    private void incr() {
        i++;
        System.out.println(i);
    }

    private class Closure implements Incrementable {
        public void increment() {
            incr();
        }
    }

    Incrementable getCallbackReference() {
        return new Closure();
    }
}

        我们可以用内部类来实现接口,这样就不会与外围类的方法冲突了。

二、深拷贝与浅拷贝

拷贝概述

        Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。
    
        举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a;  B.b=A.b;

public class Person {
    String name;
    int age;
    public Person() {

    }

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }


    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }

}

public class Test_Copy {
    public static void main(String[] args) {
        // Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。
        Person p1 = new Person("张三",12);
        Person p2 = new Person();

        // 把p1的数据统统copy到p2里面
        p2.name = p1.name;
        p2.age = p1.age;
    }
}

        在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部 数据。

        Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。

        铺垫知识:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

浅拷贝(Shallow Copy)

浅拷贝概述

        1、对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

        2、对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

        具体模型如图所示:可以看到基本数据类型的成员变量,对其值创建了新的拷贝。而引用数据类型的成员变量的实例仍然是只有一份,两个对象的该成员变量都指向同一个实例。

浅拷贝的实现方式

一、通过拷贝构造方法实现浅拷贝:

        拷贝构造方法指的是该类的构造方法参数为该类的对象。使用拷贝构造方法可以很好地完成浅拷贝,直接通过一个现有的对象创建出与该对象属性相同的新的对象。

示例代码:

/* 拷贝构造方法实现浅拷贝 */
public class CopyConstructor {
    public static void main(String[] args) {
        Age a=new Age(20);
        Person p1=new Person(a,"Jimbo");
        
        Person p2=new Person(p1);
        System.out.println("p1是"+p1);
        System.out.println("p2是"+p2);
        
        // 修改p1的各属性值,观察p2的各属性值是否跟随变化
        p1.setName("小傻瓜");
        a.setAge(99);
        System.out.println("修改后的p1是"+p1);
        System.out.println("修改后的p2是"+p2);
    }
}

class Person{
    //两个属性值:分别代表值传递和引用传递
    private Age age;
    private String name;
    
    public Person(Age age,String name) {
        this.age=age;
        this.name=name;
    }
    //拷贝构造方法
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
    }

    public void setName(String name) {
        this.name=name;
    }

    public String toString() {
        return this.name+" "+this.age;
    }
}

class Age{
    private int age;
    public Age(int age) {
        this.age=age;
    }

    public void setAge(int age) {
        this.age=age;
    }

    public int getAge() {
        return this.age;
    }

    public String toString() {
        return getAge()+"";
    }
}

运行结果为:

p1是Jimbo 20
p2是Jimbo 20
修改后的p1是小傻瓜 99
修改后的p2是Jimbo 99

        结果分析:这里对Person类选择了两个具有代表性的属性值:一个是引用传递类型;另一个是字符串类型(属于常量)。

        通过拷贝构造方法进行了浅拷贝,各属性值成功复制。其中,p1值传递部分的属性值发生变化时,p2不会随之改变;而引用传递部分属性值发生变化时,p2也随之改变。

        要注意:如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝。而对于一般的拷贝构造,则一定是浅拷贝。

二、通过重写clone()方法进行浅拷贝:

        Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。

        有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。

        API:
        protected Object clone() throws CloneNotSupportedException 创建并返回此对象的一个副本。

        实现思路: 在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

public class Test {
    public static void main(String args[]) {

        Employee e = new Employee();
        // 编译错误:Unhandled exception: java.lang.CloneNotSupportedException
        Employee ee = e.clone();
    }
}

class Employee {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

        1、Unhandled exception: java.lang.CloneNotSupportedException 表示没有处理异常java.lang.CloneNotSupportedException,这是异常的内容,暂且不深入研究。
    
        2、克隆出来的需要是一个Employee对象,而不是Object,那么就需要在clone()里面进行强转。

public class Test {
    public static void main(String args[]) {

        Employee e = new Employee();
        // 运行错误:java.lang.CloneNotSupportedException: Employee
        Employee ee = e.clone();

    }
}

class Employee {
    @Override
    protected Employee clone() {
        Employee clone = null;
        try {
            clone = (Employee) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

        return clone;
    }
}

        CloneNotSupportedException:
                如果在没有实现Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出该异常。

        Cloneable:该接口中没有方法,只是一个标记,表示可以被克隆。

        如上述例子所示,Employee没有实现Cloneable接口,main方法运行时将抛出CloneNotSupportedException异常。只要将Employee实现Cloneable接口就不会出现异常。

异常示例:

        对Student类的对象进行拷贝,直接重写clone()方法,通过调用clone方法即可完成浅拷贝。

        快捷实现:
                1、给要拷贝的类实现Cloneable接口
                2、重写clone(),在clone()里面调用父类的clone() -->快捷键直接生成

/* clone方法实现浅拷贝 */
public class ShallowCopy {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("Jimbo",a,175);

        //通过调用重写后的clone方法进行浅拷贝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());

        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }
}
/*
 * 创建学生类
 */
class Student implements Cloneable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    //eclipe中alt+shift+s自动添加所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    //设置输出的字符串形式
    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }

    //重写Object类的clone方法
    public Object clone() {
        Object obj = null;
        //调用Object类的clone方法,返回一个Object实例
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

运行结果如下:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 99, 长度是: 175

分析结果可以验证:
        基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
        引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。

        String类型非常特殊,所以我额外设置了一个字符串类型的成员变量来进行说明。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!也就是说,当我将name属性从“Jimbo”改为“大傻子"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”Jimbo“这个常量改为了指向”大傻子“这个常量。在这种情况下,另一个对象的name属性值仍然指向”Jimbo“不会受到影响。

深拷贝(Deep Copy)

深拷贝概述

        首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!

        简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

        深拷贝模型如图所示:可以看到所有的成员变量都进行了复制。

         因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。

深拷贝的实现方式

一、通过重写clone方法来实现深拷贝

        与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。

        简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。

示例代码:

/* 层次调用clone方法实现深拷贝 */
public class DeepCopy {
    public static void main(String[] args) {
        Age a = new Age(20);
        Student stu1 = new Student("Jimbo", a, 175);

        //通过调用重写后的clone方法进行浅拷贝
        Student stu2 = (Student) stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();

        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age implements Cloneable {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }

    //重写Object的clone方法
    public Object clone() {
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

/*
 * 创建学生类
 */
class Student implements Cloneable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }

    //重写Object类的clone方法
    public Object clone() {
        Object obj = null;
        //调用Object类的clone方法——浅拷贝
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        //调用Age类的clone方法进行深拷贝
        //先将obj转化为学生类实例
        Student stu = (Student) obj;
        //学生类实例的Age对象属性,调用其clone方法进行拷贝
        stu.aage = (Age) stu.getaAge().clone();
        return obj;
    }
}

运行结果:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 20, 长度是: 175

        分析结果可以验证:进行了深拷贝之后,无论是什么类型的属性值的修改,都不会影响另一个对象的属性值。

二、通过对象序列化实现深拷贝

        虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。

        将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

示例代码:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/* 通过序列化实现深拷贝 */
public class DeepCopyBySerialization {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Age a = new Age(20);
        Student stu1 = new Student("Jimbo", a, 175);

        // 通过序列化方法实现深拷贝
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(stu1);
        oos.flush();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        Student stu2 = (Student) ois.readObject();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();
        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age implements Serializable {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }
}

/*
 * 创建学生类
 */
class Student implements Serializable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    //eclipe中alt+shift+s自动添加所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    //设置输出的字符串形式
    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }
}

运行结果:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 20, 长度是: 175

        可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。
    
        transient:Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

区别:

        浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝.

        深拷贝:对基本数据类型进行值传递,对引用数据类型,会对引用指向的对象进行拷贝,此为深拷贝。也就是在clone()方法对其内的引用类型的变量再进行一次 clone()

        区别就在于是否对对象中的引用变量所指向的对象进行拷贝。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一道 Java 面向对象编程题: 题目:设计一个汽车类 Car,具有属性:品牌(brand)、颜色(color)、价格(price)、速度(speed),以及方法:加速(speedUp)、减速(speedDown)。其中,加速方法每次将速度增加 10km/h,减速方法每次将速度减少 10km/h。 代码如下: ```java public class Car { private String brand; private String color; private double price; private int speed; public Car(String brand, String color, double price, int speed) { this.brand = brand; this.color = color; this.price = price; this.speed = speed; } public void speedUp() { speed += 10; } public void speedDown() { speed -= 10; } public String getBrand() { return brand; } public String getColor() { return color; } public double getPrice() { return price; } public int getSpeed() { return speed; } public static void main(String[] args) { Car car = new Car("Toyota", "Red", 100000, 60); System.out.println(car.getBrand() + " " + car.getColor() + " car with price $" + car.getPrice()); System.out.println("The car's original speed is " + car.getSpeed() + "km/h"); car.speedUp(); System.out.println("After speed up, the car's speed is " + car.getSpeed() + "km/h"); car.speedDown(); System.out.println("After speed down, the car's speed is " + car.getSpeed() + "km/h"); } } ``` 输出结果如下: ``` Toyota Red car with price $100000.0 The car's original speed is 60km/h After speed up, the car's speed is 70km/h After speed down, the car's speed is 60km/h ``` 以上代码简要说明了一个汽车类的设计,包括属性和方法的定义以及如何使用它们。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值