Java 抽象类与接口

背景与动机#

抽象类和接口都用于定义“规范”,但侧重点不同。

抽象类更像一类对象的共同父类,可以放共享状态和部分实现;接口更像能力契约,强调“能做什么”。

核心原理拆解#

img
img

1. 抽象类不能直接创建对象#

abstract class Shape {
abstract double area();
}

abstract 方法只有声明,没有方法体,子类必须实现它,除非子类也是抽象类。

抽象类存在的意义,不是“少写代码”这么简单,而是先把一类对象的共同骨架抽出来。

比如:

  • 所有图形都可能有 area()
  • 但圆形、矩形、三角形的面积公式都不同

这时父类可以先定义“所有子类都应该能算面积”,而不急着给出统一实现。

2. 抽象类可以有普通字段和普通方法#

abstract class Shape {
String color;
void setColor(String color) {
this.color = color;
}
abstract double area();
}

这说明抽象类既能定义规范,也能承载部分公共实现。

这也是抽象类和普通父类很接近的地方:它照样可以有字段、构造方法、普通方法,只是它本身“不完整”,所以不能直接实例化。

2.1 抽象类也可以有构造方法#

abstract class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
}

虽然抽象类不能直接 new,但子类对象创建时,仍然要先完成父类那部分初始化,所以抽象类完全可以有构造方法。

2.2 抽象类适合放“共性状态 + 共性逻辑”#

abstract class Employee {
String name;
Employee(String name) {
this.name = name;
}
void printName() {
System.out.println("name=" + name);
}
abstract double salary();
}

这里:

  • name 是所有员工共有状态
  • printName() 是所有员工都能直接复用的逻辑
  • salary() 则留给不同子类自己实现

这类场景就很适合抽象类,因为它强调的是“一组对象共享同一个基础父类”。

3. 接口定义能力契约#

interface Flyable {
void fly();
}

类通过 implements 实现接口:

class Bird implements Flyable {
@Override
public void fly() {
System.out.println("bird fly");
}
}

接口方法默认是 public 语义,实现时也必须使用 public

可以把接口理解成“能力清单”:

  • 不强调你是谁
  • 只强调你能做什么

比如 BirdAirplaneSuperman 都可能会飞,但它们显然不属于同一个父类体系。这时用接口表达 Flyable 就更自然。

3.1 接口更适合表达横向能力#

interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("duck fly");
}
@Override
public void swim() {
System.out.println("duck swim");
}
}

这里 Duck 不是因为“属于某个共同父类”才会飞和游,而是因为它具备这些能力。

所以接口特别适合表达:

  • 会飞
  • 可序列化
  • 可比较
  • 可关闭

这类“横向可组合能力”。

3.2 接口也可以有默认方法,但重点仍然是契约#

interface Logger {
void log(String message);
default void info(String message) {
log("[INFO] " + message);
}
}

default 方法让接口也能提供一部分默认实现,但这不代表接口就变成了抽象类。

它的核心角色仍然是:

  • 对外声明能力
  • 让不同实现类遵守同一套调用约定

初学阶段先把重点放在“接口表达契约”上,会更不容易混。

4. 一个类只能继承一个类,但可以实现多个接口#

class Duck implements Flyable, Swimmable {
}

这也是接口在 Java 中非常重要的原因:它能表达多个能力组合。

4.1 为什么 Java 支持多实现接口,但不支持多继承类#

如果一个类同时继承多个具体父类,状态和实现来源会变得很复杂,容易产生冲突。

而接口主要描述“你必须提供哪些方法”,它更轻量,也更适合做能力组合。

所以 Java 的选择是:

  • 类继承:只能单继承
  • 接口实现:可以多实现

这保证了对象模型不会太混乱,同时又保留了很强的扩展能力。

4.2 抽象类和接口经常一起使用#

真实项目里,这两个概念并不是二选一,很多时候会一起出现:

interface Payment {
void pay(double amount);
}
abstract class AbstractPayment implements Payment {
protected String account;
AbstractPayment(String account) {
this.account = account;
}
void printAccount() {
System.out.println("account=" + account);
}
}
class AlipayPayment extends AbstractPayment {
AlipayPayment(String account) {
super(account);
}
@Override
public void pay(double amount) {
System.out.println("alipay pay " + amount);
}
}

这里分工就很清楚:

  • 接口 Payment 负责定义统一支付契约
  • 抽象类 AbstractPayment 负责沉淀共享状态和公共逻辑
  • 具体类 AlipayPayment 负责完成最终实现

这是一种很常见的设计方式。

4.3 到底什么时候选抽象类,什么时候选接口#

可以先用一句最实用的话判断:

  • 想表达“它本质上是一类东西”,优先想抽象类
  • 想表达“它具备某种能力”,优先想接口

更具体一点:

  • 需要共享字段、构造方法、公共实现时,抽象类更合适
  • 需要让不相关的多个类遵守统一约定时,接口更合适
  • 需要对外暴露扩展点、让调用方依赖抽象而不是具体类时,接口通常更灵活

最小可运行代码示例#

public class AbstractInterfaceDemo {
public static void main(String[] args) {
Shape shape = new Circle(2);
System.out.println(shape.area());
Flyable bird = new Bird();
bird.fly();
Swimmable duck = new Duck();
duck.swim();
}
}
abstract class Shape {
String color;
Shape() {
this.color = "black";
}
void printColor() {
System.out.println("color=" + color);
}
abstract double area();
}
class Circle extends Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("bird fly");
}
}
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("duck fly");
}
@Override
public void swim() {
System.out.println("duck swim");
}
}

常见陷阱与错误示例#

1. 试图直接实例化抽象类#

错误示例:

Shape shape = new Shape();

抽象类不能直接创建对象,只能创建具体子类对象。

2. 实现接口方法时漏掉 public#

错误示例:

class Bird implements Flyable {
void fly() {
System.out.println("fly");
}
}

接口方法是公开契约,实现时不能降低访问权限,应写成 public void fly()

3. 把接口当成纯代码复用工具#

接口主要表达能力和契约。若核心目的是复用共享状态或公共实现,抽象类可能更合适。

4. 把抽象类和接口理解成完全互斥关系#

很多初学者会纠结“到底只能选一个吗”。实际上它们经常配合使用:

  • 接口定义对外契约
  • 抽象类沉淀公共实现
  • 具体类完成业务细节

如果把它们硬理解成对立关系,后面读框架代码会很别扭。

5. 在接口里塞太多与能力无关的方法#

接口应该尽量聚焦在一组清晰职责上。一个接口如果同时要求“登录、支付、导出、打印、上传”,通常说明接口边界已经乱了。

6. 误以为“用了抽象类就一定比接口高级”#

不是谁更高级,而是谁更适合当前建模。

  • 抽象类偏“共同父类”
  • 接口偏“行为契约”

选错不是语法问题,而是设计问题。

面试高频问题#

1. 抽象类和接口有什么区别#

抽象类强调共同父类和部分实现,接口强调能力契约。类只能继承一个父类,但可以实现多个接口。

2. 抽象类能不能有构造方法#

可以。抽象类不能直接实例化,但子类创建时仍然会调用父类构造方法来完成父类部分初始化。

3. 什么时候用接口#

当你想表达一组可被不同类实现的能力,并希望调用方依赖能力而不是具体类时,优先考虑接口。

4. 抽象类和普通类的区别是什么#

普通类可以直接实例化;抽象类不能直接实例化,通常用于定义公共骨架,并要求子类补齐某些抽象行为。

5. 接口和抽象类怎么选#

如果重点是共享状态和公共实现,优先考虑抽象类;如果重点是定义统一能力约定,并允许多个不相关类实现,优先考虑接口。

6. 一个类能不能既继承抽象类又实现接口#

可以,而且这是很常见的写法。类先 extends 一个抽象类,再 implements 一个或多个接口。

7. 为什么很多框架更喜欢接口#

因为接口更适合解耦。调用方依赖接口,不依赖具体实现类,后续更容易替换实现、做扩展和测试替身。

一句话总结#

抽象类适合表达带共享实现的共同父类,接口适合表达可组合的能力契约。

文章目录

文章目录