云计算百科
云计算领域专业知识百科平台

Java继承与多态

文章目录

  • 前言
  • 继承
    • 概念
    • 语法
    • 父类成员变量访问
    • 父类成员方法的访问
    • super 关键字
    • 父类与子类的构造方法
    • 父类与子类代码执行的顺序
    • protected
    • 继承的方式
  • final 关键字
    • 修饰变量
    • 修饰类
    • 修饰成员方法
  • 多态
    • Java 中多态的实现条件
    • 方法的重写
      • 规则
    • 重写和重载
    • 重写的原则
    • 向上转型与动态绑定
    • 向下转型
    • 多态的使用
    • 避免在构造方法中调用重写的方法

前言

本人的技术笔记会优先发布在个人站点longyxblg.cn

继承

面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。

概念

继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性 的基础上进行扩展,增加新功能,这样产生新的类,称子类(也称派生类或扩展类)。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。

语法

在 Java 中如果要表示类之间的继承关系,需要用到 extends 关键字

修饰符 class 子类 extends 父类 {
// …
}

此时有一个 Animal 类, 有 Dog 类和 Cat 类继承他

public class Animal {
String name = "默认名称";
int age = 18;

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

public class Cat extends Animal{

}

public class Dog extends Animal{

}

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

public class Test {
public static void main(String[] args) {
//
Cat cat = new Cat();
Dog dog = new Dog();
System.out.println(cat.age);
System.out.println(cat.name);
System.out.println(dog.age);
System.out.println(dog.name);
cat.eat();
dog.eat();
}
}

在这里插入图片描述

父类成员变量访问

  • 子类和父类不存在同名成员变量

    参考上文代码。

  • 子类和父类存在同名成员变量

    public class Dog extends Animal{
    String name = "旺财";
    }
    Dog dog = new Dog();
    Animal animal = new Animal();
    System.out.println(dog.name);
    System.out.println(animal.name);

    在这里插入图片描述

    如果访问的成员变量子类中有,优先访问自己的成员变量。

父类成员方法的访问

  • 成员方法不同名(或子类中没有该成员方法)

    参考上文中的代码

  • 成员方法同名

    public class Dog extends Anima{
    String name = "旺财";

    @Override
    public void eat() {
    System.out.println(this.name + "正在吃狗粮");
    }
    }
    Dog dog = new Dog();
    Animal animal = new Animal();
    dog.eat();
    animal.eat();

    在这里插入图片描述

    同名时,子类对象会优先使用自己的成员方法。

super 关键字

在某些情况下,想要在子类中使用父类的成员方法和成员变量,这时候可以用到 super 关键字。

public class Animal {
String name = "默认名称";
int age = 18;
String variety = "我也不知道写什么";

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

public class Dog extends Animal{
String name = "旺财";
String variety = "中华田园犬";
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}

public void test1(){
System.out.println(super.variety);
System.out.println("测试用 super 访问父类中的 variety");
}
}

Dog dog = new Dog();
System.out.println(dog.variety);
dog.test1();

在这里插入图片描述


public class Animal {
String name = "默认名称";
int age = 18;
String variety = "我也不知道写什么";

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

public class Dog extends Animal{
String name = "旺财";
String variety = "中华田园犬";
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
public void animalEat(){
super.eat();
}

public void test1(){
System.out.println(super.variety);
System.out.println("测试用 super 访问父类中的 variety");
}
}

Dog dog = new Dog();
dog.eat();
dog.animalEat();

在这里插入图片描述

super 关键字也可以访问父类中的成员方法。


需要注意的是 super 关键字不能在静态方法中使用。

在这里插入图片描述

父类与子类的构造方法

public class Animal {
String name = "默认名称";
int age = 18;
String variety = "我也不知道写什么";

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

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

给 Animal 中手写一个构造方法后,编译报错了

在这里插入图片描述

这其实是因为子类也会继承父类的构造方法,前文的代码中 Animal、Cat、Dog 中都没有写构造方法,这时编译器会给这三个类中自动生成构造方法。

Animal 中自动生成的是

public Animal() {

}

而到 Cat 和 Dog 类中生成的构造方法则是

public Cat(){
super;
}

public Dog(){
super;
}

编译器给子类自动生成的构造方法会用 super 关键字自动调用父类的构造方法。这意味着开发人员也可以用 super 关键字进行调用。

public class Animal {
String name = "默认名称";
int age = 18;
String variety = "我也不知道写什么";

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

public Animal() {

}

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

public class Dog extends Animal{
String name = "旺财";
String variety = "中华田园犬";
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
public void animalEat(){
super.eat();
}

public Dog() {
}

public Dog(String name, int age, String variety) {
super(name, age, variety);
}

public static void test2(){
// super 关键字不能在静态方法中使用。
// System.out.println(super.age);
}

public void test1(){
System.out.println(super.variety);
System.out.println("测试用 super 访问父类中的 variety");
}
}

Dog dog = new Dog("哈吉汪",2,"哈士奇");
dog.eat();
dog.animalEat();

在这里插入图片描述

通过调试窗口可以看到,在 Dog 类中调用使用 Super 方法后, 是给从 Animal 中继承下来的成员变量进行赋值。Dog 类中存在的同名成员变量并没有进行赋值。

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

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

  • 在构造方法中无论是 this 引用还是 super 引用都要放在构造方法的第一句

在这里插入图片描述

在这里插入图片描述

  • 在构造方法中不能同时出现 this 引用和 super 引用

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

  • this 和 super 都只能在类的非静态方法中使用, 访问非静态方法和字段

父类与子类代码执行的顺序

public class A {
int a;

static {
System.out.println("父类的静态代码块执行了");
}

{
this.a = 10;
System.out.println("父类的构造代码块执行了");
}
}

public class B extends A{
int b;
static {
System.out.println("子类的静态代码块执行了");
}

{
this.b = 20;
System.out.println("子类的构造代码块执行了");
}
}

在这里插入图片描述

可见是静态代码块先执行,父类的代码块比子类的代码块先执行

A a1 = new A();
B b1 = new B();
System.out.println("====我是一条分割线====");
A a2 = new A();
B b2 = new B();

在这里插入图片描述

静态代码块只会执行一次

初始化顺序为父类静态代码块 → 子类静态代码块 → 父类实例代码块 → 父类构造方法 → 子类实例代码块 → 子类构造方法

protected

在类与对象中介绍了 public private default 三个访问修饰限定符。

名称同一个包同类同一个包不同类不同包中子类不同包中的非子类
public √ √ √ √
protected √ √ √
default √ √
private √

而 protected 主要用于继承中

此时新建一个 Demo2 包, 其中存放 C Test 类 , C 继承 A 类。

package Demo1;

public class A {
protected int a;

protected void test(){
System.out.println("这是一个A类中被 protected 修饰的方法");
}

public A(){

}
}

package Demo2;

import Demo1.A;

public class C extends A {

public void c_Test(){
System.out.println(super.a);
super.test();
}

public static void main(String[] args) {
C c1 = new C();
c1.c_Test();
}
}

package Demo2;

import Demo1.A;

public class Test {
public static void main(String[] args) {
A a1 = new A();
a1.test();
}
}

在这里插入图片描述


那么让 A 中出现一个被 private 修饰的变量

package Demo1;

public class A {
protected int a;

private int b;

protected void test(){
System.out.println("这是一个A类中被 protected 修饰的方法");
}

public A(){

}

public static void main(String[] args) {
C c1 = new C();
System.out.println();
}

在这里插入图片描述

通过调试查看到, 被 private 修饰的成员变量 b 也被集成到了子类中

在这里插入图片描述

在这里插入图片描述

但是就算被继承到了子类中也不能访问

继承的方式

Java 中只支持四种继承方式:

  • 单继承

    最简单的继承方式,例如

    public class A{
    ...
    }

    public class B extends A{
    ...
    }

  • 不同类继承同一个类

    多个类继承同一个类,例如

    public class A{
    ...
    }

    public class B extends A{
    ...
    }

    public class C extends A{
    ...
    }

  • 多层继承 Java 中不支持多继承,例如

    public class A{
    ...
    }

    public class B {
    ...
    }

    public class C extends A,B{
    ...
    }

    可以做到多层继承,例如

    public class A{
    ...
    }

    public class B extends A {
    ...
    }

    public class C extends B{
    ...
    }

一般不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了。如果想从语法上进行限制继承, 就可以使用 final 关键字

final 关键字

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

修饰变量

被 final 修饰的变量就会被视为常量,不允许修改,类似于 C 语言中的 const 关键字。

在这里插入图片描述

修饰类

被 final 修饰的类将不被允许继承

package Demo1;

final public class A {
}

package Demo1;

public class B extends A {
}

在这里插入图片描述

修饰成员方法

被 final 修饰的成员方法不允许被重写,会在后文介绍。

多态

面向对象的三大特性之一,简单点说就是不同的对象去完成同一个行为,会产生不同的状态。例如同样都是学习 Java , 张三学得非常轻松,李四学得非常痛苦。这就是多态。

Java 中多态的实现条件

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

  • 必须在继承体系下

  • 子类必须要对父类中方法进行重写

  • 通过父类的引用调用重写的方法

方法的重写

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


规则

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

package Demo1;

public class A {

public void test1(){
System.out.println("这是 A 类中的test1方法");
}
}

package Demo1;

public class B extends A{
@Override
public void test1() {
System.out.println("这是 B 类中的test1方法");
}
}

A a1 = new A();
B b1 = new B();
a1.test1();
b1.test1();

在这里插入图片描述

@Override 表示该方法被重写,编译器会多一层校验

  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的

在这里插入图片描述

  • 访问权限不能比父类中被重写的方法的访问权限更低

在这里插入图片描述

这里 B 类中没写修饰符默认为 default , 编译报错。

重写和重载

重写\\重载参数列表方法名返回值访问修饰限定符
重写 必须相同 必须相同 必须相同,不同必须具有父子类关系 权限大于等于被重写的方法
重载 必须不同 必须相同 可以不同 可以不同

重写的原则

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


通过重写,可以让同一个方法在被不同类型的对象使用时发生不同的行为,这正是多态的体现。

向上转型与动态绑定

现在新创建 Animal Dog Cat 类

package Demo2;

public class Animal {
String name;
int age;
String habit;

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

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

package Demo2;

public class Dog extends Animal{
public Dog(String name, int age, String habit) {
super(name, age, habit);
}

@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
}

package Demo2;

public class Cat extends Animal{
public Cat(String name, int age, String habit) {
super(name, age, habit);
}

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

实际就是创建一个子类对象,将其当成父类对象来使用。发生在三种情况中:

  • 直接在创建对象时发生向上转型

Animal animal = new Dog("大黄",1,"出去玩");
animal.eat();

在这里 new Dog 是实例化的一个 Dog 类对象,而 Animal 类对象 animal 却能引用这个对象,就是在此处发生了向上转型。

在这里插入图片描述

而 Animal 类型的对象 animal 在调用 eat 方法时输出的是 Dog 类中的 eat 方法。这是发生了动态绑定,JVM 发现 animal 实际是 Dog,就调用 Dog 的 eat。

  • 方法传参时发生向上转型:

package Demo2;

public class Animal {
String name;
int age;
String habit;

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

public Animal(){

}

public void test1(Animal animal){
System.out.println(animal.name + "这是 Animal 类的 test1 方法");
}

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

package Demo2;

public class Dog extends Animal{
public Dog(String name, int age, String habit) {
super(name, age, habit);
}

@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}

@Override
public void test1(Animal animal) {
System.out.println(animal.name + "这是 Dog 类的 test1 方法");
}
}

Dog dog = new Dog("大黄",1,"出去玩");
Animal animal = new Animal();
dog.test1(dog);

在这里插入图片描述

  • 方法返回值发生向上转型

package Demo2;

public class Dog extends Animal{
public Dog(String name, int age, String habit) {
super(name, age, habit);
}

public Dog() {
super();
}

@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}

@Override
public void test1(Animal animal) {
System.out.println(animal.name + "这是 Dog 类的 test1 方法");
}

public static Dog test2(){
return new Dog();
}
}

Animal animal = Dog.test2();
System.out.println();

在这里插入图片描述

调试窗口可以看到也成功赋值了。


需要注意的是,不能调用到子类特有的方法

package Demo2;

public class Dog extends Animal{
public Dog(String name, int age, String habit) {
super(name, age, habit);
}

public Dog() {
super();
}

@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}

@Override
public void test1(Animal animal) {
System.out.println(animal.name + "这是 Dog 类的 test1 方法");
}

public static Dog test2(){
return new Dog();
}

public void test3(){
System.out.println("这是 Dog 类独有的 test3 方法");
}
}

Animal animal = new Dog();
animal.test3();

在这里插入图片描述

向下转型

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

Animal animal = new Dog();
Dog dog = (Dog) animal;
dog.test3();

在这里插入图片描述

需要注意的是向下转型会用到强制类型转换,这本来就是不安全的。如果转换失败,运行时会抛出异常。Java 中为了提高向下转型时的安全性,引入了 instanceof 关键字。

在这里插入图片描述

Animal animal = new Animal();
if(animal instanceof Dog){
Dog dog = (Dog) animal;
}
else
{
System.out.println("不能这么转换!");
}

在这里插入图片描述

多态的使用

此时新建另一个包 Demo3,在这个包下新建 Animal Dog Cat Bird Fish 类,Dog Cat Bird Fish 继承 Animal 。

package Demo3;

public class Animal {

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

package Demo3;

public class Dog extends Animal{
@Override
public void eat() {
System.out.println("吃狗粮!");
}
}

package Demo3;

public class Cat extends Animal{
@Override
public void eat() {
System.out.println("吃猫粮");
}
}

package Demo3;

public class Bird extends Animal{
@Override
public void eat() {
System.out.println("吃虫子!");
}
}

package Demo3;

public class Fish extends Animal{
@Override
public void eat() {
System.out.println("吃蚯蚓!");
}
}

现在想输出每个类的 eat 方法。采用分支语句可能要这么写

public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Bird bird = new Bird();
Fish fish = new Fish();
Animal[] animals = {dog,cat,bird,fish};
for (Animal animal: animals){
if(animal.equals(dog)){
dog.eat();
}else if(animal.equals(cat)){
cat.eat();
}else if(animal.equals(bird)){
bird.eat();
}else if(animal.equals(fish)){
fish.eat();
}
}

在这里插入图片描述

而是用多态的话就简单多了

Dog dog = new Dog();
Cat cat = new Cat();
Bird bird = new Bird();
Fish fish = new Fish();
Animal[] animals = {dog,cat,bird,fish};
for(Animal animal: animals){
animal.eat();
}
}

在这里插入图片描述

新增一个 Monkey 类

在这里插入图片描述

这时要输出这个 Monkey 类的 eat() 方法使用多态完成也更简单

Dog dog = new Dog();
Cat cat = new Cat();
Bird bird = new Bird();
Fish fish = new Fish();
Monkey monkey = new Monkey();
Animal[] animals = {dog,cat,bird,fish,monkey};
for(Animal animal: animals){
animal.eat();
}

在这里插入图片描述

但多态并非没有缺点;

  • 属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性

  • 构造方法没有多态性

避免在构造方法中调用重写的方法

public class A {
public A(){
func();
}

public void func(){
System.out.println("A类中的func方法执行了");
}
}

public class B extends A{
private int num = 1;

@Override
public void func() {
System.out.println("B类中的func方法执行了"+num);
}
}

B b = new B();

在这里插入图片描述

在实例化 b 时, 会调用 A 中的构造方法,而构造方法中又调用了 func 方法。这个过程是由 b 这个对象调用的,触发动态绑定,直接调用了 B 类中被重写的 func 方法,而此时 b 对象还没有被构造完成,num 处于未初始化的状态,所以为 0 。如果具有多态性的话,num 值应该为 1 。

赞(0)
未经允许不得转载:网硕互联帮助中心 » Java继承与多态
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!