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