在JAVA学习有了一定基础和了解上将会继续深入学习多态,面向对象程序设计的三大支柱是封装、继承和多态,学习的步骤是循环渐进的,在学习了封装和继承的基础知识后进而了解多态。
什么是多态?多态意味着父类型的变量可以引用子类型的对象,注意这句话中的几个关键点,是父类型的变量,去引用子类型的对象。接下来的学习中就是去解释和运用这句话。
目录
HINT 1 继承
HINT 2 多态
HINT 1 继承
在引入多态之前,我们先再回顾一下继承的部分基础知识,我用最简白易懂的话来介绍自己对继承的理解:继承也就是说可以定义一个通用的类(也就是父类),之后继承该类为一个更加特定特殊的类,我们称之为子类。因为我们的关键是实实在在的对象,为了方便统筹,使用“类”对同一类型的对象建模,就是包含这些对象的共同点。那么不同的类,也就是说不同类型的对象可能会有一些共同的特征和行为,我们可以建一个通用类来表达这些共同之处,并被其他类共享,那么这些其他类就有了这些基本共同点,还可以有自己类独特的地方,这就是继承的关系,定义特定的类继承自通用类,且继承通用类中的特征和方法
这个还是比较好理解的,考虑一下几何对象,假设我们要设计类,来对像圆和矩形这样的几何对象建模,那么几何对象有许多共同的属性和行为,比如用某种颜色画出来,填充或者不填充等等,下面就是我们建的一个通用类 GeometricObject 来建模所有的几何对象,属性包括 color 和 filled,以及适用于这些属性的获取方法和设置方法(这里就涉及到私有变量等知识),当然还可以包括获取创建时间和返回该对象的 toString 字符串表示等,这些都是通用的:
public class GeometricObject //创建这样一个试用于所有几何的大类,简称父类
{
private String color = "white"; //一般设置为私有变量
private boolean filled; //布尔类型,是否填充
private java.util.Date dateCreated;
public GeometricObject() //创建一个初始的或者是错误的几何对象,初始构造函数
{
dateCreated = new java.util.Date();//记录创建时间
}
public GeometricObject(String color,boolean filled) //创建一个初始的对象包含颜色和是否填充
{
dateCreated = new java.util.Date(); //更新创建时间
this.color = color;
this.filled = filled;
}
public String getColor() //用户想得知颜色可以返回
{
return color;
}
public void setColor(String color) //用户在外部更改颜色
{
this.color = color;
}
public boolean isFilled()
{
return filled;
}
public void setFilled(Boolean filled)
{
this.filled = filled;
}
public java.util.Date getDateCreated()
{
return dateCreated;
}
//返回这个对象的基础信息,其实由前面学过的重载可知,我们这里是重载了toString这个函数,它的原始父类应该是在Object中
public String toString()
{
return "created on " + dateCreated + "\ncolor: " + color + " and filled: " + filled;
}
}
明确了这样一种关系之后(当然这里不对父类和子类的定义关系做过多阐述),我们可以再建两个子类去继承这个类,(子类可以从它的父类中继承可访问的数据域和方法,注意是可访问的!)除此之外还可以添加新的数据域方法,如下面的 Circle类(当然可以继续衍生出 Rectangle 类,这不是这篇的重点┭┮﹏┭┮,主要熟知继承关系!):
public class Circle extends GeometricObject //这就是Circle类,要记住使用该语法继承我们需要的类
{
private double radius; //然后就可以在该类里定义使用额外的数据和方法以及继承自父类的可访问的数据域和方法
public Circle() {} //构造方法,不理解的可以去搜索学习
public Circle(double radius)
{
this.radius = radius;
}
//我们在使用Circle类时也完全可以将颜色填充等等作为参数传入,因为它是子类,有继承关系
public Circle(double radius, String color, boolean filled)
{
this.radius = radius;
setColor(color); //当然不能直接写 this.color = color 这样的,因为他们是私有变量
setFilled(filled); //可以任意使用父类的可访问的方法
}
public double getRadius()
{
return radius; //因为我们设置的是私有变量,都要设置获取函数
}
public void setRadius(double radius)
{
this.radius = radius;
}
public double getArea() //返回圆面积
{
return radius * radius * Math.PI;
}
public double getDiameter() //返回圆直径
{
return 2 * radius;
}
public double getPerimeter() //返回圆周长
{
return 2 * radius * Math.PI;
}
public void printCircle() //重载的 toString方法,返回对象描述
{
System.out.println("The circle is created " + getDateCreated() + " and the radius is " + radius);
}
}
对继承有了一定的理解和掌握后,我们可以在main方法中调用这些类,创建这些类的对象来践行我们的操作代码,如下:
public class TestCircleRectangle
{
public static void main(String[] args)
{
//创建某个类的对象,即可调用该类的属性和方法等等
Circle circle = new Circle(1);
System.out.println("A circle " + circle.toString());
System.out.println("The color is " + circle.getColor());
System.out.println("The radius is " + circle.getRadius());
System.out.println("The area is " + circle.getArea());
System.out.println("The diameter is " + circle.getDiameter());
Rectangle rectangle = new Rectangle(2, 4);
System.out.println("\nA rectangle " + circle.toString());
System.out.println("The area is " + rectangle.getArea());
System.out.println("The perimeter is " + rectangle.getPerimeter());
}
}
继承的关系大概就是这样了,这里强调注意以下几个方面:
1.子类并不是父类的“子集”,而是继承了父类的方法等甚至加以属于自己这个类的更多的信息和方法
2.父类中的私有数据域在该类之外是不可访问的(对于每个类都如此),这些部分是不能在子类中直接使用的,当然如果父类中定义了公共的访问器/修改器,那么可以利用他们来达到我们的目的
3.不是所有的从属关系都应该用继承来建模,比如正方形是一种矩形,但并不能随便地就定义一个正方形类继承自矩形类,因为宽和高的属性并不适用于正方形,可以选择定义一个继承自几何类的正方形类,并为正方形的边定义一个 side 属性,这告诉我们在许多方面都需要满足父类的“通用性”,更不能仅仅为了重用某个方法而盲目地继承一个类,在不包含父类的多种属性和部分方法下这样的做法是毫无意义的,父类和它的子类之间必须存在“是一种”关系
4.这个也是我们在未知的状态下经常会选择的一种做法,就是从几个类中派生出一个子类,这种能力交“多重继承”,耨写程序设计语言是允许的,但是Java中不允许!一个 Java 类只可能直接继承自一个父类(单一继承),关于 extends 关键词的知识或者通过接口实现多重继承的知识大家可以多去了解,此处不再赘述
HINT 2 多态
首先我们前面学的继承,这个关系能使一个子类继承父类的特征,并且还能够附加一些新特征(使用重载等方式获得更广泛的用途),这样的定义使得子类是它的父类的“特殊化”,也就是每个子类的实例都是其父类的实例,当然反过来并不成立。例如:每个圆都是一个几何对象,但并非每个几何对象都是圆。
基于这层关系,表明总可以将子类的实例,传给需要父类型的参数。我们看一下下面这个代码:
public class PolymorphismDemo
{
public static void main(String[] args)
{
displayObject(new Circle(1, "red", false));
displayObject(new Rectangle(1, 1, "black", true));
}
//这里的Circle和Rectangle都是 GeometricObject 的两个子类
//我们下面设置的 displayObject函数具有 GeometricObject 类型的参数
//也就是说可以通过传递任意一个这个类型的实例(如我们刚刚创建的那两个对象,他们都是属于GeometricObject 这个父类型的实例的)去调用displayObject这个函数
public static void displayObject(GeometricObject object) //关键在这个参数的类型,理解为什么可以直接用类名定义
{
System.out.println("Created on " + object.getDateCreated() + " color is " + object.getColor());
}
//这样就做到了父类型的变量(color等)可以引用子类型的对象(如这里创建的两个对象)
}
理解上面的代码后,可以得知使用父类对象的地方都可以使用子类的对象,这就是我们通常所说的多态,简单来说就是意味着父类型的变量可以引用子类型的对象,或者这样理解:
1.多态是同一个行为具有多个不同表现形式或形态的能力 注意是同一种行为去发散,可以通过继承、接口等等去实现这样一个功能!
2.多态就是同一个接口,使用不同的实例而执行不同操作 如多个不同子类对象这样的
前面说到多态体现为父类引用变量可以指向子类对象,那么其实这个前提条件不仅仅只能是存在父子类(继承)关系,实现关系也是可以的,比如接口,判断一份代码是否存在多态,不能仅停留在判断两者是否具有继承的部分上,更应该抓住关键点:有无出现父类变量引用指向子类对象的情况。注意:在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法
怎么更好地理解上面这段话,扩展对多态的基础认知,我借鉴了下面这篇博客中提到的几个知识点,大家也可以去阅读或者去搜索更多更全面的博客推文等去加深自己的理解:
Java多态&多态的好处&多态的使用_张国昕的博客-CSDN博客_java 多态的作用
多态发生的前提是发生继承关系(或者用接口实现也行)和方法重写 (必须有!)
判断多态或者实现多态,记住这个要点:编译看左边(想要保存成功,使用父类提供的功能) 运行看右边(想要结果,找子类) 为了理解这句话,我们看一下下面这个样例:
public class TestMulti
{
public static void main(String[] args)
{
//创建子类的对象进行测试
Dog d = new Dog();
d.eat(); //输出结果显示 only eat meat
//创建多态对象测试
Animal a = new Dog(); //父类引用(Animal) 指向 子类对象(Dog)
//编译看左边:想要保存成功,必须 使用 左边 也就是父类提供的功能!
a.eat(); //运行看右边:最终执行结果以子类的实现为准,此处输出 only eat meat
}
}
class Animal // Animal是父类,下面是继承它的子类Dog
{
public void eat()
{
System.out.println("eat all");
}
}
class Dog extends Animal //多态前提一:包括继承关系(或用接口实现)
{
public void eat() //多态前提二:必须包括方法重写
{
System.out.println("only eat meat");
}
}
由上面这个例题的输出(两个输出均为 only eat meat)可以看出多态的好处:
1、可以把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程。 (也就是编译看左边)
2、统一调用标准,一切向父类看齐。
3、提高了程序的扩展性和可维护性。(这个就比较泛泛而谈了,运用在实际当中)
在了解这些之后我们再进行下面这个方法和数据域更广泛的多态例题解释,先理解代码及注释部分,其实这个多态并不算什么很高深的,就是基础的入门例题:
public class TestMulti
{
public static void main(String[] args)
{
// 创建多态对象测试
Fu f= new Zi() ; //仍然是父类引用(Fu) 指向 子类对象(这里的 new Zi())
//编译看左边,只要想用的功能,必须是父类提供的。
//运行看右边,多指发生了方法重写后,使用右边的也就是子类的方法体
//下面这个类方法的使用,一定是使用父类的(方法声明),但是!方法体由于可以重写,所以用子类的方法体!
f.study(); //结果会输出"我爱java",理解!
//成员变量的使用,一定是使用父类的,由于不存在重写,所以执行的也是父类的。
System.out.println( f.name ); // 结果输出jack,
//静态方法,可以存在重写吗? – 不可以!!!
//由于静态资源根本不存在重写,所以直接执行父类的方法声明和方法体。即使子类有一个和父类一模一样的方法也不是重写!!!
f.play(); //结果输出“儿子正在玩”
}
}
class Fu
{
String name = "Jack" ;
public void study()
{
System.out.println("爸爸正在学习");
}
static public void play() //注意区分,这是一个静态方法,有“static”关键词
{
System.out.println("爸爸正在玩儿");
}
}
class Zi extends Fu //多态前提一:包含继承关系(或者用接口实现也行)
{
String name = "xiongda"; //成员变量无法重写!
public void study() //多态前提二:必须包含方法重写
{
System.out.println("我爱java");
}
static public void play() //这个是子类特有的,不是重写!因为父类中定义的是静态方法
{
System.out.println("儿子正在玩儿");
}
}
上面这个例题还是很容易懂的,所以根据注释以及多态对象是把自己看做父类类型的基础上我们也可以总结出来以下几个要点:
1、成员变量:由于不存在重写,所以直接使用父类的
2、成员方法:由于存在重写,所以调用了父类的 方法声明,使用子类的方法体(这点在注释中也强调了!一定是使用父类的方法声明!)
3、静态成员:由于不存在重写,所以直接使用父类的方法声明和方法体(也可以说随着类的加载而加载,谁调用就返回谁的) 如果这里不太清楚静态变量(方法)和实例变量(方法),得先去复习温顾!
除此之外,我在学习多态的过程中还了解到关于向上转型和向下转型的知识点,其实就是在多态的继承关系中存在着两种转型方式,分别是向上转型(常用)和向下转型(较少见),下面复制粘贴一下他们通俗的定义:
向上转型(常用):可以把不同的子类对象都当作父类来看,进而屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,统一调用标准。
是不是觉得很熟悉,其实就是我们刚刚总结的多态的好处,所以向上转型是我们常用的,我们刚刚例题中用的那些方法和渠道都是向上转型,向上转型时,子类对象当成父类对象,只能调用父类的功能,如果子类重写了父类中声明过的方法,方法体执行的就是子类重过后的功能。但是此时对象是把自己看做是父类类型的,所以其他资源使用的还是父类型的。
这里有搜索到相关的一个挺有意思也容易理解的例子:向上转型就是,花木兰替父从军,大家都把花木兰看做她爸,但是实际从军的是花木兰,而且,花木兰只能做她爸能做的事,在军营里是不可以化妆的。
向下转型(较少):子类的引用的指向子类对象,过程中必须要采取到强制转型。这个是之前向上造型过的子类对象仍然想执行子类的特有功能,所以需要重新恢复成子类对象,其实,相当于创建了一个子类对象一样,可以用父类的,也可以用自己的,也就是向下转型时,是为了方便使用子类的特殊方法,也就是说当子类方法做了功能拓展,就可以直接使用子类功能。
这个虽然听着很复杂的样子,但如果直接自己想几个例题跑一遍还是非常容易理解的,但这样容易造成混乱,反而有种变得更不简洁的情况,还是分条件环境选择转型方式吧,当然对于这种转型,也可以按上面那个这样理解:花木兰打仗结束,就不需要再看做是她爸了,就可以”对镜贴花黄”了
以上是我在多态学习过程中自己理解的部分(加了一些继承的知识,不然不好转到多态),以及代码例题和大部分的知识点是自己写的,可能没有那么专业,但这个东西理解了还是比较容易懂的,都是新手入门所以涉及的不是特别难的部分,例题也都是入门款的,一起加油学习!