Java 面向对象进阶:抽象类与接口

59 阅读11分钟

Java 面向对象进阶:抽象类与接口

在 Java 的面向对象编程中,抽象类和接口是实现多态性和代码组织的强大工具。它们允许我们定义更通用、更灵活的结构。今天,我们将一起探索抽象类和接口的概念、用法以及它们之间的区别与联系。

👻 抽象方法:只有骨架,没有实现

  • abstract 关键字修饰。

  • 抽象方法只有方法的定义(包括方法名、参数列表和返回值类型),没有具体的实现(连 {} 都没有)。

    abstract void eat(); // 抽象方法
    

🏛️ 抽象类:不完整的类

  • abstract 关键字修饰。

  • 包含抽象方法的类必须是抽象类。但是,不包含抽象方法的类也可以声明为抽象类(虽然没有抽象方法,但一旦声明为抽象类,就不能被实例化)。

  • 不能被实例化 (new 对象):抽象类只是一个概念或模板,本身不完整,所以不能直接创建对象。

  • 需要被继承:抽象类存在的意义就是被子类继承。派生类(子类)继承抽象类后,有两种选择:

    • 必须重写所有抽象方法:将父类中不完整的抽象方法补全,使派生类成为完整的类,可以被实例化。这是最常见的使用方式。
    • 👻 也声明为抽象类:如果派生类仍然没有重写父类中的所有抽象方法,或者出于设计需要,派生类也可以继续声明为抽象类。这种情况一般不常用,除非是构建一个更深层次的抽象体系。
  • 抽象类的意义

    • 封装共有的属性和行为:像普通类一样,抽象类可以封装子类共有的成员变量和普通方法,实现代码复用。
    • 提供统一入口并强制重写:可以包含抽象方法,为所有派生类提供一个统一的方法签名(入口),并且强制派生类必须提供自己的实现(重写)。这保证了派生类都具备某种特定的行为,但具体实现由子类决定。
    public abstract class Animal { // 抽象类
        String name;
        int age;
        String color;
    
        Animal(){
        }
    
        Animal(String name,int age,String color){
            this.name = name;
            this.age = age;
            this.color = color;
        }
    
        void drink(){ // 普通方法
            System.out.println(color+"色的"+age+"岁的"+name+"正在喝水...");
        }
    
        abstract void eat(); // 抽象方法
    }
    
    // Dog 类继承抽象类 Animal
    public class Dog extends Animal{
        Dog(){
        }
        Dog(String name,int age,String color){
            super(name,age,color);
        }
    
        void lookHome(){ // Dog 特有的方法
            System.out.println(color+"色的"+age+"岁的狗狗"+name+"正在看家...");
        }
    
        // 重写抽象方法 eat(),提供具体的实现
        void eat(){
            System.out.println(color+"色的"+age+"岁的狗狗"+name+"正在吃肯头...");
        }
    }
    

🌐 接口:定义规范与能力

  • 是一种引用数据类型

  • interface 关键字定义。

  • 主要包含抽象方法:在 Java 8 之前,接口只能包含常量和抽象方法。Java 8 以后引入了默认方法(default)、静态方法(static)和私有方法(private),但这些通常被视为接口的扩展,核心还是抽象方法。(常量、默认方法、静态方法、私有方法 - 暂时搁置,后续会深入学习)

    • 接口中的方法默认都是 public abstract 的,即使你不显式写出来。
    • 接口中的方法不能有方法体。
    interface Inter {
        abstract void show(); // 抽象方法
        void test(); // 默认是 public abstract void test();
        // void say(){} // 编译错误,抽象方法不能有方法体
    }
    
  • 不能被实例化:接口也是一种规范或契约,本身没有具体的实现,所以不能直接创建对象。

    public class InterfaceDemo {
        public static void main(String[] args) {
            // Inter o = new Inter(); // 编译错误,接口不能被实例化
        }
    }
    
  • 需要被实现/继承:接口需要被类实现(使用 implements 关键字)或者被其他接口继承(使用 extends 关键字)。实现类(派生类)必须重写接口中的所有抽象方法

    • 注意:重写接口中的方法时,必须加上 public 访问修饰符。这是因为接口中的方法默认是 public 的,而子类重写父类(或实现接口)的方法时,访问权限不能低于父类(或接口)。
    interface Inter {
        void show();
        void test();
    }
    
    class InterImpl implements Inter { // InterImpl 实现 Inter 接口
        public void show(){ // 重写接口中的抽象方法时,必须加 public
            // 具体实现
        }
        public void test(){ // 重写接口中的抽象方法时,必须加 public
            // 具体实现
        }
    }
    
  • 多实现:一个类可以实现多个接口,用逗号分隔。这弥补了 Java 单一继承的不足,允许类拥有多种不同的能力或遵守多种规范。

    • 如果一个类既继承类又实现接口,应先继承后实现
    interface Inter1{
        void show();
    }
    interface Inter2{
        void test();
    }
    abstract class Aoo{
        abstract void say();
    }
    
    // Boo 类继承 Aoo 抽象类,并实现 Inter1 和 Inter2 接口
    class Boo extends Aoo implements Inter1,Inter2{
        public void show(){
            // 实现 Inter1 的 show 方法
        }
        public void test(){
            // 实现 Inter2 的 test 方法
        }
        void say(){
            // 实现 Aoo 的抽象方法 say
        }
    }
    
  • 接口继承接口:接口之间可以使用 extends 关键字进行继承。一个接口可以继承多个父接口。

    // 接口继承接口
    interface Inter3{
        void show();
    }
    interface Inter4 extends Inter3{ // Inter4 继承 Inter3
        void test(); // Inter4 拥有 show() 和 test() 两个抽象方法
    }
    
    class Coo implements Inter4{ // Coo 实现 Inter4 接口
        public void test(){
            // 实现 test 方法
        }
        public void show(){
            // 实现 show 方法
        }
    }
    

🐾 综合练习:动物园(续)与游泳能力

在之前的动物园基础上,我们为部分动物添加游泳的能力,使用接口来表示这种能力。

// 定义一个 Swim 接口,表示“游泳”的能力
public interface Swim {
    void swim(); // 游泳方法,默认 public abstract
}

// Dog 类继承 Animal,并实现 Swim 接口
public class Dog extends Animal implements Swim {
    Dog(String name,int age,String color){
        super(name,age,color);
    }

    void lookHome(){
        System.out.println(color+"色的"+age+"岁的狗狗"+name+"正在看家...");
    }
    void eat(){
        System.out.println(color+"色的"+age+"岁的狗狗"+name+"正在吃肯头...");
    }
    // 实现 Swim 接口中的 swim 方法
    public void swim(){
        System.out.println(color+"色的"+age+"岁的狗狗"+name+"正在游泳...");
    }
}

// Fish 类继承 Animal,并实现 Swim 接口
public class Fish extends Animal implements Swim {
    Fish(String name,int age,String color){
        super(name,age,color);
    }

    void eat(){
        System.out.println(color+"色的"+age+"岁的小鱼"+name+"正在吃小虾...");
    }
    // 实现 Swim 接口中的 swim 方法
    public void swim(){
        System.out.println(color+"色的"+age+"岁的小鱼"+name+"正在游泳...");
    }
}

// Chick 类只继承 Animal,不实现 Swim 接口 (小鸡不会游泳)
public class Chick extends Animal {
    Chick(String name,int age,String color){
        super(name,age,color);
    }
    void layEggs(){
        System.out.println(color+"色的"+age+"岁的小鸡"+name+"正在下蛋...");
    }
    void eat(){
        System.out.println(color+"色的"+age+"岁的小鸡"+name+"正在吃小米...");
    }
}

public class SwimTest {
    public static void main(String[] args) {
        Dog dog = new Dog("小黑", 2, "黑");
        dog.eat();
        dog.drink();
        dog.swim();     // Dog 具备游泳能力
        dog.lookHome();

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

        Chick chick = new Chick("小白", 1, "白");
        chick.eat();
        chick.drink();
        chick.layEggs();
        // chick.swim(); // 编译错误,Chick 没有实现 Swim 接口,不具备游泳能力

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

        Fish fish = new Fish("小金", 1, "金");
        fish.eat();
        fish.drink();
        fish.swim();  // Fish 具备游泳能力
    }
}

📦 引用类型数组:存放对象的容器

引用类型数组是存放对象的数组。在使用和访问时,需要注意与基本类型数组的区别:

  • 区别 1:元素需要实例化:给引用类型数组的元素赋值时,需要 new 一个对象。数组创建时,元素的默认值是 null

    Dog[] dogs = new Dog[3]; // 创建一个 Dog 数组,包含 3 个 Dog 引用,默认为 null
    dogs[0] = new Dog("小黑",2,"黑"); // 创建一个 Dog 对象并赋给第一个元素
    dogs[1] = new Dog("小白",1,"白"); // 创建另一个 Dog 对象并赋给第二个元素
    dogs[2] = new Dog("小灰",3,"灰"); // 创建第三个 Dog 对象并赋给第三个元素
    
  • 区别 2:通过点运算符访问成员:访问引用类型数组的元素的属性或调用方法时,需要使用点运算符 (.)。

    System.out.println(dogs[0].name); // 输出第一个 Dog 对象的 name 属性
    dogs[1].age = 4; // 修改第二个 Dog 对象的 age 属性
    dogs[2].swim(); // 调用第三个 Dog 对象的 swim 方法
    
    public class RefArrayDemo {
        public static void main(String[] args) {
            Dog[] dogs = new Dog[3];
            dogs[0] = new Dog("小黑",2,"黑");
            dogs[1] = new Dog("小白",1,"白");
            dogs[2] = new Dog("小灰",3,"灰");
            System.out.println(dogs[0].name);
            dogs[1].age = 4;
            dogs[2].swim();
            System.out.println("-------------------------");
            for(int i=0;i<dogs.length;i++){ // 遍历 dogs 数组
                System.out.println(dogs[i].name);
                dogs[i].eat();
            }
    
            Chick[] chicks = new Chick[2];
            chicks[0] = new Chick("小花",1,"花");
            chicks[1] = new Chick("大花",2,"花");
            for(int i=0;i<chicks.length;i++){ // 遍历 chicks 数组
                System.out.println(chicks[i].name);
                chicks[i].layEggs();
            }
    
            Fish[] fish = new Fish[4];
            fish[0] = new Fish("小金",2,"金");
            fish[1] = new Fish("大金",4,"白");
            fish[2] = new Fish("小绿",1,"绿");
            fish[3] = new Fish("小红",3,"红");
            for(int i=0;i<fish.length;i++){ // 遍历 fish 数组
                System.out.println(fish[i].color);
                fish[i].swim();
            }
        }
    }
    
  • NullPointerException (空指针异常):如果引用类型数组的元素为 null(还没有创建对象并赋值),然后尝试通过这个 null 引用去访问成员或调用方法,就会发生 NullPointerException。这表示你试图操作一个不存在的对象。

💼 综合练习:达内员工管理系统(设计思路)

这是一个运用抽象类和接口进行面向对象设计的典型案例。

需求分析:

有四类员工:教研总监、讲师、项目经理、班主任。他们有一些共同的属性(名字、年龄、工资)和行为(上班打卡、下班打卡、完成工作),同时也有各自特有的行为(解决企业问题、培训企业员工、编辑书籍)。

设计思路:

  1. 抽共性到超类:将所有员工共有的属性(名字、年龄、工资)和行为(上班打卡、下班打卡)抽到一个雇员超类中。
  2. 处理差异行为:对于“完成工作”这个行为,不同员工的实现方式不同,所以将其设计为抽象方法放在雇员超类中。这样,每个子类都必须提供自己的“完成工作”的具体实现。
  3. 将部分共性行为抽到接口:对于部分员工共有的行为,例如“解决企业问题”、“培训企业员工”、“编辑书籍”,这些是不同的“能力”,适合用接口来定义。
    • 定义一个企业顾问接口,包含“解决企业问题()”和“培训企业员工()”两个抽象方法。
    • 定义一个技术作者接口,包含“编辑书籍()”一个抽象方法。
  4. 构建派生类:根据需求和设计,创建具体的员工类,它们继承雇员超类,并根据需要实现对应的接口。
    • 教研总监类:继承雇员超类,实现企业顾问接口和技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及企业顾问和技术作者接口中的共4个抽象方法。
    • 讲师类:继承雇员超类,实现企业顾问接口和技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及企业顾问和技术作者接口中的共4个抽象方法。
    • 项目经理类:继承雇员超类,实现技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及技术作者接口中的1个抽象方法,共2个抽象方法。
    • 班主任:继承雇员超类。需要重写雇员超类中的抽象方法“完成工作()”,共1个抽象方法。

设计规则总结(适合初学者):

  • 将所有派生类共有的属性和行为,抽到超类中(抽共性)。
  • 若派生类的行为代码都一样,设计普通方法
  • 若派生类的行为代码不一样,设计抽象方法
  • 将部分派生类共有的行为,抽到接口中。

类间关系总结:

  • 类和类:继承 (extends)
  • 接口和接口:继承 (extends)
  • 类和接口:实现 (implements)

好的,我已经识别了图片中的文字内容。

总结

  • 抽象方法和抽象类:
    1. 抽象方法:由 abstract 修饰,只有方法定义,没有具体实现
    2. 抽象类:包含抽象方法的类必须是抽象类,abstract 修饰,不能被实例化
  • 接口:
    1. 由 interface 定义,只能包含抽象方法 (目前课程内容为止)
    2. 不能被实例化,实现接口必须重写所有抽象方法,可以实现多个接口
  • 引用类型数组,与基本类型数组两点区别:
    1. 给引用类型数组元素赋值时,需要 new 个对象
    2. 访问引用类型数组的元素的属性/行为时,需要打点访问

提示

  • 抽象类的应用场景
  • 接口的应用场景
  • 抽象类与接口的区别
  • 引用类型数组如何给元素赋值
  • 如何访问引用类型元素数组的属性或行为