JavaSE:抽象类和接口
一、抽象类
抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果 一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
例如,前面学习继承和多态的举过的例子:
矩形、圆形、三角形都是图形,因此和Shape类的惯性应该是继承关系。虽然Shape类中也有draw方法,但是由于Shape类并不是具体的图形,因此其内部的draw方法实际是没有办法实现的。
由于Shape类没有办法描述一个具体的图形,导致其draw()方法无法具体实现,因此可以将Shape类设计为 抽象类。
在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).
抽象类语法
在Java中,一个类如果被 abstract 修饰称为抽象类,抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
语法格式:
【访问限定符】 abstract class 抽象类名称 {
//抽象方法
【访问限定符】 abstract 【返回类型】 抽象方法名();//注意抽象方法不能有具体实现体{}
//普通方法 或者 属性
}
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法
抽象类和普通类的区别
抽象类和普通类的区别在于,抽象类可以包含抽象方法(如果一个类中有抽象方法,那么这类必须是抽象类),也可以不包含抽象方法,只包含了普通方法和属性、构造方法,但是,普通类是不能包含抽象方法的,它只能包含普通方法和属性等。
抽象类特性
- 1.抽象类不能直接实例化对象
- 2.抽象方法不能是 private 修饰的
- 3. 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
- 4. 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
前面我们说过抽象类不能被实例化,那么它如何被使用呢?—— 就是通过子类继承使用的,即抽象类的出现,是为了被继承的。
看以下的代码,这个情况就是 抽象类Shape被子类Cycle继承,并且子类重写了父类中的抽象方法:
另一种情况是,抽象类Shape被子类Cycle继承,但是子类不重写父类的抽象方法,那么子类也必须用 abstract 修饰,子类也是抽象类:
注意:如果在抽象类中写了一个构造方法,那么在子类中,除了要重写抽象方法外,还要再添加一个构造方法,否则会编译错误:
正确做法(快捷键---将光标放在错误的地方,然后按 Alt+回车),添加构造方法:
情况3:例如:
如果子类Rect继承了抽象类Shape且没有重写抽象方法,那么它也是抽象类,需要被abstract修饰,那么这时候如果再写一个子类Triangle继承抽象类Rect,在这个类中重写Rect中的抽象方法,那就不仅仅要写Rect的抽象方法,还需要写它的父类抽象类的抽象方法,即前面没有写的在这个时候都要写("之前的债,迟早要还的"),否则就会报错。
抽象类虽然不能实例化,但是它可以实现向上转型,然后调用Shape类中的内容:
- 5. 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
- 6. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
抽象类的作用
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
思考:普通的类也可以被继承, 普通的方法也可以被重写, 为什么非得用抽象类和抽象方法 呢?
因为使用抽象类相当于多了一重编译器的校验,我们知道抽象类中没有包含足够的信息去描述一个具体的对象,因此它不能被实例化。
用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类 了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。
(我们曾经用过的 final 也是类似. 创建的变量用户不去修改,相当于常量;加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们)
二、接口
接口的概念
在现实生活中,有许多接口,比如:笔记本上的USB口,电源插座等。
电脑的USB口上,可以插:U盘、鼠标、键盘...所有符合USB协议的设备
电源插座插孔上,可以插:电脑、电视机、电饭煲...所有符合规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。 在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
语法规则
接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。
语法格式:
【访问限定符】 interface 接口名称{
public static final 数据类型 变量名称;//接口中成员变量默认是public static final修饰的
//例如:
public static final int a = 10;
int a = 10;//一般可以略写前面的修饰
public abstract 返回类型 抽象方法名称();//接口中的抽象方法,默认都是public abstract修饰的
//例如:
public abstract void draw();
void draw();//一般可以略写前面的修饰
}
注意:
- 1. 创建接口时, 接口的命名一般以大写字母 I 开头.
- 2. 接口的命名一般使用 "形容词" 词性的单词.
- 3.接口中成员变量/常量默认是public static final修饰的,一般可以略写
- 4.接口中的抽象方法,默认都是public abstract修饰的,一般可以略写
- 5.抽象方法不能具体实现
- 6.接口中的方法默认是抽象方法,如果想要实现具体的方法,要加上一个访问限定符default、static、private
- 7.和抽象类一样,接口不可以进行实例化
接口使用
接口不能直接使用,必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。
public class 类名称 implements 接口名称{
// ...
}
在子类中,必须实现父类 / 抽象类中的抽象方法;而在类中,必须实现接口中的抽象方法:
示例:实现笔记本电脑使用USB鼠标、USB键盘的:
1. USB接口:包含打开设备(鼠标/键盘)、关闭设备功能(鼠标/键盘)
2. 笔记本类:包含开机功能、关机功能、使用USB设备功能
3. 鼠标类:实现USB接口,并具备点击功能
4. 键盘类:实现USB接口,并具备输入功能
运行结果:
接口特性
- 1. 接口类型是一种引用类型,但是不能直接new接口的对象
- 2.接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)
- 3.接口中的方法是不能在接口中实现的(不能具体实现),只能由实现接口的类来实现
接口的抽象方法都是不用具体实现的,即不能有方法体,写完一个抽象方法直接加一个 ; 即可
- 4.重写接口中方法时,不能使用默认的访问权限
前面我们学习多态的时候说过,如果子类要重写父类中的方法,子类的权限要大于等于父类的权限,在接口中也一样,如果一个类想要重写接口中的方法,那么它的权限要大于等于接口中方法的权限。
- 5.接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
- 6. 接口中不能有静态代码块和构造方法
接口定义 - 只能包含抽象方法、默认方法、静态方法和常量
不能有静态代码块和构造方法原因:
- 接口无法实例化 - 构造方法用于初始化对象实例,但接口不能直接实例化
- 静态代码块用于类初始化 - 接口不需要这种初始化机制,因为它只定义行为规范
- 接口字段隐式静态 - 接口中的所有字段默认都是public static final的,不需要静态块初始化
- 7. 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
- 8. 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
如果一个类不想实现这个接口当中的方法,那么此时这个类就可以被定义为抽象类,但是这个抽象类如果被继承,就得实现所有的没有被实现的方法
实现多个接口
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。
下面通过抽象类来表示一组动物:
思考:跑、飞、游泳 表示这些行为的抽象方法 可以都写在Animal这个抽象类当中吗?
显然不可以,如果写在这里,那意味着所有的子类都必须要重写这些方法,但是这些行为并不适合每一个子类(例如,狗并不会飞),并不是共性的。
那能不能将这些方法都写成一个父类,让符合的子类去继承它们?
当然不可以,Java不支持多继承,类和类之间是单继承,一个子类只能有一个父类
但是一个类可以实现多个接口,因此我们可以将这些行为写成一个个接口,然后让符合的子类去继承。所以接口解决了Java不能多继承的问题。
注意:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类。
(IDEA 中使用 ctrl + i 快速实现接口)
测试:
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .
例如:
鱼是一种动物, 具有会游泳的特性.
狗也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞
这样设计有什么好处呢?
有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.
例如,前面的跑、飞、游泳接口,针对的是动物类型的,但是接口并不关注具体类型,那我们可以写一个机器人类,它具有跑的功能,那么也可以继承 跑 这个接口。即 即使是不同的类型,只要这个类型具备实现接口的能力,就可以继承。
接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。
即:用接口可以达到 多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.
示例:
创建好两个接口IA、IB,然后再创建一个新的接口 IAmphibious(表示 "两栖的")去继承IA、IB接口,然后创建一个 D 类,去实现 IAmphibious 接口,除了实现 IAmphibious 自己的方法,还要继续实现 IA、IB的方法。
(接口间的继承相当于把多个接口合并在一起. )
三、Object类
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
范例:使用Object接收所有类的对象
所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下:
对于整个Object类中的方法需要实现全部掌握。
本文章当中,我们主要来熟悉这几个方法:toString()方法,equals()方法,hashcode()方法
对象比较equals方法
看以下的代码,obj1 和 obj2 引用的对象 姓名和年龄一模一样,它们应该相等,那么运行结果应该是 true:
但是,实际的输出却是 false,我们知道Student类是一个引用类型,那么obj1和obj2这两个引用存储的是指向的内容的地址,而这两个地址是不同的,因此它们是不相等的,输出的结果是 false
在理论上,这两个内容相同的引用应该是相等的,那么如何做才能得到我们所想的结果,也就是比较的是内容而不是引用变量的地址?
那就是使用 Object类 中的定义好的方法 equals,它的作用是指示其他的对象(作为参数传入的)是否等于当前对象,即用于比较任何两个对象的内容是否相等。
但是,我们看到输出的结果还是 false:
这是什么原因呢?—— 我们查看 equals 的源码(按住ctrl鼠标点击equals即可查看)发现,它的源码的意思就是我们原先写的 obj1 == obj2 的那段代码的意思(this表示obj1,obj表示obj2):
现在我们需要做的就是,重写 equais 方法,让它变成我们想要的样子,让它比较的是内容而不是引用:
- 如果obj这个引用是一个空引用,则返回false;
- 如果传入的对象就是当前对象本身,即引用指向同一个对象(引用同一个内存地址),就直接返回true,因为一个对象肯定等于自己;
- 如果不是同一个类型,即obj这个引用没有引用Student这个对象,就返回false;
- 判断完前面的情况,没有符合的条件,就进行下面这一步:
tmp表示obj2,此时让是父类Object的引用变量obj2 向下转型是为了能够使用子类Student类中的成员变量进行比较;this表示obj1;判断姓名和年龄,如果所有的内容都相等就会返回true,否则为false。
此时再次运行输出的结果就是 true:
总结
在Java中,==进行比较时:
- a.如果==左右两侧是基本类型变量,比较的是变量中值是否相同
- b.如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
- c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的
记住,比较对象中内容是否相同的时候,一定要重写equals方法。
获取对象信息toString方法
Object类中的toString方法作用是返回对象的字符串表示形式。
看以下的代码,我们使用toString方法的目的是获取对象中的信息内容:
但是,输出的结果是是一个地址,是student引用的地址,因为student引用的是一个对象,它存放的是这个对象的地址:
我们查看toString方法的源码,发现它默认输出就是一个地址:
如果要打印对象中的内容,可以直接重写Object类中的toString()方法(可以直接使用快捷键快速生成重写的toString方法,在之前讲的类和对象部分有也toString方法详细讲解):
此时输出的就是对象的信息内容:
hashCode方法
回忆刚刚的toString方法的源码:
我们看到了hashCode()这个方法,它帮忙算了一个具体的对象位置,它是个内存地址,然后调用Integer.toHexString()方法,将这个地址以16进制输出(了解即可)。
hashcode方法的作用就是返回对象的哈希码值(地址)
hashcode方法源码:
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。
我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来看示例代码:
输出的16进制地址是不一样的,也就是说不在同一个位置,即两个对象的hash值不一样:
重写hashcode方法,让它按照内容决定是否是同一个位置:
Objects类 是 java.util 包中的一个类,这个类中的 hash 方法的作用是 为输入值序列生成哈希码。可以利用这个方法为两个内容相同的引用生成同一个哈希码值。
equals、toString、hashCode方法的快捷键
IDEA 其实提供了重写这三个方法的快捷方式,可以快速生成:
首先右击鼠标,点击 Generate :
然后选择创建就好了:
四、接口使用实例
Comparable 接口
写一个学生类,重写toString方法:
然后创建两个学生对象,比较这两个学生谁大谁小,显然,这两个学生对象是不能直接比较大小的
和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确,但是两个学生对象的引用存放的是地址,地址是不能够比大小的,那么两个学生对象的大小关系怎么确定呢?
根据前面的经验,我们知道肯定是根据两个学生对象的内容进行比较,那么问题又来了,是根据姓名比较,还是年龄比较呢?
所以对于这种自定义类型,我们需要格外注意
1.当前的自定义类 到底是根据什么样的规则进行比较(姓名还是年龄)
2.这个规则该怎么定义
首先我们解决第一个问题,根据什么规则比较?
—— 我们需要让自定义类 Student 实现一个接口 Comparable ,并重写(实现)其中的 compareTo 方法。
我们查看Comparable接口的源码,可以看到它有一个抽象方法 compareTo,并且Comparable接口旁边还跟着一个 <T>,<>内 T 表示的是 比较哪个类型,就写哪个类(比如要比较的是Student类)
那么重写compareTo方法,就要知道按什么规则重写的,例如按年龄大小规则重写:
可以看到运行的结果是-9,那么我们就可以知道student1对象的年龄比student2对象的年龄小。
按姓名比较(姓名是一个String,即是一个引用类型,不能通过相减的方式比较大小)
我们可以看到String的源码中也实现了一个Comparable接口,所以它一定也会实现compareTo方法
所以我们可以这样比较姓名(注意:compareTo和C语言中的strcmp意思是一样的,比如'z' 比 'l' 大,就返回这两个字母间的差值):
(为什么不使用equals呢?—— equals的返回值是boolean布尔类型,即true/false,是比较两个对象是否一样的,并不是比较大小,而现在比较的是大小,且返回值是int,所以只能用compareTo)
那么,根据运行结果,说明student1中的姓名是比student2中的姓名要大的:
但是,使用compareTo这个方法进行比较,非常不方便,例如,一开始写的是根据年龄比较,那么每次在用这个类的时候,默认的比较方式都是根据年龄进行比较的,那么这时候突然改成按姓名进行比较,那么比较的结果就会完全相反,容易出错。
compareTo的缺陷:这个比较一般用于固定的比较,不适合非常灵活的比较,也就是说,非常的不解耦。所以,它一般用在默认的比较上。
解决这个问题:换一个接口解决(后面讲解)
我们再看compareTo方法的另一个问题:有许多个学生对象进行比较应该怎么比较呢?
创建一个数组,存放多个学生对象,即给对象数组排序:
在前面学习数组的时候,我们知道有一个方法 sort ,可以对数组进行排序,那么在这里能否直接使用这个方法呢?
运行结果发现报错了:
出现类型转换异常的错误,说明在我们想将学生对象强转为Comparable时出错了,不能进行转换,原因:因为我们上面写的这个Student类和Comparable接口并没有关系,即Student类和Comparable不是实现关系。
所以我们必须让Student类实现Comparable接口,并重写compareTo抽象方法,就能实现类型转换
再次运行起来,就会发现根据年龄排了:
通过上述的例子说明,使用sort方法时会实现Comparable接口并调用 compareTo 方法,sort方法对数组进行排序,默认是从小到大排序。
结论:只要是自定义类型 涉及到了 大小的比较,一定会实现Comparable接口。
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序的思路):
mySort方法的参数是Comparable接口数组的原因:做到标准统一,只要实现这个接口的。都可以使用mySort方法进行比较,不在乎类型(是人或者是动物只要符合就可以使用)
回忆冒泡排序的思路:是数组前一个元素比后一个元素大就交换,而现在我们是对象数组,里面的元素都是学生对象的引用,即学生对象内容的地址,地址不能进行比大小,这就需要用到Compara接口中的compareTo方法来比较,如果对象数组中前一个引用大于后一个引用,就交换。
如果想要从大到小排序,那么就是前一个引用比后一个引用大就不需要交换,如果是小于的话就交换:
那么我们回到compareTo方法中,按年龄为例:
this.age - o.age 如果相减大于0,返回结果就大于0,那就对应mySort方法中comparables[j].compareTo(comparables[j+1]) > 0,按从小到大排序;
o.age - this.age 如果相减大于0,返回结果就大于0,那就对应mySort方法中
comparables[j].compareTo(comparables[j+1]) < 0,按从大到小排序。
Comparator 接口
现在解决前面的一个问题——compareTo方法的缺陷:比较方式固定,不灵活。
解决方式:换一个接口,即根据不同的属性进行比较,不能每次重新修改 类 已经写好的方法
例如,此时要根据姓名进行比大小,使用另一个接口 Comparator
查看源码,它依然需要知道比较的是哪一个类型 <T> ,即为Student类;并且有一个抽象方法compate,它有两个Student类型的参数
那么我们想要按照姓名进行比大小,就要让一个类实现Comparator接口,并重写compare方法
那么这个实现了Comparator接口的类要怎么使用呢?
对于Arrays.sort()方法来说,它除了可以传一个对象数组引用,还可以传入第二个参数——实例化一个NameComparable类对象,然后将这个对象的引用作为第二个参数传入(sort方法重载),
这个引用可以调用其中的compare方法,让对象数组按照姓名比大小:
又例如,我们想按照年龄进行比大小,依然是让一个类实现Comparator接口,并重写compare方法:
然后再创建一个AgeComparable类的实例化对象,将其作为第二个参数传入sort方法中,按年龄从小到大排序:
这体现了Comparator比较器的灵活性/解耦,根据不同的属性进行比较,它不会修改 类 已经写好的默认的比较方法。
当然它也可以比较两个学生对象之间:
总结
使用Comparable接口进行比较,只能以一种方式进行比较,如果想要以另一种方式比较,还要对其进行修改,即在类原本写好的方法上进行修改;而使用Comparator接口非常灵活,可以写多个类,想用什么方式比较就传哪一个类,不会修改类已经写好的默认方法。
----------------------------------------------------------------------------------------------------------------------------
前面我们学习 方法 的时候,写过一个交换两个数的数值,当时说过是值传递,形参的改变不会影响实参:
解决方法之一就是使用引用类型——数组,进行传递参数并进行修改
那么还有另外一个方式——创建对象,通过对象的引用进行交换:
Clonable 接口和深拷贝
Clonable接口
看以下的代码:
根据Person类创建一个实例化的对象:
那么现在问题来了,能不能根据当前的这个person1对象克隆出来一份新的对象?
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝",而我们知道所有的类都是默认继承Object父类的,那么我们自定义的Person类也是如此,可以利用clone这个方法来进行克隆:
但是,我们发现实际操作起来clone方法不能被访问:
这是什么原因呢?
我们看到 clone 方法的源码,发现它是被 protected 修饰符修饰,这意味着clone方法只能在同一个包中或者不同包的子类访问;而当前的Person类和Object类是不同的包,但是Person类是Object类的子类,可以在Person子类中使用,但是在Test类中无法使用
如果想要在Test类中使用,必须重写Object类的clone方法:
重写快捷键:
那么此时在Test类中,就可以使用clone方法了:
但是为何还报错呢?——因为clone方法的返回值是父类,相当于想要将父类类型给子类,就必须发生向下转型,将其强转为Person类:
但是,为为为何还是报错呢?—— 注意,自定义类型如果想要被克隆,必须得实现一个接口Clonable
我们看Clonable接口的源码,发现它是一个空接口,意味着我们不需要重写该接口的任何抽象方法,那必须实现这个接口有什么用?—— 空接口也叫做标记接口,表示当前类是可以被克隆的
也就是说自定义的类型一定要实现这个接口,表示标记这个类可以被克隆
最后一步就是处理异常,我们重写的clone方法中有 throws CloneNotSupportedException
这个异常,必须处理完这个异常才可以使用(之后再讲),在Test类相应的位置也写上这一行异常:
按照上面的步骤做完就能成功将person1所指向的对象克隆一份出来,成为一个和person1对象内容相同的新的对象,它们有各自的地址,不指向同一个位置。
总结:想要实现clone方法,必须先实现Clonable接口。
浅拷贝
我们在前面代码的基础上,增加一个新的类Money,为Person类新增加一个Money类型的成员变量m,实例化一个Money类型的对象:
那我们在实例化person1对象后,person1这个引用存放了对象的地址,指向了对象;而对象中的成员变量m也是一个引用变量,它指向了Money类型的对象:
那么此时我们再创建一个Person类的对象,将person1引用的对象克隆给这个新的对象:
新对象person2成为一个和person1对象内容相同的新的对象,并且对象中的m引用变量指向了同一个Money类型的对象
基于以上的分析,我们写这样一段代码:
person1和person2对象中的引用变量m中的money成员变量修改前都是99.9,现在通过person2去访问m中的money,修改成9.9,那么它们都会被修改成9.9:
根据上面的分析,我们知道出现这样的结果是因为只克隆了Person对象,没有克隆Money对象
此时看到的这种现象叫做浅拷贝。
即说明Cloneable 拷贝出的对象是一份 "浅拷贝",通过clone,我们只是拷贝了Person对象。但是Person对象中的Money对象,并 没有拷贝。通过person2这个引用修改了m的值后,person1这个引用访问m的时候,值也发生了改变,这里就是发生了浅拷贝。
如果将Money也克隆了,那么就是深拷贝
深拷贝
想要让Money也进行克隆,需要Money类也实现Clonable接口并重写clone方法、然后再调用。
那么如何调用呢?
——通过修改Person类的clone方法来调用:首先创建一个Person类的tmp引用变量,调用Object类的clone方法赋值给tmp,此时tmp相当于先克隆了Person类对象;然后再调用当前对象(this,即是person1)的Money类型成员变量m中重写的clone方法,将其赋值给tmp的m变量,此时tmp在克隆了Person类对象的基础上,再克隆了Money类对象,最后将tmp返回,即返回给person2,达到了深拷贝的效果:
此时再次运行此代码,修改person2的m成员的money成员的值,不再会影响person1:
深拷贝和浅拷贝是根据代码的实现去实现的。
五、抽象类和接口的区别
抽象类和接口都是 Java 中多态的常见使用方式。
核心区别:
抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写),
而接口中 不能包含普通方法, 子类必须重写所有的抽象方法.