这是一篇面向初学者的原创学习笔记,适合在学习 Java 面向对象三大特征之后继续深入。本文将系统讲解抽象类、接口、内部类的概念、作用、使用场景以及它们之间的区别,并配合代码示例帮助理解。


一、为什么学这三个知识点?

很多同学学完封装、继承、多态之后,会感觉 Java 面向对象已经差不多了。其实这只是基础部分。接下来最重要的三个知识点,就是:

  • 抽象类
  • 接口
  • 内部类

这三个知识点的共同作用是:让程序结构更清晰、扩展性更强、代码更符合面向对象思想。

简单来说:

  • 抽象类:用于提取“共同特征”,但不适合直接创建对象。
  • 接口:用于定义“规范”或“能力”。
  • 内部类:用于表示“类与类之间更紧密的从属关系”。

如果把它们学明白,后面学习集合、Lambda、设计模式、Spring 框架时会轻松很多。


二、抽象类详解

1. 什么是抽象类?

在现实开发中,有些父类本身只是为了让子类继承,并不是为了直接创建对象。

比如:

  • 动物类 Animal
  • 图形类 Shape
  • 员工类 Employee

这些类更像是一种“模板”或者“规范”,它们描述的是一类事物的共同属性和行为,但具体实现要交给子类完成。

Java 中使用 abstract 关键字修饰的类,叫做抽象类

abstract class Animal {
}

2. 抽象类的特点

抽象类有以下几个核心特点:

(1)不能直接创建对象
abstract class Animal {
}

public class Test {
    public static void main(String[] args) {
        // Animal a = new Animal(); // 错误,抽象类不能实例化
    }
}

因为抽象类本身就是“不完整”的类,所以不能直接 new

(2)可以有普通方法、成员变量、构造方法

很多初学者误以为抽象类里只能写抽象方法,这是不对的。

abstract class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }

    public void sleep() {
        System.out.println(name + "正在睡觉");
    }
}

说明:

  • 可以有属性
  • 可以有构造器
  • 可以有普通方法
  • 也可以有抽象方法
(3)可以包含抽象方法

抽象方法就是:只有方法声明,没有方法体的方法。

abstract class Animal {
    public abstract void eat();
}

抽象方法必须放在抽象类中,或者放在接口中。

(4)子类必须重写抽象方法

如果一个类继承了抽象类,那么它必须实现父类中的所有抽象方法,否则这个子类也必须声明为抽象类。

abstract class Animal {
    public abstract void eat();
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗啃骨头");
    }
}

3. 抽象类的作用

抽象类最大的作用是:提取共性,强制子类完成某些功能。

比如定义一个动物父类:

abstract class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }

    public void sleep() {
        System.out.println(name + "正在睡觉");
    }

    public abstract void eat();
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(name + "正在吃鱼");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(name + "正在啃骨头");
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Animal a1 = new Cat("小花");
        Animal a2 = new Dog("大黄");

        a1.sleep();
        a1.eat();

        a2.sleep();
        a2.eat();
    }
}

输出效果:

小花正在睡觉
小花正在吃鱼
大黄正在睡觉
大黄正在啃骨头

这里就体现了:

  • 父类负责定义公共内容
  • 子类负责实现个性行为
  • 多态让程序更灵活

4. 什么时候用抽象类?

如果多个子类之间有:

  • 相同的属性
  • 相同的普通方法
  • 但某些方法的实现不同

那么就很适合使用抽象类。

比如:

  • 员工类:都要计算工资,但不同岗位计算方式不同
  • 图形类:都要求面积,但不同图形公式不同
  • 支付类:都要支付,但支付方式不同

5. 抽象类的注意事项

(1)抽象类不一定有抽象方法
abstract class A {
    public void show() {
        System.out.println("hello");
    }
}

这种写法是允许的。

(2)有抽象方法的类一定是抽象类
class A {
    public abstract void show(); // 错误
}
(3)抽象类可以继承抽象类
abstract class A {
    public abstract void show();
}

abstract class B extends A {
}
(4)抽象类不能用 final 修饰

因为 final 表示不能被继承,而抽象类本来就是给子类继承用的,两者冲突。


三、接口详解

1. 什么是接口?

接口可以理解为一种“规则”、“标准”或者“能力说明书”。

比如现实中:

  • USB 接口规定了设备接入方式
  • 充电接口规定了电流、电压、连接方式

在 Java 中,接口也是类似的思想:

它规定某个类“应该具备哪些功能”,但一般不关心这个类内部是怎么实现的。

定义接口使用 interface 关键字。

interface Flyable {
    void fly();
}

2. 接口的特点

(1)接口不能直接创建对象
interface Flyable {
    void fly();
}

public class Test {
    public static void main(String[] args) {
        // Flyable f = new Flyable(); // 错误
    }
}
(2)类实现接口使用 implements
interface Flyable {
    void fly();
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟在飞");
    }
}
(3)实现类必须重写接口中的方法

否则这个实现类也要声明为抽象类。

(4)一个类可以实现多个接口

这也是接口非常重要的一个优势。

interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("鸭子会飞");
    }

    @Override
    public void swim() {
        System.out.println("鸭子会游泳");
    }
}

Java 类只能单继承,但可以多实现接口。


3. 接口中的成员特点

在学习接口时,初学者最容易混乱的就是接口里的成员默认修饰符。

(1)成员变量默认是常量

接口中的变量默认会被 public static final 修饰。

interface A {
    int NUM = 10;
}

实际上等价于:

interface A {
    public static final int NUM = 10;
}

因此:

  • 必须赋初值
  • 值不能修改
  • 通常通过 接口名.变量名 访问
System.out.println(A.NUM);
(2)抽象方法默认是 public abstract
interface A {
    void show();
}

实际上等价于:

interface A {
    public abstract void show();
}

所以实现类重写时,访问权限不能更小,通常写成:

@Override
public void show() {
}

4. JDK 8 以后接口的新特性

很多教材只讲老版本接口,但现在开发中还要知道新特性。

(1)默认方法 default

接口中可以有带方法体的方法,但必须用 default 修饰。

interface A {
    default void test() {
        System.out.println("这是接口中的默认方法");
    }
}

作用:

  • 给接口扩展功能时,不会影响所有实现类
  • 适合添加“默认实现”
(2)静态方法 static
interface A {
    static void show() {
        System.out.println("接口中的静态方法");
    }
}

调用方式:

A.show();
(3)私有方法 private

JDK 9 开始,接口中还可以有私有方法,主要用于复用默认方法或静态方法中的重复代码。


5. 接口的作用

接口最核心的作用是:定义规范,降低耦合,增强扩展性。

来看一个例子。

interface USB {
    void connect();
    void disconnect();
}

class Mouse implements USB {
    @Override
    public void connect() {
        System.out.println("鼠标连接成功");
    }

    @Override
    public void disconnect() {
        System.out.println("鼠标断开成功");
    }
}

class Keyboard implements USB {
    @Override
    public void connect() {
        System.out.println("键盘连接成功");
    }

    @Override
    public void disconnect() {
        System.out.println("键盘断开成功");
    }
}

class Computer {
    public void useDevice(USB usb) {
        usb.connect();
        System.out.println("设备正在工作...");
        usb.disconnect();
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.useDevice(new Mouse());
        computer.useDevice(new Keyboard());
    }
}

这个例子说明:

  • 电脑不需要关心接入的是鼠标还是键盘
  • 只要它们实现了 USB 接口即可
  • 程序扩展时,只需要新增实现类

这就是接口带来的灵活性。


6. 抽象类和接口的区别

这是面试和学习中非常高频的问题。

对比项 抽象类 接口
关键字 abstract class interface
是否能实例化 不能 不能
是否有构造方法 没有
是否能有普通方法 可以 JDK8 之前不可以,之后可以有 default/static 方法
是否能有成员变量 可以 只能是常量
继承/实现关系 单继承 可多实现
设计目的 提取子类共性 定义行为规范

7. 怎么理解两者的使用场景?

你可以这样记:

  • 抽象类表示“是什么”
    • 例如:猫、狗都是动物
  • 接口表示“能做什么”
    • 例如:鸟会飞,鱼会游泳

举例:

abstract class Animal {
    String name;
    public abstract void eat();
}

interface Flyable {
    void fly();
}

class Bird extends Animal implements Flyable {
    @Override
    public void eat() {
        System.out.println("鸟在吃虫子");
    }

    @Override
    public void fly() {
        System.out.println("鸟在飞翔");
    }
}

这里:

  • Animal 表示鸟属于动物这一类
  • Flyable 表示鸟具备飞的能力

这个理解方式非常重要。


四、内部类详解

1. 什么是内部类?

如果一个类定义在另一个类的内部,这样的类就叫内部类

class Outer {
    class Inner {
    }
}

内部类本质上也是类,只不过它和外部类的关系更紧密。


2. 为什么要使用内部类?

内部类主要适用于以下场景:

  • 某个类只为另一个类服务,不希望单独暴露出去
  • 内部类需要直接访问外部类的成员
  • 让代码结构更集中、更清晰

也就是说:

如果一个类离不开另一个类,那么就可以考虑把它写成内部类。


五、内部类的分类

Java 中常见的内部类主要有四种:

  1. 成员内部类
  2. 静态内部类
  3. 局部内部类
  4. 匿名内部类

下面分别讲解。


六、成员内部类

1. 定义方式

成员内部类就是定义在外部类的成员位置上的类,和成员变量、成员方法是同一级别。

class Outer {
    private String name = "外部类变量";

    class Inner {
        public void show() {
            System.out.println(name);
        }
    }
}

2. 特点

  • 可以直接访问外部类的所有成员,包括私有成员
  • 创建内部类对象,必须先有外部类对象

3. 创建对象方式

public class Test {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.show();
    }
}

注意这个语法:

Outer.Inner inner = outer.new Inner();

这是成员内部类最容易考的地方。


七、静态内部类

1. 定义方式

使用 static 修饰的内部类,叫做静态内部类。

class Outer {
    static class Inner {
        public void show() {
            System.out.println("静态内部类方法");
        }
    }
}

2. 特点

  • 不依赖外部类对象
  • 可以直接创建对象
  • 只能直接访问外部类的静态成员

3. 创建对象方式

public class Test {
    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner();
        inner.show();
    }
}

和成员内部类相比,静态内部类更独立一些。


八、局部内部类

1. 定义方式

局部内部类是定义在方法内部的类。

class Outer {
    public void method() {
        class Inner {
            public void show() {
                System.out.println("局部内部类");
            }
        }

        Inner inner = new Inner();
        inner.show();
    }
}

2. 特点

  • 作用范围只在当前方法中
  • 外部无法直接访问
  • 适合只在某个方法里临时使用的类

这种类在实际开发中不算特别常见,但理解它有助于理解匿名内部类。


九、匿名内部类

1. 什么是匿名内部类?

匿名内部类本质上是:没有名字的局部内部类

它通常用于:

  • 某个类或接口的子类对象只使用一次
  • 为了简化代码书写

例如:

interface Swim {
    void swim();
}

public class Test {
    public static void main(String[] args) {
        Swim s = new Swim() {
            @Override
            public void swim() {
                System.out.println("正在游泳");
            }
        };

        s.swim();
    }
}

这里:

  • new Swim() { ... } 就是匿名内部类
  • 它相当于创建了一个实现 Swim 接口的匿名对象

2. 匿名内部类的使用场景

匿名内部类在以前的 GUI 编程、事件监听、多线程中很常见。

例如创建线程:

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("线程执行中");
            }
        };

        t.start();
    }
}

或者:

Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("任务执行中");
    }
};

当然,Java 8 之后很多这类场景会被 Lambda 表达式替代,但匿名内部类依然要会看、会写。


十、内部类的总结对比

类型 定义位置 是否依赖外部类对象 常见用途
成员内部类 类中、方法外 表示强依赖关系
静态内部类 类中、方法外,带 static 工具型、辅助型结构
局部内部类 方法内部 依赖所在方法环境 方法内临时使用
匿名内部类 方法内部 视场景而定 简化一次性对象创建

十一、学习这三个知识点时最容易混淆的地方

1. 抽象类和接口不是一回事

很多初学者会觉得它们都不能创建对象,所以差不多。其实并不是。

  • 抽象类更强调“公共模板”
  • 接口更强调“行为规范”

2. 抽象类可以有构造方法,接口没有

这是一个非常典型的考点。

抽象类虽然不能创建对象,但它的构造器可以给子类初始化父类部分。

3. 成员内部类创建对象必须依赖外部类对象

这个语法必须熟悉:

Outer.Inner inner = outer.new Inner();

4. 匿名内部类不是接口,也不是对象名

它本质是“一个没有名字的子类对象”或者“实现类对象”。


十二、实际开发中怎么理解这三个知识点?

如果你想从“应试思维”进入“开发思维”,可以这样理解:

抽象类

适合多个子类共享代码时使用。

比如项目里有很多不同类型的订单处理类,它们都有公共字段和公共方法,这时就可以抽取一个抽象父类。

接口

适合做系统解耦。

比如支付模块中,不同支付方式都实现同一个支付接口。这样以后新增支付宝、微信、银行卡,只需要新增实现类,不用大改原有代码。

内部类

适合让某些类“只在局部生效”或“只服务于某个外部类”。

比如构建者模式中的 Builder,经常就会用静态内部类。


十三、一个综合示例帮你彻底理解

下面用一个例子把抽象类、接口、内部类串起来。

abstract class Person {
    protected String name;

    public Person(String name) {
        this.name = name;
    }

    public abstract void work();
}

interface Study {
    void study();
}

class Student extends Person implements Study {
    public Student(String name) {
        super(name);
    }

    @Override
    public void work() {
        System.out.println(name + "当前的主要任务是学习");
    }

    @Override
    public void study() {
        System.out.println(name + "正在认真听课");
    }

    static class Tool {
        public void show() {
            System.out.println("学生使用学习工具");
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Student student = new Student("小明");
        student.work();
        student.study();

        Student.Tool tool = new Student.Tool();
        tool.show();
    }
}

在这个例子中:

  • Person 是抽象类,表示“人”的共性
  • Study 是接口,表示“学习能力”
  • Student 继承抽象类并实现接口
  • Tool 是静态内部类,表示学生相关的辅助工具

如果你能理解这个例子,说明这几个知识点已经掌握得比较扎实了。


十四、学习建议

学完这部分后,不要停留在“会背概念”,而要做到下面三点:

1. 自己手写代码

至少把下面例子各写一遍:

  • 抽象类 + 子类重写
  • 接口 + 实现类
  • 成员内部类创建对象
  • 匿名内部类实现接口

2. 自己总结区别

尤其是:

  • 抽象类和接口的区别
  • 成员内部类和静态内部类的区别

3. 带着“为什么”去学

不要只记语法,要理解:

  • 为什么抽象类不能实例化?
  • 为什么接口可以多实现?
  • 为什么成员内部类必须依赖外部类对象?

当你开始思考这些“为什么”的时候,说明你正在真正理解 Java。


十五、结语

抽象类、接口、内部类,是 Java 面向对象中非常关键的进阶内容。

如果说封装、继承、多态是让你“入门面向对象”,那么抽象类、接口、内部类,就是让你开始真正写出更规范、更灵活代码的重要一步。

你可以把它们这样记住:

  • 抽象类:提取共性,定义模板
  • 接口:定义规范,体现能力
  • 内部类:强化关联,局部封装

只要把这三个知识点打牢,后面学集合、多线程、Lambda、Spring 框架时,理解速度会明显提升。


十六、给初学者的练习题

练习 1

定义一个抽象类 Shape,包含抽象方法 getArea(),再定义 CircleRectangle 两个子类,分别计算面积。

练习 2

定义一个接口 Playable,包含方法 play(),让 PianoGuitar 类实现该接口。

练习 3

定义一个外部类 Computer,在里面写一个成员内部类 CPU,并在内部类中访问外部类的属性。

练习 4

使用匿名内部类创建一个 Runnable 对象,并启动线程。


如果你准备把这篇文章发到 CSDN,建议你发布时可以搭配下面这个标题:

Java 抽象类、接口、内部类超详细讲解,看完终于弄懂了

或者:

学完 Java 三大特征后该学什么?抽象类、接口、内部类详解

如果你愿意,我下一步还可以继续帮你做两件事:

  1. 把这篇文章再优化成更像 CSDN 爆款的排版版本
  2. 顺手再给你生成一版配套练习题答案版 md 文档

你要的话我可以继续直接帮你生成。

更多推荐