0 前言

初学JAVA时,总会对一些概念一知半解,相互混淆,不明其设计的用意,如类、对象、重载、封装、继承、多态、覆盖、抽象类、接口概念。为便于理解和巩固,本文将基于一个案例及其变形,展现各个概念的定义、设计用意、使用规范和注意事项。

长文警告,建议先收藏后阅读!

1 类(Class)、对象(Object)和构造器(Constructor)

1.1 案例

要求设计一个矩形的面积计算器,输入为矩形的高(height)和宽(width),输出为矩形的面积(area)。

1.2 代码

对JAVA的语法有最基本的了解后可以写出如下代码:

class Rectangle{                                //创建矩形类
    public double height;                       //定义类的成员变量
    public double width;                           
    
    public Rectangle() {                        //定义无参构造器--可省略
    }
    
    public void calcuArea() {                   //定义类的方法--面积计算
        System.out.println("面积为:"+height*width);
    }
}

public class Test{
    public static void main(String[] args) {
        Rectangle rec=new Rectangle();          //创建矩形对象--调用构造器
        rec.height=1;                           //高度赋值
        rec.width=2;                            //宽度赋值
        rec.calcuArea();                        //调用面积计算方法
    }
}

1.3 代码分析

以上代码包含以下概念:
1、类(Class):类是构造对象的模板或蓝图,其由成员变量和方法构成,前者记录数据,后者记录数据的操作过程。
2、对象(Object):对象是类的实例化,一个类可以有多个对象。
3、构造器(Constructor):构造器是一种特殊的方法,用于对象实例化时的初始化操作。

注意:

  • 构造器总是和关键字“new”一起使用。
  • 构造器的方法名必须和类名一致!
  • 任意一个类都包含至少一个构造器,当没有自定义构造器时,编译器会自动设定为无参构造器;但如果自定义了有参构造器,并需要调用无参构造器时,就必须自己手动写。
  • 构造器中,this()用于调用同一类中的其他构造方法;super()用于调用父类的构造方法。都必须位于构造方法中的第一行,且两者不能同时存在。

思考:以上代码虽然实现了基本功能,但其功能不够完善,比如当用户输入的高度和宽度为字符串时,代码就会报错,那怎么才能在不给用户输入添加麻烦的情况下实现功能呢?

2 重载(Overload)

2.1 案例

要求设计一个矩形的面积计算器,输入为矩形的高和宽(数字或者字符串输入,假定用户输入的字符串都是数值型字符串),输出为矩形的面积。

2.2 代码

class Rectangle{                                    
    public double height;                           
    public double width;                           
    
    //定义无参构造器--可省略
    public Rectangle() {                            
    }
    
    //定义有参构造器1
    public Rectangle(double height,double width) { 
        this.height=height;       //this指代当前对象,用于区分方法中的形参
        this.weight=width;
    }
    
    //定义有参构造器2
    public Rectangle(String height,String width) { 
        this.height=Double.valueOf(height);
        this.weight=Double.valueOf(width);
    }
    
    public void calcuArea() {                   
        System.out.println("面积为:"+height*width);
    }
}

public class Test{
    public static void main(String[] args) {
        Rectangle rec=new Rectangle(1,2);       //创建矩形对象--调用构造器1
        rec.calcuArea();                        //调用面积计算方法
        
        Rectangle rec1=new Rectangle("1","2");  //创建矩形对象--调用构造器2
        rec1.calcuArea();                       //调用面积计算方法
    }
}

2.3 代码分析

以上代码包含以下概念:
重载(Overload):指类中多个方法具有相同的名字,但参数类型或返回类型不同的现象。重载的设定是为了方便用户操作,以相同的方法名实现特定的功能,同时匹配不同的参数类型以满足功能的扩展性和需求的多样性。在上面的代码中我们实现了构造器方法的重载,完美解决了用户输入多样性的问题,但重载并不局限于构造器方法,它可以适用于类中的任何方法。

注意:

  • 不允许方法名和参数类型相同,但返回类型不同的情况出现,因为程序无法判断返回哪一个。
  • 方法的形参类型相同但顺序不同也构成重载,如add(double a,int b)和add(int a,double b)。

思考:以上代码虽然解决了之前提出的问题,但是仍存在一个巨大的安全隐患,即用户可以直接通过“rec.height”和“rec.width”对矩形的高和宽赋值,这会导致两个我们不愿意看到的情景。一是,当用户输入非常规数值(比如-1)时,计算的结果是没有意义的;二是,后期更改程序时,难以调试,比如我们后期要将height的变量类型改为String类型,那么就必须更改每一处height赋值的地方(如代码1.2)。那么,如何改进呢?

3 封装(Encapsulation)

3.1 案例

要求设计一个矩形的面积计算器,输入为矩形的高和宽(数值输入),且数值都应大于零,输出为矩形的面积。同时要避免2.3中所述的问题。

3.2 代码

class Rectangle{
    private double height;                          //私有化实例字段
    private double width;
    
    public void setHeight(double height) {          //定义更改器方法
        if (height<0) {
            this.height =0;
        }
        else {
            this.height = height;
        }
    }
    public void setWidth(double width) {
        if (width<0) {
            this.width =0;
        }
        else {
            this.width = width;
        }
    }
    public double getHeight() {                     //定义访问器方法
        return height;
    }
    public double getWidth() {
        return width;
    }
    public void calcuArea() {
        System.out.println("面积为:"+height*width);
    }
}

public class Test{
    public static void main(String[] args) {
        Rectangle rec=new Rectangle();
        rec.setHeight(-1);                         //设定高
        rec.setWidth(2);                           //设定宽
        rec.calcuArea();   
        //读取高和宽
        System.out.println("高为:"+rec.getHeight()+" 宽为:"+rec.getWidth());
    }
}

3.3 代码分析

以上代码包含以下概念:
封装(Encapsulation):通过私有化类的成员变量,并创建相应的公有化的更改器(即设定成员变量的独立方法,如setHeight)和访问器(即读取成员变量的独立方法,如getHeight)实现对成员变量的封装。

设计用意:

  • 成员变量私有化的设计,使用户无法通过“rec.height”和“rec.width”对矩形的高和宽赋值与读取。
  • 更改器和访问器公有化的设计,使用户可以随意调用,实现对成员变量的赋值与读取。
  • 在更改器中我们可以设定数值规范,避免因用户的错误输入所带来的问题。
  • 在后期调试中,我们只需要改写类的成员变量类型及相应的更改器便可,无需修改每个实例化对象的相关代码。

4 继承(Inheritance)

4.1 案例

咱们稍微加一点难度。
现在要求设计一个面积计算器,计算的对象不仅仅是矩形,还包括平行四边形。输入仍然为高和宽(假定都是规范的数值输入),输出为面积。

4.2 代码

为了简化代码,前面已经实现过的内容(比如封装)就不再展开写了。

class Rectangle{                                  //定义矩形类
    public double height; 
    public double width;
    
    public Rectangle(double height,double width) {
        this.height=height;
        this.width=width;
    }
    
    public void calcuArea() {
        System.out.println("矩形的面积为:"+height*width);
    }
}

class Parallelogram{                               //定义平行四边形类
    public double height; 
    public double width;
    
    public Parallelogram(double height,double width) {
        this.height=height;
        this.width=width;
    }
    
    public void calcuArea() {
        System.out.println("平行四边形的面积为:"+height*width);
    }
}

public class Test {
    public static void main(String[] args) {
        Rectangle rec=new Rectangle(1, 2);
        rec.calcuArea();
        Parallelogram par=new Parallelogram(1, 3);
        par.calcuArea();
    }
}

4.3 代码分析

思考:以上代码写起来挺简单,但是有个问题,就是代码重复率太高!这才两个类,要是有成百上千个这种类,不仅写的累死,后期维护也得累死。

有没有什么偷懒的办法呢?当然有啦,就是后面要介绍的继承。

4.4 改进版代码

class Quadrangle{                               //定义父类--四边形类
    public double height; 
    public double width;
    public String name;
    
    public Quadrangle(double height,double width,String name) {
        this.height=height;
        this.width=width;
        this.name=name;
    }
    
    public void calcuArea() {
        System.out.println(name+"面积为:"+height*width);
    }
}

class Rectangle extends Quadrangle{             //定义子类--矩形类
    public Rectangle(double height,double width,String name) {
        super(height,width,name);               //super指调用父类的构造方法
    }
}

class Parallelogram extends Quadrangle{         //定义子类--平行四边形类
    public Parallelogram(double height,double width,String name) {
        super(height,width,name);
    }
}

public class Test {
    public static void main(String[] args) {
        Rectangle rec=new Rectangle(1, 2,"矩形");
        rec.calcuArea();
        Parallelogram par=new Parallelogram(1, 3,"平行四边形");
        par.calcuArea();
    }
}

4.5 改进版代码分析

诶?咋一看,好像并没有省事哦?
那是因为我们这里只有两个子类,如果换成实现成百上千个这种子类,差距就会拉开了。咱们来分析一下这是如何实现的。

以上代码包含以下概念:
继承(Inheritance):即基于已有的类(称之为父类或超类)来创建新的类(称之为子类),顾名思义,子类将继承父类所有的成员变量及方法。继承一般应用于类与类较相似的情况下,比如本案例中,矩形类与平行四边形类的成员变量和方法高度相似,可以提取两者的共同代码,构造一个四边形类作为父类,从而避免了重复代码,也方便了后期功能的扩展及维护。

注意:

  • JAVA中类只能单继承(即只能继承一个父类),但不仅限于一个层次,Object是所有类的终极父类,是万物之源。
  • 子类继承父类时需要用到关键字“extends”。
  • 子类能继承父类所有的成员变量和方法(除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
  • 若父类定义了构造函数,子类需要用super调用父类的构造方法,且必须位于子类构造方法中的第一行。
  • 子类继承了父类,不代表只能用父类的东西,还可以定义自己的成员变量及方法,甚至于可以改写父类的方法(即后文讲到的覆盖)。

5 覆盖(Override)和多态(Polymorphism)

5.1 案例

咱们稍微再加一点难度。
现在要求设计一个面积计算器,计算的对象不仅仅是矩形和平行四边形,还包括梯形。矩形和平行四边形的输入为高和宽,梯形的输入为高、上底长和下底长(假定都是规范的数值输入),输出都为面积。
规定:矩形和平行四边形的面积计算公式为宽x高;梯形的面积计算公式为(上底+下底)x 高/2

5.2 代码

思考:我们仍然可以采用继承来实现,但是梯形的面积计算方法与矩形和平行四边形不同,如何以最简洁的方法实现代码?

具体代码如下:

class Quadrangle{                               //定义父类--四边形类
    public double height; 
    public double width;
    public String name;
    
    public Quadrangle(double height,double width,String name) {
        this.height=height;
        this.width=width;
        this.name=name;
    }
    
    public void calcuArea() {
        System.out.println(name+"面积为:"+height*width);
    }
}

class Rectangle extends Quadrangle{             //定义子类--矩形类
    public Rectangle(double height,double width,String name) {
        super(height,width,name);               
    }
}

class Parallelogram extends Quadrangle{         //定义子类--平行四边形类
    public Parallelogram(double height,double width,String name) {
        super(height,width,name);
    }
}

class Trapezoid extends Quadrangle{             //定义子类--梯形类
    public double width_up;                     //自定义成员变量--上底宽
    public Trapezoid(double height,double width_up,double width_down,String name) {
        super(height,width_down, name);
        this.width_up=width_up;
    }
    @Override
    public void calcuArea() {                   //覆盖父类的面积计算方法
        System.out.println(name+"面积为:"+height*(width_up+width)/2);
    }
}
public class Test {
    public static void main(String[] args) {
        Rectangle rec=new Rectangle(1, 2,"矩形");
        rec.calcuArea();
        Parallelogram par=new Parallelogram(1, 3,"平行四边形");
        par.calcuArea();
        Trapezoid tra=new Trapezoid(1, 1, 2,"梯形");
        tra.calcuArea();
    }
}

5.3 代码分析

以上代码用到了以下概念:
覆盖(Override):指在继承中,父类的有些方法在子类中不适用,子类重新定义的手段。在本案例中,梯形类对calcuArea方法实现了覆盖。

注意:

  • 若子类中被“覆盖”方法的参数类型不同,返回类型不一致,这不是覆盖,而是重载。覆盖要求参数类型必须一样,且返回类型必须兼容。总之,子类对象得保证能够执行父类的一切。
  • 不能降低覆盖方法的存取权限,如public变成private。
  • 若不希望父类的某个方法被子类覆盖,可以用final修饰该方法。甚至可以扩展到将类用final修饰,则其中所有的方法均不可覆盖,但不影响成员变量的赋值。

思考:Test类中的语句块有点啰嗦,同样是初始化加调用面积计算方法,三个对象实现了三次,那如果有成百上千个类岂不是要累死,这能否优化呢?

5.4 改进版代码

对Test类进行优化可以得到如下代码:

public class Test {
    public static void main(String[] args) {
        //创建对象
        ArrayList<Quadrangle> quadrangles = new ArrayList<Quadrangle>();
        quadrangles.add(new Rectangle(1, 2,"矩形"));
        quadrangles.add(new Parallelogram(1, 3,"平行四边形"));
        quadrangles.add(new Trapezoid(1, 1, 2,"梯形"));
        //循环执行各个对象的面积计算方法
        for (Quadrangle qua : quadrangles) {
            qua.calcuArea();
        }
    }
}

5.5 改进版代码分析

以上代码用到了以下概念:
多态(Polymorphism):指一个对象变量(如代码中的qua)可以指示多种实际类型的现象。由于矩形类、平行四边形类和梯形类都是继承于四边形父类,所以其方法名一致,可以通过一个父类的对象变量来实现子类的自动匹配,从而简化了代码。

多态的优缺点

  • 优点:可以提高可维护性(多态前提所保证的),提高代码的可扩展性
  • 缺点:无法直接访问子类特有的成员。

思考:这个缺点怎么解决呢?比如在上述代码中,无法通过qua.width_up获取只有梯形的才有的成员变量。
解决方法:可以通过instanceof判断对象变量的实际类型以及对象类型转换实现相应的操作,代码如下:

public class Test {
    public static void main(String[] args) {
        //创建对象
        ArrayList<Quadrangle> quadrangles = new ArrayList<Quadrangle>();
        quadrangles.add(new Rectangle(1, 2,"矩形"));
        quadrangles.add(new Parallelogram(1, 3,"平行四边形"));
        quadrangles.add(new Trapezoid(1, 1, 2,"梯形"));
        //循环执行各个对象的面积计算方法
        for (Quadrangle qua : quadrangles) {    
            if (qua instanceof Trapezoid) {         //判断对象类型
                Trapezoid tra=(Trapezoid)qua;       //对象类型转换
                //输出梯形类的上底宽
                System.out.println("梯形的上底宽为:"+tra.width_up);
            }
            qua.calcuArea();
        }
    }
}

6 抽象(Abstrac)

6.1 案例

咱们再加一点难度。
现在要求设计一个面积计算器,计算的对象包括平行四边形、梯形和圆。平行四边形的输入为高和宽,梯形的输入为高、上底长和下底长,圆的输入为直径(假定都是规范的数值输入),所有的输出均为面积。
规定:平行四边形的面积计算公式为宽x高;梯形的面积计算公式为(上底+下底)x 高/2;圆的面积计算公式为圆周率x半径的平方。

那么,基于继承,怎样设计最好?

6.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);
        }
    }
}

6.3 代码分析

思考:以上代码确实实现了我们的需求,完成了继承,避免了重复代码的出现,但是总感觉哪里不对劲。仔细观察可以发现,每增加一个新的子类,我们就必须得在父类方法中做相应的修改,才能使新增子类也具备面积计算方法。作为一个堂堂正正的父类怎么能跟着子类的需求而变化呢?那这个父类岂不是很没“面子”?

所以,在后期功能拓展时,如何才能避免对上层结构的改动呢?

6.4 改进版代码

思考:既然在父类中难以提取通用的方法体,那我们可不可以只声明方法,而不具体实现呢?当然可以呀,我们可以用到前文提到的“覆盖”,实现子类方法的定义(如代码5.2),这样就避免了对父类的修改。

但是,这个实例化后的父类(比如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();
        }
    }
}

抽象类图.jpg

6.5 改进版代码分析

任何封闭的几何图形都应该具有面积计算方法,但方法不一,难以提取出相同的实现代码,所以将其抽象。

以上代码用到了以下概念:
抽象方法(Abstrac Method):指使用abstract修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。
抽象类(Abstrac Class):指包含抽象方法的类。通过abstract方法定义规范,然后要求子类必须定义具体实现。抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。同时,通过在抽象类中定义封装的更改器和访问器,减少了子类的代码重复。

抽象的意义:
到这里,你有可能会有个疑问,既然子类都得通过覆盖实现自己的面积计算方法,为什么我们执意要用继承呢?

因为,我们需要用继承来提供一个规范,规范我们的成员变量和方法(即使没有具体的实现,也要有一致的方法名),没有规范,多态就无从谈起。如果说继承的基本用意是实现代码的复用性,那抽象就是继承的升华,它追求更高的精神境界,即契约(或规范)。抽象要求所有使用它的“用户”(即子类)都必须签订一份合约,这份合约规定子类必须实现抽象类所规定的所有方法。抽象方法意义在于就算无法实现出方法的内容,但还可以定义出一组子型共同的协议。

注意:

  • 有抽象方法的类只能定义成抽象类。
  • 抽象类中仍然可以定义非抽象方法。
  • 抽象类中可以定义构造方法,只能用于子类的调用,不能对其自身实例化。
  • 抽象类的子类必须对抽象类定义的抽象方法覆盖。

7 接口(Interface)

7.1 案例

咱们再加一点难度。
在案例6.1的基础上增加功能,要求轴对称图形输出其绕垂直中心轴旋转得到的几何体的体积,体积计算方法定义为calcuVolume。

那么,考虑到代码的复用性和功能的扩展性,怎样设计最优呢?

7.2 代码

思考:平行四边形不是轴对称图形,无法得到旋转的几何体;梯形旋转后可以得到圆台;圆形旋转后可以得到球。分析三个图形的特点可以得到如图所示的结构:
接口图.jpg

为此,可以想到如下两种方法:
方法一:将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();
        }
    }
}

7.3 代码分析

以上代码用到了以下概念:
接口(Interface):接口不是类,而是对希望符合这个接口的类的一组需求。可以说接口是比抽象更抽象的概念。抽象类还提供某些具体实现,而接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了一批类具有的公共方法规范。接口的意义在于全面地、专业地实现了规范和具体实现的分离,便于实现模块化设计。

类与接口的关系:

  • 类与类之间: 继承关系,一个类只能直接继承一个父类,但是支持多重继承。
  • 类与接口之间: 只有实现关系,一个类可以实现多个接口。
  • 接口与接口之间: 只有继承关系,一个接口可以继承多个接口。

类与接口的区别:

  • 普通类:具体实现
  • 抽象类:具体实现,规范(抽象方法)
  • 接口:规范!

抽象类与接口的区别:

a.成员变量

  • 抽象类可以有成员变量,也可以有常量
  • 接口只能有常量,默认修饰符public static final

b.方法

  • 抽象类可以有抽象方法,也可以有非抽象方法
  • 接口只能有抽象方法,默认修饰符 public abstract

c.构造方法

  • 抽象类有构造方法,为子类提供
  • 接口没有构造方法

注意:

  • 接口不能被实例化。
  • 接口中的所有方法默认为public abstract。
  • 接口不能有成员变量,但是可以包含常量,默认为public static final。
  • 将类声明为实现某个接口,需要用到implements关键字,且这个类必须实现接口中所有的方法,并且这些方法只能是public的。(抽象类不是必须实现接口的方法,因为抽象类是抽象的,如果实现了具体的方法,那就丧失了抽象类的意义)
  • 接口可以多继承,像类一样继承高层次的接口。
  • 接口同样有多态。

8 练习

8.1 案例

好了,概念讲完了。咱们来做个练习,把以上所有概念都用起来!
要求:尽量使用本所介绍的知识,设计一个几何图形计算器,能计算图形的面积以及轴对称图形旋转后的体积。

  1. 几何图形:矩形、平行四边形、梯形、等腰三角形、非等腰三角形。
  2. 输入:宽和高(梯形再多输入一个上底宽)
  3. 输出:图形的面积以及轴对称图形绕垂直中心轴旋转后的体积。

8.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 Parallelogram extends Quadrangle{
    public Parallelogram(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 Parallelogram(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();
            }
        }
    }
}

8.3 代码分析

注意!以上代码一定不是最优方案,只是为了练习本文所学知识,因此,仅做参考。关于程序的模式设计问题以后再聊。

层次结构如下:
练习图.jpg

  • Geometry和AxialSymmetry为接口。
  • Quadrangle和Triangle为抽象类,都实现Geometry。
  • Parallelogram、Rectangle、Trapezoid为类,都继承于Quadrangle;其中,Rectangle和Trapezoid另外实现AxialSymmetry。
  • IsoscelesTriangle和NotIsoscelesTriangle为类,都继承于Triangle,其中IsoscelesTriangle另外实现AxialSymmetry。

9 参考文献

【1】《Head First Java(第二版·中文版)》
【2】《Java核心技术·卷 I(原书第11版)》
【3】菜鸟教程:https://www.runoob.com/java/j...
【4】速学堂:https://www.sxt.cn/Java_jQuer...


阿飞爱学习
1 声望5 粉丝