15 接口与抽象类

前置知识:14 继承与多态
本章目标:掌握 abstract 抽象类、interface 接口、多实现如何解决菱形问题,学会 Comparable/Comparator 排序、函数式接口与 Lambda 初步,理解增强 for 底层的 Iterable/Iterator 协议,建立「面向接口编程」的思维。

概述

继承只能有一个父类,但「能力」往往需要多个:一个类既要能比较又要能序列化还能被遍历。Java 的答案是接口——只声明「会做什么」,不关心「是什么」。这对应 C 中「约定一组函数指针签名」的协议思想,但由编译器强制检查。

抽象类 abstract

抽象类是「不完整的类」:可以包含抽象方法(只有声明没有实现),不能被实例化。对应 C++ 的纯虚函数:

public class AbstractDemo {
    public static void main(String[] args) {
        // Shape s = new Shape();      // 编译错误!抽象类不能实例化
        Shape c = new Circle(2);
        Shape r = new Rect(3, 4);
        System.out.printf("圆面积 %.2f%n", c.area());     // 12.57
        System.out.printf("矩形面积 %.2f%n", r.area());   // 12.00
    }
}
 
abstract class Shape {              // 抽象类
    String name;
 
    Shape(String name) {            // 可以有构造方法和字段——供子类 super 调用
        this.name = name;
    }
 
    abstract double area();         // 抽象方法:只有签名,分号结尾,无方法体
 
    void describe() {               // 也可以有普通方法(与纯接口的区别)
        System.out.println(name + " 面积 = " + area());
    }
}
 
class Circle extends Shape {
    double radius;
 
    Circle(double radius) {
        super("圆");
        this.radius = radius;
    }
 
    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}
 
class Rect extends Shape {
    double w, h;
 
    Rect(double w, double h) {
        super("矩形");
        this.w = w;
        this.h = h;
    }
 
    @Override
    double area() {
        return w * h;
    }
}
对比项C++Java
纯虚函数virtual double area() = 0;abstract double area();
含纯虚函数的类自动成为抽象类必须显式写 abstract
抽象类能否有数据成员和实现同样能
是否可有构造方法有(供派生链调用)

interface 全解

接口比抽象类更纯粹:默认所有方法 public abstract,所有字段 public static final。一个类可以实现多个接口——这正是突破单继承限制的方式:

public class InterfaceDemo {
    public static void main(String[] args) {
        Duck d = new Duck();
        d.fly();
        d.swim();
        d.breathe();
 
        // 接口也是类型:可以声明接口变量指向实现对象
        Flyer f = d;       // 向上转型为接口类型
        f.fly();
    }
}
 
// 一个类可实现多个接口,用逗号分隔 —— C++ 多继承做不到的安全版本
class Duck implements Flyer, Swimmer, Animal {
    public void fly()    { System.out.println("鸭子扑棱着飞"); }
    public void swim()   { System.out.println("鸭子划水游"); }
    public void breathe(){ System.out.println("鸭子用肺呼吸"); }
}
 
interface Flyer {
    void fly();                        // 默认就是 public abstract
}
 
interface Swimmer {
    void swim();
}
 
interface Animal {
    void breathe();
 
    int LUNGS = 2;                     // 字段自动是 public static final 常量
    // private int x;                  // 普通实例字段不允许!接口没有状态
}

多实现解决菱形问题

C++ 多继承的经典灾难:D 同时继承 B 和 C,而 B、C 都继承 A,导致 D 里有两份 A(需 virtual 继承补救)。Java 用「单继承 + 多接口」从结构上杜绝了这个问题:

flowchart TD
    subgraph CPP["C++ 多继承: 菱形问题"]
        CA["A"] --> CB["B"] --> CD2["D"]
        CA --> CC["C"] --> CD2
        CD2["D 内含两份 A!"]
    end
    subgraph JAVA["Java: 单继承+多接口"]
        IA["接口 Swimmer"] --> ID["Duck 实现两个接口<br/>接口无状态,不存在两份副本"]
        IB["接口 Flyer"] --> ID
    end

即使两个接口有同名 default 方法,Java 也强制你在实现类中重写来解决冲突:

interface A {
    default String hello() { return "A"; }
}
 
interface B {
    default String hello() { return "B"; }
}
 
// 实现 A 和 B 却不重写 hello() 会编译错误:
// class AB implements A, B { }          // 错误!必须解决冲突
class AB implements A, B {
    @Override
    public String hello() {               // 显式重写消歧义
        return "AB 合成: " + A.super.hello() + B.super.hello();
    }
}

default 默认方法

Java 8 起接口可以有带实现的 default 方法。动机:给已有接口加新方法时不破坏千千万万的旧实现类(Collection 接口新增 stream() 就是靠这个):

public class DefaultMethodDemo {
    public static void main(String[] args) {
        new LegacyPrinter().print("hello");
        new FancyPrinter().print("hello");
    }
}
 
interface Printable {
    void print(String msg);                       // 抽象方法,实现类必须写
 
    default void printTwice(String msg) {         // 默认方法带实现
        print(msg);
        print(msg);
    }
}
 
// 只实现了抽象方法,免费获得 printTwice
class LegacyPrinter implements Printable {
    public void print(String msg) { System.out.println("[旧] " + msg); }
}
 
// 也可以覆盖默认方法
class FancyPrinter implements Printable {
    public void print(String msg) { System.out.println("[新] >> " + msg); }
 
    @Override
    public void printTwice(String msg) {
        System.out.println("[新] 双倍输出:");
        Printable.super.printTwice(msg);          // 复用接口默认实现
    }
}

函数式接口与 Lambda 初步

只有一个抽象方法的接口叫函数式接口,可以用 Lambda 表达式实现——这是 Java 对「函数是一等公民」的妥协版支持,类似 C 的函数指针但类型安全:

import java.util.Arrays;
import java.util.List;
 
@FunctionalInterface                 // 注解强制检查"只有一个抽象方法"
interface IntOperation {
    int apply(int a, int b);
 
    // int extra();                  // 再加一个就不再是函数式接口,注解报错
}
 
public class LambdaDemo {
    public static void main(String[] args) {
        // 匿名内部类写法(Lambda 之前的传统方式)
        IntOperation addOld = new IntOperation() {
            public int apply(int a, int b) { return a + b; }
        };
 
        // Lambda 写法:(参数) -> 表达式,参数类型可推断时省略
        IntOperation add    = (a, b) -> a + b;
        IntOperation mul    = (a, b) -> a * b;
        IntOperation max    = Integer::max;      // 方法引用:直接复用现成静态方法
 
        System.out.println(add.apply(3, 4));     // 7
        System.out.println(mul.apply(3, 4));     // 12
        System.out.println(max.apply(3, 4));     // 4
        System.out.println(addOld.apply(1, 2));  // 3
 
        // JDK 自带四大函数式接口(java.util.function 包):
        java.util.function.Predicate<String> isEmpty = String::isEmpty;
        java.util.function.Function<String, Integer> len = String::length;
        java.util.function.Supplier<List<String>> maker = java.util.ArrayList::new;
        java.util.function.Consumer<String> printer = System.out::println;
        printer.accept("长度=" + len.apply("hello") + " 空?" + isEmpty.test(""));
        printer.accept("新建列表大小=" + maker.get().size());
    }
}

Comparator 排序实战:自然序与定制序

接口最经典的实战场景是排序。Comparable 定义「自然顺序」(类自己实现),Comparator 定义「外部定制顺序」(调用方按需传入):

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
 
public class SortDemo {
    public static void main(String[] args) {
        List<Student> list = new ArrayList<>(Arrays.asList(
            new Student("张三", 88),
            new Student("李四", 95),
            new Student("王五", 72),
            new Student("赵六", 88)
        ));
 
        // 自然序:按成绩(Student 实现了 Comparable)
        list.sort(null);
        System.out.println("自然序(分数): " + list);
 
        // 定制序一:按姓名排序,Lambda 实现 Comparator
        Comparator<Student> byName = (s1, s2) -> s1.name.compareTo(s2.name);
        list.sort(byName);
        System.out.println("按姓名: " + list);
 
        // 定制序二:Comparator.comparing 链式组合 + thenComparing 多级排序
        list.sort(
            Comparator.comparingInt((Student s) -> -s.score)   // 分数降序
                      .thenComparing(s -> s.name)              // 同分按姓名
        );
        System.out.println("多级排序: " + list);
 
        // 数组同样适用
        int[] nums = {5, 3, 9, 1};
        Arrays.sort(nums);
        System.out.println(Arrays.toString(nums));
    }
}
 
class Student implements Comparable<Student> {
    String name;
    int score;
 
    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
 
    @Override
    public int compareTo(Student other) {
        // 约定:负数表示 this 排前面,正数排后面,0 相等
        return Integer.compare(this.score, other.score);   // 升序
    }
 
    @Override
    public String toString() {
        return name + "(" + score + ")";
    }
}

C 里对应 qsort 的比较函数指针——但 qsort 没有类型检查(void* 强转错类型即灾难),Java 的泛型 Comparator 编译期就能查出来。

对比项C qsortJava sort
比较逻辑函数指针 int (*)(const void*, const void*)Comparable/Comparator
类型安全无,void* 全靠自觉泛型编译期保证
稳定性标准未规定对象排序保证稳定

Iterable 与 Iterator:增强 for 的底层协议

任何实现了 Iterable<E> 的对象都能用增强 for 遍历——编译器把它脱糖成 iterator() 调用加 while 循环。理解这个协议就理解了集合框架的统一入口:

import java.util.Iterator;
import java.util.NoSuchElementException;
 
public class IterableDemo {
    public static void main(String[] args) {
        Countdown c = new Countdown(5);
        for (int n : c) {                 // 自定义类也能被增强 for 遍历!
            System.out.print(n + " ");    // 5 4 3 2 1
        }
        System.out.println();
 
        // 增强for的等价底层形式:
        Iterator<Integer> it = c.iterator();
        while (it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();
 
        // 手动迭代时若要删除元素,必须用 iterator.remove()
        // 否则抛 ConcurrentModificationException(fail-fast 机制)
    }
}
 
// 实现Iterable<Integer>表示"我能产生一个Integer迭代器"
class Countdown implements Iterable<Integer> {
    private final int from;
 
    Countdown(int from) {
        this.from = from;
    }
 
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {         // 匿名内部类实现迭代器
            private int current = from;
 
            public boolean hasNext() {
                return current > 0;       // 还有元素吗
            }
 
            public Integer next() {
                if (current <= 0) {
                    throw new NoSuchElementException();
                }
                return current--;         // 返回当前并前进
            }
        };
    }
}

C 里遍历容器靠 for (p = head; p; p = p->next) 手工推进指针;Iterator 把这套指针操作封装成了标准协议。

标记接口

不含任何方法的接口,仅用于给类「贴标签」,运行期可用 instanceof 检测:

// JDK 经典标记接口
// java.io.Serializable —— 允许被序列化
// java.lang.Cloneable  —— 允许被克隆
// java.util.RandomAccess —— 支持 O(1) 随机访问
 
public class MarkerDemo {
    public static void main(String[] args) {
        Object doc = new ExportableDoc();
        if (doc instanceof java.io.Serializable) {   // 检测标签
            System.out.println("该对象可序列化");
        }
    }
}
 
interface Auditable { }                    // 自定义空标记接口
 
class ExportableDoc implements Auditable { }

现代替代方案是注解(见 注解与反射),但标准库中标记接口仍大量存在。

接口 vs 抽象类:如何选择

flowchart TD
    Q{"需要定义<br/>字段状态或构造方法?"} -->|"是"| A{"部分方法有<br/>通用实现可复用?"}
    Q -->|"否"| I["用 interface"]
    A -->|"否"| I2["仍可用 interface<br/>+ default 方法"]
    A -->|"是"| AB["用 abstract class"]
    I --> M["面向接口编程:<br/>变量、参数、返回值尽量声明为接口类型"]
    I2 --> M
    AB --> M
维度抽象类接口
关系语义is-a(是什么)can-do(能做什么)
数量限制只能继承一个可实现多个
字段任意只能常量
构造方法
方法实现都可以abstract/default/static
典型例子AbstractListList, Comparable, Runnable

经验法则:「模板复用骨架代码」用抽象类(如 AbstractList 已实现 size/isEmpty 等);「声明能力契约」用接口。两者也常配合:接口定契约 + 抽象类做骨架实现。

本章要点回顾

  • 抽象类是不完整的父类(对标 C++ 纯虚函数),接口是纯能力契约
  • 单继承 + 多实现从语言层面消除了 C++ 菱形继承问题;default 方法冲突必须显式重写
  • 接口字段天然 public static final;default 方法让接口可以平滑演进
  • 函数式接口 + Lambda 是类型安全的函数指针;四大内置接口 Predicate/Function/Supplier/Consumer 覆盖绝大多数场景
  • Comparable 自然序、Comparator 定制序,比 qsort 的函数指针安全得多
  • 增强 for 的本质是 Iterable.iterator() 协议;标记接口给类贴运行期标签
  • 面向接口编程:依赖抽象而非具体实现


练习

题号题目链接知识点
P1023ticketshttps://www.luogu.com.cn/problem/P1023接口、抽象