文章目录
封装
装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外的提供的接口来访问该对象。
对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作。
1 封装的好处
使用封装有以下几个好处:
1、良好的封装能够减少耦合
2、类内部的结构可以自由修改
3、可以对成员进行更精确的控制
4、隐藏信息,实现细节
2 简单实现
从上面两个实例我们可以看出Husband里面wife引用是没有getter()的,同时wife的age也是没有getter()方法的,没有那个女人愿意别人知道她的年龄。
所以封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。比如我们将一个房子看做是一个对象,里面的漂亮的装饰,如沙发、电视剧、空调、茶桌等等都是该房子的私有属性,但是如果我们没有那些墙遮挡,是不是别人就会一览无余呢?没有一点儿隐私!就是存在那个遮挡的墙,我们既能够有自己的隐私而且我们可以随意的更改里面的摆设而不会影响到其他的。但是如果没有门窗,一个包裹的严严实实的黑盒子,又有什么存在的意义呢?所以通过门窗别人也能够看到里面的风景。所以说门窗就是房子对象留给外界访问的接口。
通过这个我们还不能真正体会封装的好处。现在我们从程序的角度来分析封装带来的好处。如果我们不使用封装,那么该对象就没有setter()和getter(),那么Husband类应该这样写:
public class Husband {
public String name ;
public String sex ;
public int age ;
public Wife wife;
}
我们应该这样来使用:
Husband husband = new Husband();
husband.age = 30;
husband.name = "张三";
husband.sex = "男"; //貌似有点儿多余
但是那天如果我们需要修改Husband,例如将age修改为String类型的呢?你只有一处使用了这个类还好,如果你有几十个甚至上百个这样地方,你是不是要改到崩溃。如果使用了封装,我们完全可以不需要做任何修改,只需要稍微改变下 Husband 类的***setAge()***方法即可。
public class Husband {
/*
* 对属性的封装
* 一个人的姓名、性别、年龄、妻子都是这个人的私有属性
*/
private String name ;
private String sex ;
private String age ; /* 改成 String类型的*/
private Wife wife;
public String getAge() {
return age;
}
public void setAge(int age) {
//转换即可
this.age = String.valueOf(age);
}
/** 省略其他属性的setter、getter **/
}
其他的地方依然那样引用***(husband.setAge(22))***保持不变。
到了这里我们确实可以看出,封装确实可以使我们容易地修改类的内部实现,而无需修改使用了该类的客户代码。
我们再看这个好处:可以对成员变量进行更精确的控制。
还是那个Husband,一般来说我们在引用这个对象的时候是不容易出错的,但是有时你迷糊了,写成了这样:
Husband husband = new Husband();
husband.age = 300;
也许你是因为粗心写成了,你发现了还好,如果没有发现那就麻烦大了,毕竟谁也没见过300岁的老妖怪啊!
但是使用封装我们就可以避免这个问题,我们对age的访问入口做一些控制**(setter)**如:
public class Husband {
/*
* 对属性的封装
* 一个人的姓名、性别、年龄、妻子都是这个人的私有属性
*/
private String name ;
private String sex ;
private int age ; /* 改成 String类型的*/
private Wife wife;
public int getAge() {
return age;
}
public void setAge(int age) {
if(age > 120){
System.out.println("ERROR:error age input...."); //提示錯誤信息
}else{
this.age = age;
}
}
/** 省略其他属性的setter、getter **/
}
上面都是对setter方法的控制,其实通过使用封装我们也能够对对象的出口做出很好的控制。例如性别我们在数据库中一般都是已1、0方式来存储的,但是在前台我们又不能展示1、0,这里我们只需要在**getter()**方法里面做一些转换即可。
public String getSexName() {
if("0".equals(sex)){
sexName = "女";
}
else if("1".equals(sex)){
sexName = "男";
}
else{
sexName = "人妖???";
}
return sexName;
}
在使用的时候我们只需要使用sexName即可实现正确的性别显示。同理也可用于针对不同的状态做出不同的操作。
继承
一、类继承简单实现
我们以动物、猫、狗,三个类做一个继承的简单讲解。
首先是动物类:
父类:动物类
我们在这里面完成一个属性,两个方法的编写:
public class Animal{
String name;
//方法:动物吃
public void eat(){
System.out.println("动物吃");
}
//方法:动物睡觉
public void sleep(){
System.out.println("动物睡觉");
}
}
接下来,我们看子类代码的实现:
子类
猫类
public class Cat extends Animal{
}
此时,我们在猫类中什么都不写,再编写测试类
//测试类
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Cat cat1 = new Cat();
cat1.sleep();
cat1.eat();
}
}
我们运行一下,可以看到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsTekVSR-1628211015903)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210803151640462.png)]
大家看一下,明明我们在Cat类中什么都没写,为什么我们还能调用eat、sleep方法?
那就是因为继承,通过代码可以发现,子类(Cat)并没有定义任何的操作,而在主类中所使用的全部操作都是由动物类(Animal)定义的,这证明:子类即使不扩充父类,也能维持父类的操作。
狗类
我们再来写一个狗类,使其继承动物类,并且在狗类中也添加一个方法:狗看家(look方法)
public class Dog extends Animal {
public void look() {
System.out.println("狗看家");
}
}
此时,我们已经在狗类中添加了一个方法,再在测试类中编写代码:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
//将刚才的猫类测试注释掉
/*
Cat cat1 = new Cat();
cat1.sleep();
cat1.eat();
*/
Dog dog1 = new Dog();
dog1.eat();
dog1.sleep();
dog1.look();
}
}
我们再运行一下,大家可以看到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oecW56qN-1628211015907)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210803152639743.png)]
以上的代码,子类对于父类的功能进行了扩充(狗类添加了一个look方法)。但是思考一下:子类从外表看是扩充了父类的功能,但是对于以上的代码,子类还有一个特点:子类实际上是将父类定义的更加的具体化的一种手段。父类表示的范围大,而子类表示的范围小。
二、类继承的详细讲解
1. 类继承中成员变量的访问
1.1 成员变量之间的访问 (变量不重名的情况)
先创建一个父类(Fu)
public class Fu {
public int numFu = 10; //关键字为public,可以直接通过(对象.变量名)访问,方便说明问题
}
在创建一个子类(Zi)
public class Zi extends Fu{
public int numZi = 20;
}
在测试类中分别建立父类和子类的对象:
public class Test {
public static void main(String[] args) {
//创建父类对象
Fu fu = new Fu();
//父类只能找到自己的成员numFu,并没有找到子类的成员numZi
System.out.println(fu.numFu);
//创立一个子类对象
Zi zi = new Zi();
//子类对象既可以打印父类的成员numFu,也可以打印自己的成员numZi
//还是那句"先人不知道后人的事情,而后人知道先人的事情"
System.out.println(zi.numFu);
System.out.println(zi.numZi);
}
}
输出打印结果为:
10
10
20
1.2 成员变量之间的访问 (变量重名的情况)
有两种情况:
1、直接通过对象访问成员变量:
等号左边是谁,就优先用谁,没有则向上找
2、间接通过成员方法访问成员变量:
该方法属于谁,就优先用谁,没有则向上找
现在,我们修改父类与子类:
父类Fu
public class Fu {
public int num = 10;
public void methodFu(){
//这里打印的num,一定是本类的,不会再往下找子类的
System.out.println(num);
}
}
子类Zi
public class Zi extends Fu{
public int num = 20;
public void methodZi(){
//这里打印的num,如果本类有,就优先打印本类的,如果没有再往上找
System.out.println(num);
}
}
第一种情况:直接通过对象访问成员变量
等号左边是谁,就优先用谁,没有则向上找。
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
// Zi zi = new Fu(); 不能通过父类来构造子类,先人(父类)根本不知道后人(子类)长什么样子
Fu fu = new Zi(); //可以通过子类来构造父类,这时等号左边是父类
System.out.println(fu.num); //10,打印的是父类的num
}
}
运行Test后,输出打印的是:
10
第二种情况:间接通过成员方法访问成员变量
该方法属于谁,就优先用谁,没有则向上找
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Fu fu = new Fu();
Zi zi = new Zi();
//打印的是父类的num,因为该类没有继承其它类,他自己肯定有一个num,才能写出这个方法
fu.methodFu(); //父类的num 10,补充:没有fu.methodZi(), 先人不知道后人的方法
//如果子类有一个num,那就优先打印本类的,没有的话再往父类那里找
zi.methodZi(); //子类的num 20
//重点!子类用的是父类的方法打印num,这就要看这个方法属于谁,是谁定义的这个方法
//因为methodFu()这个方法是属于父类的,打印的当然就是父类的num
zi.methodFu(); //父类的num 10
}
}
运行Test后,输出打印结果为:
10
20
10
1.3 成员变量在子类方法中的重名
父类:
public class Fu {
public int num = 10;
}
子类:
public class Zi extends Fu {
public int num = 20;
public void methodZi(){
int num = 30;
System.out.println(num); //30, 局部变量
System.out.println(this.num); //20, 本类的成员变量
System.out.println(super.num);//10, 父类的成员变量
}
}
此时,我们子类中有三个重名的变量num,我们怎么正确输出打印自己想要的那个呢?我们通过methodZi()方法能不能正确区分三个num呢?
我们在测试类中测试一下:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Zi zi = new Zi();
zi.methodZi(); //30, 20, 10
}
}
我们可以看到输出打印结果:
30
20
10
因此,我们可以得到这样一个结论:要想正确地打印想要的num,可以这样打
局部变量,上面的那个num = 30,就可以直接写
本类的成员变量,上面的num = 20, 用this.成员变量名
父类的成员变量,上面的num = 10, 用super.成员变量名
1.4 继承中成员方法重名问题
假如子类和父类都有一个方法叫 method() , 那怎么知道用的是哪一个呢?
父类:
public class Fu {
public int num = 10;
public void method(){
System.out.println("父类重名方法执行");
}
}
子类:
public class Zi extends Fu {
public int num = 20;
public void method(){
System.out.println("子类重名方法执行");
}
}
此时我们在Test中做出测试:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Fu fu1 = new Fu();
Fu fu2 = new Zi(); //通过子类来构造fu2
Zi zi = new Zi();
fu1.method(); //父类重名方法执行, 用的是父类方法
fu2.method(); //子类重名方法执行,用的是子类方法
zi.method(); //子类重名方法执行, 用的是子类方法
}
}
我们观察一下输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XqBSoiuI-1628211015909)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804145816891.png)]
其实这里涉及到了java三大特性中的多态。在这里只是大体说一下结论,后面我们会详细讲解一下java中的多态性。
我们根据上面运行结果,可以得到以下结论:
创建的对象是谁,用谁的类来构造对象的,就优先用谁,如果没有就向上找。比如Fu fu2 = new Zi(),fu2是用子类来构造的,那*fu2.method()*就是用的子类的方法。这也就是我们在多态中常说的“编译看左边,执行看右边”。
2. 继承的限制
2.1 单继承限制
在Java中,不允许类的多重继承(即子类从多个父类继承属性和行为),也就是说子类只允许有一个父类。父类派生多个子类,子类又可以派生多个子子类…… 这样就构成了类的层次结构。
例如:
class A{}
class B{}
class C extends A,B{} //C类继承了A,B两个父类
这就是多重继承
如果我们希望C类即继承了B类的操作,又继承了A类的操作,也就是希望一个子类,可以同时继承多个类的功能,我们应该怎么办呢?
class A{}
class B extends A{}
class C extends B{}
C实际上是属于(孙)子类,这样一来就相当于B类继承了A类的全部方法,而C类又继承了A和B类的方法,这种操作称为多层继承。
结论:Java之中只允许多层继承,不允许多重继承,Java存在单继承局限。
多层继承实现:
我们再定义一个哈士奇类,它是狗的一种,属于狗类的子类。
public class Hashiqi extends Dog{
}
此时,我们同样再哈士奇类中不实现任何方法,只让他继承狗类,我们再在测试类中编写如下代码,并运行查看结果:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
/*
Cat cat1 = new Cat();
cat1.sleep();
cat1.eat();
*/
Dog dog1 = new Dog();
dog1.eat();
dog1.sleep();
dog1.look();
System.out.println("*****哈士奇*******");
Hashiqi h1 = new Hashiqi();
h1.eat();
h1.sleep();
h1.look();
}
}
我们并不注释掉以前的狗类测试代码,而是让两者作比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hbL9d30H-1628211015910)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804081655119.png)]
可以看到,哈士奇类输出了与狗类测试代码相同的结果,这就说明,哈士奇类,同样继承了动物类(Animal)中的属性与方法,这就实现了哈士奇类即继承了狗类,也继承了动物类。
2.2 显隐继承
在一个子类继承的时候,实际上会继承父类之中的所有操作(属性、方法),但是需要注意的是,对于所有的非私有(no private)操作属于显式继承(可以直接利用对象操作),而所有的私有操作属于隐式继承(间接完成)。
首先,我们将动物类中的name属性进行封装:
public class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
我们已经对name属性进行了封装,并且实现了其set、get方法。此时,我们再让猫类继承动物类,并且在猫类中尝试对动物类的name属性进行操作:
public class Cat extends Animal{
public void Speak() {
System.out.println("I am "+name);
}
}
我们再在测试类中进行调用:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Cat cat1 = new Cat();
cat1.sleep();
cat1.eat();
cat1.setName("小花");
cat1.Speak();
}
}
当我们运行后,会发现,cat1对象在调用sleep、eat方法时很正常,当调用Cat中的Speak方法时,出现了报错:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-quC6yAH2-1628211015911)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804090529555.png)]
Unresolved compilation problem:
The field Animal.name is not visible
翻译过来就是:
未解决的编译问题:
字段Animal.name不可见
这是为什么呢?
其实这就是因为子类虽然继承了父类的属性,但是因为父类中name属性是私有的,外部类不能直接对其进行访问,需要通过get、set方法对其进行间接操作。
对上面猫类代码做出以下修改:
public class Cat extends Animal{
public void Speak() {
System.out.println("I am "+super.getName());
}
}
我们通过super关键字,调用父类中的get方法,实现对父类私有属性name的操作。
此时,再运行测试类,我们就会得到正确的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2OEHygQM-1628211015912)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804091429873.png)]
2.3 构造方法
在继承关系之中,如果要实例化子类对象,会默认先调用父类构造,为父类之中的属性初始化,之后再调用子类构造,为子类之中的属性初始化,即:默认情况下,子类会找到父类之中的无参构造方法。
我们在动物类Animal中编写一个无参构造方法,并在猫类中也写一个,最后在测试类中进行测试:
动物类:
public class Animal {
private String name;
public Animal() {
System.out.println("动物类无参构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
猫类:
public class Cat extends Animal{
public Cat() {
System.out.println("猫类无参构造方法");
}
public void Speak() {
System.out.println("I am "+super.getName());
}
}
测试:我们在测试类中只声明创建一个对象,不执行任何操作;
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Cat cat1 = new Cat();
}
}
我们会看到如下结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mp9wMNCV-1628211015912)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804092353233.png)]
这个时候虽然实例化的是子类对象,但是发现它会默认先执行父类构造,调用父类构造的方法体执行,而后再实例化子类对象,调用子类的构造方法。而这个时候,对于子类的构造而言,就相当于隐含了一个super()的形式:。即猫类的代码实际上是:
public class Cat extends Animal{
public Cat() {
super();
System.out.println("猫类无参构造方法");
}
public void Speak() {
System.out.println("I am "+super.getName());
}
}
现在默认调用的是无参构造,而如果这个时候父类没有无参构造,则子类必须通过super()调用指定参数的构造方法:
我们对动物类进行一下修改:
public class Animal {
private String name;
public Animal(String string) {
System.out.println("动物类有参构造方法"+"*********"+string);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
动物类中无参构造方法已经被修改成有参构造方法,此时,我们如果不对猫类做任何修改,我们会看到:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rDgPIhZD-1628211015913)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804094035766.png)]
可以看到,在猫类的无参构造方法中报错了。因此,我们也必须对其进行一下修改;
public class Cat extends Animal{
public Cat() {
super("父类");
System.out.println("猫类无参构造方法");
}
public void Speak() {
System.out.println("I am "+super.getName());
}
}
此时我们看一下测试类运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGCKnPik-1628211015914)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210804094252682.png)]
很显然,在任何的情况下,子类都逃不出父类构造的调用,super调用父类构造,这个语法和this()很相似:super调用父类构造时,一定要放在构造方法的首行上。
3 变量的隐藏:super与this关键字
3.1 super关键字的三种用法
第一种情况:在子类的成员方法中,访问父类的成员变量。如:
父类:
public class Fu {
public int num = 10;
}
子类:
public class Zi extends Fu {
public int num = 20;
public void methodZi(){
// System.out.println(num); 这样打印的一定是本类的num
System.out.println(super.num); //打印的是父类的num
}
}
第二种情况:在子类的成员方法中,访问父类的成员方法。如:
父类:
public class Fu {
public void methodFu(){
System.out.println("父类的成员方法执行");
}
}
子类:
public class Zi extends Fu{
public void methodZi(){
super.methodFu(); //访问父类的methodFu()方法
System.out.println("子类的成员方法执行");
}
}
第三种情况:在子类的构造方法中,访问父类的构造方法。在上面继承的限制中有讲到。
3.2 this关键字
this关键字同样有三种用法:
在本类的成员方法中,访问本类的成员变量
在本类的成员方法中,访问本类的另一个成员方法
在本类的构造方法中,访问本类的另一个构造方法
如:
ublic class Zi extends Fu {
private int num = 10;
public Zi(){
this(123); //在本类的无参构造中调用有参构造
}
public Zi(int num){
this.num = num;
}
public void methodZi(){
System.out.println(this.num); //在本类的成员方法中,访问本类的成员变量
}
public void methodA(){
System.out.println("A方法");
}
public void methodB(){
this.methodA(); //在本类的成员方法中,访问本类的另一个成员方法
System.out.println("B方法");
多态
1 覆盖(Override)方法/重写
在类继承中,子类可以修改从父类继承来的方法,也就是说子类能创建一个与父类方法有不同功能的方法,但具有相同的名称、返回值类型、参数列表。
如果在子类中定义一个方法,其名称、返回值类型和参数列表正好与父类中的相同,那么,新方法被称做覆盖旧方法即重写父类方法。
我们再动物类中定义了两个方法:eat、sleep
public class Animal {
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
现在,我们在狗类中重写一下eat方法:
public class Dog extends Animal {
public void eat() {
System.out.println("狗吃肉");
}
public void look() {
System.out.println("狗看家");
}
}
在测试类中体会一下重写:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Dog d1 = new Dog();
d1.eat(); //这里调用的eat方法是狗类中重写后的方法
d1.sleep();
}
}
运行结果为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1UA8rDb6-1628211015915)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210805144725386.png)]
那么,问题来了,我还能不能通过对象 d1 调用 Animal 中的 eat 方法呢?
比如,我们在main方法中这样写:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Animal d1 = new Dog();
d1.eat();
d1.sleep();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IpwYf74A-1628211015915)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210805150046689.png)]
可以看到,依旧是调用的子类的方法。
所以如果不是类内部,而是外部调用,比如在我们例子中的 main 方法,答案是不能***。你不能调用已经被覆盖掉的父类的方法。 这也是覆盖override*的最初设计意图之一。 大多数时候,覆盖父类意味着子类想做些特殊的处理。如果能够跳过子类的特殊处理,就会打开一个无法控制的缺口,会导致很多很多问题。
唯一可以调用父类方法的地方,就只有类内部。子类自己控制,什么时候该调用父类,什么时候做自己的处理。也就是通过我们上面说讲到的super关键字在子类内部进行调用。(这里我们就不再讲述了)
所以,对于重写,总结如下:
如果子类没有重写父类的方法,调用父类的方法的时候,实际上是去父类的内存中实现,可以调用父类方法。
如果子类重写了父类的方法,那么,我们即使上溯造型到了父类,由于内存还是子类,该方法的实现还是在子类,所以用实例化的对象是调用不到父类的,这种情况下,只能用super关键字。
注意:覆盖不会删除父类中的方法,而是对子类的实例隐藏,暂时不使用。
覆盖和重载的不同:
方法覆盖要求参数列表必须一致,而方法重载要求参数列表必须不一致。
方法覆盖要求返回类型必须一致,方法重载对此没有要求。
方法覆盖只能用于子类覆盖父类的方法,方法重载用于同一个类中的所有方法(包括从父类中继承而来的方法)。
方法覆盖对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制。
父类的一个方法只能被子类覆盖一次,而一个方法可以在所有的类中可以被重载多次。
2 多态的基本介绍
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
我们依旧回到上面我们所讲述的动物类、猫类、狗类中,我们家中有两个动物,一只小猫和一条小狗:
动物 a = 小猫;
动物 b = 小狗;
这里所表现的的就是多态。猫、狗都是动物类的子类,我们只是通过动物这一个父类就能够引用不同的子类,这就是多态——我们只有在运行的时候才会知道引用变量所指向的具体实例对象。
诚然,要理解多态我们就必须要明白什么是**“向上转型”**。在继承中我们简单介绍了向上转型,这里就在啰嗦下:
Dog d1 = new Dog();
对于这个代码我们非常容易理解无非就是实例化了一个狗类的对象嘛!但是这样呢?
Animal a1 = new Dog();
在这里我们这样理解,这里定义了一个 Animal 类型的 a1 ,它指向 Dog 对象实例。由于Dog是继承于Animal,所以 Dog 可以自动向上转型为 Animal ,所以 a 是可以指向 Dog 实例对象的。这样做存在一个非常大的好处,在继承中我们知道子类是父类的扩展,它可以提供比父类更加强大的功能,如果我们定义了一个指向子类的父类引用类型,那么它除了能够引用父类的共性外,还可以使用子类强大的功能。
但是向上转型存在一些缺憾,那就是它必定会导致一些方法和属性的丢失,而导致我们不能够获取它们。所以父类类型的引用可以调用父类中定义的所有属性和方法,对于只存在与子类中的方法和属性它就望尘莫及了。
现在,我们对动物类进行一下修改:
public class Animal {
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
猫类:
public class Cat extends Animal{
//重载
public void eat(String a){
System.out.println("猫吃鱼");
}
//重写
public void sleep() {
System.out.println("猫睡觉");
}
}
我们在子类猫类中对动物类中的两个方法进行了重写与重载,我们在测试类中看一下测试结果:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Animal a1 = new Animal();
a1.eat();
a1.sleep();
System.out.println("**************");
Cat c1 = new Cat();
c1.eat();
c1.eat("111");
c1.sleep();
System.out.println("**************");
Animal a2 = new Cat();
a2.eat();
a2.sleep();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pTPEL27e-1628211015915)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210805160647027.png)]
我们可以看到,在 a2 调用 eat 方法时,由于 Cat 中的 eat(String a)是重载之后的,父类 Animal 中并没有这个方法,所以在 a2 的方法调用时并没有 eat(String a)这个方法。
3 多态的实现
3.1 多态的前提
Java实现多态有三个前提:
1、继承
2、重写(不是必须)
3、有父类引用指向子类对象
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
3.2 多态的实现
3.2.1 基于继承实现的多态
多态中成员变量访问特点:
成员变量:编译看左边,执行看左边
成员方法:编译看左边,执行看右边
比如:
我们对动物类进行修改:
public class Animal {
public String name = "动物";//在这里,为了方便讲解,我们定义为public
public void eat(){
System.out.println("动物吃");
}
public void sleep() {
System.out.println("动物睡觉");
}
}
狗类:
public class Dog extends Animal {
public String name = "狗";
public int age = 20;
public void eat() {
System.out.println("狗吃肉");
}
public void look() {
System.out.println("狗看家");
}
}
测试类:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Animal a1 = new Dog();
System.out.println(a1.name);
System.out.println(a1.age);
a1.eat();
a1.look();
a1.sleep();
}
}
我们看一下运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQqBUMgz-1628211015916)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210806082731284.png)]
竟然报错了。这是因为 age 是子类独有的成员变量,a1 并不能访问,编译不通过。方法 look 也是同样道理。
正确的应该是:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Animal a1 = new Dog();
System.out.println(a1.name);
//System.out.println(a1.age);
a1.eat();
//a1.look();
a1.sleep();
}
}
此时,我们看一下运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TspAAhmJ-1628211015917)(F:\我的安装包\Typora\工作\java语言程序设计\类继承\image-20210806083104799.png)]
我们来分析一下运行结果,name 属性是成员变量,遵循编译看左边,执行看左边的原则,所以,虽然子类中也有一个 name ,但其实 a1 并不访问,而方法 eat 是成员方法,并且在子类中重写了,遵循编译看左边,执行看右边原则,因此我们调用的是a1 = new Dog() 的 Dog 中的方法。
3.2.2 基于接口实现的多态
继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。
具体,会在接口中讲到。
3.3 多态的优劣
多态的好处:提高了程序的扩展性
具体体现:定义方法的时候,使用父类型作为参数,将来在使用的时候,使用具体的子类型参与操作。
多态的弊端:不能使用子类的特有功能
附:经典案例
public class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
public class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
public class C extends B{
}
public class D extends B{
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D