Skip to main content

第九课:面向对象(补充)

第二课 中,我们已经介绍过 类与对象的常用用法,这里对几个重要的内容加以补充

(一)接口

接口是一个抽象类型,包含类要实现的方法的声明(方法签名),而不提供具体实现。

接口可以看作是一个特殊的抽象类,无法直接实例化。

和普通的类一样 , 接口文件使用 .java 扩展名, 编译后会生成 .class 文件

1. 接口的声明和实现

声明的语法格式:

[可见性修饰符] interface 接口名称 [extends 其他接口名] {
    // 常量
    // 抽象方法
}

实现的语法格式:

class 类名 implements 接口名 [, 接口名2, 接口名3...] {
    // 实现接口中定义的方法
}

示例:

// 定义接口
public interface Animal {
    void eat();
    void travel();
}

// 类实现接口
public class MammalInt implements Animal {
    @Override
    public void eat() {
        System.out.println("Mammal eats");
    }

    @Override
    public void travel() {
        System.out.println("Mammal travels");
    }

    public static void main(String[] args) {
        MammalInt mammal = new MammalInt();
        mammal.eat();    // 输出:Mammal eats
        mammal.travel(); // 输出:Mammal travels
    }
}

注意点:

  1. 如果一个类实现了接口,则必须实现接口中定义的 所有方法(除非该类本身是抽象类)
  2. @Override 注解用来明确表示方法是接口方法的实现

2. 接口的特点

  1. 方法默认是 public abstract

  2. 变量默认是 public static final (必须是常量,且为静态变量)

    interface Example {
        int VALUE = 10;  // 等价于 public static final int VALUE = 10;
    }
    
  3. Java 的类不支持继承多个父类,但接口支持多继承。

    语法:

    public interface NewInterface extends InterfaceA, InterfaceB {
        // 新方法定义
    }
    

    示例:

    public interface Sports {
        void setHomeTeam(String name);
        void setVisitingTeam(String name);
    }
    
    public interface Football extends Sports {
        void homeTeamScored(int points);
        void visitingTeamScored(int points);
        void endOfQuarter(int quarter);
    }
    
    public interface Hockey extends Sports {
        void homeGoalScored();
        void visitingGoalScored();
        void overtimePeriod(int ot);
    }
    

接口与类的不同点:

特性接口
构造方法没有可以有构造方法
成员变量只能是 staticfinal 的常量可以包含普通成员变量
方法默认是 public abstract;从 Java 8 起可包含定义的默认方法和静态方法包含普通方法和静态方法,可以有实现
实现方式通过 implements 关键字实现子类通过 extends 关键字继承父类
多继承支持多继承不支持多继承
是否能实例化不能实例化可以实例化对象

3. 接口中允许的方法

默认方法 (default)

语法:

interface Example {
    default void defaultMethod() {
        System.out.println("This is a default method");
    }
}

实现类可选择重写默认方法:

public class MyClass implements Example {
    @Override
    public void defaultMethod() {
        System.out.println("Overridden default method");
    }
}

静态方法 (static)

  • 接口可以包含静态方法,供直接通过接口名调用。

  • 语法:

    interface Example {
        static void staticMethod() {
            System.out.println("This is a static method in an interface");
        }
    }
    
  • 调用:

    Example.staticMethod(); // 不需要实例化
    

私有方法

interface Example {
    private void helperMethod() {
        System.out.println("Helper method");
    }
}

4. 标记接口

标记接口是没有任何方法和字段的接口,仅用于给类 "打标记",从而赋予类某种特权或能力。

常见标记接口:

  1. java.io.Serializable:标记可以序列化的类。
  2. java.util.EventListener:标记事件监听器类。

示例:

public class MyClass implements java.io.Serializable {
    private static final long serialVersionUID = 1L;
    // 该类可以被序列化
}

(二)抽象类

抽象类在面向对象编程中用于定义具有通用特性的类,但由于其抽象性,这种类不能直接实例化,只能被其他子类继承并实现其抽象方法。抽象类定义了一个框架,具体的实现由子类完成。

1. 定义抽象类

在 Java 中使用 abstract class 定义抽象类

包含 抽象方法(没有方法体的方法)以及 普通方法(有方法体的方法)

public abstract class Animal {
    String name;          // 普通属性
    int age;

    Animal(String name, int age) { // 构造方法
        this.name = name;
        this.age = age;
    }

    public void eat() {   // 普通方法
        System.out.println(name + " 正在吃饭");
    }

    public abstract void sound(); // 抽象方法
}

2. 定义和使用子类

通过继承抽象类并实现抽象方法,完成具体实现:

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

    @Override
    public void sound() { // 重写抽象方法
        System.out.println(name + " 汪汪叫");
    }
}

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

    @Override
    public void sound() { // 重写抽象方法
        System.out.println(name + " 喵喵叫");
    }
}

使用子类:

public class TestAbstractClass {
    public static void main(String[] args) {
        Animal dog = new Dog("小狗", 3);
        dog.eat();        // 输出:小狗 正在吃饭
        dog.sound();      // 输出:小狗 汪汪叫

        Animal cat = new Cat("小猫", 2);
        cat.eat();        // 输出:小猫 正在吃饭
        cat.sound();      // 输出:小猫 喵喵叫
    }
}

3. 抽象类规则总结

  1. 抽象类不能直接实例化: 只能通过子类实例化。
    • 如果子类是抽象类,则可以选择不实现该抽象方法,但最终必须由具体的子类完成。
    • 如果子类是普通类,则必须实现 所有 从抽象类继承的抽象方法。
  2. 抽象类可以没有抽象方法: 但是一旦有抽象方法,类必须声明为抽象类。
  3. 抽象方法不能有方法体: 仅提供方法签名。
  4. 构造方法不能是抽象的: 抽象方法用于行为定义,构造方法是用于实例化的。

4. 抽象类和接口的对比

特性抽象类接口
继承关键字extendsimplements
继承数量一个类只能继承一个抽象类一个类可以实现多个接口
方法实现可以包含具体方法实现方法没有实现(JDK 8+ 可以有默认方法)
构造方法可以有构造方法不能有构造方法
成员变量可以有普通成员变量只能有 staticfinal 修饰的常量
方法默认 public abstract,可以有默认和静态方法普通方法和抽象方法均支持
使用场景用于实现行为规范用于描述类的共有属性和方法,实现代码复用

默认方法 (default) 静态方法 (static) 私有方法 (private)

(二)继承

1. 接口多继承

一个子类只能继承一个父类,但支持接口的多继承(implements)。

示例:

public interface A { void eat(); }
public interface B { void sleep(); }
public class C implements A, B {
    public void eat() { System.out.println("C 正在吃"); }
    public void sleep() { System.out.println("C 正在睡"); }
}

特点

  • 子类继承父类非 private 的属性和方法。
  • 子类可以重写(override)父类方法。
  • 继承提高了类的耦合性,耦合度过高时可能降低代码独立性。

2. super 关键字

  1. super 关键字:

    • 用于访问父类的属性或方法。
    • 调用父类构造方法时必须放在子类构造器的第一行。

    示例:

    class Animal {
        void eat() { System.out.println("Animal 正在吃"); }
    }
    
    class Dog extends Animal {
        @Override
        void eat() { System.out.println("Dog 正在吃"); }
    
        void test() {
            this.eat();  // 调用子类的 eat 方法
            super.eat(); // 调用父类的 eat 方法
        }
    }
    

    运行结果:

    Dog 正在吃
    Animal 正在吃

3. 构造方法的继承

  • 子类不继承父类的构造方法,但会调用父类的构造方法。
  1. 显式调用:

    • 使用 super 调用父类的带参构造方法。
    class Animal {
        public Animal(String name) { 
            System.out.println("父类构造:" + name); 
        }
    }
    class Dog extends Animal {
        public Dog(String name) { super(name); }
    }
    
  2. 隐式调用:

    • 如果父类有无参构造,系统自动调用:
    class Animal { public Animal() { System.out.println("父类无参构造"); }}
    class Dog extends Animal {}
    

示例带构造器的继承:

class SuperClass {
    public SuperClass() { System.out.println("SuperClass 无参构造"); }
    public SuperClass(int n) { System.out.println("SuperClass 带参构造:" + n); }
}

class SubClass extends SuperClass {
    public SubClass() { super(); System.out.println("SubClass 无参构造"); }
    public SubClass(int n) { super(n); System.out.println("SubClass 带参构造:" + n); }
}

public class Test {
    public static void main(String[] args) {
        new SubClass();
        new SubClass(100);
    }
}

输出结果:

SuperClass 无参构造
SubClass 无参构造
SuperClass 带参构造:100
SubClass 带参构造:100

(三)重写和重载

重写重载 是 Java 面向对象编程中 方法多态性 的两种重要体现。

1. 重写 (Override)

重写是指 子类 中定义了与 父类 中具有相同名称、参数列表和返回类型的方法,并提供了自己的实现,从而覆盖父类的方法。

规则:

  1. 参数列表必须与被重写的方法完全相同。

  2. 返回类型必须与被重写方法的返回类型相同,或者是其 子类

  3. 子类的访问控制修饰符,不能比父类的更严格。

    • 例如:父类方法是 public,子类方法不能是 protectedprivate
  4. 重写的方法可以抛出更少或更具体的异常,但不能抛出新的或更广泛的检查异常。

    • 例如:父类方法抛出 IOException,子类方法可以抛出其子类 FileNotFoundException,但不能抛出 ExceptionIOException 的父类)。
  5. finalstatic 修饰的方法不能被重写。

  6. 构造方法不能被重写。

示例

// 父类 Animal
class Animal {
    public void move() {
        System.out.println("动物可以移动");
    }
}

// 子类 Dog
class Dog extends Animal {
    @Override
    public void move() { // 重写父类的 move 方法
        System.out.println("狗可以跑和走");
    }
}

public class TestOverride {
    public static void main(String[] args) {
        Animal a = new Animal(); // 父类对象
        Animal b = new Dog();    // 子类对象

        a.move(); // 输出:动物可以移动
        b.move(); // 输出:狗可以跑和走(运行时多态,调用子类的 move 方法)
    }
}

重写时可以使用 super 关键字调用父类的相同方法

class Animal {
    public void move() {
        System.out.println("动物可以移动");
    }
}

class Dog extends Animal {
    @Override
    public void move() {
        super.move(); // 调用父类的方法
        System.out.println("狗可以跑和走");
    }
}

public class TestSuper {
    public static void main(String[] args) {
        Animal b = new Dog(); // 创建 Dog 对象
        b.move();
    }
}

输出:

动物可以移动
狗可以跑和走

2. 重载 (Overload)

重载是指在同一个类中定义多个方法,这些方法具有相同的方法名,但参数的类型、数量或顺序不同。

规则:

  1. 参数列表必须改变,具体体现在 参数数量 参数类型 参数的顺序

  2. 返回类型可以相同,也可以不同。

  3. 访问修饰符可以不同(如 publicprivate)。

  4. 静态方法同样可以被重载。

  5. **无法仅通过返回值判断重载:**仅改变返回值类型不能成为区分重载方法的标准。

示例

public class Overloading {
    // 无参方法
    public void display() {
        System.out.println("无参数方法");
    }

    // 一个参数
    public void display(String name) {
        System.out.println("参数是 String 类型:" + name);
    }

    // 两个参数 - 顺序不同
    public void display(int age, String name) {
        System.out.println("年龄:" + age + ", 姓名:" + name);
    }

    // 两个参数 - 顺序不同
    public void display(String name, int age) {
        System.out.println("姓名:" + name + ", 年龄:" + age);
    }

    public static void main(String[] args) {
        Overloading overload = new Overloading();

        // 调用不同的重载方法
        overload.display();
        overload.display("Alice");
        overload.display(18, "Bob");
        overload.display("Charlie", 20);
    }
}

输出:

无参数方法
参数是 String 类型:Alice
年龄:18, 姓名:Bob
姓名:Charlie, 年龄:20

3. 重写与重载的区别

特性重写 (Override)重载 (Overload)
定义位置子类与父类之间同一类中
方法名必须相同必须相同
参数列表必须相同必须不同
返回类型必须相同或父类的子类(协变返回类型)可以相同,也可以不同
访问修饰符子类不能降低父类的访问权限可以随意设置
异常只能抛出相同或更少范围的异常(不能扩大检查异常类型)可以抛出任意异常
关键字影响finalstatic 的方法不能重写与关键字无关
典型场景改写父类行为一个方法需要多个版本来处理不同的参数
多态性支持运行时多态不支持运行时多态(编译时决定)

(四)多态

多态是指 同一个方法 在不同的对象中有不同的实现。。简单来说,多态实现了 “一个接口,多种行为” 的能力。

在 Java 中,多态通常通过 继承接口 实现,是 动态绑定 的一种体现。

1. 父类引用指向子类对象 实现多态

class Animal {
    void eat() {
        System.out.println("动物吃饭");
    }
}

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

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

public class Test {
    public static void main(String[] args) {
        Animal a1 = new Cat();  // 父类引用指向子类 Cat
        a1.eat();  // 输出:猫吃鱼

        Animal a2 = new Dog();  // 父类引用指向子类 Dog
        a2.eat();  // 输出:狗吃骨头
    }
}

解释:

子类重写了父类的 方法。

Animal a1 = new Cat(); 将父类引用指向子类对象

在运行时,JVM 决定调用真正属于子类的方法,而不是父类的方法,

在 Java 中,方法调用有两种绑定方式:

动态绑定 , 如 Animal a1 = new Cat(); , 在运行时决定调用哪个方法,动态绑定是多态实现的基础。

静态绑定 , 如 Cat a1 = new Cat(); ,在编译时决定调用哪个方法

注意: 父类的引用类型只能调用父类中 声明 的方法。

2. 使用接口 实现多态

通过实现接口的多态,实现统一标准下的不同行为。

interface Animal {
    void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

class Cat implements Animal {
    public void makeSound() {
        System.out.println("喵喵喵");
    }
}

public class TestInterface {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound(); // 输出:汪汪汪
        cat.makeSound(); // 输出:喵喵喵
    }
}

3. 向上转型与向下转型

向上转型 (Upcasting)

  • 子类对象赋给父类引用,这种转换是隐式的。
  • 示例:
    Animal animal = new Cat(); // 子类 Cat 转为父类 Animal
    animal.eat(); // 调用 Cat 的 eat() 方法
    

向下转型 (Downcasting)

  • 父类引用转为子类对象,这种转换需要显式声明。
  • 示例:
    Animal animal = new Cat();
    Cat cat = (Cat) animal; // 将 Animal 转为 Cat
    cat.eat(); // 调用 Cat 的 eat() 方法
    

向下转型示例 + 类型检查

在进行向下转型时,通常需要先通过 instanceof 检查。

public class Test {
    public static void show(Animal a) {
        a.eat();

        if (a instanceof Cat) {
            Cat c = (Cat) a;  // 向下转型
            c.work();         // 调用 Cat 特有的方法
        } else if (a instanceof Dog) {
            Dog d = (Dog) a;  // 向下转型
            d.work();         // 调用 Dog 特有的方法
        }
    }

    public static void main(String[] args) {
        show(new Cat()); // 输出:猫吃鱼 -> 猫抓老鼠
        show(new Dog()); // 输出:狗吃骨头 -> 狗看家
    }
}

class Animal {
    void eat() {}
}

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

    void work() { System.out.println("猫抓老鼠"); }
}

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

    void work() { System.out.println("狗看家"); }
}

输出结果:

猫吃鱼
猫抓老鼠
狗吃骨头
狗看家

4. 虚函数与动态绑定

虚函数在 Java 中的特点

  • 在 Java 中,所有非 static 方法默认是虚函数。
  • Java 默认采用 动态绑定Dynamic Binding),也就是说在运行时按照对象的实际类型调用方法,而非引用类型。
class Employee {
    void mailCheck() {
        System.out.println("Employee 邮寄支票");
    }
}

class Salary extends Employee {
    @Override
    void mailCheck() {
        System.out.println("Salary 邮寄支票");
    }
}

public class VirtualFunctionDemo {
    public static void main(String[] args) {
        Employee e = new Salary(); // 父类引用指向子类对象
        e.mailCheck(); // 动态绑定,调用 Salary 的 mailCheck 方法
    }
}

输出:

Salary 邮寄支票