JAVA 接口、抽象类的关系和用处 详细解析

embedded/2025/2/3 19:21:26/

接口 - Java教程 - 廖雪峰的官方网站

一个 抽象类 如果实现了一个接口,可以只选择实现接口中的 部分方法(所有的方法都要有,可以一部分已经写具体,另一部分继续保留抽象),原因在于:

  • 抽象类本身是 不完整的类,它可以有未实现的方法(即抽象方法),因此可以选择不完全实现接口。
  • 由继承该抽象类的具体子类去完成未实现的方法。

这也是抽象类的一个强大功能,它在实现接口时,提供了一个“中间层次”,部分实现接口的行为,为具体的子类提供基础。

这里有两个箭头指向同一个类(例如 AbstractList),是因为:

  1. 接口(如 List)定义了行为规范:接口是用来定义类应该具有的功能和行为,例如 List 定义了与列表相关的方法(如 add(), get() 等),但不提供具体实现。
  2. 抽象类(如 AbstractList)提供了部分实现:抽象类用于实现接口的一部分行为,同时为具体类(如 ArrayListLinkedList)提供可以复用的代码。

AbstractListList 的区别

  • List 接口

    • 是一个完全抽象的接口,只定义了列表操作的规范。
    • 方法如 add(E element), get(int index), remove(int index) 等都只是方法声明,没有实现。
  • AbstractList 抽象类

    • 是一个抽象类,实现了 List 接口的大部分通用功能。
    • 目的是让具体实现类(如 ArrayListLinkedList)复用这些功能,只需实现特定的方法即可。例如,AbstractList 中实现了 addAll(),具体类无需再写这部分代码。
示例代码

假设你要实现一个自定义的列表,直接实现 List 和继承 AbstractList 的区别如下:

直接实现 List 接口

如果从零实现 List 接口,你需要定义接口中所有的方法(包括很多通用方法,比如 size()addAll())。

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;public class CustomList<E> implements List<E> {private Object[] elements = new Object[10];private int size = 0;@Overridepublic boolean add(E e) {if (size == elements.length) {Object[] newElements = new Object[size * 2];System.arraycopy(elements, 0, newElements, 0, size);elements = newElements;}elements[size++] = e;return true;}@Overridepublic int size() {return size;}@Overridepublic E get(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index: " + index);}return (E) elements[index];}// 还需实现 List 中所有的方法,如 remove()、iterator() 等,工作量很大。
}
继承 AbstractList 抽象类

通过继承 AbstractList,你只需实现一些关键方法,剩下的方法由 AbstractList 提供默认实现。

import java.util.AbstractList;public class CustomList<E> extends AbstractList<E> {private Object[] elements = new Object[10];private int size = 0;@Overridepublic E get(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index: " + index);}return (E) elements[index];}@Overridepublic int size() {return size;}@Overridepublic boolean add(E e) {if (size == elements.length) {Object[] newElements = new Object[size * 2];System.arraycopy(elements, 0, newElements, 0, size);elements = newElements;}elements[size++] = e;return true;}// 不需要手动实现 addAll() 等通用方法,AbstractList 已提供默认实现。
}
运行示例
public class Main {public static void main(String[] args) {CustomList<String> list = new CustomList<>();list.add("A");list.add("B");list.add("C");System.out.println(list.get(1)); // 输出: BSystem.out.println(list.size()); // 输出: 3}
}

为什么 Java 集合框架中要设计接口和抽象类的这种关系?

1. 灵活性:接口用于定义行为规范

接口(如 List)允许不同的实现方式,适配多种需求,例如:

  • ArrayList:基于数组实现的列表,适合随机访问操作。
  • LinkedList:基于链表实现的列表,适合插入和删除操作。
  • 自定义列表:可以实现特定的逻辑,比如线程安全或固定容量。
2. 代码复用:抽象类减少重复代码

抽象类(如 AbstractList)避免了在每个实现类中重复编写通用逻辑。例如:

  • size() 的计算逻辑。
  • 批量添加方法(addAll())的实现。
  • 迭代器(iterator())的通用实现。

通过这种设计,新实现类只需关注特定的细节。

3. 多层次抽象设计

如下图中的设计:

  • 接口层: 定义行为规范(如 List, Collection)。
  • 抽象类层: 提供部分实现(如 AbstractList, AbstractCollection)。
  • 具体类层: 提供特定实现(如 ArrayList, LinkedList)。

这种多层次设计提供了灵活性和代码复用的平衡。


问题 1:调用的时候执行的是接口的方法还是抽象类的方法?

调用的是 对象的实际实现类中的方法,而不是接口或抽象类本身。尽管我们通过 List 这样的接口来引用一个对象,但具体执行的代码取决于 对象的具体实现类

假设我们写了一段代码:

List<String> list = new ArrayList<>();
list.add("Hello");
  1. 编译时看接口,运行时看实现类:

    • list 的编译时类型是 List,所以编译器只会允许你调用 List 接口中声明的方法,比如 add()remove() 等。
    • list 的运行时类型是 ArrayList,所以具体执行的 add() 方法是 ArrayList 类中定义的实现。
  2. 接口 vs 抽象类:

    • List 是接口,定义了 add() 的方法规范。
    • AbstractList 是一个抽象类,部分实现了 List 的规范,并提供了通用实现。
    • 但是:在 ArrayList 中,它直接继承了 AbstractList,并可能覆写了某些方法,所以最终调用的是 ArrayList 的实现。
       

为什么我们总是见到 List,而没有见过 AbstractList

  • AbstractList 是设计细节:

    • AbstractList 是为具体实现类(如 ArrayListLinkedList)服务的,目的是 减少代码重复
    • 它为实现类提供了一些通用功能,比如:
      • 默认实现 addAll() 方法。
      • 默认实现 iterator() 方法。
    • 但是,AbstractList 是抽象的,不能直接使用,所以开发者不会直接实例化或引用它。
  • 面向接口编程的原则:我们习惯通过接口(如 List)去引用对象,这是面向接口编程的核心思想。



default 方法

    • 接口 中,default 方法允许有具体的实现,提供一个方法体。
    • 抽象类 中不需要使用 default 关键字,因为抽象类本身可以包含普通的具体方法(带方法体)和抽象方法(没有方法体)。
  1. 为什么 default 方法存在于接口

    • 原本接口中的方法必须全部是抽象的,这意味着接口升级时(比如增加新方法),所有实现这个接口的类都必须修改,去实现新增的方法。
    • 为了兼容老代码,同时给接口增加新功能,Java 8 引入了 default 方法。default 方法是为了 在接口中提供默认实现,而不破坏已有的实现类
  2. 抽象类和接口在具体方法上的区别

    • 抽象类的普通方法天然支持具体实现,不需要额外关键字。
    • 接口中的 default 方法则是接口为了支持具体实现而引入的额外能力。

具体对比:抽象类和接口中的具体方法

特点抽象类中的具体方法接口中的 default 方法
是否需要关键字不需要,直接定义普通方法即可需要使用 default 关键字
是否可以有具体实现是的,普通方法都可以有实现是的,default 方法允许提供具体实现
是否可以被覆写可以,子类可以选择覆写抽象类中的普通方法可以,子类可以选择覆写接口中的 default 方法
是否强制实现不是,子类可以选择继承普通方法的实现或覆写它不是,默认继承接口中的 default 方法

default 方法的实际意义

1. 向接口新增方法时的兼容性问题

假设你有一个接口 MyInterface 和两个实现类:

interface MyInterface {void methodA();
}class ClassA implements MyInterface {@Overridepublic void methodA() {System.out.println("ClassA: methodA");}
}class ClassB implements MyInterface {@Overridepublic void methodA() {System.out.println("ClassB: methodA");}
}

如果你需要给 MyInterface 添加一个新方法 methodB,所有的实现类(ClassAClassB)都必须实现这个方法,否则代码无法编译。

2. 使用 default 方法解决兼容问题

在这种情况下,可以用 default 方法为新方法提供一个默认实现,从而避免修改所有实现类:

interface MyInterface {void methodA();// 新增一个 default 方法default void methodB() {System.out.println("Default implementation of methodB");}
}class ClassA implements MyInterface {@Overridepublic void methodA() {System.out.println("ClassA: methodA");}
}class ClassB implements MyInterface {@Overridepublic void methodA() {System.out.println("ClassB: methodA");}
}
运行示例
public class Main {public static void main(String[] args) {MyInterface objA = new ClassA();objA.methodA(); // 输出: ClassA: methodAobjA.methodB(); // 输出: Default implementation of methodBMyInterface objB = new ClassB();objB.methodA(); // 输出: ClassB: methodAobjB.methodB(); // 输出: Default implementation of methodB}
}

如果某个实现类需要对 default 方法提供自定义实现,可以覆写它:

class ClassB implements MyInterface {@Overridepublic void methodA() {System.out.println("ClassB: methodA");}@Overridepublic void methodB() {System.out.println("ClassB: Custom implementation of methodB");}
}

运行后:

MyInterface objB = new ClassB();
objB.methodB(); // 输出: ClassB: Custom implementation of methodB

结合数据库任务的实际场景

在你的数据库任务中,default 方法可以为某些操作提供通用实现。例如:

接口定义
public interface DBOperations {boolean createTable(String tableName, List<String> columns);default boolean dropTable(String tableName) {System.out.println("[OK] Dropped table: " + tableName);return true;}
}
实现类

具体类可以选择覆写或继承接口中的 default 方法或者覆写



http://www.ppmy.cn/embedded/159248.html

相关文章

第21节课:前端构建工具—自动化与模块化的利器

目录 前端构建工具的重要性任务运行器&#xff1a;Gulp与GruntGulpGulp的工作原理安装与使用Gulp GruntGrunt的工作原理安装与使用Grunt 模块打包器&#xff1a;WebpackWebpack简介Webpack的工作原理安装与使用Webpack 实践&#xff1a;使用Gulp和Webpack构建前端项目示例&…

TensorFlow 示例摄氏度到华氏度的转换(二)

这是一个完整的神经网络实现&#xff0c;用于将摄氏度转换为华氏度。下面&#xff0c;我会逐步描述各个步骤&#xff0c;并提供完整代码。 1. 数据准备与预处理 在这部分&#xff0c;我们准备了摄氏度&#xff08;features&#xff09;与对应的华氏度&#xff08;labels&…

32. C 语言 安全函数( _s 尾缀)

本章目录 前言什么是安全函数&#xff1f;安全函数的特点主要的安全函数1. 字符串操作安全函数2. 格式化输出安全函数3. 内存操作安全函数4. 其他常用安全函数 安全函数实例示例 1&#xff1a;strcpy_s 和 strcat_s示例 2&#xff1a;memcpy_s示例 3&#xff1a;strtok_s 总结 …

【论文阅读笔记】“万字”关于深度学习的图像和视频阴影检测、去除和生成的综述笔记 | 2024.9.3

论文“Unveiling Deep Shadows: A Survey on Image and Video Shadow Detection, Removal, and Generation in the Era of Deep Learning”内容包含第1节简介、第2-5节分别对阴影检测、实例阴影检测、阴影去除和阴影生成进行了全面的综述。第6节深入讨论了阴影分析&#xff0…

每日 Java 面试题分享【第 16 天】

欢迎来到每日 Java 面试题分享栏目&#xff01; 订阅专栏&#xff0c;不错过每一天的练习 今日分享 3 道面试题目&#xff01; 评论区复述一遍印象更深刻噢~ 目录 问题一&#xff1a;Java 运行时异常和编译时异常之间的区别是什么&#xff1f;问题二&#xff1a;什么是 Jav…

2181、合并零之间的节点

2181、[中等] 合并零之间的节点 1、问题描述&#xff1a; 给你一个链表的头节点 head &#xff0c;该链表包含由 0 分隔开的一连串整数。链表的 开端 和 末尾 的节点都满足 Node.val 0 。 对于每两个相邻的 0 &#xff0c;请你将它们之间的所有节点合并成一个节点&#xff…

minimind - 从零开始训练小型语言模型

大语言模型&#xff08;LLM&#xff09;领域&#xff0c;如 GPT、LLaMA、GLM 等&#xff0c;虽然它们效果惊艳&#xff0c; 但动辄10 Bilion庞大的模型参数个人设备显存远不够训练&#xff0c;甚至推理困难。 几乎所有人都不会只满足于用Lora等方案fine-tuing大模型学会一些新的…

【开源免费】基于SpringBoot+Vue.JS体育馆管理系统(JAVA毕业设计)

本文项目编号 T 165 &#xff0c;文末自助获取源码 \color{red}{T165&#xff0c;文末自助获取源码} T165&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…