Java继承和多态-创新互联

前言:

创新互联建站是一家集网站建设,蒲县企业网站建设,蒲县品牌网站建设,网站定制,蒲县网站建设报价,网络营销,网络优化,蒲县网站推广为一体的创新建站企业,帮助传统企业提升企业形象加强企业竞争力。可充分满足这一群体相比中小企业更为丰富、高端、多元的互联网需求。同时我们时刻保持专业、时尚、前沿,时刻以成就客户成长自我,坚持不断学习、思考、沉淀、净化自己,让我们为更多的企业打造出实用型网站。

本博客将带大家了解Java中继承和多态相关的知识。

目录

继承

何为继承

为什么需要继承

继承的使用语法

extends关键字

super关键字

继承方式

组合

多态

何为多态

多态的实现条件

重写

两种转型

向上转型

向下转型

多态优缺点


继承

谈到继承,我们不免想到继承遗产,继承者,继承权等词汇,那么,在Java中,何为继承呢,和我们生活中常用的那个继承的意思一样吗?现在让我们一起来揭开这层面纱。

何为继承

继承机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生的类,称为派生类。

举个例子:

现在有两个类,一个叫做Dog,一个叫做Cat。Dog和Cat中都有各自的成员属性和方法,

代码如下:

class Dog{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }
    public void wangWang(){
        System.out.println(name+" 正在汪汪叫!");
    }
}
class Cat{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }
    public void miaoMiao(){
        System.out.println(name+" 正在喵喵叫!");
    }
}

从上面的代码来看,name、age、eat()在Dog类和Cat类都有。同时我们思考:如果再定义几个和Cat、Dog类类似的类,那么我们又要再次定义name、age和eat(),难免冗余。Java中对此提出了继承的概念,将这些共性抽取出来,以实现代码的复用。本例中的抽取如下:

class Animals{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }
}

图示:


为什么需要继承

通过继承我们可以解决:共性的抽取,实现代码的复用。实现多态。


继承的使用语法

首先我们应该明确:

被继承的类称为:父类,基类,超类

继承的类称为:子类,派生类

如:上例中的Animal就是父类(基类、超类),而Dog和Cat则可以称为Animal的子类(派生类)

extends关键字

在Java中,我们要表明两个类的继承关系,我们可以通过extends关键字

如:

通过如下代码我们将更好地体会extends关键字的作用:

class Animals{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }
}

class Dog extends Animals {
    public void wangWang(){
        System.out.println(name+" 正在汪汪叫!");
    }
}

class Cat extends Animals {
    public void miaoMiao(){
        System.out.println(name+" 正在喵喵叫!");
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.name = "小白";
        dog.wangWang();//小白 正在汪汪叫!

        Cat cat = new Cat();
        cat.name = "小黑";
        cat.miaoMiao();//小黑 正在喵喵叫!
    }
}

通过调试,我们将会更加直观地明白这段代码是如何运行的。

同时,通过上面代码的运行,我们也可以知晓:

子类会将父类中的成员方法和成员属性都继承下来。子类中也最好能有不同于父类的成员,如果没有,全靠继承的话,那我们为什么还要再写一个子类呢?

在粗略地了解过继承中的关键字extends以后,我们不禁思考:如何在子类中访问父类的成员呢?

下面将继续为大家介绍:

谈到如何在子类中访问父类的成员,我们会想到两种情况:

一是子类的成员名和父类的不一样的时候如何访问,

二是子类的成员名和父类的一样的时候如何访问。

就这两种分类,我们对继承进行更加深入的了解。

子类成员名和父类不一样的:

class Base{
    public int a = 20;
    public int b = 2;
    public int c = 199;

    public void methodBase(){
        System.out.println("Base");
    }
}

class Derived extends Base {
    public int d = 10;//和父类里面的成员变量的名字都不一样

    public void methodDerived(){//和父类里面的成员方法的名字都不一样
        System.out.println("Derived");
    }

    //可以在子类里面调用父类的方法:
    public void func1(){
        methodBase();
        methodDerived();
    }

    public void func(){
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
    }
}

public class Main {
    public static void main(String[] args) {
        Derived derived = new Derived();
        derived.func();
        derived.func1();
    }
}

效果:

由运行结果我们可以知晓,当父类和子类的成员的名称不一样的时候,直接对父类的成员进行访问即可。

子类成员名和父类一样的:

class Base{
    public int a = 20;
    public int b = 3;
    public int c = 199;

    public void methodBase(){
        System.out.println("Base");
    }
}

class Derived extends Base {
    public int c = 9;//此成员变量父类也有

    public void methodBase(){//此成员方法父类也有
        System.out.println("Derived");
    }

    public void func1(){//通过func1方法来观察:面对两个方法名一样的方法,代码是如何运行的
        methodBase();
        methodBase();
    }

    public void func(){
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);//观察此处的输出来确定编译器运行的是哪个类的代码
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Derived derived = new Derived();
        derived.func();
        derived.func1();
    }
}

效果:

由编译器运行的结果,我们可以知晓:在父类和子类的成员的名称(即使它们的数据类型不一样)一样的时候,如果直接访问的话,访问的一定是子类的成员,也就是所谓的就近原则 。注意,当父类和子类中同名的成员方法构成重载的话,就是按照参数来确定访问的是哪一个。

那么我们该如何实现在父类和子类的名称一样的时候,访问到父类的那些和子类成员名称一样的成员呢?通过super关键字。

class Base{
    public int a = 20;
    public int b = 2;
    public int c = 199;

    public void methodBase(){
        System.out.println("Base");
    }
}

class Derived extends Base {
    //子类有就拿子类的,子类没有再拿父类的
    //如果父类和子类有同名的成员变量,优先使用子类自己的(就近原则)
    public int c = 9;

    public void methodBase(){
        System.out.println("Derived");
    }

    public void func1(){
        methodBase();
        super.methodBase();
    }

    public void func(){
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(super.c);
        //通过关键字super可以访问到当前类的父类的成员变量
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Derived derived = new Derived();
        derived.func();
        derived.func1();
    }
}

效果:

super关键字

通过上面的代码,我们对super关键字已经有所了解,那么super关键字到底是什么呢?它又有哪些作用呢?

super关键字的主要作用就是在子类中访问父类的成员。如果要明确访问父类中的成员,只需借助super关键字即可。

通过super,我们还能调用父类的构造方法:

class Animals{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }

    //构造方法:
    public Animals() {
        System.out.println("父类");
    }
}

class Dog extends Animals {
    //构造方法:
    public Dog(){
        super();//注意子类构造方法中默认会调用基类的无参构造方法:super()
        //用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
        //并且只能出现一次
        System.out.println("子类");

    }

    public void wangWang(){
        System.out.println(name+" 正在汪汪叫!");
    }
}
public class KnowledgeOne {
    public static void main(String[] args) {
        Dog dog = new Dog();
    }
}

效果:

通过上面的代码我们可以知晓:

在子类构造对象的时候,必须要先调用父类的构造方法,然后再执行子类的构造方法。

这是因为:子类对象中成员是由两部分组成的:从父类继承下来的部分以及自己特有的部分 。在构造子类对象时候 ,先要调用父类的构造方法,将从父类继承下来的成员构造完整 ,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。

对于上面代码段,如果父类中的构造方法是含参数的,只能依靠我们自己在子类成员的构造方法中完善父类的构造方法,编译器并不会自动为我们提供。

如:

class Animals{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }

    //构造方法:
    public Animals(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animals {
//构造方法:
   public Dog(){
        super("小一",2);
    }

    public void wangWang(){
        System.out.println(name+" 正在汪汪叫!");
    }
}

class Cat extends Animals {

    public int size;

    //构造方法:
   public Cat(){
        super("小白",1);
    }

    public void miaoMiao(){
        System.out.println(name+" 正在喵喵叫!");
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.wangWang();

        Cat cat = new Cat();
        cat.miaoMiao();
}

效果:

这段代码也可以写作:

class Animals{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+" 正在吃饭!");
    }

    //构造方法:
    public Animals(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animals {
    //构造方法:
    public Dog(String name, int age) {
        super(name, age);
    }

    public void wangWang(){
        System.out.println(name+" 正在汪汪叫!");
    }
}

class Cat extends Animals {

    public int size;

    //构造方法:

    public Cat(String name, int age,int size) {
        super(name, age);
        this.size = size;
        //注意这里的size的构造一定要在父类的构造结束以后才能构造,否则会报错。
        //super在子类构造方法内调用父类构造方法的时候一定要放在第一行

        //同样,this引用在构造方法里使用的时候也是要放在第一行的,所以,this和super不能同时出现在在同一个构造方法中

    }

    public void miaoMiao(){
        System.out.println(name+" 正在喵喵叫!");
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Dog dog = new Dog("小一",2);
        dog.wangWang();//小一 正在汪汪叫!

        Cat cat = new Cat("小白",1,3);
        cat.miaoMiao();//小白 正在喵喵叫!
    }
}

效果:

通过对super的了解,我们会发现super和this有许多相同之处,那么我们现在就来讨论一下这两个关键字的异同点:

相同点:

1. 都是Java中的关键字

2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段

3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在

不同点:

1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用

2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性

3. 在构造方法中:this用于调用本类构造方法,super用于调用父类构造方法,两种调用不能同时在构造方法中出现

4. 构造方法中一定会存在super的调用,用户没有写编译器也会增加,但是this用户不写则没有

了解了如何在子类和父类中实现构造。那在子类和父类中,代码的运行顺序又是怎么样的呢?让我们继续往下了解:

cclass Animals{
    public String name;
    public int age;

    static {
        System.out.println("父类静态代码块");
    }
    {
        System.out.println("父类实例代码块");
    }

    //如果父类和子类中没有构造方法,编译器会帮你在父类中加一个无参的构造方法:
    public Animals(){
        System.out.println("父类构造方法");
    }
}

class Dog extends Animals {

    static {
        System.out.println("子类静态代码块");
    }
    {
        System.out.println("子类实例代码块");
    }
    public Dog(){
        System.out.println("子类构造方法");
    }
}

public class KnowledgeOne {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println("==============");
        Dog dog1 = new Dog();
    }
}

效果:

通过代码的运行结果,我们可以直观地看到代码的运行顺序:

父类静态代码块-->子类静态代码块-->父类示例代码块-->父类构造代码块-->子类示例代码块-->子类构造方法

同时,我们也可以发现:静态代码块只执行一次。

继承方式

继承方式一共有单继承、多层继承、不同类继承同一个类

单继承:

多层继承:

不同类型继承同一个类:

注意:

在Java中不支持多继承(即,一个类继承了多个类的成员),同时,写代码的时候,尽量不要超过三层继承关系,继承关系太多就要考虑对代码重构。如果想要从语法上限制继承,可以使用final关键字。下面我们一起来了解一下final关键字。

final关键字可以修饰变量、成员方法以及类。

当final关键字修饰变量的时候,表明该变量是常量,不可以再进行修改了(当final修饰成员变量的时候,要将成员变量初始化)。如图:

当final修饰类的时候,表明这个类无法被继承。

当final关键字修饰成员方法的时候,表明该成员方法无法被重写。


组合

组合是将一个或几个类的实例作为另一个类的字段。如:

class Teacher{

}

class Student{

}

class School{
    public Teacher[] teachers = new Teacher[3];//类类型
    public Student[] students = new Student[3];//类类型
}

上面代码中,School类里面通过Teacher类和Student类new了两个类类型的数组。

继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物

而组合则表示对象之间是has-a的关系,比如:学校是由老师和学生组成的。

继承和组合都可以实现代码的复用,使用继承还是组合是由场景决定的,一般来说,能用组合尽量不要用继承。


多态

何为多态

通俗来说就是多种形态。具体点来说就是完成某个行为时,不同的对象去完成时会产生不同的效果。

如:打印一份材料的时候,用彩印机打印打出来的就是彩色的;而用黑白的打印出来的就是黑白的。

再如:同样是吃饭,猫吃的就是猫粮,狗吃的是狗粮,鱼吃的是鱼粮。

这就是完成同一件事情,不同的对象去完成时就产生了多种形态。


多态的实现条件

在Java中要实现多态必须要满足一下这几个条件:

  1. 必须在继承体系下
  2. 子类必须对父类中的方法进行重写
  3. 通过父类的引用调用重写方法

多态体现:在代码运行的时候,当传递不同的对象时,会调用对应类中的方法。

重写

何为重写?

重写也称为覆盖(override)。重写是子类对父类非静态、非private修饰,非final修饰(被final修饰的方法叫做密封方法,不能被重写),非构造方法等实现过程进行重写,返回值和形参都不能改变。即外壳不变,核心重写。

重写的好处在于可以根据子类的需要重新定义自己的行为,也就是说,子类可以根据自己的需要实现父类的方法。(通俗来说就是上面所说的不同对象实现同一个方法都有它自己的表现形式)

方法重写的规则:

  1. 子类在重写父类的方法的时候,一般必须与父类方法原型一致:返回值类型、方法名(参数列表)要完全一致。
  2. 被重写的方法可以返回类型不同,但是必须要是父子类关系。
  3. 访问权限不能比父类中被重写的方法的访问权限更低。如:父类中的那个被重写的方法被protected修饰,那这个方法在子类中也只能被protected和public修饰。
  4. 父类被static、private修饰的方法,以及构造方法都不能被重写。
  5. 重写的方法可以使用@Override注解来指定。这个注解能帮我们进行合法的校验。(比如:如果在重写的时候不小心把方法名字拼写错了(假设正确的是eat,而我们不小心写成了aet,有了@Override以后,此时编译器就会发现父类中没有eat方法,然后报错,提示无法构成重写)

重写和重载的区别:

方法重载:

public class KnowledgeTwo {
    public static int func(int a , int b){
        return a+b;
    }
    public static int func(int a , int b, int c){//func方法进行了重载
        return a+b+c;
    }
    public static void main(String[] args) {
        int a = 10;
        int b = 29;
        int ret1 = func(a,b);
        System.out.println(ret1);
        
        int c = 21;
        int ret2 = func(a,b,c);
        System.out.println(ret2);
    }
}

效果:

方法重写:

归纳: 

方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

重写的设计原则:

对于已经投入使用的类,尽量不要修改。最好的方法是重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。

比如:对于老人机来说,只能打电话,无法在拨号的时候开视频;而对于智能手机来说,打电话的同时还可以开视频。在实现智能手机打电话的同时可以开视频这个功能中,我们应该要使用重写,因为打电话无法开视频的老人机仍旧有人在使用。可以把老人机和智能手机看成两个对象(和上面的例子中的猫、狗、鸟一样的那种对象),在老人机这个对象中,打电话这个方法调用的时候,只能传递声音;而在智能手机这个对象中,打电话这个方法调用的时候不光能传递声音,还能传递图像。

静态绑定(前期绑定、早绑定):在编译时,根据用户所传递实参类型就确定了具体调用那个方法。如:重载。

动态绑定(后期绑定、晚绑定):在编译时无法确定方法的行为,要在运行时才能确定调用的是哪个方法。如:重写。


两种转型

向上转型

向上转型就是创建一个子类对象,把它当成父类来使用。向上转型是实现多态的前提。

语法格式:父类类型名 对象名 = new 子类类型();

class Animal{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+"正在吃饭!");
    }
}

class Dog extends Animal {
    public void eat(){
        System.out.println(name+"正在吃狗粮!");
    }
    public void wangWang(){
        System.out.println(name+"正在汪汪叫!");
    }
}

public class KnowledgeTwo {
    public static void main(String[] args) {
        Animal animal1 = new Dog();//直接赋值式的向上转型
        animal1.name = "小红";
        animal1.age = 2;
        animal1.eat();

        System.out.println("==========");

        Animal animal2 = new Cat();直接赋值式的向上转型
        animal2.name = "小二";
        animal2.age = 3;
        animal2.eat();
    }
}

效果:

使用场景:直接赋值、方法传参、方法返回 

直接赋值:

方法传参:

方法返回:

向上转型的优点是可以让代码更加简单灵活,而向上转型的缺点就是无法使用子类中特有的方法。

向下转型

向上转型之后,子类对象被当成父类对象使用,无法调用自己特有的方法,有时候可能会需要调用子类特有的方法,此时就可以通过将父类引用还原为子类对象即可,即向下转型。

class Animal{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+"正在吃饭!");
    }
}

class Dog extends Animal {
    public void eat(){
        System.out.println(name+"正在吃狗粮!");
    }
    public void wangWang(){
        System.out.println(name+"正在汪汪叫!");
    }
}

class Bird extends Animal{
    public String wing;

    public void eat(){
        System.out.println(name+"正在吃鸟粮!");
    }
    public void fly(){
        System.out.println(name+"正在飞!");
    }
}

public class KnowledgeTwo {
    public static void main(String[] args) {
        Animal animal = new Dog();//向上转型
        animal.name = "小黑";
        animal.age = 1;
        animal.eat();

        Bird bird = (Bird) animal;//向下转型,非常不安全
        bird.name = "小飞";
        bird.age = 2;
        bird.eat();
    }
}

效果:

向下转型如此不安全,那么有没有什么方法使它不报错呢?

答案是有的,通过insteadof。

class Animal{
    public String name;
    public int age;

    public void eat(){
        System.out.println(name+"正在吃饭!");
    }
}

class Dog extends Animal {可以理解为animal这个引用是不是真的引用了Dog对象
    public void eat(){
        System.out.println(name+"正在吃狗粮!");
    }
    public void wangWang(){
        System.out.println(name+"正在汪汪叫!");
    }
}

class Bird extends Animal{可以理解为animal这个引用是不是真的引用了Bird对象
    public String wing;

    public void eat(){
        System.out.println(name+"正在吃鸟粮!");
    }
    public void fly(){
        System.out.println(name+"正在飞!");
    }
}

public class KnowledgeTwo {
    public static void main(String[] args) {
        Animal animal = new Dog();//向上转型
        animal.name = "小黑";
        animal.age = 1;
        animal.eat();

        System.out.println();
        
        if(animal instanceof Bird){
            Bird bird = (Bird) animal;//向下转型
            bird.name = "小飞";
            bird.age = 2;
            bird.eat();
            bird.fly();
        }
        if(animal instanceof Dog){
            Dog dog1 = (Dog)animal;//向下转型
            dog1.name = "小白";
            dog1.age = 1;
            dog1.eat();
            dog1.wangWang();
        }
    }
}

效果:


多态优缺点

使用多态的优点:可以降低代码的圈复杂度,避免使用大量的if-else。

何为圈复杂度?

圈复杂度是一种描述一段代码复杂程度的方式。如果一段代码平铺直叙,表明这段代码比较容易理解,圈复杂度低。如果一段代码有很多条件分支或者循环语句,则理解起来比较困难,圈复杂度高。

因此,我们可以粗略地将一段代码中的循环语句和条件语句的个数来表示这段代码的圈复杂度。

一段代码的圈复杂度过高是要考虑对其进行重构的。一般最好不要超过10.

以按照⚪ ⬜ ⚪ ⬜ ❀的顺序打印为例:

不使用多态:

class Shape{
    public void draw(){
        System.out.println("画图形!");
    }
}

class Square extends Shape {
    @Override
    public void draw() {
        System.out.println("⬜");
    }
}

class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("⚪");
    }
}

class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

public class KnowledgeTwo {
    public static void drawMap(Shape shape) {//在参数的位置发生了向上转型
        shape.draw();
    }
    public static void main(String[] args) {
        Square square = new Square();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        //顺序:⚪ ⬜ ⚪ ⬜ ❀

        String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
        for (String s : shapes) {
            if (s.equals("cycle")) {
                drawMap(cycle);
            }
            if (s.equals("rect")) {
                drawMap(square);
            }
            if (s.equals("flower")) {
                drawMap(flower);
            }
//使用了多个if语句
        }
    }
}

效果:

使用多态:

class Shape{
    public void draw(){
        System.out.println("画图形!");
    }
}

class Square extends Shape {
    @Override
    public void draw() {
        System.out.println("⬜");
    }
}

class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("⚪");
    }
}

class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}
public class KnowledgeTwo {
    public static void drawMap() {
        Square square = new Square();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        //顺序:⚪ ⬜ ⚪ ⬜ ❀
        Shape[] shapes = {cycle, square, cycle, square, flower};
        //发生了向上转型
        for (Shape shape: shapes) {
            shape.draw();
        }
    }
    public static void main(String[] args) {
        drawMap();
    }
}

效果:

使用多态的缺点:代码的运行效率降低。

注意:

  1. 属性没有多态性:当父类和子类有同名属性的时候,通过父类引用,只能引用到父类自己的成员属性。
  2. 构造方法没有多态性。
  3. 避免在构造方法中调用重写的方法。
    class B{
        public B(){//父类的构造方法
            func();
        }
    
        public void func(){
            System.out.println("B.func");
        }
    }
    
    class D extends B{
        private int num = 1;
    
        public void func(){//被重写
            System.out.println("D.func"+ num);
        }
    }
    public class KnowledgeTwo {
        public static void main(String[] args) {
            D d = new D();
        }
    }

    效果:

    从运行结果上来看,func方法在运行的时候调用的应该是子类里面的,但是为什么结果中的num的值却是0?

综上,尽量不要在构造代码块中去重写方法,容易出现问题。 

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


文章题目:Java继承和多态-创新互联
浏览路径:http://scyanting.com/article/cepiid.html