【Java】java继承的理解和应用

by Microanswer

标签: java 继承 覆盖


创建时间:Sep 27, 2019 9:59:15 PM | 最后更新:Oct 14, 2019 8:17:08 PM 


重要声明:本文章仅仅代表了作者个人对此观点的理解和表述。读者请查阅时持自己的意见进行讨论。

继承这一特性在Java中的应用十分广泛,几乎任意一个项目甚至demo代码片段里,也可以见到java继承的身影。因此有关继承相关的知识点也必然是学习java编程必须要掌握的。在了解继承之前,你需要对 java中的类 有一定的认识,否则可能本文阅读相对困难。

一、继承的作用

学习一个新事物前,往往都会有一个疑问。这是一个什么样的东西以至于我们要去学习它。它又能为我们带来什么样的便利呢?

在大多数的资料里,几乎都把java中的继承比喻为现实世界里,你和你父母的关系那样。不得不说的的却却没有问题。但是似乎总有一点差强人意。假如你父亲是一位成功的商人,可你不一定是;假如你母亲是一位著名的画家,你也不一定是画家!在别人的眼里,你顶多是成功商人的儿子、是著名画家的女儿。但你不是他们,你也不一定能做到他们那样。

而在程序里,不一样。程序里所说的继承,是指“类”之间的继承。而现实里,并不存在“类”相关的继承的说话(不杠应该就没有吧[滑稽保命])。你父亲是一个对象,你是一个对象,你们都是“人类”这个类的实例。这也许就是程序里的继承与现实中的继承的区别了。

程序中的类,通常是指:对一种有相共通的属性和行为功能的实例进行一个抽象描述。描述方式则通过class来定义。当你定义好了之后,便可通过这个定义的规则来构造具有相同或相似功能的不同对象了。

程序是人写的,不可能一开始就明白一个程序将来会遇到什么样的功能和需求。因此程序是不断去完善的。有一天突然要求添加一个功能,但此功能的绝大部分和已有功能模块相同,仅仅只有一部分扩展的新功能。这时候,如果之前程序设计得当,便可以十分方便的通过继承来实现新功能需求了。

简单来说,java程序里的继承是“类”与“类”进行继承。对象与对象之间不存在继不继承的关系。子类可以使用父类提供的非私有方法。

“父类”又有人叫:基类、超类。

二、使用继承

要最终明白参透继承,使用动物~阿猫、阿狗,人类~老师、学生,这类具体事务进行类比。会把人带入死胡同,会感觉: 合着我在这儿继来继去,就只是print了一堆name和age?到底继承干了啥、带来了啥。没感觉!

1、创建第一个类

所以我将通过具体功能需求来演示使用继承为我们开发带来的便利。请看下面定义的一个类:

// Rectangle.java
public class Rectangle {
    public float sideLength1;
    public float sideLength2;

    public Rectangle (float sideLength1, float sideLength2){
        this.sideLength1 = sideLength1;
        this.sideLength2 = sideLength2;
    }

    public float calcArea(){
        return sideLength1 * sideLength2;
    }
}

很明显,这是一个矩形类,sideLength 表示了矩形的边长,矩形有四边,但对边相等,因此只需要定义两边的变量来保存数据就好了。构造函数要求了必须传入两个边长来创建对象。同时,提供了一个方法 calcArea 计算这个矩形的面积,并返回结果。这个类十分简单,相信每一个有心学习java的人都能够看懂。

不如先来看看如果使用它,实现面积的计算:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle rect1 = new Rectangle(12.24f, 3.15f);
        float rect1Area = rect1.calcArea();
        System.out.println("矩形的面积是:" + rect1Area);
    }
}

构造矩形,计算面积,输出结果。很是easy。那么现在新需求来了。请开发一个类,实现立方体的体积计算。

2、继承第一个类

要实现立方体体积计算,数学公式:体积=底面积×高。而我们第一个类的矩形面积计算已经是实现好了,有现成的了,那我们的体积计算还需要再写一遍面积计算的代码吗?既然我们有这样的实现,何不直接利用呢?继承的有用之处就体现出来了,开始继承实现:

// Cuboid.java
public class Cuboid extends Rectangle {
    public float sideLength3;

    public Cuboid (float sideLength1, float sideLength2, float sideLength3) {
        super(sideLength1, sideLength2);
        this.sideLength3 = sideLength3;
    }

    public float calcVolume() {
        return super.calcArea() * sideLength3;
    }
}

由于立方体存在第三条边长,因此在矩形的基础上,只需要再多定义一个参数用来保存第三条边长的值就好了。提示提供了一个 calcVolume 方法用于计算体积。可以看到,在方法内部,直接使用了父类(矩形类)实现好的计算面积方法,然后再乘以第三条边长,就计算出了体积。

立方体(Cuboid)类的构造函数注意

可以看到,子类构造函数里面使用了一句:super(int,int)。它的作用就是将调用父类的构造函数,将父类的值得到初始化。为什么要这样做?要弄明白这件事,可以由另一件事推导出来。首先,你肯定知道并对这句话没有疑问:“子类继承了父类,那么子类就可以使用父类里面所有的非私有方法”。既然能使用,那么肯定父类就离不开正常初始化这样一个操作流程。而这里我们的 Rectangle 类有且只有一个构造函数,子类继承它,要想 Rectangle 正常初始化,并且子类能够顺利使用得到希望的结果,那就必须要在构造子类时先将父类初始化完毕,这也是为什么必须保证调用父类构造函数的位置必须是构造函数第一行。因为你有可能第二行就要使用父类里的方法,不调用父类的构造函数又如何正常的使用呢。

另外,父类如果存在没有参数的构造函数(或者没有写构造函数),那么子类就可以不需要在代码里调用父类的构造函数(但不代表系统不会调用)。在初始化子类的过程中,也依然会先调用父类的构造函数去初始化父类。你要明白,父类也是人写的,根据这些特性,写父类的人知道自己的类如何使用才能正常,他会明确指定构造函数有没有参数的。当使用者使用父类时,不必与原作者沟通即可知道父类的使用方式。也正是因为这一套规矩的作用。

现在,不妨试试,使用 Cuboid 类来进行一次体积计算:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Cuboid cubo1 = new Cuboid(12.24f, 3.15f, 4.3f);
        float cubo1Volu = cubo1.calcVolume();
        System.out.println("立方体的体积是:" + cubo1Volu);
    }
}

这就是继承。使用现有实现了某功能的类作为父类,在此基础上再实现自己需要的功能。达到重用、扩展功能的效果。

3、重写

重写:是指子类和父类有相同名字,相同参数个数的方法,我们就说子类的这个方法重写了父类的对应方法。上文我们创建了 Rectangle 类,他可以计算矩形面积。现在我们希望它计算正方形的面积,虽然目前来看,它可以实现。但人们对于正方形的感知是只需要一个参数,而 Rectangle 需要两个参数。因此,我们不妨将其简化一下:

// Square.java
public class Square extends Rectangle {
    public Square(float size) {
        super(size, size);
    }

    @Overide
    public float calcArea() {
        return Math.pow(super.sideLength1, 2);
    }
}

@Overide 注解是可选项,它的作用标记了此方法重写了父类的 calcArea 方法,其作用更多是告诉阅读代码的人,这个方法重写了父类的方法。由于我们都知道正方形只需要提供一个长度即可,因此 Square 构造函数只需要传入一个参数。而父类 Rectangle 构造函数明确要求传递两个参数,既然正方形两边相同长,因此可以直接将 size 同时传递给父类构造函数,这没有什么问题,并且是很常见的做法。

再往下看,可以看到子类里也有一个和父类相同的 calcArea 方法,我们开发 Square 时,认为父类计算面积的方式欠佳,希望使用自己的逻辑来计算正方形的面积。方法中使用了 Math.pow() 方法来计算面积,直接使用熟知的数学公式:正方形面积=边长的平方。这样当我们构造正方形时,再调用 calcArea () 方法,就不再使用父类的方法了,而是使用咱们重写的方法了。

当然,你认为父类计算面积的方法十分棒,而你又想在计算面积时额外新增一些其他功能,比如:输出正方形的周长。那么你可以这样:

// Square#calcArea();
@Overide
public float calcArea() {
    float result = super.calcArea(); // 先使用父类实现好的方法得到面积。

    // 输出正方形的周长。
    System.out.println("正方形的周长为:" + (super.sideLength1 * 4));

    // 将面积结果返回。
    return result;
}

上述代码,旨在告诉你,你可以重写父类的方法,并且你还可以在你的方法里面调用父类的这个方法来为你达到前一步已达到的功能,然后再在此基础上实现你更多的扩展功能。上方代码的扩展功能就是在计算了面积的同时还打印了正方形的周长。周长和面积始终是两个不同的东西,同时出现计算面积方法里显得不妥。那么我们可以进一步优化 Square 类,来完成周长和面积的分别计算。

// Square.java
public class Square extends Rectangle {
    public Square(float size) {
        super(size, size);
    }

    @Overide
    public float calcArea() {
        return Math.pow(super.sideLength1, 2);
    }

    public float roundLength() {
        return super.sideLength1 * 4;
    }
}

新增了一个方法 roundLength ,用它来计算正方形的周长。这是只有 Square 正方形类才有的方法。父类没有这个方法。 不如先来测试一下:

// Demo.java
public class Demo {
    public static void main(String []args) {
        Square squ1 = new Square(7.24f);
        float squ1RoundLength = squ1.roundLength();
        System.out.println("正方形的面积是:" + squ1RoundLength);
    }
}

运行程序,正常输出结果。下面我们再来看看另一个在继承中值得关注的问题“引用”问题。

三、父类引用子类

“父类引用子类”。其实不应该这样命名,这是极其不规范、甚至不是很正确的。但它或许又表达了这一个知识点的大意。在程序里,我们经常使用父类的引用去保存一个具体子类的实例。实际上,只要类A 是B、C、D、E类的父类甚至好几级之上的父类,那么这些所有子类的实例对象都可以通过A类创建一个引用字段去保存。比如下面这样:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle shape1 = new Square();
        Rectangle shape2 = new Cuboid();
    }
}

这是没有任何问题的。上述代码中, 等号左边,我们可以描述为:父类创建的引用。等号右边,具体子类实例化对象。通过等号,将实例对象使用父类进行引用。这和所有引用类似,把对象的内存地址保存在了 shape1、shape2 这两个字段里面。它们永远都是new的时候对应的类的对象。至于为什么能把子类的引用赋值给父类创建的引用字段呢?

你需要思考。这件事也和“子类可以使用父类所有非私有的方法”有关系。且看我给你一个描述:子类继承了父类,那么子类就必定拥有了父类拥有的方法,所以父类引用保存了子类实例时,能够使用的方法只能是父类里提供的方法,而这个子类实例的对象又必然拥有这些方法。无论如何,都可以正确调用而不会出现找不到的问题。

反过来则不可以

很显然,子类可以自己新增很多自己的方法,父类并没有这些方法,如果反过来引用,则可以调起子类方法,但父类对象并没有这些方法。不行不行。

关于强制转换

对于基本变量,int a = (int)2.3f。这样的强制转换你应该非常熟悉了。现在,对象也可以强制转换了。实际上,只是做了一个引用类型转换。具体实例还是那个不变的实例。就像下面这样:

// Demo.java
public class Demo {
    public static void main(String[] args) {
        Rectangle shape1 = new Square();
        Square square = (Square)shape1;
    }
}

你看到上述代码时可能会表示很白痴。因为你看到了实例创建的时候本来就是一个 Square ,最后你又强制转换成 Square,意义何在?先撇开它的意义,先梳理一下,首先建立一个对象并将其赋值给 Rectangle 父类引用。当你使用父类引用时,在代码里只能显示的调用父类里的方法,当你希望调用 Square 自己的方法时,就办不到了。不过幸好,你有强制转换。转换了啥?其实就只将引用类型进行了转化,从父类引用转化为了子类引用。

注意了,这样的强制转换只能在你知道它具体是哪一个子类时才能进行。否则强制转换将报错的。通过 instanceof 语句,可以判断某个对象是否是某个类的实例:

if (shape1 instanceof Square) {
    System.out.println("shape1是一个正方形。");
}

四、Object

在java里,Object类是所有类的父类,意味着,不论什么对象,你都可以使用 Object 来创建引用进行传递。

五、其它项

要注意,继承只能继承一个父类,不可以继承多个父类。如果你的类担心别人继承。你可以将类定义为final的:

// Test.java
public final class Test {
    // 使用 final 修饰的类,是不允许被继承的。
}

有时候,你希望别人继承你的类,但你不希望其中某一个方法被重写。你也可以使用final修饰:

// Test.java
public class Test {

    // 使用final修饰的方法不能被重写。
    public final void function1 () {
        // todo...
    }
}

全文完, 转载请注明出处。 对你有帮助?不如赞一个吧:
发表评论(发表评论需要登录,你现在还没有登录。)

评论列表 (0条)

导航加载中…