写在前面:
转载自:知乎 飞爱学习 作者:一个敲代码的汽车人
原文链接: https://zhuanlan.zhihu.com/p/139388786
我觉得对于多态、抽象、接口、覆盖的案例讲解很是生动、详细和透彻了。
必须强推!原作者是个人才!
1 抽象(Abstrac)
1.1 案例
咱们再加一点难度。
现在要求设计一个面积计算器,计算的对象包括平行四边形、梯形和圆。平行四边形的输入为高和宽,梯形的输入为高、上底长和下底长,圆的输入为直径(假定都是规范的数值输入),所有的输出均为面积。
规定:平行四边形的面积计算公式为宽x高;梯形的面积计算公式为(上底+下底)x 高/2;圆的面积计算公式为圆周率x半径的平方。
那么,基于继承,怎样设计最好?
1.2 代码
思考:将可继承的方法体(即有具体内容的方法)放在父类中以避免子类中重复代码的出现是继承的一大优势,但其并非是万能的。比如在这个案例中,三个面积计算公式都不一样,很难抽取出共同的方法体,但我们又希望子方法中都有面积计算方法且尽可能避免重复代码的出现,怎么办呢?
聪明如你,肯定想到了可以用刚才学到的多态知识实现,代码如下:
class Geometry { //定义几何图形类
public double height;
public String name;
public final double PI=3.1415;
public Geometry(double height,String name) {
this.height=height;
this.name=name;
}
public void calcuArea(Geometry geo) { //定义面积计算方法,用到多态
if (geo instanceof Parallelogram) {
Parallelogram par=(Parallelogram)geo;
System.out.println(par.name+"面积为:"+par.height*par.width);
}
else if (geo instanceof Trapezoid) {
Trapezoid tra=(Trapezoid)geo;
System.out.println(tra.name+"面积为:"+height*(tra.width_up+tra.width_down)/2);
}
else if (geo instanceof Cycle) {
Cycle cyc=(Cycle)geo;
System.out.println(name+"面积为:"+PI*Math.pow(cyc.height/2,2));
}
}
}
class Parallelogram extends Geometry{ //定义平行四边形类
public double width;
public Parallelogram(double height,double width,String name) {
super(height,name);
this.width=width;
}
}
class Trapezoid extends Geometry{ //定义梯形类
public double width_up;
public double width_down;
public Trapezoid(double height,double width_up,double width_down,String name) {
super(height, name);
this.width_up=width_up;
this.width_down=width_down;
}
}
class Cycle extends Geometry{ //定义圆形类
public Cycle(double diameter,String name) {
super(diameter,name);
}
}
public class Test {
public static void main(String[] args) {
//创建对象
ArrayList<Geometry> geometries = new ArrayList<Geometry>();
geometries.add(new Parallelogram(1, 3,"平行四边形"));
geometries.add(new Trapezoid(1, 1, 2,"梯形"));
geometries.add(new Cycle(2,"圆形"));
//循环执行各个对象的面积计算方法
for (Geometry geo : geometries) {
geo.calcuArea(geo);
}
}
}
1.3 代码分析
思考:以上代码确实实现了我们的需求,完成了继承,避免了重复代码的出现,但是总感觉哪里不对劲。仔细观察可以发现,每增加一个新的子类,我们就必须得在父类方法中做相应的修改,才能使新增子类也具备面积计算方法。作为一个堂堂正正的父类怎么能跟着子类的需求而变化呢?那这个父类岂不是很没“面子”?
所以,在后期功能拓展时,如何才能避免对上层结构的改动呢?
1.4 改进版代码
思考:既然在父类中难以提取通用的方法体,那我们可不可以只声明方法,而不具体实现呢?当然可以呀,我们可以用到中篇中提到的“覆盖”实现子类方法的定义,这样就避免了对父类的修改。
但是,这个实例化后的父类(比如Geometry类)是什么呢?有意义吗?没有意义的话怎么才能避免其被实例化呢?
解决方法见代码:
abstract class Geometry { //定义抽象类
public double height;
public Geometry(double height) {
this.height=height;
}
abstract public void calcuArea(); //定义抽象方法
}
class Parallelogram extends Geometry{
public double width;
public Parallelogram(double height,double width) {
super(height);
this.width=width;
}
@Override
public void calcuArea() {
System.out.println("平行四边形面积为:"+height*width);
}
}
class Trapezoid extends Geometry{
public double width_up;
public double width_down;
public Trapezoid(double height,double width_up,double width_down) {
super(height);
this.width_up=width_up;
this.width_down=width_down;
}
@Override
public void calcuArea() {
System.out.println("梯形面积为:"+height*(width_up+width_down)/2);
}
}
class Cycle extends Geometry{
public final double PI=3.1415;
public Cycle(double diameter) {
super(diameter);
}
@Override
public void calcuArea() {
System.out.println("圆形面积为:"+PI*(height/2)*(height/2));
}
}
public class Test {
public static void main(String[] args) {
ArrayList<Geometry> geometry = new ArrayList<Geometry>();
geometry.add(new Parallelogram(1, 2));
geometry.add(new Trapezoid(1, 1, 2));
geometry.add(new Cycle(2));
for (Geometry geo : geometry) {
geo.calcuArea();
}
}
}
1.5 改进版代码分析
任何封闭的几何图形都应该具有面积计算方法,但方法不一,难以提取出相同的实现代码,所以将其抽象。
以上代码用到了以下概念:
抽象方法(Abstrac Method):指使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
抽象类(Abstrac Class):指包含抽象方法的类。通过abstract方法定义规范,然后要求子类必须定义具体实现。抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。同时,通过在抽象类中定义封装的更改器和访问器,减少了子类的代码重复。
抽象的意义:
到这里,你有可能会有个疑问,既然子类都得通过覆盖实现自己的面积计算方法,为什么我们执意要用继承呢?
因为,我们需要用继承来提供一个规范,规范我们的成员变量和方法(即使没有具体的实现,也要有一致的方法名),没有规范,多态就无从谈起。如果说继承的基本用意是实现代码的复用性,那抽象就是继承的升华,它追求更高的精神境界,即契约(或规范)。抽象要求所有使用它的“用户”(即子类)都必须签订一份合约,这份合约规定子类必须实现抽象类所规定的所有方法。抽象方法意义在于就算无法实现出方法的内容,但还可以定义出一组子型共同的协议。
注意:
- 有抽象方法的类只能定义成抽象类。
- 抽象类中仍然可以定义非抽象方法。
- 抽象类中可以定义构造方法,只能用于子类的调用,不能对其自身实例化。
- 抽象类的子类必须对抽象类定义的抽象方法覆盖。
2 接口(Interface)
2.1 案例
咱们再加一点难度。
在案例1.1的基础上增加功能,要求轴对称图形输出其绕垂直中心轴旋转得到的几何体的体积,体积计算方法定义为calcuVolume。
那么,考虑到代码的复用性和功能的扩展性,怎样设计最优呢?
2.2 代码
思考:平行四边形不是轴对称图形,无法得到旋转的几何体;梯形旋转后可以得到圆台;圆形旋转后可以得到球。分析三个图形的特点可以得到如图所示的结构:
为此,可以想到如下两种方法:
方法一:将calcuVolume方法直接定义在具有轴对称性的子类中。
- 优点:简单粗暴。(我是一块砖,哪里需要就往哪里搬)
- 缺点:失去相似类之间的契约精神,比如有的定义的calcuVolume,有的定义的calcVolume,导致维护困难。其次,父类中没有共同的方法,从而无法实现多态。
方法二:将calcuVolume方法定义在Geometry抽象类中。
- 优点:所有Geometry的子类都将继承calcuVolume方法,包括后续扩展的子类,从而避免了方法一的缺点。
- 缺点:所有子类都必须实现calcuVolume方法,即便是不具备轴对称性质的子类,也被迫在类中声明这个方法(不含实际内容的空方法),显然太粗鲁(正所谓“强扭的瓜不甜”),这种行为也太浪费时间,特别在非轴对称子类数较多的情况下。
那么,如何才能优雅地解决这个问题呢?
详见如下代码:
abstract class Geometry { //定义抽象类
public double height;
public Geometry(double height) {
this.height=height;
}
public abstract void calcuArea(); //定义抽象方法
}
interface AxialSymmetry{
public static final double PI=3.1415;
public abstract void calcuVolume();
}
class Parallelogram extends Geometry{
public double width;
public Parallelogram(double height,double width) {
super(height);
this.width=width;
}
@Override
public void calcuArea() {
System.out.println("平行四边形的面积为:"+height*width);
}
}
class Trapezoid extends Geometry implements AxialSymmetry{
public double width_up;
public double width_down;
public Trapezoid(double height,double width_up,double width_down) {
super(height);
this.width_up=width_up;
this.width_down=width_down;
}
@Override
public void calcuArea() {
System.out.println("梯形的面积为:"+height*(width_up+width_down)/2);
}
@Override
public void calcuVolume() {
System.out.println("圆台的体积为:"+PI*height*(Math.pow(width_up/2,2)+Math.pow(width_down/2,2)+width_up*width_down/4)/3);
}
}
class Cycle extends Geometry implements AxialSymmetry{
final double PI=3.1415;
public Cycle(double diameter) {
super(diameter);
}
@Override
public void calcuArea() {
System.out.println("圆形的面积为:"+PI*(height/2)*(height/2));
}
@Override
public void calcuVolume() {
System.out.println("球的体积为:"+PI*Math.pow(height/2,3)*4/3);
}
}
public class Test {
public static void main(String[] args) {
//实现类的多态
ArrayList<Geometry> geometry = new ArrayList<Geometry>();
geometry.add(new Parallelogram(1, 2));
geometry.add(new Trapezoid(1, 1, 2));
geometry.add(new Cycle(2));
for (Geometry geo : geometry) {
geo.calcuArea();
}
//实现接口的多态
ArrayList<AxialSymmetry> axialSymmetry = new ArrayList<AxialSymmetry>();
axialSymmetry.add(new Trapezoid(1, 1, 2));
axialSymmetry.add(new Cycle(2));
for (AxialSymmetry axi : axialSymmetry) {
axi.calcuVolume();
}
}
}
2.3 代码分析
以上代码用到了以下概念:
接口(Interface):接口不是类,而是对希望符合这个接口的类的一组需求。可以说接口是比抽象更抽象的概念。抽象类还提供某些具体实现,而接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。接口的意义在于全面地、专业地实现了规范和具体实现的分离,便于实现模块化设计。
类与接口的关系:
- 类与类之间: 继承关系,一个类只能直接继承一个父类,但是支持多重继承。
- 类与接口之间: 只有实现关系,一个类可以实现多个接口。
- 接口与接口之间: 只有继承关系,一个接口可以继承多个接口。
类与接口的区别:
- 普通类:具体实现
- 抽象类:具体实现,规范(抽象方法)
- 接口:规范!
抽象类与接口的区别:
a.成员变量
- 抽象类可以有成员变量,也可以有常量
- 接口只能有常量,默认修饰符public static final
b.方法
- 抽象类可以有抽象方法,也可以有非抽象方法
- 接口只能有抽象方法,默认修饰符 public abstract
c.构造方法
- 抽象类有构造方法,为子类提供
- 接口没有构造方法
注意:
- 接口不能被实例化。
- 接口中的所有方法默认为public abstract。
- 接口不能有成员变量,但是可以包含常量,默认为public static final。
- 将类声明为实现某个接口,需要用到implements关键字,且这个类必须实现接口中所有的方法,并且这些方法只能是public的。
- 接口可以多继承,像类一样继承高层次的接口。
- 接口同样有多态。
3 练习
3.1 案例
好了,概念讲完了。咱们来做个练习,把所有讲到的概念都用起来!
要求:尽量使用本所介绍的知识,设计一个几何图形计算器,能计算图形的面积以及轴对称图形旋转后的体积。
- 几何图形:矩形、平行四边形、梯形、等腰三角形、非等腰三角形。
- 输入:宽和高(梯形再多输入一个上底宽)
- 输出:图形的面积以及轴对称图形绕垂直中心轴旋转后的体积。
3.2 代码
结合本文所学的知识,编写如下代码:
//定义几何图形的接口
interface Geometry {
public static final double PI=3.1415;
public abstract void calcuArea();
}
//定义轴对称图形的接口
interface AxialSymmetry{
public abstract void calcuVolume();
}
//定义四边形抽象类
abstract class Quadrangle implements Geometry{
private double height;
private double width;
public void setHeight(double height) {
this.height = height;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public double getWidth() {
return width;
}
public Quadrangle(double height, double width) {
setHeight(height);
setWidth(width);
}
}
//定义三边形抽象类
abstract class Triangle implements Geometry{
private double height;
private double width;
private String name;
public void setHeight(double height) {
this.height = height;
}
public void setWidth(double width) {
this.width = width;
}
public void setName(String name) {
this.name = name;
}
public double getHeight() {
return height;
}
public double getWidth() {
return width;
}
public String getName() {
return name;
}
public Triangle(double height, double width) {
setHeight(height);
setWidth(width);
}
@Override
public void calcuArea() {
System.out.println(getName()+"的面积为:"+getHeight()*getWidth()/2);
}
}
//定义矩形类
class Rectangle extends Quadrangle implements AxialSymmetry{
public Rectangle(double height, double width) {
super(height, width);
}
@Override
public void calcuArea() {
System.out.println("矩形的面积为:"+getHeight()*getWidth());
}
@Override
public void calcuVolume() {
System.out.println("矩形旋转得到圆柱体体积为:"+getHeight()*Math.pow(getWidth()/2, 2)*PI);
}
}
// 定义平行四边形类
class Rhombus extends Quadrangle{
public Rhombus(double height, double width) {
super(height, width);
}
@Override
public void calcuArea() {
System.out.println("平行四边形的面积为:"+getHeight()*getWidth());
}
}
//定义梯形类
class Trapezoid extends Quadrangle implements AxialSymmetry{
private double width_up;
public void setWidth_up(double width_up) {
this.width_up = width_up;
}
public double getWidth_up() {
return width_up;
}
public Trapezoid(double height, double width_up, double width) {
super(height,width);
setWidth_up(width_up);
}
@Override
public void calcuArea() {
System.out.println("梯形的面积为:"+getHeight()*(getWidth_up()+getWidth())/2);
}
@Override
public void calcuVolume() {
System.out.println("梯形旋转得到的圆台体积为:"+PI*getHeight()*(Math.pow(getWidth_up()/2,2)+Math.pow(getWidth()/2,2)+getWidth_up()*getWidth()/4)/3);
}
}
//定义等腰三角形类
class IsoscelesTriangle extends Triangle implements AxialSymmetry{
public IsoscelesTriangle(double height, double width) {
super(height, width);
setName("等腰三角形");
}
@Override
public void calcuVolume() {
System.out.println("等腰三角形旋转得到的圆锥体体积为:"+getHeight()*(PI*Math.pow(getWidth()/2, 2))/3);
}
}
//定义非等腰三角形类
class NotIsoscelesTriangle extends Triangle{
public NotIsoscelesTriangle(double height, double width) {
super(height, width);
setName("非等腰三角形");
}
}
public class Test {
public static void main(String[] args) {
// 实例化
ArrayList<Object> objects=new ArrayList<Object>();
objects.add(new Rectangle(1, 2));
objects.add(new Rhombus(1, 2));
objects.add(new Trapezoid(1, 1, 2));
objects.add(new IsoscelesTriangle(1, 2));
objects.add(new NotIsoscelesTriangle(1, 2));
// 多态
for (Object obj : objects) {
if (obj instanceof Geometry) {
Geometry geo=(Geometry)obj;
geo.calcuArea();
}
if (obj instanceof AxialSymmetry) {
AxialSymmetry axi=(AxialSymmetry) obj;
axi.calcuVolume();
}
}
}
}
3.3 代码分析
注意!以上代码一定不是最优方案,只是为了练习本文所学知识,因此,仅做参考。关于程序的模式设计问题以后再聊。
层次结构如下:
- Geometry和AxialSymmetry为接口。
- Quadrangle和Triangle为抽象类,都实现Geometry。
- Rhombus、Rectangle、Trapezoid为类,都继承于Quadrangle;其中,Rectangle和Trapezoid另外实现AxialSymmetry。
- IsoscelesTriangle和NotIsoscelesTriangle为类,都继承于Triangle,其中IsoscelesTriangle另外实现AxialSymmetry。
4 参考文献
[1]《Head First Java(第二版·中文版)》
[2]《Java核心技术·卷 I(原书第11版)》
[3] 菜鸟教程:https://www.runoob.com/java/j...
[4] 速学堂:https://www.sxt.cn/Java_jQuer...