Java 面向对象进阶:抽象类与接口的舞蹈
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 继承 Inter3void 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
。这表示你试图操作一个不存在的对象。
💼 综合练习:达内员工管理系统(设计思路)
这是一个运用抽象类和接口进行面向对象设计的典型案例。
需求分析:
有四类员工:教研总监、讲师、项目经理、班主任。他们有一些共同的属性(名字、年龄、工资)和行为(上班打卡、下班打卡、完成工作),同时也有各自特有的行为(解决企业问题、培训企业员工、编辑书籍)。
设计思路:
- 抽共性到超类:将所有员工共有的属性(名字、年龄、工资)和行为(上班打卡、下班打卡)抽到一个雇员超类中。
- 处理差异行为:对于“完成工作”这个行为,不同员工的实现方式不同,所以将其设计为抽象方法放在雇员超类中。这样,每个子类都必须提供自己的“完成工作”的具体实现。
- 将部分共性行为抽到接口:对于部分员工共有的行为,例如“解决企业问题”、“培训企业员工”、“编辑书籍”,这些是不同的“能力”,适合用接口来定义。
- 定义一个企业顾问接口,包含“解决企业问题()”和“培训企业员工()”两个抽象方法。
- 定义一个技术作者接口,包含“编辑书籍()”一个抽象方法。
- 构建派生类:根据需求和设计,创建具体的员工类,它们继承雇员超类,并根据需要实现对应的接口。
- 教研总监类:继承雇员超类,实现企业顾问接口和技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及企业顾问和技术作者接口中的共4个抽象方法。
- 讲师类:继承雇员超类,实现企业顾问接口和技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及企业顾问和技术作者接口中的共4个抽象方法。
- 项目经理类:继承雇员超类,实现技术作者接口。需要重写雇员超类中的抽象方法“完成工作()”,以及技术作者接口中的1个抽象方法,共2个抽象方法。
- 班主任:继承雇员超类。需要重写雇员超类中的抽象方法“完成工作()”,共1个抽象方法。
设计规则总结(适合初学者):
- 将所有派生类共有的属性和行为,抽到超类中(抽共性)。
- 若派生类的行为代码都一样,设计普通方法。
- 若派生类的行为代码不一样,设计抽象方法。
- 将部分派生类共有的行为,抽到接口中。
类间关系总结:
- 类和类:继承 (
extends
) - 接口和接口:继承 (
extends
) - 类和接口:实现 (
implements
)
好的,我已经识别了图片中的文字内容。
总结
- 抽象方法和抽象类:
- 抽象方法:由 abstract 修饰,只有方法定义,没有具体实现
- 抽象类:包含抽象方法的类必须是抽象类,abstract 修饰,不能被实例化
- 接口:
- 由 interface 定义,只能包含抽象方法 (目前课程内容为止)
- 不能被实例化,实现接口必须重写所有抽象方法,可以实现多个接口
- 引用类型数组,与基本类型数组两点区别:
- 给引用类型数组元素赋值时,需要 new 个对象
- 访问引用类型数组的元素的属性/行为时,需要打点访问
提示
- 抽象类的应用场景
- 接口的应用场景
- 抽象类与接口的区别
- 引用类型数组如何给元素赋值
- 如何访问引用类型元素数组的属性或行为