Java 继承与多态

Java 继承与多态

继承与多态是面向对象编程(OOP)的核心特性,继承用于抽取类的公共特征、减少代码冗余,多态则实现“一种事物多种表现形态”,提升代码灵活性与扩展性。

继承的基本概念

继承的定义

继承是从已存在的类(父类)中定义新类(子类)的过程,子类会自动继承父类的非私有属性和方法,同时可添加自身特有的属性和方法。

  • 核心作用:代码复用、简化系统设计与维护。
  • 示例场景:宠物游戏中,Dog(狗)、Penguin(企鹅)、Pig(猪)均为宠物,可抽取公共特征形成父类Pet,子类继承后仅需关注自身特有属性(如狗的“品种”、企鹅的“性别”)。

不使用继承的问题

若直接定义DogPenguin类,会出现大量重复代码(如namehealth属性及eatplay方法),维护成本高。

Dog类(无继承)

package com.inherit.inherit;

/**
 * 狗
 * @author Jing61
 */
public class Dog {
    private String name;
    private int health;
    private int love;
    private String strain;

    public Dog() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHealth() {
        return health;
    }

    public void setHealth(int health) {
        this.health = health;
    }

    public int getLove() {
        return love;
    }

    public void setLove(int love) {
        this.love = love;
    }

    public String getStrain() {
        return strain;
    }

    public void setStrain(String strain) {
        this.strain = strain;
    }

    /**
     * 吃东西
     */
    public void eat() {
        if(this.health <= 95) {
            this.health += 3;
            this.love -= 5;
        }
    }

    /**
     * 玩
     */
    public void play() {
        if(this.health > 60) {
            this.health -= 5;
            this.love += 3;
        }
    }

    /**
     * 展示
     */
    public void showMe() {
        System.out.print("我的名字叫" + this.name + ",我的健康值是" + health + ",我和主人的亲密度是" + love + ",我的品种是" + strain);
    }
}

Penguin类(无继承)

package com.inherit.inherit;

/**
 * 企鹅
 * @author Jing61
 */
public class Penguin {
    private String name;
    private int health;
    private int love;
    private String sex;

    public Penguin() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHealth() {
        return health;
    }

    public void setHealth(int health) {
        this.health = health;
    }

    public int getLove() {
        return love;
    }

    public void setLove(int love) {
        this.love = love;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    /**
     * 吃东西
     */
    public void eat() {
        if(this.health <= 95) {
            this.health += 3;
            this.love -= 5;
        }
    }

    /**
     * 玩
     */
    public void play() {
        if(this.health > 60) {
            this.health -= 5;
            this.love += 3;
        }
    }

    /**
     * 展示
     */
    public void showMe() {
        System.out.print("我的名字叫" + this.name + ",我的健康值是" + health + ",我和主人的亲密度是" + love + ",我的性别是" + sex);
    }
}

继承的实现(抽取父类Pet)

通过extends关键字实现继承,Java支持单继承(一个子类仅能直接继承一个父类)。

父类Pet(公共特征)

package com.inherit.inherit;

/**
 * 宠物
 * @author Jing61
 */
public class Pet {
    private String name;
    private int health;
    private int love;

    public Pet() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHealth() {
        return health;
    }

    public void setHealth(int health) {
        this.health = health;
    }

    public int getLove() {
        return love;
    }

    public void setLove(int love) {
        this.love = love;
    }

    /**
     * 吃东西
     */
    public void eat() {
        if(this.health <= 95) {
            this.health += 3;
            this.love -= 5;
        }
    }

    /**
     * 玩
     */
    public void play() {
        if(this.health > 60) {
            this.health -= 5;
            this.love += 3;
        }
    }

    /**
     * 展示
     */
    public void showMe() {
        System.out.println("我的名字叫" + this.name + ",我的健康值是" + health + ",我和主人的亲密度是" + love);
    }
}

子类Dog(继承Pet)

package com.inherit.inherit;

/**
 * 狗
 * @author Jing61
 */
public class Dog extends Pet {
    private String strain;

    public Dog() {

    }
    
    public String getStrain() {
        return strain;
    }

    public void setStrain(String strain) {
        this.strain = strain;
    }
}

子类Penguin(继承Pet)

package com.inherit.inherit;

/**
 * 企鹅
 * @author Jing61
 */
public class Penguin extends Pet{
    private String sex;

    public Penguin() {

    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

子类Pig(继承Pet)

package com.inherit.inherit;

public class Pig extends Pet{
    private String sex;
    public Pig() {

    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
}

继承的测试验证

子类可直接使用父类的非私有方法(如setNameshowMe),无需重复定义。

package com.inherit.inherit;

/**
 * 继承测试类
 * @author Jing61
 */
public class PetTest {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.setName("huihui");
        dog.showMe();

        Pig pig = new Pig();
        pig.setName("peppa");
        pig.showMe();
    }

}

Dog 类中并没有setName 和 showMe 方法,是继承父类的方法。在java术语中,如果Dog类继承自Pet类,那么就将Dog类称为次类,将Pet称为超类。超类也称为父类或基类;次类也称为子类,扩展类或者派生类。

继承的关键注意事项

  1. 子类与父类的关系:子类不是父类的子集,而是父类的“扩展”,子类包含父类的公共特征+自身特有特征。
  2. 私有成员的访问:父类的private属性/方法无法被子类直接访问,但可通过父类的public getter/setter方法间接操作。
  3. is-a关系:继承仅适用于“是一种”关系(如Dog是一种Pet),不要仅仅为了重用方法而盲目扩展一个类,例如:尽管Person类和Tree类可以共享类似高度和重量这样的通用特性,但是从Person类扩展出Tree类是毫无意义的。不可为复用代码盲目继承(如Tree不应继承Person)。
  4. 单继承限制:Java不支持多重继承(一个子类不能直接继承多个父类),但可通过接口实现类似功能。
  5. 特殊is-a关系:并非所有is-a关系都适合继承(如Square是一种Rectangle,但Rectanglewidthheight属性不适用于正方形,不建议继承)。

super关键字

super关键字指代父类,用于在子类中访问父类的构造方法、普通方法和属性,与this(指代当前对象)相对应。

调用父类构造方法

  • 父类的构造方法不会被子类继承,仅能通过super(参数)在子类构造方法中调用。
  • 语法要求:super(参数)必须是子类构造方法的第一条语句
  • 构造方法链:在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类对象时,子类构造方法会在完成自己任务之前,首先调用它的父类构造方法。如果父类继承自其他类,那么父类构造方法又会在完成自己任务前调用它自己的父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为止。如果没有被显式的调用,编译器将会自动添加super()作为构造方法的第一条语句。

示例:调用父类有参构造

// Pet 父类
public Pet(String name) {
    this.name = name;
    System.out.println("我是Pet的构造方法");
}
// Penguin 子类
public Penguin(String name,String sex) {
    //显式的调用父类的构造方法
    super(name);//必须是构造方法的第一题语句
    this.sex = sex;
    System.out.println("我是Penguin的构造方法");
}

调用父类普通方法

当子类方法与父类方法重名时,用super.方法名()访问父类方法;无重名时,super可省略。

// 子类中调用父类的eat方法
public class Dog extends Pet {
    private String strain;

    public void eat() {
        super.eat(); // 调用父类的eat方法
        System.out.println("狗狗吃完东西后摇尾巴"); // 子类扩展逻辑
    }
}

方法重写(Override)

方法重写的定义

当子类继承父类的方法后,发现父类方法的逻辑不适用于子类时,可在子类中重新定义该方法(方法名、参数列表、返回值类型与父类一致),这一过程称为方法重写。

方法重写的规则

  1. 签名一致:子类方法与父类方法的方法名、形参列表、返回值类型必须完全相同。
  2. 权限不缩小:子类方法的访问权限不能小于父类方法(如父类方法为public,子类方法不能为protectedprivate)。
  3. 异常不扩大:子类方法抛出的异常类型不能大于父类方法(如父类抛出IOException,子类不能抛出Exception)。
  4. 静态方法特殊处理:子类可重写父类的静态方法,但必须也声明为static(本质是“方法隐藏”,而非真正重写,可通过“父类名.方法名”访问父类静态方法)。
  5. @Override注解:用于校验方法是否符合重写规则,若不符合则编译报错,建议重写方法加上。

方法重写示例

父类PetshowMe方法未包含子类特有属性,子类DogPenguin重写该方法:

子类Dog重写showMe

package com.inherit.inherit;

/**
 * 狗
 * @author Jing61
 */
public class Dog extends Pet {
    private String strain;

    public Dog() {

    }

    public String getStrain() {
        return strain;
    }

    public void setStrain(String strain) {
        this.strain = strain;
    }

    @Override
    public void showMe() {
        System.out.println("我的名字叫" + getName() + ",我的健康值是" + getHealth() + ",我和主人的亲密度是" + getLove() + ",我的品种是" + strain);
    }
}

子类Penguin重写showMe

package com.inherit.inherit;

/**
 * 企鹅
 * @author Jing61
 */
public class Penguin extends Pet{
    private String sex;

    public Penguin(String name) {
        super(name);
        System.out.println("我是Penguin的构造方法");
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public void showMe() {
        System.out.println("我的名字叫" + getName() + ",我的健康值是" + getHealth() + ",我和主人的亲密度是" + getLove() + ",我的性别是" + sex);
    }

}

实战练习:几何图形建模

需求:为几何图形建模(公共属性:创建时间、颜色、是否填充;公共方法:获取面积、周长),扩展Circle(圆)和Rectangle(矩形)子类并实现方法重写。

父类GeometricShapes(几何图形)

package com.inherit.inherit;

import java.time.LocalDate;

public class GeometricShapes {
    private LocalDate creationTime;
    private String color;
    private boolean fill;

    public GeometricShapes() {

    }

    public GeometricShapes(LocalDate creationTime, String color, boolean fill) {
        this.creationTime = creationTime;
        this.color = color;
        this.fill = fill;
    }

    public LocalDate getCreationTime() {
        return creationTime;
    }

    public void setCreationTime(LocalDate creationTime) {
        this.creationTime = creationTime;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public boolean isFill() {
        return fill;
    }

    public void setFill(boolean fill) {
        this.fill = fill;
    }

    public double getArea() {
        return 0;
    }

    public double getPerimeter() {
        return 0;
    }
}

子类Circle(圆)

package com.inherit.inherit;

import java.time.LocalDate;

public class Circle extends  GeometricShapes{
    private double radius;

    public Circle() {
        super();
    }

    public Circle(LocalDate createDate, String color, Boolean fill, double radius){
        super(createDate, color, fill);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

子类Rectangle(矩形)

package com.inherit.inherit;

import java.time.LocalDate;

public class Rectangle extends GeometricShapes{
    private double width;
    private double height;

    public Rectangle() {
        super();
    }

    public Rectangle(LocalDate createDate, String color, Boolean fill, double width, double height) {
        super(createDate, color, fill);
        this.width = width;
        this.height = height;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }

    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}

Object类与toString()方法

Java中所有类都间接继承自java.lang.ObjectObject是所有类的根类),Object类提供了多个基础方法,其中toString()方法最常用。

toString()方法的作用

  • 默认行为:未重写时,toString()返回“类名@哈希码”(如com.inherit.inherit.Pet@1b6d3586),即对象的引用地址。
  • 重写后行为:可自定义对象的字符串表示形式,方便打印和调试。
  • 常见重写类:StringFileDate等系统类已重写toString(),返回对象的实际内容(如String返回字符串本身)。

在Pet中重写toString

package com.inherit.inherit;

/**
 * Pet类重写toString()
 * @author Jing61
 */
public class Pet {
    private String name;
    private int health;
    private int love;

    // 其他构造方法、getter/setter省略

    @Override
    public String toString() {
        return "Pet{" +
                "name='" + name + '\'' +
                ", health=" + health +
                ", love=" + love +
                '}';
    }
}

// 测试:打印对象时自动调用toString()
public class Test {
    public static void main(String[] args) {
        Pet pet = new Pet("huihui");
        pet.setHealth(90);
        pet.setLove(80);
        System.out.println(pet); // 输出:Pet{name='huihui', health=90, love=80}
    }
}

多态

多态的定义

多态意味着“父类的变量可以指向子类对象”,即同一方法调用可根据对象的实际类型执行不同的逻辑,实现“一种事物多种表现形态”。

多态的核心概念

  • 子类型与父类型:子类定义的类型为子类型(如Dog),父类定义的类型为父类型(如Pet),子类实例是父类型的实例,但反之不成立。
  • 向上转型:将子类对象赋值给父类变量(如Pet pet = new Dog()),是多态的基础,编译器自动允许。
  • 动态绑定:编译时根据父类类型检查方法是否存在,运行时根据对象的实际类型执行对应的方法(重写后的子类方法),核心逻辑为“编译看左边,运行看右边”。

多态的示例

package com.inherit.inherit;

/**
 * 多态测试
 * @author Jing61
 */
public class PolymorphismTest {
    public static void main(String[] args) {
        // 向上转型:父类变量指向子类对象
        Pet[] pets = {
            new Dog("毛毛", 100, 100, "斑点狗"), // Dog对象
            new Penguin("pedro", "male"), // Penguin对象
            new Dog("天天", 100, 100, "二哈") // Dog对象
        };

        // 多态调用:同一方法调用,执行不同逻辑
        for (Pet pet : pets) {
            pet.showMe(); // 运行时调用子类重写的showMe方法
            System.out.println();
        }
    }
}

// 补充Dog和Penguin的有参构造(适配测试代码)
// Dog类添加有参构造
public class Dog extends Pet {
    private String strain;

    public Dog(String name, int health, int love, String strain) {
        super(name);
        setHealth(health);
        setLove(love);
        this.strain = strain;
    }

    // showMe重写方法省略
}

// Penguin类添加有参构造
public class Penguin extends Pet {
    private String sex;

    public Penguin(String name, String sex) {
        super(name);
        this.sex = sex;
    }

    // showMe重写方法省略
}

动态绑定的实现原理

动态绑定是多态性得以实现的重要因素,动态绑定通过方法表(method table) 实现:

  1. 每个类被加载到虚拟机时,在方法区保存元数据,其中,包括一个叫做方法表(methodtable)的东西,表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。
  2. 若子类重写父类方法,方法表中对应表项会指向子类的方法代码;未重写则指向父类方法代码。
  3. 调用方法时,JVM根据对象的实际类型查找其方法表,执行对应代码。

多态的限制

  • 父类变量不能调用子类的特有方法(如Pet pet = new Dog()pet不能调用DogsetStrain方法),需通过向下转型实现。
  • 编译时仅检查父类是否有该方法,不关心子类是否重写,确保程序安全运行。

动态绑定与静态绑定

程序在JVM运行过程中,会把类的类型信息、static属性和方法、final常量等元数据加载到方法区,这些在类被加载时就已经知道,不需对象的创建就能访问的,就是静态绑定的内容;需要等对象创建出来,使用时根据堆中的实例对象的类型才进行取用的就是动态绑定的内容。编译器在每次调用方法时都要进行搜索,时间开销相当大。因此虚拟机会预先为每个类创建一个方发表(method table),其中列出了所有方法的签名和实际调用的方法。

对象转换与instanceof运算符

对象转换的分类

转换类型 定义 语法 编译器是否允许
向上转型 子类对象 → 父类变量 Pet pet = new Penguin() 允许(自动转换)
向下转型 父类变量 → 子类变量 Penguin penguin = (Penguin) pet 不允许(需强制转换)

向下转型的风险与解决方案

  • 风险:若父类变量指向的实际对象不是目标子类类型,强制转换会抛出ClassCastException(类型转换异常)。如果没有捕获这个异常,程序就会终止。因此,应该养成一个良好的程序设计习惯,在进行类型转换之前,先查看一下是否能够成功地转换。
Pet pet = new Penguin();
Dog dog = (Dog)pet; // java运行时系统将报告这个错误,产生一个ClassCastException异常
  • 解决方案:使用instanceof运算符先判断对象的实际类型,再进行转换。

instanceof运算符的使用

  • 作用:判断一个对象是否是某个类(或接口)的实例,返回boolean值。
  • JDK 14+ 模式匹配:可直接在if条件中声明变量,简化代码。

示例:安全的向下转型

if(pet instanceof Penguin){ // 判断 pet 是否是 Penguin 实例
   Penguin penguin = (Penguin)pet;
   ...
}
//jdk 14的模式匹配写法
if(pet instanceof Penguin penguin){
  ...
}

转型的使用场景

实际上,通过类型转换调整对象的类型并不是一个好的做法。在列举的示例中,大多数情况并不需要将Pet对象转成Penguin对象。两个类的对象都能够调用play方法,这是因为实现多态性的动态绑定机制能够自动的找到相应的方法。只有在使用Penguin中特有的方法时才需要进行类型转换。仅当需要调用子类特有方法时,才需要向下转型;若仅调用重写方法,直接通过多态调用即可,无需转型。

Object类的equals方法

equalsObject类的基础方法,用于比较两个对象的“相等性”,需注意与==的区别。

equals与==的区别

  • ==:用于比较基本数据类型的值是否相等,或引用数据类型的地址是否相等(是否指向同一个对象)。
  • equals:仅适用于引用数据类型,未重写时就是==(比较地址),重写后可自定义比较逻辑(如比较对象的属性值)。

equals的默认实现

Object类的equals方法源码:

public boolean equals(Object obj) {
    return (this == obj); // 比较对象地址
}

image

重写equals的示例(如String类)

String类重写了equals,比较字符串的实际内容:

public final class String {
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        return (anObject instanceof String aString)
                && (!COMPACT_STRINGS || this.coder == aString.coder)
                && StringLatin1.equals(value, aString.value);
    }
}

image

自定义类重写equals

建议自定义类重写equals,确保“属性值相同的对象判定为相等”:

package com.inherit.inherit;

public class Pet {
    private String name;
    private int health;
    private int love;

    // 其他构造方法、getter/setter省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 地址相同直接返回true
        if (o == null || getClass() != o.getClass()) return false; // 类型不同返回false
        Pet pet = (Pet) o;
        // 比较属性值
        return health == pet.health && love == pet.love && name.equals(pet.name);
    }
}

protected修饰符

protected用于控制成员的访问权限,平衡安全性与继承性:

  1. 同一包内的类可直接访问。
  2. 不同包的子类可访问自身继承的protected成员,但不能访问父类实例的protected成员,但可以访问父类实例的protected方法。

示例:protected的访问规则

// 包1:com.parent
package com.parent;

public class Parent {
    protected String name = "父类";

    protected void show() {
        System.out.println("父类protected方法");
    }
}

// 包2:com.child(子类)
package com.child;

import com.parent.Parent;

public class Child extends Parent {
    public void test() {
        // 允许:访问自身继承的protected成员
        System.out.println(name); // 输出:父类
        show(); // 输出:父类protected方法

        // 不允许:访问父类实例的protected成员
        Parent parent = new Parent();
        // System.out.println(parent.name); // 编译报错
        // parent.show(); // 编译报错
        // super.name; // 不允许,父类实例的protected成员不能被访问
        super.show();// 允许,父类实例的protected方法可以访问
    }
}

final关键字(防止扩展和重写)

final意为“最终的”,可修饰类、方法、属性,限制其被修改或扩展:

  1. 修饰类:该类不能被继承(如String类是final,不能创建其子类)。
  2. 修饰方法:该方法不能被子类重写(适用于功能固定、无需修改的方法)。
  3. 修饰属性:该属性为常量,必须初始化(可在声明时或构造方法中),且值不能修改。
posted @ 2025-11-06 08:56  Jing61  阅读(13)  评论(0)    收藏  举报