Bootstrap

继承与多态(下)

目录

一.关键字final

1.修饰变量

2.修饰方法

3.修饰类

二.继承与组合

三.多态

1.方法重写

2.方法重载(严格上来说非多态)

3.向上转型

4.向下转型

5.向上向下转型综合例子

四.重载和重写的区别


一.关键字final

在 Java 中,final关键字是一个修饰符,可以用于 变量方法,其主要作用是限制修改。

1.修饰变量

(1)基本数据类型

final 修饰的基本数据类型的变量一旦被初始化,其值就不能再更改。

final int number = 10;
// number = 20; // 编译错误,无法更改

(2)引用类型

final 修饰引用类型变量后,引用本身不能更改(即不能指向新的对象),但对象的内容仍然可以修改。

final StringBuilder builder = new StringBuilder("Hello");
builder.append(" World"); // 可以修改对象内容
// builder = new StringBuilder("Hi"); // 编译错误,不能更改引用

2.修饰方法

final 修饰的方法不能被子类重写,但可以被继承和调用。

class Parent {
    public final void show() {
        System.out.println("This is a final method.");
    }
}

class Child extends Parent {
    // @Override
    // public void show() { } // 编译错误,无法重写
}

3.修饰类

final 修饰的类不能被继承,因此所有的方法都被隐式地认为是 final 的。

final class FinalClass {
    public void display() {
        System.out.println("This is a final class.");
    }
}

// class SubClass extends FinalClass { } // 编译错误,无法继承

二.继承与组合

对于继承

继承是一种某某“是一个 (is-a)”某某的关系,子类通过继承父类,自动拥有父类的属性和方法,可以重写(override)父类的方法,也可以扩展新的功能。

对于组合

组合是一种某某“有一个 (has-a)”某某的关系,表示一个对象包含另一个对象作为其成员。通过组合,可以在一个类中直接调用另一个类的方法来实现功能。

举一个汽车例子,解释一下组合:

// 引擎类 Engine
class Engine {
    public void start() {
        System.out.println("Engine is starting.");
    }
}

// 汽车类 Car 包含一个 Engine 实例(组合关系)
class Car {
    private Engine engine;

    // 通过构造器注入一个 Engine 实例
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void startCar() {
        engine.start();  // 调用 Engine 的 start 方法
        System.out.println("Car is starting.");
    }
}

public class CompositionExample {
    public static void main(String[] args) {
        Engine engine = new Engine(); // 创建 Engine 实例
        Car car = new Car(engine);    // 将 Engine 组合到 Car 中
        car.startCar();               // 启动汽车
    }
}

三.多态

多态(Polymorphism),来源于希腊语,意为“多种形式”。在编程中,多态允许同一个方法在不同的上下文中表现出不同的行为。简单来说,同一接口,不同实现

对于实现多态的条件,必须满足,缺一不可:

1. 必须在继承体系下

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

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

原因:

  • 没有继承,无法重写

    • 如果没有继承,子类和父类不存在关联,也就无法实现方法的动态绑定。
  • 没有重写,行为无法变化

    • 如果子类没有对父类的方法进行重写,那么调用父类的引用时,执行的永远是父类的方法,体现不出多态的动态特性。
  • 不通过父类引用,无法体现多态

    • 如果直接用子类引用调用方法,这只是普通的调用,不是多态。

1.方法重写

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

举个例子:

class Animal {
    void sound() {
        System.out.println("动物发出声音~");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("汪汪叫~");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("喵喵叫~");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal;

        animal = new Dog();  // 父类引用指向子类对象
        animal.sound();      // 输出: 汪汪叫~

        animal = new Cat();
        animal.sound();      // 输出: 喵喵叫~
    }
}

(1)注意事项

1.子类重写的方法的方法名参数列表类型和顺序)必须与父类方法一致。

2.返回类型可以相同或是父类返回类型的子类型

3.子类的重写方法的访问权限不能低于父类的访问权限。例如,如果父类的方法是 public,子类的方法也必须是 public,不能改为 protectedprivate

4.使用 @Override 注解(建议),不写也不会有报错。但使用 @Override 注解可以帮助编译器检查是否正确进行了重写

5.父类中使用 final 修饰的方法不能被子类重写。

6.静态方法属于类本身,不属于对象,不能被重写。子类中的同名静态方法只是隐藏了父类的静态方法,不能称为重写。

7.构造方法是为初始化类而设计的,不继承自父类,因此不能被重写。

8.重写只适用于实例方法,不适用于类变量、类方法(静态方法)或实例变量。

9.通过父类的引用调用被重写的方法时,执行的是子类的实现(动态绑定)。

对于注意事项的第2项,举个例子:

class Parent {
    Number getValue() {
        return 10;
    }
}

class Child extends Parent {
    @Override
    Integer getValue() { // Integer 是 Number 的子类
        return 20;
    }
}

对于注意事项的第6项,举个例子:

class Parent {
    static void show() {
        System.out.println("Parent static method");
    }
}

class Child extends Parent {
    static void show() {
        System.out.println("Child static method");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent.show(); // 输出: Parent static method
        Child.show();  // 输出: Child static method
    }
}

2.方法重载(严格上来说非多态)

方法重载(Overloading)是指在同一个类中,定义多个方法,它们具有相同的名称,但参数列表不同(参数类型、数量或顺序不同)。

下面举几个例子:

(1).通过参数类型重载

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(5, 10));        // 调用第一个 add
        System.out.println(calc.add(5.5, 10.2));    // 调用第二个 add
    }
}

(2).通过参数个数重载

class Greeting {
    void greet() {
        System.out.println("Hello!");
    }

    void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }
}

public class Main {
    public static void main(String[] args) {
        Greeting g = new Greeting();
        g.greet();             // 输出: Hello!
        g.greet("Alice");      // 输出: Hello, Alice!
    }
}

(3).通过参数顺序重载

class Display {
    void show(int a, String b) {
        System.out.println(a + " " + b);
    }

    void show(String b, int a) {
        System.out.println(b + " " + a);
    }
}

public class Main {
    public static void main(String[] args) {
        Display d = new Display();
        d.show(10, "Test");    // 输出: 10 Test
        d.show("Test", 10);    // 输出: Test 10
    }
}

(4).注意事项

1.同一类中方法名必须相同:重载的方法属于同一个类,方法名相同但参数列表不同。

2.参数列表必须不同:参数类型、参数的个数或参数的顺序至少有一个不同。

3.返回类型可以相同或不同:但仅靠返回类型的不同不能区分方法。

4.与访问修饰符无关:访问权限修饰符(如 publicprivate)可以不同,但它们不会影响重载的识别。

5.与抛出的异常无关:重载方法可以声明不同的异常,但编译器不会依据异常区分方法。

6.构造方法也可以重载:在 Java 中,构造方法支持重载,可以通过不同的参数列表初始化对象。

7.与方法的修饰符无关:重载与方法是否为 staticfinal 无关。

3.向上转型

向上转型是将子类对象的引用赋值给其父类类型的引用

特点:

  • 自动完成:编译器会自动执行向上转型。
  • 使用父类引用:通过父类引用只能调用父类中声明的方法或属性,但这些方法会在运行时绑定到实际对象(子类)的实现。
  • 实现多态:向上转型是多态的基础,可以统一对父类或接口的操作。

举个例子:

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

class Child extends Parent {
    @Override
    void show() {
        System.out.println("子类show方法");
    }
    
    void display() {
        System.out.println("子类display方法");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child(); // 向上转型
        obj.show();               // 输出: 子类show方法
        // obj.display();         // 编译错误: 父类引用无法访问子类特有的方法
    }
}

4.向下转型

向下转型是将父类类型的引用强制转换为子类类型的引用。这需要显式地进行类型转换,并且可能会抛出 ClassCastException

特点

  1. 强制转换:必须显式使用强制类型转换语法 (Child)
  2. 运行时检查:向下转型在运行时会验证对象是否是目标类型。如果父类引用实际上不指向目标子类类型的对象,会抛出异常。
  3. 恢复子类功能:向下转型后可以访问子类的特有方法和属性。

举个例子:

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

class Child extends Parent {
    @Override
    void show() {
        System.out.println("子类show方法");
    }
    
    void display() {
        System.out.println("子类display方法");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child(); // 向上转型
        obj.show();               // 输出: 子类show方法
        
        // 向下转型
        if (obj instanceof Child) {
            Child childObj = (Child) obj; // 向下转型
            childObj.display();          // 输出: 子类display方法
        }
    }
}

5.向上向下转型综合例子

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

    String name;
    int age;
    public void eat(){
        System.out.println(name+"正在吃饭");
    }
    public void sleep(){
        System.out.println(name+"正在睡觉");
    }
    public void sound(){
        System.out.println(name+"正在叫");
    }
}
-------------------------------------------------
public class Cat extends Animal{
    public Cat(String name, int age) {
        super(name, age);
    }

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

    @Override
    public void sleep() {
        System.out.println(name+"正在猫窝睡觉");
    }

    @Override
    public void sound() {
        System.out.println(name+"正在喵喵叫");
    }
}
--------------------------------------------------
public class Dog extends Animal{
    public Dog(String name, int age) {
        super(name, age);
    }

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

    @Override
    public void sleep() {
        System.out.println(name+"正在狗窝睡觉");
    }

    @Override
    public void sound() {
        System.out.println(name+"正在发出狗叫");
    }
    public void run(){
        System.out.println(name+"正在跑步");
    }

}
--------------------------------------------------
//在向上转型后,可以访问子类重写的方法,这是因为在 Java 中调用方法时采用的是*
// *动态绑定(Dynamic Binding)**的机制。动态绑定意味着在程序运行
// 时,JVM 会根据对象的实际类型来决定调用哪个方法,而不是根据引用的类型。这正
// 是多态的核心。

public class TestAnimal {

    //2. 方法传参:形参为父类型引用,可以接收任意子类的对象
    public static void eatfood(Animal a){
        a.eat();
    }

    //作返回值:返回任意子类对象
    public static Animal buyAnimal(String var){
        if("狗".equals(var)){
            return new Dog("狗子",1);
        }else if("猫".equals(var)){
            return new Cat("咪咪",2);
        }else{
            return null;
        }
    }
    public static void main(String[] args) {
        // //向上转型
        //animal是一个 Animal 类型的引用,指向一个 Dog 类型的对象。
        Animal animal=new Dog("小七",2);
        animal.sound();
//        animal.run();  //报错:向上转型后,只能访问父类中定义的方法和变量,而不能直接访问子类中特有的方法和变量。
        Animal cat=new Cat("元宝",1);
        cat.sound();

        eatfood(animal);
        eatfood(cat);

        Animal animal1=buyAnimal("狗");
        animal1.sleep();

        Animal animal2=buyAnimal("猫");
        animal2.sound();

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

        //向下转型
        //instanceof关键字检查后,可以确认它是 Dog 类型
        if(animal instanceof Dog){
            //(Dog) myAnimal 是将 myAnimal 转换为 Dog 类型的引用
            Dog myDog=(Dog)animal;
            //这样就可以执行子类专有的方法
            myDog.run();
        }
//         myDog.run();//报错:直接写在外面会报错,因为在 if 外面 myDog 变量并不可见。myD
//         og 是在 if 语句的代码块中声明的局部变量,所以它的作用范围仅限于 if 代码块内部,一旦
//         离开 if 代码块,myDog 就无法访问了。
        //可以这样写:

//        Dog myDog = null; // 提前声明 myDog
//        if (animal instanceof Dog) {
//            myDog = (Dog) animal; // 向下转型
//        }
//
//        if (myDog != null) {
//            myDog.run(); // 现在可以在 if 外调用 myDog.run()
//        }

        // 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
        // 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
        cat = (Cat)animal;

    }
}

四.重载和重写的区别

特性重载(Overloading)重写(Overriding)
定义方法名相同,参数列表不同子类方法与父类方法具有相同签名
是否依赖继承
参数列表必须不同必须相同
返回类型可以相同或不同可以相同或是父类返回类型的子类型
访问修饰符无要求子类修饰符不能更严格
适用于静态方法可以不适用(只能隐藏)
抛出异常无要求子类异常不能比父类更广

;