《JAVASE系列》继承与多态

# 《JAVASE系列》继承与多态

前言:

本章学习内容:

  • 熟悉继承的语法
  • 学会如何使用继承
  • 理解多态以及背后知识

参考书籍:《java核心卷1》

你若盛开,清风自来。

1.继承

1.1 继承的作用

在面向对象的世界里,对象用来表示现实的事物,但是现实事物中必有很多共同之处与联系,所以java语言需要考虑这个问题:

例如:狗和猫都是动物,都有年龄体重

在这里插入图片描述

在这里插入图片描述

如果没有继承思想的话:

就会产生代码的重复

class Dog{
    public String name;
    public int age;
    public float weight;
    public void eat(){
        System.out.println(name+"吃饭");
    }
    public void sleep(){
        System.out.println(name+"睡觉");
    }
    public void bark(){
        System.out.println(name+"汪汪");
    }
}
class Cat{
    public String name;
    public int age;
    public float weight;
    public void eat(){
        System.out.println(name+"吃饭");
    }
    public void sleep(){
        System.out.println(name+"睡觉");
    }
    public void mew(){
        System.out.println(name+"喵");
    }
}
public class test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        dog.name = "小王";
        cat.name = "小咪";
        dog.eat();
        dog.sleep();
        dog.bark();
        cat.eat();
        cat.sleep();
        cat.mew();

    }
}

运行结果:

在这里插入图片描述

我们可以从代码中看到:狗类与猫类有很多相同之处。我们可以用继承来解决这个代码重复的问题,面相对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。

1.2 继承概念

继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。 继承是面向对象 软件技术当中的一个概念。 这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。 子类的创建可以增加新数据、新功能,可以继承父类全部的功能,但是不能选择性的继承父类的部分功能。

简单来说:就是子类可以继承父类的属性与方法,实现代码的复用,子类抽取了父类,来实现代码的复用。

在这里插入图片描述

其中父类为Animal,具有狗和猫的共性。

狗和猫则作为子类,具有自己特有的特征。

1.3 继承语法

修饰符 class 子类 extends 父类 {

    //.......

}

对代码以继承方式是实现:

class Animal{
    public String name;
    public int age;
    public float weight;
    public void eat(){
        System.out.println(name+"吃饭");
    }
    public void sleep(){
        System.out.println(name+"睡觉");
    }
}
class Dog extends Animal{
    public void bark(){
        System.out.println(name+"汪汪");
    }
}
class Cat extends Animal{
    public void mew(){
        System.out.println(name+"喵");
    }
}
public class test {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        dog.name = "小王";
        cat.name = "小咪";
        dog.eat();
        dog.sleep();
        dog.bark();
        cat.eat();
        cat.sleep();
        cat.mew();
    }
}
  1. 子类会将父类中的成员变量或者成员方法继承到子类中了
  2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了

1.4 如何访问父类成员

1.4.1 子类中访问父类的成员变量
  • 子类和父类中不存在同名成员变量时,可以直接在子类中直接访问。(父类成员不被private修饰情况下)

    public class Base {
        int a;
        int b;
    }
     
    public class Derived extends Base{
        int c;
        public void method(){
            a = 10;    // 访问从父类中继承下来的a
            b = 20;    // 访问从父类中继承下来的b
            c = 30;    // 访问子类自己的c
        }
    }
    
  • 子类和父类中存在同名成员变量时

    class Base{
        int a;
        int b;
        int c;
    }
    class Dire extends Base{
        char a;
        char b;
        public void givenum(){
            a = 97;
            b = 98;
            c = 99;
        }
        public void print(){
            System.out.println(a+" "+b+" "+c);
        }
    }
    public class test {
        public static void main(String[] args) {
            Dire dire = new Dire();
            dire.givenum();
            dire.print();
        }
    
    }
    

    运行结果:

    在这里插入图片描述

    **可以明显看出代码赋值与打印的都是子类的成员变量,所以才会打印出a,b。**而c在子类中没用,所以访问的就是父类的

  • 结论:

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

    1. 如果访问的成员变量子类中有,优先访问自己的成员变量。
    2. 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
    3. 如果访问的成员变量与父类中成员变量同名,则优先访问自己的,即:子类将父类同名成员隐藏了。
    4. 成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
    5. 同名情况下的访问见super关键字
1.4.2 子类中访问父类的成员方法
  • 父类子类成员方法不同名时

    public class Base {
     public void methodA(){
         System.out.println("Base中的methodA()");
     }
    }
     
    public class Derived extends Base{
        public void methodB(){
            System.out.println("Derived中的methodB()方法");
       }
        
        public void methodC(){
            methodB();         // 访问子类自己的methodB()
            methodA();         // 访问父类继承的methodA()
            // methodD();     // 编译失败,在整个继承体系中没有发现方法methodD()
       }
    }
    

    成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。

  • 父类和子类成员方法名字相同

    public class Base {
     public void methodA(){
         System.out.println("Base中的methodA()");
     }
     
     public void methodB(){
         System.out.println("Base中的methodB()");
     }
    }
     
    public class Derived extends Base{
        public void methodA(int a) {
            System.out.println("Derived中的method(int)方法");
       }
     
        public void methodB(){
            System.out.println("Derived中的methodB()方法");
       }
     
        public void methodC(){
            methodA();      // 没有传参,访问父类中的methodA()
            methodA(20);    // 传递int参数,访问子类中的methodA(int)
            methodB();      // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到
        }
    }
    
    
    1. 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
    2. 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;如果父类和子类同名方法的原型一致,则只能访问到子类的,父类的无法通过派生类对象直接访问到。

1.5 super关键字

当子类与父类存在相同名称的成员,子类需要访问父类同名成员时,就需要使用super关键字。

java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。

class A {
   public int a;
   public int b;
   public int c;
   public A(){
        a = 10;        
        b = 10;
        c = 10;
    }
    public void func(){
        System.out.println("父类方法");
    }
}
class B extends A{
    public int a = 100;
    public int b = 100;
    public void func(){
        System.out.println("子类方法");
    }
   public void print(){
        System.out.println(super.a);
            System.out.println(this.a);
        super.func();
        this.func();

    }
}
public class test {
    public static void main(String[] args) {
        B b = new B();
        b.print();
    }
}

运行结果:

在这里插入图片描述

通过子类调用super 关键字可以调用父类中的成员。即使不同名的情况也可以使用super关键字。

super是父类对象的引用是错误的,super只是一个关键字,用来向使用者说明这个成员变量是父类的,this是一个隐藏的参数,而super并不是。

即:

在这里插入图片描述

注意:

不能在静态方法中使用super关键字。

在这里插入图片描述

1.6 子类构造方法

在子类接受父类的继承以后,需要先调用父类的构造方法再调用子类的构造方法。

一个编译器默认实现的

class A {
   public int a;
   public int b;
   public int c;
   public A(){
        a = 10;        
        b = 10;
        c = 10;
    }
    public void func(){
        System.out.println("父类方法");
    }
}
class B extends A{
    public int a = 100;
    public int b = 100;
    public void func(){
        System.out.println("子类方法");
    }
}

其中子类B中并没有构造方法,编译器为子类B默认实现了一个构造方法:

public B(){
    super();
}

super() 相当于调用了父类的构造方法。

​ 父类子类肯定是先有父再有子,所以在构造子类对象时候 ,先要调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。

当我们自己初始化构造方法时候,编译器不会再默认为我们生成一个构造方法,所以在自定义初始化构造方法的时候,要注意为子类构造方法加上super() ,同时super() 必须是子类构造方法第一条语句,super(…)只能在子类构造方法中出现一次,并且不能和this同时出现。

1.7 super与this

【相同点】

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

【不同点】

  1. this是当前对象的引用,super不是。

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

  3. this是非静态成员方法的一个隐藏参数,super不是一个隐藏的参数。

  4. 成员方法中直接访问本类成员时,编译之后会将this还原,即本类非静态成员都是通过this来访问的;在子类中如果通过super访问父类成员,编译之后在字节码层面super实际是不存在的。

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

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

1.8子类父类中的代码执行顺序

代码说明:

class A{
    //实例代码块
    {
        System.out.println("父类实例代码块");
    }
    static {
        System.out.println("父类静态代码块");
    }
    public A(){
        System.out.println("父类构造方法");
    }
}
class B extends A{
    //实例代码块
    {
        System.out.println("子类实例代码块");
    }
    static {
        System.out.println("子类静态代码块");
    }
    public B(){
        super();
        System.out.println("子类构造方法");
    }
}
public class test {
    public static void main(String[] args) {
        B b = new B();
        System.out.println("=============");
        B b1 = new B();
    }
}

运行结果:

在这里插入图片描述

结论:

  • 优先级: 静态代码>实例代码>构造方法

  • 并且创建多个对象时,静态代码只执行一次。

1.9 继承权限proteced

访问限定修饰符:

  • public : 程序中都可以访问

  • private : 只能在本类中访问

  • 默认权限:也叫包访问权限,可以在该文件包低下的类中访问

  • protected: 除了拥有包访问权限,还可以在继承了该类的子类中访问。

如表格:

序号范围private默认protectedpublic
1同一个包的同一个类yesyesyesyes
2同一个包的不同类yesyesyes
3不同包中的子类yesyes
4不同包中的非子类yes

如何使用访问权限呢:

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public.另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private,将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 写代码的时候需要认真思考, 该类提供的字段方法到底是谁来使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).

1.10 继承方式

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

  • 单继承
  • 多层继承
  • 不同类继承同一个类

但是java不支持多继承

在这里插入图片描述

1.11 final关键字

  1. 修饰变量或字段,表示常量(即不能修改)。

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

final public class Animal {
   ...
}
 
public class Bird extends Animal {
   ...
}
//编译失败
  1. 修饰方法:表示该方法不能被重写。

1.12 组合

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

继承表示对象之间是is-a的关系

组合表示对象之间是has-a的关系

实现一个组合

class graphics{
    public graphics(){
        System.out.println("显卡");
    }
}
class harddisk{
    public harddisk(){
        System.out.println("硬盘");
    }
}
class mainboard{
    public mainboard(){
        System.out.println("主板");
    }
}
class computer{
    private graphics a;
    private mainboard b;
    private harddisk c;
    //...............
    public computer(){
        System.out.println("电脑");
    }
}
class lianxiang extends computer{
    //..........
}

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择。

2.多态

2.1 多态思想

去完成某个行为,当不同的对象去完成时会产生出不同的状态,同一件事情,发生在不同对象身上,就会产生不同的结果。

例如: 都是吃,猫和狗是对象,它们一个吃的是猫粮,吃的是狗粮,即使是同一个动作吃,但是不同的对象去完成,就会出现不同的结果。

要理解多态就得想了解:向上转型和方法重写

2.2 向上转型

向上转型概念:

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

语法规则:

Animal animal = new Cat("元宝",2);
//父类               子类

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

这样的父类对象animal 虽然 new 的是子类,但是animal并不能访问子类cat的方法。

向上转型作用有三个:

  • 直接赋值,也就是直接讲子类赋值给父类对象使用。

  • 作为方法的参数使用

    public static void eatFood(Animal a){
            a.eat();
    }
    

    a可以接收 Animal 的所有子类对象以及本身 Animal 对象。

  • 作为方法的返回值

    public static Animal buyAnimal(String var){
            if("狗" == var){
                return new Dog("狗狗",1);
           }else if("猫" == var){
                return new Cat("猫猫", 1);
           }else{
                return null;
           }
       }
    

    该方法可以返回 Animal 的所有子类 包括 本身 Animal。

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

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

2.3 方法重写

概念(看看就行)

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

class Animal {
    public String name;
    public int age;
    protected  void eat() {
        System.out.println(this.name+" 吃饭!Animal");
    }
}

class Cat extends Animal {
    public String hair;
    @Override
    protected void eat() {
        System.out.println(this.name+" 吃猫粮!");
    }
}

重点

  • 子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表) 要完全一致。

    注意 返回值可以不同,但是返回值必须是子父类的关系

  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected,也就是说子类的权限必须高于或者等于父类

  • 父类被static、private、final修饰的方法都不能被重写。

  • 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有aet 方法, 就会编译报错, 提示无法成重写。

方法重载与方法重写:

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

绑定相关知识:

静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。

其中重写属于动态绑定,即使在字节码,调用的也是父类的方法而不是子类重写后的方法,而是在运行时才确定调用子类的方法。

2.4 向下转型(不安全)

    public static void main(String[] args) {
        Animal animal = new Bird();
        Bird bird = (Bird)animal;//向下转型
        bird.fly();
    }

Animal 是 父类,父类引用子类对象,本身是没什么问题的这是向上转型,

anmial这个对象重新赋值给子类,就是向下转型。在这段代码中,animal本身是Bird类,重新赋值在Bird类的对象中,没有问题。

但是如果animal是Dog类型的,赋值在Brid类的对象中,就会发生异常。

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

public static void main(String[] args) {
        Animal animal = new Bird();
       if(animal instanceof Bird)   //保证向下转型的安全性
       {
           Bird bird = (Bird)animal;//向下转型
           bird.fly();
       }
    }

2.5 理解多态

通过了解向上转型,方法重写,实现一个多态。

class Animal {
    public String name;
    public int age;

    protected  void eat() {
         System.out.println(this.name+" 吃饭!Animal");
    }
}

class Cat extends Animal {
    public String hair;

    @Override
    protected void eat() {
        System.out.println(this.name+" 吃猫粮!");
    }


    public void mew() {
        System.out.println(this.name+" 正在叫!");
    }
}
class Bird extends Animal {
    public void eat() {
        System.out.println(this.name+" 吃鸟粮!");
    }

    public void fly() {
        System.out.println("正在飞!!!!");
    }
}

public class test {
    public static void eating(Animal animal){
        animal.eat();
    }
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.name = "mimi";
        Cat cat = new Cat();
        cat.name = "FIFI";
        eating(bird);
        eating(cat);
    }
}

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

eating 是一个行为,但是子类与父类通过向上转型与重写,实现了由不同的对象去完成相同的行为来产生不同的结果。

2.6 多态的好处与缺陷

好处:

  1. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

什么是圈复杂度:可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为 “圈复杂度”.如果一个方法的圈复杂度太高, 就需要考虑重构.

不使用多态:

Shape 是 Cycle,Rect,Flower的父类

public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
    
    for (String shape : shapes) {
        if (shape.equals("cycle")) {
            cycle.draw();
       } else if (shape.equals("rect")) {
            rect.draw();
       } else if (shape.equals("flower")) {
            flower.draw();
       }
   }
}

使用多态:(避免if-else)

Shape 是 Cycle,Rect,Flower的父类

public static void drawShapes() {
    // 我们创建一个 Shape 对象的数组. 
    Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
                      new Rect(), new Flower()};
    for (Shape shape : shapes) {
        shape.draw();
   }
}
  1. 可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

我们只需要利用继承父类并且重写父类的方法来实现。

缺陷:

代码的运行效率降低。

2.7 不要在父类的构造方法中调用重写的方法

class B {
    public B() {
        // do nothing
        func();
   }
 
    public void func() {
        System.out.println("B.func()");
   }
}
 
class D extends B {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("D.func() " + num);
   }
}
 
public class Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

该代码本意是父类去调用父类的func,但是由于子类重写了父类的func,触发了动态绑定,所以父类会调用子类的func。

所以:尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

总结:

学习完本章需要充分了解:

  • 继承中如何访问父类成员以及注意事项
  • 熟练掌握修饰限定符
  • 理解super,this关键字
  • 知晓final关键字的作用
  • 了解组合
  • 明白向上转型与方法重写
  • 理解多态思想以及多态中的注意事项

感谢阅读,与君共勉!
在这里插入图片描述

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小连~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值