在Java的面向对象世界中,有两个常见但是经常被混淆的关键词:抽象类和接口。 很多初学者一脸问号:“这俩不都不能实例化、都可以定义抽象方法、都能被继承/实现吗?到底有什么区别?我啥时候用哪个?”
今天,我们就来系统梳理二者的本质区别、底层特性与使用场景。
一、抽象类
抽象类不是完整的类,它是给子类提供通用模版的基类。
抽象类里可以包含这些东西:
普通成员变量(字段)
构造方法
普通方法(带方法体)
抽象方法(没有方法体)
package com.lazy.snail.day14;
/**
* @ClassName AbstractAnimal
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 13:46
* @Version 1.0
*/
public abstract class AbstractAnimal {
String name;
public AbstractAnimal() {
}
public void breathe() {
System.out.println(\"呼吸空气\");
}
public abstract void shout();
}
AbstractAnimal是一个abstract关键字修饰的类,表示这是一个抽象类。
这个类中包含一个普通成员变量name。
一个无参构造方法AbstractAnimal()。
一个普通方法breathe(),有方法体(大括号)。
一个抽象方法shout(),没有方法体(没有大括号)。
抽象类不能被实例化(不能通过new创建对象)。
如果有子类继承了抽象类,必须实现抽象类的抽象方法:
当Dog类继承了抽象类AbstractAnimal时,IDE会给出提示:
Dog要么也声明成抽象的(Dog也用abstract关键字修饰)。
要么就实现抽象类AbstractAnimal中的shout抽象方法。
JDK中部分典型的抽象类:
java.io.InputStream
java.util.AbstractList
java.lang.Number
它们抽象了一类对象的本质属性(比如“输入流”“列表”“数值”),而不是具体实现。
二、接口
接口相对于抽象类更加的纯粹,它强调的是能力,而不是身份。
接口里定义的方法,默认是public abstract的,用来强制实现类具备某些功能。
package com.lazy.snail.day14;
/**
* @ClassName Flyable
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 14:14
* @Version 1.0
*/
public interface Flyable {
void fly();
}
Flyable是自定义的一个接口,fly()是一个没有方法体的抽象方法,等着实现类实现具体的细节。
当Bird类通过implements关键字实现了Flyable接口,意味着Bird会飞,具体怎么飞由Bird中的fly方法决定。
使用implements关键字实现接口时,IDE会提示:
要么Bird类也声明成抽象的(abstarct),要么就要实现Flyable接口中的抽象方法fly。
package com.lazy.snail.day14;
/**
* @ClassName Bird
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 14:15
* @Version 1.0
*/
public class Bird implements Flyable{
@Override
public void fly() {
System.out.println(\"小鸟扑腾翅膀飞\");
}
}
JDK中部分典型的接口:
java.util.List
java.lang.Runnable
java.util.Iterator
接口只声明方法签名,不提供任何实现,完全面向能力定义。
它们定义了一类对象对外暴露的行为能力,而不关注具体实现细节或继承关系。
三、接口的演化
在Java 8之前,接口只能包含抽象方法。但从Java 8开始,接口变得更强大:
可以定义default方法(有方法体)。
可以定义static静态方法。
但是仍然不能定义实例变量、构造方法。
package com.lazy.snail.day14;
/**
* @ClassName Moveable
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 14:38
* @Version 1.0
*/
public interface Moveable {
default void move() {
System.out.println(\"我能动\");
}
static void info() {
System.out.println(\"接口的静态方法\");
}
}
Moveable是interface定义的接口,包含了一个default修饰的默认方法move、一个static修饰的静态info方法。
package com.lazy.snail.day14;
/**
* @ClassName Robot
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 14:39
* @Version 1.0
*/
public class Robot implements Moveable {
@Override
public void move() {
System.out.println(\"机器人晃晃荡荡的走起来\");
}
}
Robot类通过implements实现了Moveable接口。
move方法的重写是可选的(可以不重写)。
为什么Java 8之后要引入这些变动?那必然是之前的设计有了局限性,不适应后续Java的发展了。
在Java 8之前,接口一旦发布,新增方法会强制要求所有实现类必须实现它,否则编译失败。这对已有代码库的兼容性是灾难性的。
Java 8引入Lambda表达式和Stream API,需要接口能够灵活扩展以支持新操作。
接口可以自行定义静态工具方法,替代传统的工具类(比如Collections类)。
四、抽象类和接口差异对比
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 是否可实例化 | X | X |
| 是否可被继承 | 单继承 | √ 多实现 |
| 是否支持构造方法 | √支持 | X 不支持 |
| 是否可包含字段 | √普通字段 | √仅常量(public static final) |
| 是否可有普通方法 | √支持 | √支持(default 方法) |
| 多继承支持 | X 不支持 | √支持多个接口 |
| 设计定位 | 模板(骨架) | 行为规范(能力) |
| 推荐使用时机 | 表示“是什么” | 表示“能做什么” |
五、组合使用
虽然抽象类和接口存在差异,但是二者并不是互相排斥,而是常常协同工作。
来看下下面的例子:
package com.lazy.snail.day14;
/**
* @ClassName AbstractDuck
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 15:15
* @Version 1.0
*/
public abstract class AbstractDuck {
public void swim() {
System.out.println(\"鸭子扑哧大脚板游泳\");
}
public abstract void display();
}
鸭子一般都会游泳,而且鸭子游泳一般都差不多是扑哧大脚板,抽象鸭子直接写了游泳的普通方法swim。
display方法是抽象方法,主要表示各类鸭子(实现类)有不同的展示,具体的由实现类去定义。
package com.lazy.snail.day14;
/**
* @ClassName WildDuck
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 15:18
* @Version 1.0
*/
public class WildDuck extends AbstractDuck implements Flyable{
@Override
public void display() {
System.out.println(\"我是一只野鸭子\");
}
@Override
public void fly() {
System.out.println(\"野鸭子展翅高飞\");
}
}
WildDuck继承了AbstractDuck且实现了Flyable。
那么这个野鸭子就会游泳(继承自抽象类的共性),有自己的展示方式(重写抽象方法),野鸭子还有个飞翔的能力(实现了飞翔接口,重写了fly方法)。
比如你后续定义了Bird,但是鸟跟鸭子区别很大,没什么共性抽象,但是都具备有飞的能力。
那么Bird直接实现Flyable接口,则具备了飞翔的行为能力。
六、应用场景和注意点
下面整了抽象类和接口的一些应用场景,在实际的开发过程中可以进行参考(看源代码的时候也可参考):
| 业务场景 | 推荐机制 | 理由 |
|---|---|---|
| 定义多个类共有的逻辑和字段 | 抽象类 | 支持字段和构造器 |
| 让多个类具备相同行为(比如“可比较”、“可序列化”) | 接口 | 接口能被多个类实现 |
| 构建插件机制、策略模式等 | 接口 | 规范行为,更灵活 |
| 为框架层提供通用父类 | 抽象类 | 可封装默认行为与状态 |
再说两点注意点:
接口并不是高级抽象类,它是另一种设计思路:接口强调的是能力和规范,抽象类强调的是共性和骨架。
类可以继承一个抽象类,实现多个接口
public class Dog extends AbstractAnimal implements Jumpable, Moveable {
...
}
在实际开发中,我们通常这样做:
用抽象类提供共享的代码、状态和结构(比如Controller父类)
用接口定义横向的能力模块(比如Serializable、Runnable、Comparable)
结语
记住并理解下面这段话:
抽象类定义身份的骨骼,承载共性逻辑与状态传承; 接口规范能力的边界,实现灵活的行为契约组合。
当需要构建血脉相连的家族树时,选择抽象类; 当需要为不同血脉赋予相同能力时,拥抱接口。 在后续的设计中,抽象类作为根基,接口作为羽翼自由扩展。
下一篇预告
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》



还没有评论呢,快来抢沙发~