非常详细的关于继承与多态的知识

一、继承

1、为什么要继承?

举个例子:

class Dog {
    public String name;
  	private int age;
    public void eat() {
        System.out.println(this.name + "正在吃饭");
    }
    public void bark() {
        System.out.println(this.name + "正在汪汪");
    }
}
class Cat {
    public String name;
    private int age;
    public void eat() {
        System.out.println(this.name + "正在吃饭");
    }
    public void meow() {
        System.out.println(this.name + "正在喵喵");
    }
}
class wolf {//...}

可以看到,猫和狗都有一个共同的特点:

在这里插入图片描述

那么可以把这个共同的特性抽象出来,放到另外一个Animal类中:

class Animal {
    public String name;
    private int age;
    public void eat() {
        System.out.println(this.name + "正在吃饭");
    }
}
class Dog extends Animal{

    public void bark() {
        System.out.println(this.name + "正在汪汪");
    }
}
class Cat extends Animal{
    public void meow() {
        System.out.println(this.name + "正在喵喵");
    }
}

这样一来,每个类中的代码量明显减少,还避免了代码的重复。

可以看出,“is-a”关系是继承的一个明显特征:Cat “is-a” Anmal;Dog “is-a” Animal。

关键字extends表明正在构造的新类派生于一个已存在的类。这个已存在的类成为超类(superclass)、基类(bass class)或父类(parent class);新类成为子类(subclass)、派生类(derived class)或孩子类(child class)。

例如:class Dog extends Animal 中,Dog被成为子类(派生类);Animal被成为父类(基类、超类);extends则是关键字。

总结:继承是一种思想,对共性进行抽取,从而达到代码的复用效果。

注:

  1. 子类会将父类中的成员方法和成员变量继承到子类中。

    比如:Animal类中的private int age也会被继承到子类中,只是子类不能直接访问。

    说明:public和private只是访问修饰符,并不影响

  2. 子类继承父类后,必须添加自己特有的成员,体现与基类的不同,否则就没有必要继承了。

2、继承的概念

继承(inheritance)机制:是面向对象程序设计使代码复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展(extends)、增加新功能,这样产生的新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码的复用

例如:猫和狗都是动物,我们将其共性内容eat()进行抽取,然后采用继承的思想达到共用。

在这里插入图片描述

3、继承的语法

使用extends关键字表示类之间的继承关系

class 子类 extends 父类 {
    //。。。
}
4、覆盖方法

很多时候父类的有些方法对子类不一定适用,此时,我们需要一个新的方法来**重写/覆盖(Override)**父类的这个方法。

具体来说:

class Animal {
    private String name;
    private int age;
    
    public void eat() {
        System.out.println(this.name + "eating");
    }
}
class bird extends Animal{
    @Override
    public void eat() {
        System.out.println(name + "is eating");
    }
}

这看起来似乎很简单,但问题是父类中name是通过private修饰的,子类虽然能继承到但是不能直接访问。这是我们就可以使用Animal中的公共方法getName();

class Animal {
    private String name;
    private int age;
    
    public void eat() {
        System.out.println(this.name + "eating");
    }
    
    public String getName() {
        return name;
    }
}
class bird extends Animal{
    @Override
    public void eat() {
        System.out.println(getName() + "is eating");
    }
}

但是如果子类中也有一个getName()方法呢?

class Animal {
    //。。。
}
class bird extends Animal {
    private String name;
    @Override
    public void eat() {
        System.out.println(getName() + "is eating");
    }
    public String getName() {
        return name;
    }
}

这时我们就会发现getName()调用的是子类自己的方法而非父类的方法。那如果我们想访问父类的getName()方法呢?此时,我们可以使用特殊关键字super来解决这个问题。

class Animal {
    //。。。
}
class bird extends Animal {
    private String name;
    @Override
    public void eat() {
        System.out.println(super.getName() + "is eating");
    }
    public String getName() {
        return name;
    }
}

这样,我们调用的就是父类的方法而非子类重写的方法了。

在子类方法中 或者 通过子类对象访问成员时:

  • 如果访问的成员变量子类有,优先访问自己的成员变量。
  • 如果访问的成员变量子类中无,则访问父类继承下来的;如果父类也没有定义,则编译报错。
  • 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。

成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。

注释:有些人认为super与this引用时类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用。例如,不能将值super赋给另一个对象变量,它只是一个指示编辑器调用超类方法的特殊关键字。

继承可以增加字段、增加方法或重写超类的方法,不过,继承绝对不会删除任何字段或方法。

5、super关键字

子类和超类中可能会存在相同名称的成员,如果要在子类方法中访问超类同名成员时,该如何操作?无法直接访问,但可以使用Java提供的super关键字。该关键字主要作用:在子类方法中访问父类的成员。

public class Base {
    int a;
    int b;
    public void methodA() {
        System.out.println("methodA");
    }
    public void meethodB() {
        System.out.println("methodB");
    }
}
class Derived extends Base {
    int a;//与父类成员变量同名同类型
    char b;//与父类成员变量同名不同类型
    
    //与超类methodA方法重载
    @Overload
    public void methodA(int a) {
        System.out.println("Overload methodA");
    }
    
    //与超类methodB方法重写
    @Override
    public void methodB() {
        System.out.println("Override methodB");
    }
    
    public void methodC() {
        //对于同名的成员变量,直接访问时,访问的都是子类的。
        a = 100;//等价于this,a = 100;
        //this时当前对象的引用
        
        //访问父类的成员变量时,需要借助super关键字
        //super是获取到子类对象中从父类继承下来的部分
        super.a = 200;
        super.b = 201;
        
        //父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
        methodA();//没有传参,访问父类中的methodA();
        methodA(20);//传递int参数,访问子类中methodA(int a);
        
        // 如果在子类中要访问重写的基类方法,则需要借助super关键字
        methodB();//访问子类methodB方法
        super.methodB();访问父类methodB方法
6、super和this

super和this都可以再成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那它们之间有什么区别呢?

【相同点】

  1. 都是Java中的关键字。
  2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段。
  3. 在构造方法中调用时,必须时构造方法中的第一条语句,并且不能同时存在。

【不同点】

  1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用。
  2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性。
  3. 在构造方法中,this(…)用于调用本类的构造方法;super(…)用于调用父类的构造方法。两种调用不能同时在构造方法中出现。
  4. 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加;但是this(…)用户不写就没有.
6、子类的构造方法

当子类继承超类时:

class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void eat() {
        System.out.println(this.name + "eat food.");
    }
}
class bird extends Animal{
    private boolean whetherFly;

    public bird(String name, int age, boolean whetherFly) {
        super(name, age);
        this.whetherFly = whetherFly;
    }
    public void fly() {
        whetherFly = true;
        System.out.println("Can birds fky?" + this.whetherFly);
    }
}

​ 这里的关键字super具有不同的涵义。语句super(name,age);是“调用超类Animal中带有name、age参数的构造方法”的简写形式。

​ 由于bird的构造方法不能访问超类Animal中的私有字段,所以必须通过一个构造方法来初始化这些私有字段。可以利用特殊的super语法调用这个构造方法。使用super调用构造方法的语句必须是子类构造方法的第一条语句。

如果子类的构造方法没有显示地调用超类的构造方法,将自动地调用超类的无参构造方法。如果超类没有无参数的构造方法,并且在子类的构造方法中又没有显示地调用超类的其他构造方法,Java编译器就会报告一个错误。

关键字this有两个含义:一是指示隐式参数的引用,二是调用该类的其他构造方法。类似地,super关键字也有两个含义:一是调用超类的方法,二是调用超类的构造方法。在调用构造方法的时候,this和super这两个关键字紧密相关。调用构造方法的语句只能作为另一个构造方法的第一条语句出现。构造方法参数可以传递给当前类(this)的另一个构造方法也可以传递给超类(super)的构造方法。

7、实例代码块和静态代码块

实例代码块和静态代码块在无继承关系是执行结果如下:

public class ExtendsTest {
    public static void main(String[] args) {
        Person1 person1 = new Person1("warframe",10);
        System.out.println("=============");
        Person1 person2 = new Person1("Mesa Prime",100);
    }
}
class Person1 {
    private String name;
    private int age;

    public Person1(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("构造方法执行");
    }
    {
        System.out.println("实例代码块执行");
    }
    static {
        System.out.println("静态代码块执行");
    }
}

执行结果:

在这里插入图片描述

  1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
  2. 当对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行。

【继承关系上的执行顺序】

public class ExtendsTest {
    public static void main(String[] args) {
        Student student1 = new Student("张三",19);
        System.out.println("=================");
        Student student2 = new Student("李四",20);
    }
}
class Person1 {
    private String name;
    private int age;

    public Person1(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Peson:构造方法执行");
    }
    {
        System.out.println("Peson:实例代码块执行");
    }
    static {
        System.out.println("Peson:静态代码块执行");
    }
}

class Student extends Person1 {

    public Student(String name, int age) {
        super(name, age);
        System.out.println("Student:构造方法执行。");
    }
    {
        System.out.println("Student:实例代码块执行");
    }
    static {
        System.out.println("Student:静态代码块执行");
    }
}

执行结果:

在这里插入图片描述

  1. 父类静态代码块优先于子类静态代码块执行,且是最早执行。
  2. 父类实例代码块和父类构造方法紧接着执行。
  3. 子类的实例代码块和子类构造方法紧接着再执行。
  4. 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行。
8、protected关键字

为了实现封装性,Java中引入了访问修饰符。主要作用:类或者类中成员能否再类外或者其他包中访问。

NO范围privatedefaultprotectedpublic
1同一包中的同一类
2同一包中的不同类
3不同包中的子类
4不同包中的非子类

注意:虽然父类中private修饰的成员变量在子类中不能直接访问,但也继承到子类中了。

什么时候用哪一种呢?

我们希望类要尽量做到”封装“,即隐藏内部实现细节,只暴露出必要的信息给类的调用者。

因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如:如果一个方法能用private,就尽量不要用public。

另外,还有一种简单粗暴的做法:将所有字段设为private,将所有的方法设为public,不过这种方式属于是对访问权限的滥用,写代码时应该认真思考,该类提供的字段方法到底给“谁”使用(是类内部自己用,还是类的调用者使用,还是子类使用)。

这张表清楚的展现了访问情况,这里我们重点说一下protected再不同包的子类中的访问情况:

//包ProtectedTest
package ProtectedTest;


public class B {
    private int a = 1;
    protected int b = 100;
    public int c = 200;
    int d = 300;
}

//包ProtectedTest1
package ProtectedTest1;

import ProtectedTest.B;

public class D extends B {
    public static void main(String[] args) {
        B test = new B();
        System.out.println(test.b);
    }
}

此时将会产生错误:在这里插入图片描述

因为b不能通过对象的引用访问,而是应该通过super。但是当我们用super调用会发现:

 public static void main(String[] args) {
        B test = new B();
        System.out.println(super.b);
    }

还是会出现错误提示:在这里插入图片描述

这是因为main方法是通过static修饰的,static方法不依赖于对象,不能用super。那么应该如何调用呢?

public class D extends B {
    public void func() {
        System.out.println(super.b);
    }
    
    public static void main(String[] args) {
        D test = new D();
        test.func();
    }
}

结果为:在这里插入图片描述

显然这样访问成功了。

注意:类的修饰不能用protected和private。

9、继承层次

显示中,事物之间的关系错综复杂,例如:

在这里插入图片描述

像这样,由一个公共超类派生出来的所有类合称为继承层次。

在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)

在Java中只支持一下几种继承方式:

在这里插入图片描述

注意:Java中不支持多继承.。

我们写的类是现实事物的抽象,在实际应用过程中,可能会涉及到一系列复杂的概念,都需要我们使用代码来表示。所以我们实际开发中写的类会有很多,类之间的关系也会更加复杂。

但即使如此,我们并不希望类之间的继承层次太复杂,**一般我们不希望出现超过三层的继承关系。**如果继承层次太多,就需要考虑对代码进行重构了。

Java虽然不支持多继承,但提供了一些类似多重继承的功能,有关内容请看本人写的抽象类与接口:http://t.csdn.cn/uGmPx

10、final关键字

final可以用来修饰变量、成员方法和类。

  1. 修饰变量或者字段:表示常量(即不可更改)

    final int a = 10;
    a = 20;//编译出错
    
  2. 修饰类:表示此类不能被继承

    final public class Base {
        //...
    }
    class Derived extends Base {
        //...
    }
    //编译出错,无法继承
    

    经典例子:String类:

  3. 修饰方法:表示该方法不能被重写

11、继承和组合

和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合没有涉及特殊的语法(诸如extends这样的关键字),仅仅是将一个类的实例作为另一个类的字段。

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

组合表示对象之间是has-a的关系:比如汽车和其发动机、方向盘、刹车系统等等之间的关系就是组合,因为契合是由这些部件组成的。

//轮胎类
class Tire {
    //...
}
//发动机类
class Engine {
    //...
}

...

//汽车类
class Car {
    private Tire tire;//可以复用轮胎类中的属性和方法
    private Engine engine;//可以复用发动机类中的属性和方法
    //...
}
//玛莎拉蒂属于汽车
class Maserati {
    //将汽车中包含的:轮胎、发动机等等全部继承下来
}

​ 组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。

二、多态

1、多态的概念

具体来说就是去完成某个行为,当不同的对象去完成时会产生不同的状态。

在这里插入图片描述

同样是吃,富人就吃美食,穷人就吃糟糠,这就是同一件事情在不同对象身上,就会产生不同的结果。

就如同下面将会讲到的例子

public class test {
    public static void eatFunc(Animal animal) {
    animal.eat();
	}
public static void main(String[] args) {
    Dog dog = new Dog("旺财"7);
    eatFunc(dog);
    Cat cat = new Cat("喵呜",7);
    eatFunc(cat);
	}
}
class Dog extends Animal{
    //...
    @Override
    public void eat() {
        //...
    }
}
class Cat extends Animal{
    //...
    @Override
    public void eat();
}
class Animal {
    //...
    public void eat() {
        
    }
}

在方法main中,传递给eatFunc()的子类对象不同,调用eat这个重写方法所表现出来的行为就是不一样,传猫就调用猫的重写方法,传狗就调用狗的重写方法,这种思想就叫多态

2、多态实现的条件

在Java中实现多态,必须满足一下条件,缺一不可:

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

当完成以上三部分,就会发生动态绑定。动态绑定时多态的基础。

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

3、动态绑定和静态绑定

动态绑定:也成为前期绑定(早绑定、运行时绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表:函数重载

静态绑定:也成为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。

3.1、重写

重写(Override):也称为覆盖。重写是子类对父类非静态、非private修饰、非final修饰、非构造方法等实现过程进行重新编写,**返回值和型材不能改变,即外壳不变,核心重写。**重写的好处在于子类可以根据需求,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。

【重写的规则】

  • 子类在重写父类的方法时,必须与父类方法原型一致:返回值类型、方法名、参数列表要完全一致。
  • 被重写的方法返回值可以不同,但是必须时具有父子关系的。
  • **访问权限不能比父类中被重写的方法的访问权限更低。**例如:如果父类方法public修饰,则子类中重写该方法就不能声明为protected。
  • 父类被static、private修饰的方法、构造方法都不能被重写。
  • 重写的方法,可以使用Override注解来显示指定,有了这个注解能帮我们进行一些合法性校验,例如不小心将方法名字拼写错了(例如eat写成了aet),那么此时编译器就会发现父类中没有aet方法,就会编译报错,提示无法构成重写。

【重写与重载的区别】

区别点重写(Override)重载(Overload)
参数列表一定不能修改必须修改
返回类型一定不能修改(除非能构成父子类关系)可以修改
访问限定符一定不能做更严格的限制(可以降低限制)可以修改

【重写的设计原则】

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

4、向上转型和向下转型
4.1向上转型

当我们使用实例化子类对象调用父类和子类方法时:

public class Transition {
    public static void main(String[] args) {
        Dog dog = new Dog("旺财",7);
        dog.bark();
        dog.eat();
    }
}
class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(this.name+"吃");
    }

    public String getName() {
        return name;
    }
}
class Dog extends Animal{

    public Dog(String name, int age) {
        super(name, age);
    }
    public void bark() {
        System.out.println(super.getName()+"正在狗叫");
    }
}
结果为:
旺财正在狗叫
旺财吃

但如果我们实例化父类对象试图调用子类成员方法时:

public class Transition {
    public static void main(String[] args) {
        
        System.out.println("===================");
        Animal animal = new Animal("乐迪",8);
        animal.eat();
        animal.bark();//报错

    }
}
class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void eat() {
        System.out.println(this.name+"吃");
    }

    public String getName() {
        return name;
    }
}
class Dog extends Animal{

    public Dog(String name, int age) {
        super(name, age);
    }
    public void bark() {
        System.out.println(super.getName()+"正在狗叫");
    }
}

此时会出现错误:

无法解析 ‘Animal’ 中的方法 ‘bark’

得出结论:通过父类引用只能调用父类自己特有的成员方法或者成员变量。

而向上转型实际就是创建一个子类对象,将其当成父类对象来使用。

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

Animal animal = new Dog("旺财",7);//代表了animal这个引用指向了
等同于
Dog dog = new Dog("旺财",7);
Animal animal = dog;
//一般来说等号两边要赋值需要两边的类型一样,但因为两者是继承关系,所以可以进行赋值操作

Animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围转换。

除了以上的直接赋值的向上转型方式,另外两种向上转型:

public static void func(Animal animal){
    //...
}
public static Animal func2() {
    Dog dog = new Dog("旺财",7);
    return dog;
}
public static void main(String[] args) {
    Animal animal = func2();
}

总结:常见的可以发生向上转型的3个时机

  1. 直接赋值
  2. 方法的参数,传参的时候进行向上转型
  3. 返回值向上转型

向上转型的优点:让代码实现更简单灵活。

向上转型的缺点:不能调用子类特有的方法。

4.2、向下转型

将子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。

Animal animal = new Dog("旺财",7);
//若此时我们调用bark()方法
animal.bark();
//会编译失败,因为animal中没有bark()方法。
//向下转型
dog = (Dog)animal;//因为animal本来指向的就是Dog,因此animal能被还原为狗。
dog.bark();

//但是如果我们还原为猫
cat = (Cat)animal;
cat.mew();
//程序能通过编译但会抛出ClassCastException的异常
//因为animal实际指向的是Dog

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof,如果该表达式为true,则可以安全转换。

//向上转型
Dog dog = new Dog("旺财",7);
Cat cat = new Cat("喵呜",7);
Animal animal = dog
animal.eat();
animal = cat;
animal.eat();

if(animal instanceof Dog) {
    dog = (Dog)animal;
    dog.bark();
}
if (animal instanceof Cat) {
    cat = (Cat)animal;
    cat.mew();
}

总结:常见的可以发生向上转型的3个时机:

  1. 直接赋值
  2. 方法的参数,传参的时候进行向上转型
  3. 返回值向上转型

向上转型的有点:让代码实现更简单灵活。
向上转型的缺点:不能调用子类特有的方法。

4.2、向下转型

将子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型。

Animal animal = new Dog("旺财",7);
//若此时我们调用bark()方法
animal.bark();
//会编译失败,因为animal中没有bark()方法。
//向下转型
dog = (Dog)animal;//因为animal本来指向的就是Dog,因此animal能被还原为狗。
dog.bark();

//但是如果我们还原为猫
cat = (Cat)animal;
cat.mew();
//程序能通过编译但会抛出ClassCastException的异常
//因为animal实际指向的是Dog

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof,如果该表达式为true,则可以安全转换。

//向上转型
Dog dog = new Dog("旺财",7);
Cat cat = new Cat("喵呜",7);
Animal animal = dog
animal.eat();
animal = cat;
animal.eat();

if(animal instanceof Dog) {
    dog = (Dog)animal;
    dog.bark();
}
if (animal instanceof Cat) {
    cat = (Cat)animal;
    cat.mew();
}
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值