实验一——Python Vibe Coding 考核:Excel 报表生成与 FastAPI 记账接口

实验一——Python Vibe Coding 考核:Excel 报表生成与 FastAPI 记账接口

两道考题 + 一个网页演示版:从需求澄清、TDD 小步开发到独立核验的完整记录

一、项目背景

本次考核采用 Vibe Coding 方式完成:人与 AI 协作,通过「需求澄清 → 设计文档 → 实施计划 → TDD 小步实现 → 独立验证 → Git 全程留痕」的流程交付两个独立项目。两道考题均从零搭建,并额外产出一个 Streamlit 网页演示版,把两个项目合并到同一界面,方便展示。

考核按”结果 + 过程 + 验证 + 风险 + 复盘”五维评分,交付物包括:① 需求澄清清单;② 可运行源代码工程(目录结构清晰、含 README 运行说明);③ 测试用例(正常用例 + 至少 2 个边界/异常用例)及运行结果截图;④ AI 协作过程记录(关键对话摘要 + 代码修改 diff);⑤ 一段文字复盘。本文即围绕这五项展开。

二、环境与技术栈

  • 操作系统:Windows;语言:Python 3.13

  • 考题一:Pandas、openpyxl(Excel 读写)、pytest(测试)

  • 考题二:FastAPI、Uvicorn、SQLite、Pydantic、pytest + httpx(接口测试)

  • 网页演示版:Streamlit 1.65(双页面 + 文件上传)、Playwright(浏览器实测截图)

三、考题一:Excel 报表生成脚本

(一)需求与样例数据

读取含脏数据的销售明细 sales_raw.xlsx(六列:日期、销售员、产品、数量、单价、地区),完成清洗与统计,输出 report.xlsx。

样例数据由脚本生成(固定随机种子可复现),共 91 行,注入 5 类脏数据:

脏数据类型 数量 示例
关键字段缺失 4 行 缺日期 / 缺销售员等
重复行 3 行 整行完全相同
数量 ≤ 0 2 行 数量 0、-5
非法日期 1 行 2026-02-30(历法不存在)
单价 ≤ 0 1 行 单价 -50

(二)清洗规则(5 类,逐类剔除)

  1. 删除关键字段缺失的行(dropna)

  2. 剔除数量 ≤ 0 的异常行

  3. 剔除日期非法(格式错误或历法不存在)的行(pd.to_datetime 校验)

  4. 剔除单价 ≤ 0 的异常行

  5. 按全字段去重

清洗结果:91 → 80 行。开发过程中曾自查发现最初清洗只覆盖 3 类(缺失 / 数量≤0 / 重复),非法日期与负单价两行会流入统计并污染月度总额(负单价行销售额为 -250),已补全规则并同步更新口径。

(三)统计口径与输出

  • 销售额 = 数量 × 单价;按销售员、按地区分别汇总

  • 月度总额 = 841853.21 元(全部有效数据合计,四舍五入保留 2 位小数)

  • 输出 4 个工作表:明细清洗后 / 按销售员 / 按地区 / 总览

  • 约束:不得修改原始文件;异常输入(文件缺失、缺列、空文件、清洗后无有效数据)给出明确可读的报错提示

(四)测试与独立核验

  • pytest 9/9 全绿(清洗规则、异常输入、四表输出、新口径行数与总额)

  • verify_report.py 独立重算:按销售员合计、按地区合计、独立重算总额三处与总览一致 → PASS

图1 考题一 一键运行结果

图2 考题一 独立核验结果

四、考题二:FastAPI 记账接口

(一)功能

提供记账接口(收入 / 支出流水):

  • POST /records:录入流水,合法返回 201;参数非法(金额≤0、非法类型、非法日期如 2026-02-30)返回 422 与校验详情

  • GET /records:流水列表,支持按类型、分类、日期区间筛选并分页

  • GET /summary:收入合计、支出合计、结余

(二)实现要点

  • SQLite 存储层:_write_lock 模块级锁串行化写事务,保证多线程并发写入不丢数据

  • Pydantic 校验:金额 gt=0、类型枚举(收入 / 支出)、field_validator 校验日期合法性,非法参数统一转 422(而非 500)

(三)测试与实测

  • pytest 19/19 全绿,含 20 线程 × 5 = 100 条并发写入无丢失 用例

  • Uvicorn 启动实测:合法录入 3×201、非法参数 3×422、筛选分页、summary 结余均符合预期

图3 考题二 API 实测

五、网页演示版(Streamlit 双页面)

把两个考题合并为一个 Streamlit 应用(端口 8505),复用两个考题的同一份代码与口径,不复制实现:

页面 演示内容
📊 报表演示 数据来源可切换(内置样例 / 上传文件):91 行 → 5 类脏数据卡片 → 清洗 91→80 行 → 按销售员 / 按地区统计 → 月度总额 841853.21 → 下载 report.xlsx(附样例下载按钮)
💰 记账演示 表单录单 → 201/422 校验语义 → 流水列表(类型 / 分类 / 日期区间筛选 + 分页)→ 收支汇总 → 并发写入 100 条无丢失

上传文件模式支持任意符合六列结构的 Excel(含我额外生成的 127 行多脏数据样例 原数据.xlsx:缺失 12 / 重复 6 / 数量≤0 5 / 非法日期 4 / 单价≤0 4,清洗后 100 行)。

图4 网页演示 报表演示页(5 类脏数据卡片)

图5 网页演示 记账 201 入账

图6 网页演示 记账 422 校验

图7 网页演示 并发写入 100 条无丢失

图8 网页演示 上传文件模式

六、工作流:Vibe Coding + Superpowers

两个项目全程按 Superpowers 流程执行:

  1. 头脑风暴(HARD-GATE):先书面写清需求澄清清单——目标(两个可独立运行的 Python 工程)、输入输出(考题一读 sales_raw.xlsx 出 report.xlsx;考题二为 HTTP 记账服务)、边界(不修改原始文件、异常可读报错、非法参数返回 4xx)、验收标准(一键运行 + 数值人工核对一致 + pytest 覆盖核心用例);关键决策拍板:样例数据由 AI 生成(固定种子可复现)、单工程双子目录组织、过程记录由 AI 草拟用户审改

  2. 设计文档与实施计划:两份 spec + 两份 plan(考核 9 任务 + 网页版 5 任务),均获用户批准后动工

  3. TDD 小步实现:每个功能先写失败测试(RED)→ 最小实现(GREEN)→ 提交;过程中 RED 暴露并修复了 3 类真实缺陷(测试数据缺陷、422 语义、并发事务冲突)

  4. 独立核验:verify_report.py 三处比对、接口实测、浏览器实测截图

  5. Git 全程留痕:15+ 次提交,每步可追溯

七、运行与复现

1
2
3
4
5
6
7
8
9
10
# 考题一:生成样例 → 一键运行 → 独立核验
cd 考题一_excel_report
python generate_sample.py && python run.py && python verify_report.py

# 考题二:启动接口服务(:8001)
cd 考题二_fastapi_ledger
python run.py

# 网页演示版:一条命令启动(:8505)
streamlit run web_demo/app.py --server.port 8505

八、测试统计汇总

模块 测试数 说明
考题一 9 清洗规则 / 异常输入 / 四表输出 / 新口径
考题二 19 校验边界 / 筛选分页 / 并发 100 条无丢失
网页演示 19 报表 / 记账逻辑(含分类筛选)/ 上传模式
合计 47 全绿 独立核验 PASS(841853.21)

各模块均含正常用例与 ≥2 个边界/异常用例:非法日期 422、全为脏数据报错、缺列/损坏文件上传、并发 100 条无丢失等。

九、复盘:哪些环节交给 AI、哪些自己拍板、哪些人工审查

环节 主导方 说明
需求澄清与验收口径 用户拍板 样例数据 AI 生成(固定种子)、单工程双子目录、过程记录 AI 草拟用户审改、上传文件固定六列结构
技术选型与实现 AI 主导 清洗/统计规则、SQLite + 模块锁、Pydantic 校验、Streamlit 双页面方案
测试与独立核验 双方协作 AI 按 TDD 写用例并跑绿;关键数值由 verify_report.py 独立重算,人工复核一致
风险人工审查 人工接管 拦截 3 类真实缺陷:测试数据日期笔误(去重测不到点)、GET 非法日期 500 应转 422、多线程并发写共享连接事务冲突

十、结语

这次考核最大的收获是完整走了一遍「人与 AI 协作开发」的正规流程:先想清楚再动手、每个功能有测试兜底、每个结论有独立核验、每一步都留在 Git 里。两道考题本身并不复杂,但流程带来的确定性让「看起来能跑」变成了「可验证地正确」—— 这也是 Vibe Coding 和随手写脚本的区别。

实验2-数据看板的设计与实现(电商销售数据看板·进阶版)

实验2 数据看板的设计与实现

电商销售数据看板(进阶版)实验报告

一、实验目的

本实验以电商销售数据为业务场景,完成数据看板从需求分析、版面设计、数据处理到看板落地的完整流程,具体目的如下:

  • 掌握数据看板的基本概念、核心功能与应用场景,理解数据可视化、数据大屏、实时看板的区别与联系;
  • 熟悉数据看板的设计原则与布局规范,包括视觉层级、色彩搭配、图表选型、信息降噪与适配布局等核心设计逻辑;
  • 学会根据业务数据特征选择适配的可视化图表(柱状图、折线图、饼图、仪表盘、数据卡片等),实现数据的直观展示与维度分析;
  • 培养数据思维与可视化设计能力,能够独立完成从需求分析、版面设计、数据处理到看板落地的完整流程;
  • 理解静态看板与动态实时看板的差异,掌握数据刷新、动态轮播、联动交互等基础功能的实现原理。

二、实验环境

硬件环境:计算机、网络环境。软件环境:Windows 操作系统、现代浏览器(Edge)、Visual Studio Code 开发工具。

技术栈:Python 3.13 作为运行环境,Streamlit 1.65.0 搭建看板页面,Pandas 3.0.6 完成数据处理,Plotly 7.1.0 生成可视化图表,openpyxl 支持 Excel 文件读写。

三、实验内容与过程

(一)需求分析与版面设计

自选电商销售数据场景,数据窗口为 2026-10-10 21:00 至 2026-10-11 20:59(近24小时,小时粒度),核心展示需求为销售额、订单量、客单价、转化率、履约与退款等运营指标,以及品类、渠道、商品、时段等多维分析。

看板版面划分为四大模块:数据概览区(KPI 数据卡片)、维度统计区(分类对比与占比图表)、趋势分析区(时间序列图表)、明细数据区(订单与商品明细表)。

整体采用深色科技风配色:背景色 #0d1117、卡片底色 #161b22、主色 #00d4ff(数据强调)、辅助色 #ff6b35 与 #a855f7(预警与对比)、文本色 #e6edf3 与 #8b949e,保证视觉层级清晰、信息降噪。

(二)数据准备与预处理

使用脚本生成模拟数据集并保存为 CSV 与 Excel 格式,共生成订单明细 4,205 行,覆盖 4 个销售渠道(APP、小程序、直播、线下)、5 个商品类目、7 个销售地区、4 种订单状态。

为验证数据清洗能力,向原始数据中注入 101 条脏数据,包括:缺失值 50 条、重复记录 12 条、负金额 8 条、单价超 2000 元 6 条、零销量 6 条、越界时间 5 条、空白地区 8 条、乱码状态 6 条(如”巳完戌”)。

预处理脚本按规则清洗:剔除缺失行、按内容去重(排除订单号列)、剔除负金额与超高价与零销量记录、剔除越界时间、去除空白地区、乱码状态映射为合法状态。清洗后得到干净订单 4,119 行,sales_metrics 指标表由清洗后订单重新聚合,与订单明细口径差异为 0 元、0 单,保证指标自洽。

(三)基础可视化图表搭建

基于预处理数据完成 6 种以上图表组件:

  • 数据卡片 6 张:总销售额、总订单数、客单价、转化率、待发货订单、退款率,并标注较上一时段的环比变化;
  • 柱状图:品类销售额对比(数码电器、美妆个护、服饰鞋包、食品生鲜、家居日用);
  • 环形图:渠道销售额占比(APP 39.8%、小程序 25.1%、直播 20.2%、线下 14.9%);
  • 折线/面积图:近24小时销售额、订单量、转化率时间趋势;
  • 横向条形图:商品销售额 Top10 排行榜。

图表构建函数统一接收过滤后的 DataFrame 作为入参,返回 Plotly 图形对象,供基础版与进阶版看板复用,保证同一数据源的图表口径一致,并通过测试脚本验证图表聚合结果与订单明细一致。

(四)静态数据看板整合实现

基础版看板(app.py)将标题栏、KPI 数据卡片、维度统计三栏(柱状图、环形图、Top10)、趋势分析三图(销售额、订单量、转化率)与订单明细表整合到同一页面,通过深色 CSS 统一字体、颜色、间距与卡片样式,实现整洁清晰的静态看板效果。使用 Streamlit AppTest 无头测试验证页面无异常、标题与四大模块齐全。

(五)进阶功能实现

进阶1 动态视觉效果优化:为 KPI 数值添加滚动动画、组件渐入动画、经营播报轮播(约 3.5 秒切换)、图表 hover 悬浮提示,并通过媒体查询实现自适应布局,避免页面错位与溢出。

进阶2 图表联动与筛选功能实现:侧边栏提供 3 个多选筛选器(销售渠道、商品类目、订单状态)与时间范围滑块(近 N 小时),筛选后所有图表与指标同步更新;页面提供品类联动按钮,点击后可聚焦展示对应品类的统计视图,交互状态由 session_state 管理。

进阶3 多维度数据分析可视化:新增渠道×时段销售额热力图、品类×渠道销售结构雷达图、3 个进度仪表盘(销售额、转化率、客单价达成率),以及热销商品 Top8(绿色标注)与滞销商品 Top8(红色标注)双榜单,实现数据对比、预警与极值展示。

四、实验结果与验证

(一)KPI 指标核验

对清洗后订单数据独立重算核心指标,与预处理阶段生成的 kpi_summary.txt 基线逐项对照,六项指标全部一致,结果如表1所示。

指标 独立重算 基线(kpi_summary.txt) 结论
总销售额 ¥799,540.61 799540.61 一致
总订单数 4,119 4119 一致
客单价 ¥194.11 194.11 一致
转化率 4.43% 4.43 一致
待发货订单 1,017 1017 一致
退款率 5.07% 5.07 一致

表1 KPI 指标核验结果(独立重算 vs 基线)

(二)看板运行截图

两版看板在本地服务启动后,通过浏览器真实渲染并截图验证。基础版看板完整呈现四大模块,图表与数据均正常显示,如图1、图2所示。

图1 基础版看板首屏(数据概览、维度统计、趋势分析、订单明细)

图1 基础版看板首屏

图2 基础版看板中部(趋势分析三图与订单明细)

图2 基础版看板中部

进阶版看板集成动态效果、筛选联动与多维分析组件,首屏与中部如图3、图4所示。

图3 进阶版看板首屏(多维筛选、经营播报、品类联动、KPI 与维度统计)

图3 进阶版看板首屏

图4 进阶版看板中部(趋势分析、渠道×时段热力图、品类×渠道雷达图)

图4 进阶版看板中部

两版看板页尾均以明细数据收尾:基础版底部为订单明细表与商品状态表,进阶版底部在左侧多维筛选面板之外展示商品档案表(含热销/正常/滞销状态标注),如图5、图6所示。

图5 基础版看板底部(订单明细与商品状态表)

图5 基础版看板底部

图6 进阶版看板底部(多维筛选与商品档案状态表)

图6 进阶版看板底部

(三)自动化测试结果

  • test_charts.py:验证全部图表组件与订单聚合结果一致,通过;
  • test_app.py:基础看板 AppTest 无头测试 0 异常,标题、KPI 卡片、四大模块均存在,通过;
  • test_app_advanced.py:进阶看板 AppTest 无头测试 0 异常,经营播报、筛选器、联动按钮、多维模块均存在,且代码不依赖外部网络资源,通过;
  • 浏览器渲染验证:以 headless Edge 打开看板页面,图表均正常渲染(非空白),截图 OCR 复核数据与 KPI 基线一致。

五、遇到的问题及解决方法

  • 图表渲染空白问题:初版使用 plotly.io.to_html 且 include_plotlyjs 指向外网 CDN,运行环境无法访问外网导致图表空白。解决方法:改用 Streamlit 内置的 st.plotly_chart 离线渲染,彻底去除外网依赖,截图验证通过;
  • Plotly 7.x 语法差异:仪表盘(gauge)的 axis 不再支持 gridcolor 属性,构造时抛出 ValueError。解决方法:改用 tickcolor 属性设置刻度颜色,兼容新版 API;
  • 截图自动化中页面无法滚动:新版 Streamlit 的滚动容器为 .stMain 而非 body,直接对窗口滚动无效。解决方法:定位 .stMain 容器并设置其 scrollTop 完成分屏截图;
  • 客单价超出设计区间:初版价格分布导致客单价约 282 元,超出 100–200 元目标。解决方法:调整价格分层(88% 常规价 29–199 元 + 12% 大额价 200–400 元),并设置价格-销量系数,最终客单价 194.11 元落入目标区间。

六、实验总结

本实验以电商销售数据为场景,完成了数据看板从需求分析、版面设计、数据生成与清洗、图表搭建、静态看板整合到进阶动态交互的完整流程。基础任务部分完成了四大模块版面设计与静态看板整合;进阶任务部分实现了动态视觉效果、图表联动筛选与多维度分析组件。

通过本实验,掌握了数据看板的设计原则与图表选型方法,熟悉了 Streamlit 与 Plotly 的工程化开发流程,理解了离线渲染、交互状态管理与自适应布局等关键技术点,并积累了数据清洗与口径自洽的实践经验。后续可在此基础上拓展实时数据源接入、指标预警、大屏与移动端适配等扩展任务,进一步提升看板的实用性与工程价值。

Java作业-面向对象设计原则——单一职责原则与开闭原则

第7章 面向对象设计原则——单一职责原则与开闭原则

耿祥义、张跃平《Java面向对象程序设计(第4版)》第7章

一、什么是面向对象设计原则?

在面向对象程序设计中,除了掌握继承、多态、接口等语法特性外,更重要的是理解面向对象设计的原则。这些原则是前人总结出的经验法则,指导我们如何设计出可维护、可扩展、可复用的软件系统。

本章介绍两个最基本、最重要的原则:单一职责原则和开闭原则。

二、单一职责原则

2.1 定义

单一职责原则(Single Responsibility Principle,SRP) 是最简单的面向对象设计原则,也是最难正确运用的原则之一。

它的核心思想是:一个类只负责一个功能领域中的相应职责。

通俗地说:一个类,只有一个引起它变化的原因。

2.2 如何理解”职责”?

一个类的”职责”可以理解为”它为什么存在”或”它做什么事情”。

  • 如果一个类有多个引起它变化的原因,就说明它承担了多个职责
  • 职责过多,就会导致类变得臃肿、复杂、难以维护

2.3 违反单一职责原则的后果

当一个类承担了多项职责时,会出现以下问题:

  • 可维护性差:修改某一职责的代码可能会影响其他职责的正常功能
  • 可复用性差:无法单独使用某个职责的功能,因为所有功能都耦合在一个类中
  • 可测试性差:测试一个职责时,必须依赖其他职责的代码
  • 代码臃肿:一个类包含了大量无关的方法,代码量过大

2.4 如何识别违反单一职责原则的类?

可以通过以下几个问题来判断:

  1. 这个类的方法是否涉及多个不同的功能领域?
  2. 修改这个类的某个方法,是否会影响其他不相关的方法?
  3. 这个类的名称是否过于宽泛(如”Manager”、”Util”、”Handler”)?
  4. 这个类是否有多个不同的”使用者”,每个使用者只用到其中一部分功能?

如果上述问题多数为”是”,说明该类违反了单一职责原则。

2.5 课堂作业

图1.1
图1.2

三、开闭原则

3.1 定义

开闭原则(Open-Closed Principle,OCP) 是面向对象设计中最重要的原则,由Bertrand Meyer于1988年提出。

它的核心思想是:对扩展开放,对修改关闭。

  • 对扩展开放:当系统需要增加新功能时,可以通过添加新代码来扩展系统的行为
  • 对修改关闭:在添加新功能时,不应修改已有的源代码

通俗地说:写好的代码,不要再动了;要加新功能,就写新类。

3.2 为什么开闭原则如此重要?

在软件工程中,修改已有代码是有风险的:

风险 说明
引入新Bug 修改一处代码可能破坏原本正常的功能
影响其他模块 被修改的类可能被多个其他类依赖,修改后影响范围难以预估
增加测试成本 每次修改都需要重新测试所有相关功能
难以维护 频繁修改的代码会变得混乱,难以理解

开闭原则通过”不修改已有代码,只添加新代码”的方式,有效规避了上述风险。

3.3 如何实现开闭原则?

实现开闭原则的关键是抽象化:

  1. 定义抽象层:使用抽象类或接口定义系统的核心规范
  2. 实现具体类:让不同的具体类实现这个抽象层
  3. 面向抽象编程:客户端代码针对抽象层编程,不依赖具体实现类
  4. 扩展新功能:当需要新增功能时,只需添加一个新的具体类,无需修改已有代码

这一过程依赖Java的多态机制——父类引用可以指向任意子类对象,从而实现”新增子类即可扩展功能”。

3.4 违反开闭原则的后果

当系统不遵循开闭原则时,会出现以下问题:

  • 分支泛滥:大量使用if-else或switch来判断类型,每增加一种类型都要修改判断逻辑
  • 代码重复:相似的功能无法复用,只能复制粘贴后再修改
  • 扩展困难:每次新增功能都要修改多处已有代码,且改动的类和数量不断增加
  • 回归风险:每次改动都可能引入新的Bug,需要重新测试整个系统

3.5 课堂作业

图2.1
图2.2

四、两个原则的对比总结

对比项 单一职责原则 开闭原则
核心思想 一个类只做一件事 对扩展开放,对修改关闭
关注点 类的内部职责分配 系统的可扩展性
解决问题 类的臃肿和高耦合 新增功能时被迫修改已有代码
实现方式 将多个职责拆分到不同类中 引入抽象层,利用多态扩展
目标 提高可维护性、可复用性 提高可扩展性、降低修改风险

单一职责原则解决的是类的设计质量问题,开闭原则解决的是系统的可扩展性问题。两者相辅相成——遵循单一职责原则设计的类,通常也更容易符合开闭原则。

参考资料:耿祥义、张跃平《Java面向对象程序设计(第4版)》第7章

Java作业:两种创建线程方式的对比

一、作业概述
在Java中,创建线程主要有两种方式:

方式 核心类/接口 实现方法
方式一 继承Thread类 重写run()方法
方式二 实现Runnable接口 实现run()方法,传入Thread对象
这两种方式都可以创建并启动线程,但在设计理念、使用场景和灵活性 上存在显著差异。

核心问题:我们应该在什么时候选择继承Thread类?什么时候选择实现Runnable接口?

二、基础知识回顾
2.1 什么是线程?
线程(Thread)是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2.2 Java中的线程模型
text

Java线程模型
├── Thread类(java.lang.Thread)
│ └── 代表一个线程对象
└── Runnable接口(java.lang.Runnable)
└── 定义可执行的任务代码
2.3 线程的生命周期
text

新建(New) → 就绪(Runnable) → 运行(Running) → 阻塞(Blocked) → 终止(Terminated)
↑ ↓
└── 时间片用完 ────┘
三、方式一:继承Thread类
3.1 实现步骤
定义一个类继承Thread类

重写run()方法,将线程要执行的任务代码放入其中

创建该类的实例(即创建线程对象)

调用线程对象的start()方法启动线程

3.2 代码实现
/**

  • 方式一:继承Thread类创建线程
    */
    public class ThreadDemo {
    public static void main(String[] args) {
    // 3. 创建线程对象
    MyThread t1 = new MyThread(“线程A”);
    MyThread t2 = new MyThread(“线程B”);

    // 4. 启动线程(会自动调用run方法)
    t1.start();
    t2.start();

    // 主线程也执行任务
    for (int i = 0; i < 5; i++) {
    System.out.println(“主线程正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

/**

    1. 继承Thread类
    1. 重写run()方法
      */
      class MyThread extends Thread {
      private String name;

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

    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    System.out.println(name + “正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

AI写代码
java
运行

3.3 运行结果(示例)

text

线程A正在执行:0
主线程正在执行:0
线程B正在执行:0
线程A正在执行:1
主线程正在执行:1
线程B正在执行:1
线程A正在执行:2
主线程正在执行:2
…
3.4 继承Thread类的特点
特点 说明
简单直接 代码结构简单,易于理解
线程独立 每个线程对象都是独立的Thread实例
单继承限制 Java不支持多继承,继承Thread后无法再继承其他类
四、方式二:实现Runnable接口
4.1 实现步骤
定义一个类实现Runnable接口

实现run()方法,将线程要执行的任务代码放入其中

创建该类的实例(即任务对象)

创建Thread对象,将任务对象作为参数传入

调用Thread对象的start()方法启动线程

4.2 代码实现
/**

  • 方式二:实现Runnable接口创建线程
    */
    public class RunnableDemo {
    public static void main(String[] args) {
    // 3. 创建任务对象
    MyRunnable task1 = new MyRunnable(“任务A”);
    MyRunnable task2 = new MyRunnable(“任务B”);

    // 4. 创建Thread对象,将任务对象传入
    Thread t1 = new Thread(task1);
    Thread t2 = new Thread(task2);

    // 5. 启动线程
    t1.start();
    t2.start();

    // 主线程执行
    for (int i = 0; i < 5; i++) {
    System.out.println(“主线程正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

/**

    1. 实现Runnable接口
    1. 实现run()方法
      */
      class MyRunnable implements Runnable {
      private String name;

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

    @Override
    public void run() {
    for (int i = 0; i < 5; i++) {
    System.out.println(name + “正在执行:” + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

AI写代码
java
运行

4.3 Lambda表达式简化(Java 8+)
/**

  • 使用Lambda表达式简化Runnable创建
    */
    public class RunnableLambdaDemo {
    public static void main(String[] args) {
    // 使用Lambda表达式创建Runnable任务
    Runnable task = () -> {
    for (int i = 0; i < 5; i++) {
    System.out.println(Thread.currentThread().getName() + “: “ + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    };

    // 创建并启动线程
    new Thread(task, “线程A”).start();
    new Thread(task, “线程B”).start();
    }

}

AI写代码
java
运行

4.4 实现Runnable接口的特点

特点 说明
避免单继承限制 类可以实现多个接口,更灵活
任务与线程分离 Runnable定义任务,Thread定义线程,职责清晰
便于资源共享 多个线程可以共享同一个Runnable任务对象
符合面向接口编程 推荐的设计模式
五、两种方式的核心区别
5.1 代码层面对比
对比项 继承Thread类 实现Runnable接口
实现方式 class MyThread extends Thread class MyTask implements Runnable
启动方式 new MyThread().start() new Thread(new MyTask()).start()
资源共享 每个线程独立的对象 多个线程可共享同一个任务对象
单继承限制 受限制(无法继承其他类) 无限制(可实现多个接口)
获取当前线程 this(直接使用) Thread.currentThread()
线程名字设置 setName()或构造方法 new Thread(task, name)
5.2 设计理念对比
text

继承Thread类:
┌─────────────────┐
│ MyThread │
│ (is-a Thread) │ ← 线程就是任务本身
└─────────────────┘

实现Runnable接口:
┌─────────────┐ ┌─────────────┐
│ MyTask │ │ Thread │ ← 线程和任务分离
│ (is-a 任务) │ ─────→│ (is-a 线程) │ (组合关系)
└─────────────┘ └─────────────┘
5.3 资源共享示例对比
继承Thread类(无法共享资源):

class CounterThread extends Thread {
private int count = 0;

@Override
public void run() {
    for (int i = 0; i < 5; i++) {
        count++;
        System.out.println(getName() + " count=" + count);
    }
}

}

// 测试
public class ThreadShareTest {
public static void main(String[] args) {
// 创建两个独立的对象,每个有自己的count
CounterThread t1 = new CounterThread();
CounterThread t2 = new CounterThread();

    t1.start();  // 输出:Thread-0 count=1,2,3,4,5
    t2.start();  // 输出:Thread-1 count=1,2,3,4,5
    // 两个线程各自计数,互不影响
}

}

AI写代码
java
运行

实现Runnable接口(可以共享资源):

class CounterRunnable implements Runnable {
private int count = 0; // 共享的资源

@Override
public void run() {
    for (int i = 0; i < 5; i++) {
        count++;
        System.out.println(Thread.currentThread().getName() + " count=" + count);
    }
}

}

// 测试
public class RunnableShareTest {
public static void main(String[] args) {
// 创建一个任务对象
CounterRunnable task = new CounterRunnable();

    // 多个线程共享同一个任务对象
    Thread t1 = new Thread(task, "线程A");
    Thread t2 = new Thread(task, "线程B");
    
    t1.start();
    t2.start();
    // 两个线程共享同一个count,会产生竞态条件
    // 输出:线程A count=1,2,3,4,5;线程B count=6,7,8,9,10
}

}

AI写代码
java
运行

六、详细区别对比表

对比维度 继承Thread类 实现Runnable接口
灵活性 低(单继承限制) 高(可实现多接口)
代码耦合度 高(任务与线程绑定) 低(任务与线程分离)
资源共享 困难(需用静态变量) 容易(自然共享)
适用场景 简单的独立任务 复杂任务、资源共享场景
面向对象原则 违反“组合优于继承” 符合“组合优于继承”
线程池支持 不友好 友好(Runnable可放入线程池)
多次执行同一任务 需创建多个对象 可多次传入不同线程
可读性 简单直观 稍复杂但更清晰
七、适用场景分析
7.1 继承Thread类适用场景
场景 说明
简单临时线程 只需执行一次,不需要复用
需要直接覆盖Thread的其他方法 如重写run()以外的interrupt()、isAlive()等
代码量很小 不需要复杂的任务拆分
// 示例:简单的一次性线程
new Thread() {
@Override
public void run() {
System.out.println(“执行一次的任务”);
}
}.start();

AI写代码
java
运行
7.2 实现Runnable接口适用场景
场景 说明
多个线程执行相同任务 资源共享,如卖票系统
类已继承其他类 无法再继承Thread
需要配合线程池使用 ExecutorService提交Runnable
任务需要复用 同一个Runnable可多次执行
追求高内聚低耦合设计 任务与线程分离
// 示例:多个线程共享任务(模拟卖票)
class TicketTask implements Runnable {
private int tickets = 100;

@Override
public void run() {
    while (tickets > 0) {
        System.out.println(Thread.currentThread().getName() + "卖出第" + tickets-- + "张票");
    }
}

}

// 启动多个卖票窗口
TicketTask task = new TicketTask();
new Thread(task, “窗口1”).start();
new Thread(task, “窗口2”).start();
new Thread(task, “窗口3”).start();

AI写代码
java
运行

八、面试常考对比

8.1 为什么推荐使用Runnable而不是Thread?
避免单继承限制:Java只支持单继承,继承Thread后就无法继承其他类了

任务与线程解耦:Runnable专注于定义任务,Thread专注于执行任务

便于资源共享:多个线程可以共享同一个Runnable实例

便于配合线程池:线程池接受Runnable和Callable,而不是Thread

符合面向接口编程原则:面向接口编程比面向具体类编程更灵活

8.2 两者可以混用吗?
可以。Runnable最终还是要交给Thread来执行:

线程创建的两种方式
Lambda表达式实现Runnable接口

Runnable task = () -> System.out.println(“Hello”);
new Thread(task).start();
AI写代码
java
运行
通过Lambda表达式创建Runnable实现类,简洁地定义任务逻辑。Thread构造函数接收Runnable参数,调用start()启动新线程执行。

继承Thread类重写run方法

Thread t = new Thread() {
@Override
public void run() {
System.out.println(“Thread中的run”);
}
};
new Thread(t).start();
AI写代码
java
运行
Thread类本身实现了Runnable接口,其实例可作为Runnable对象传递给其他Thread。这种嵌套方式虽然合法,但通常直接调用t.start()更合理。

关键区别
Runnable是函数式接口,适合与Lambda结合实现任务与线程控制的分离
Thread作为Runnable的实现类,重写run()时可直接访问线程方法,但会失去Java单继承的灵活性
嵌套Thread实例作为参数的方式在实际开发中较少使用,容易造成理解困难
注意事项
线程启动必须调用start()而非run(),后者会在当前线程同步执行。两种方式创建的线程在功能上没有本质区别,但实现接口的方式更符合面向对象设计原则。

九、完整对比示例:银行取款
/**

  • 综合示例:模拟银行取款

  • 演示两种方式的区别
    */
    // 方式一:继承Thread类
    class ATMThread extends Thread {
    private static int balance = 1000; // 静态变量实现共享
    private String customerName;

    public ATMThread(String customerName) {
    this.customerName = customerName;
    }

    @Override
    public void run() {
    for (int i = 0; i < 3; i++) {
    synchronized (ATMThread.class) {
    if (balance >= 100) {
    balance -= 100;
    System.out.println(customerName + “取款100元,余额:” + balance);
    } else {
    System.out.println(customerName + “取款失败,余额不足!”);
    }
    }
    try {
    Thread.sleep(50);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

}

// 方式二:实现Runnable接口
class ATMRunnable implements Runnable {
private int balance = 1000; // 实例变量,自然共享
private String customerName;

public ATMRunnable(String customerName) {
    this.customerName = customerName;
}

@Override
public void run() {
    for (int i = 0; i < 3; i++) {
        synchronized (this) {
            if (balance >= 100) {
                balance -= 100;
                System.out.println(customerName + "取款100元,余额:" + balance);
            } else {
                System.out.println(customerName + "取款失败,余额不足!");
            }
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}

public class BankDemo {
public static void main(String[] args) {
System.out.println(“========== 方式一:继承Thread类 ==========”);
ATMThread t1 = new ATMThread(“小明”);
ATMThread t2 = new ATMThread(“小红”);
t1.start();
t2.start();

    try {
        Thread.sleep(1000);  // 等待线程执行完
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println("\n========== 方式二:实现Runnable接口 ==========");
    ATMRunnable task = new ATMRunnable("张三");
    new Thread(task, "线程A").start();
    new Thread(task, "线程B").start();
}

}

AI写代码
java
运行

十、总结

10.1 核心知识点回顾
知识点 要点
继承Thread类 简单直接,但受单继承限制
实现Runnable接口 灵活、解耦、可资源共享
启动线程 必须调用start(),不能直接调用run()
资源共享 Runnable天然支持,Thread需静态变量
推荐方式 优先使用Runnable
10.2 选择建议
text

是否需要资源共享?
├── 是 → 使用Runnable接口
└── 否 → 继续判断
├── 类是否已继承其他类?
│ ├── 是 → 使用Runnable接口
│ └── 否 → 两种都可以,但Runnable更推荐
└── 是否需要重写Thread的其他方法?
├── 是 → 使用Thread子类
└── 否 → 使用Runnable接口(推荐)
————————————————
版权声明:本文为CSDN博主「江城墨雨xs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2401_87661118/article/details/161711152

Java作业:文件复制四种实现方式

一、作业概述
本次作业要求实现文件复制的四种不同方式:

序号 方式 核心类 适用场景
1 文本文件复制 FileReader / FileWriter 仅文本文件
2 字符缓冲流 BufferedReader / BufferedWriter 文本文件(最常用)
3 任意文件复制 FileInputStream / FileOutputStream 任意类型文件
4 字节缓冲流 BufferedInputStream / BufferedOutputStream 任意文件(万能复制)
核心原理:从一个文件中读取数据,写入到另一个文件中。

二、基础知识回顾
2.1 Java IO流分类
text

Java IO流
├── 字节流(处理所有文件)
│ ├── InputStream(输入流)—— FileInputStream
│ ├── OutputStream(输出流)—— FileOutputStream
│ ├── BufferedInputStream(缓冲输入流)
│ └── BufferedOutputStream(缓冲输出流)
└── 字符流(仅处理文本文件)
├── Reader(输入流)—— FileReader
├── Writer(输出流)—— FileWriter
├── BufferedReader(缓冲输入流)
└── BufferedWriter(缓冲输出流)
2.2 字符流 vs 字节流
对比项 字符流 字节流
处理单位 字符(16-bit) 字节(8-bit)
适用文件 文本文件(.txt, .java, .csv) 任意文件(图片、视频、音频等)
核心类 Reader, Writer InputStream, OutputStream
是否支持中文 是 需要手动处理编码
2.3 缓冲流的作用
特性 无缓冲流 有缓冲流
读取方式 每次读一个字节/字符 一次读一批数据到内存缓冲区
IO次数 频繁 减少
性能 较低 较高
特有方法 无 readLine()(字符缓冲流)
三、方式一:文本文件复制(字符流)
3.1 实现原理
使用 FileReader 读取文本文件,FileWriter 写入到目标文件。一次读取一个字符数组,提高效率。

3.2 代码实现
以下是整理后的代码块,符合Markdown 格式要求:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**

  • 方式一:文本文件复制

  • 使用字符流 FileReader/FileWriter

  • 适用于:.txt, .java, .csv 等文本文件
    */
    public class TextFileCopy {
    public static void main(String[] args) {
    String sourceFile = “source.txt”;
    String destFile = “dest_text.txt”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    System.out.println(“源文件:” + sourceFile);
    System.out.println(“目标文件:” + destFile);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    e.printStackTrace();
    }
    }

    public static void copyFile(String sourcePath, String destPath) throws IOException {
    try (FileReader fr = new FileReader(sourcePath);
    FileWriter fw = new FileWriter(destPath)) {

    char[] buffer = new char[1024];
    int len;

    while ((len = fr.read(buffer)) != -1) {
    fw.write(buffer, 0, len);
    }

    System.out.println(“已复制 “ + len + “ 个字符”);
    }
    }

}
AI写代码
java
运行

3.3 代码解释

代码 说明
FileReader fr = new FileReader(sourcePath) 创建文件字符输入流,用于读取源文件
FileWriter fw = new FileWriter(destPath) 创建文件字符输出流,用于写入目标文件
char[] buffer = new char[1024] 创建字符数组缓冲区,一次读取1024个字符
fr.read(buffer) 从源文件读取数据到缓冲区,返回实际读取的字符数
fw.write(buffer, 0, len) 将缓冲区中的数据写入目标文件
try-with-resources 自动关闭流资源,不需要手动调用close()
3.4 运行效果
假设 source.txt 内容为:

text

Hello Java!
你好,世界!
这是文件复制测试。
复制后的 dest_text.txt 内容完全相同。

四、方式二:字符缓冲流(最常用)
4.1 实现原理
使用 BufferedReader 和 BufferedWriter 包装基本的字符流,提供缓冲功能,提高读写效率。BufferedReader 提供了 readLine() 方法,可以一次读取一行文本,非常方便。

4.2 代码实现
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**

  • 方式二:字符缓冲流复制文本文件

  • 使用 BufferedReader/BufferedWriter

  • 特点:最常用、支持按行读取、性能高
    */
    public class BufferedTextFileCopy {

    public static void main(String[] args) {
    String sourceFile = “source.txt”;
    String destFile = “dest_buffered.txt”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    }
    }

    /**

    • 使用缓冲流复制文本文件
    • @param sourcePath 源文件路径
    • @param destPath 目标文件路径
    • @throws IOException IO异常
      */
      public static void copyFile(String sourcePath, String destPath) throws IOException {
      // 使用 try-with-resources 自动关闭资源
      try (BufferedReader br = new BufferedReader(new FileReader(sourcePath));
      BufferedWriter bw = new BufferedWriter(new FileWriter(destPath))) {

      String line;
      int lineCount = 0;

      // 按行读取,逐行写入
      while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine(); // 写入换行符
      lineCount++;
      }

      System.out.println(“共复制 “ + lineCount + “ 行”);
      }

    }

}
AI写代码
java
运行

4.3 为什么是最常用的?
优点 说明
按行读取 readLine() 方法非常方便处理文本
缓冲性能 减少实际IO次数,提高效率
编码友好 正确处理中文字符
代码简洁 逻辑清晰,易于理解和维护
4.4 代码解释
代码 说明
new BufferedReader(new FileReader(…)) 在 FileReader 外层包装缓冲流
br.readLine() 读取一行文本(不包含换行符),返回 null 表示读到文件末尾
bw.write(line) 写入一行文本
bw.newLine() 写入系统相关的换行符(Windows: \r\n, Linux: \n)
五、方式三:任意文件复制(字节流)
5.1 实现原理
使用 FileInputStream 和 FileOutputStream 处理字节数据。这种方式可以复制任意类型的文件,因为所有文件在计算机中本质上都是字节序列。

5.2 代码实现
以下是转换后的代码块格式:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**

  • 方式三:任意文件复制

  • 使用字节流 FileInputStream/FileOutputStream

  • 适用于:图片、视频、音频、压缩包等任意文件
    */
    public class AnyFileCopy {
    public static void main(String[] args) {
    // 示例:复制图片文件
    String sourceFile = “photo.jpg”;
    String destFile = “photo_copy.jpg”;

    // 示例:复制视频文件
    // String sourceFile = “video.mp4”;
    // String destFile = “video_copy.mp4”;

    try {
    copyFile(sourceFile, destFile);
    System.out.println(“文件复制成功!”);
    } catch (IOException e) {
    System.out.println(“文件复制失败:” + e.getMessage());
    }
    }

    /**

    • 使用字节流复制任意文件
    • @param sourcePath 源文件路径
    • @param destPath 目标文件路径
    • @throws IOException IO异常
      */
      public static void copyFile(String sourcePath, String destPath) throws IOException {
      try (FileInputStream fis = new FileInputStream(sourcePath);
      FileOutputStream fos = new FileOutputStream(destPath)) {

      // 字节数组缓冲区,一次读取1024个字节
      byte[] buffer = new byte[1024];
      int len;
      long totalBytes = 0;

      while ((len = fis.read(buffer)) != -1) {
      fos.write(buffer, 0, len);
      totalBytes += len;
      }

      System.out.println(“共复制 “ + totalBytes + “ 字节”);
      System.out.println(“约 “ + (totalBytes / 1024) + “ KB”);
      }

    }

}
AI写代码
java
运行

5.3 支持的任意文件类型

文件类型 扩展名 是否支持
文本文件 .txt, .java, .xml, .json 支持
图片 .jpg, .png, .gif, .bmp 支持
视频 .mp4, .avi, .mkv, .flv 支持
音频 .mp3, .wav, .flac 支持
压缩包 .zip, .rar, .7z 支持
可执行文件 .exe, .dll 支持
5.4 字节流 vs 字符流选择
if (fileType == 文本文件) {
// 使用字符流(方便处理文本)
useReaderWriter();
} else {
// 使用字节流(万能)
useInputStreamOutputStream();
}

AI写代码
java
运行
六、方式四:字节缓冲流(万能复制)

6.1 实现原理
使用 BufferedInputStream 和 BufferedOutputStream 包装基本的字节流,提供内部缓冲区,减少实际IO次数,是最通用、性能最好的文件复制方式。

6.2 代码实现
以下是转换后的代码块格式:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedAnyFileCopy {
public static void main(String[] args) {
copyFile(“photo.jpg”, “photo_buffered_copy.jpg”);
}

public static boolean copyFile(String sourcePath, String destPath) {
    if (sourcePath == null || destPath == null) {
        System.err.println("文件路径不能为空");
        return false;
    }

    long startTime = System.currentTimeMillis();

    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourcePath));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath))) {
        
        byte[] buffer = new byte[8192];
        int len;
        long totalBytes = 0;

        while ((len = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
            totalBytes += len;
        }

        bos.flush();

        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;

        System.out.println("========== 复制完成 ==========");
        System.out.println("源文件:" + sourcePath);
        System.out.println("目标文件:" + destPath);
        System.out.println("文件大小:" + formatSize(totalBytes));
        System.out.println("耗时:" + duration + " 毫秒");
        System.out.println("================================");

        return true;
    } catch (IOException e) {
        System.err.println("文件复制失败:" + e.getMessage());
        return false;
    }
}

private static String formatSize(long bytes) {
    if (bytes < 1024) {
        return bytes + " B";
    } else if (bytes < 1024 * 1024) {
        return String.format("%.2f KB", bytes / 1024.0);
    } else if (bytes < 1024 * 1024 * 1024) {
        return String.format("%.2f MB", bytes / (1024.0 * 1024));
    } else {
        return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024));
    }
}

}
AI写代码
java
运行

6.3 为什么是“万能复制”?

特点 说明
文件类型无关 无论是文本、图片、视频还是可执行文件,都能正确处理
编码无关 按字节处理,不涉及编码问题
性能优化 内置缓冲区,减少IO次数
大文件支持 边读边写,不会一次性加载整个文件到内存
6.4 性能对比测试
/**

  • 性能对比测试

  • 比较四种复制方式的效率
    */
    public class PerformanceTest {
    public static void main(String[] args) {
    String testFile = “large_file.txt”; // 假设有一个10MB的文件

    // 测试四种方式的复制时间
    testMethod(“方式一:字符流”, () -> {
    TextFileCopy.copyFile(testFile, “test1.txt”);
    });

    testMethod(“方式二:字符缓冲流”, () -> {
    BufferedTextFileCopy.copyFile(testFile, “test2.txt”);
    });

    testMethod(“方式三:字节流”, () -> {
    AnyFileCopy.copyFile(testFile, “test3.txt”);
    });

    testMethod(“方式四:字节缓冲流”, () -> {
    BufferedAnyFileCopy.copyFile(testFile, “test4.txt”);
    });
    }

    private static void testMethod(String name, Runnable method) {
    long start = System.nanoTime();
    method.run();
    long end = System.nanoTime();
    System.out.println(name + “ 耗时:” + (end - start) / 1000000 + “ ms”);
    }

}

AI写代码
java
运行

典型运行结果(复制10MB文件):

text

方式一:字符流 耗时:450 ms
方式二:字符缓冲流 耗时:120 ms ← 比无缓冲快3-4倍
方式三:字节流 耗时:480 ms
方式四:字节缓冲流 耗时:100 ms ← 最快,万能选择
七、四种方式对比总结
对比项 方式一 方式二 方式三 方式四
核心类 FileReader/Writer BufferedReader/Writer FileInput/OutputStream BufferedInput/OutputStream
处理单位 字符 字符(按行) 字节 字节
适用文件 仅文本 仅文本 任意文件 任意文件
缓冲支持 手动数组 内置缓冲 手动数组 内置缓冲
性能 中 高 中 最高
编码问题 自动处理 自动处理 需注意 需注意
推荐场景 小文本文件 文本文件(最常用) 任意小文件 万能复制(推荐)
选择建议:

复制文本文件 → 用方式二(字符缓冲流)

复制任意文件(图片、视频等) → 用方式四(字节缓冲流)

简单小文件测试 → 方式一或方式三

生产环境 → 方式四

八、完整代码整合版
以下是一个完整的工具类,整合了四种复制方式:

import java.io.*;

/**

  • 文件复制工具类

  • 提供四种不同的文件复制方式
    */
    public class FileCopyUtil {

    /**

    • 方式一:字符流复制文本文件
      */
      public static void copyByCharStream(String src, String dest) throws IOException {
      try (FileReader fr = new FileReader(src);
      FileWriter fw = new FileWriter(dest)) {
      char[] buffer = new char[1024];
      int len;
      while ((len = fr.read(buffer)) != -1) {
      fw.write(buffer, 0, len);
      }
      }
      }

    /**

    • 方式二:字符缓冲流复制文本文件(推荐文本文件使用)
      */
      public static void copyByBufferedCharStream(String src, String dest) throws IOException {
      try (BufferedReader br = new BufferedReader(new FileReader(src));
      BufferedWriter bw = new BufferedWriter(new FileWriter(dest))) {
      String line;
      while ((line = br.readLine()) != null) {
      bw.write(line);
      bw.newLine();
      }
      }
      }

    /**

    • 方式三:字节流复制任意文件
      */
      public static void copyByByteStream(String src, String dest) throws IOException {
      try (FileInputStream fis = new FileInputStream(src);
      FileOutputStream fos = new FileOutputStream(dest)) {
      byte[] buffer = new byte[1024];
      int len;
      while ((len = fis.read(buffer)) != -1) {
      fos.write(buffer, 0, len);
      }
      }
      }

    /**

    • 方式四:字节缓冲流复制任意文件(万能复制,推荐)
      */
      public static void copyByBufferedByteStream(String src, String dest) throws IOException {
      try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
      byte[] buffer = new byte[8192];
      int len;
      while ((len = bis.read(buffer)) != -1) {
      bos.write(buffer, 0, len);
      }
      bos.flush();
      }
      }

    // 测试
    public static void main(String[] args) {
    try {
    // 文本文件复制测试
    copyByBufferedCharStream(“source.txt”, “dest.txt”);

         // 图片文件复制测试
         copyByBufferedByteStream("photo.jpg", "photo_copy.jpg");
    
         System.out.println("所有文件复制完成!");
     } catch (IOException e) {
         e.printStackTrace();
     }
    

    }

}

AI写代码
java
运行

九、常见问题与解决方案
9.1 文件找不到异常(FileNotFoundException)
// 错误示例
FileReader fr = new FileReader(“nonexistent.txt”);

// 解决方案1:检查文件路径是否正确
// 解决方案2:使用绝对路径
FileReader fr = new FileReader(“D:/project/files/source.txt”);

// 解决方案3:先判断文件是否存在
File file = new File(“source.txt”);
if (!file.exists()) {
System.out.println(“文件不存在:” + file.getAbsolutePath());
return;
}

AI写代码
java
运行

9.2 中文乱码问题

// 问题:使用字节流读取中文文本文件可能出现乱码
// 解决:指定正确的字符集编码
// 方式一:使用字符流(默认使用系统编码)
BufferedReader br = new BufferedReader(new FileReader(“source.txt”));

// 方式二:指定编码(推荐)
BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(“source.txt”), “UTF-8”));

AI写代码
java
运行
9.3 大文件复制内存 溢出
// 错误做法:一次性读取整个文件
byte[] allBytes = fis.readAllBytes(); // 大文件会内存溢出

// 正确做法:边读边写,使用缓冲区
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}

AI写代码
java
运行

十、总结

10.1 核心知识点回顾
知识点 要点
字符流 用于文本文件,处理字符数据
字节流 用于任意文件,处理字节数据
缓冲流 提高IO性能,减少实际读写次数
try-with-resources 自动关闭流资源
缓冲区大小 通常 1024、2048、8192 字节

JAVA作业:了解Java的反射机制

一、什么是反射?
1.1 定义
反射(Reflection) 是Java语言提供的一种强大机制,允许程序在运行时动态地获取类的完整信息(如成员变量、方法、构造方法等),并且可以动态地创建对象、调用方法、访问属性。

通俗地说:反射就是在程序运行时,让Java程序能够“ introspection”(自省),看清自己的结构,甚至修改自己的行为。

1.2 正常方式 vs 反射方式
对比项 正常方式 反射方式
时机 编译时确定 运行时动态确定
类的信息 写代码时已知 运行时才获取
对象创建 new 类名() Class.forName(“类名”).newInstance()
方法调用 对象.方法名() method.invoke(对象, 参数)
特点 高效、类型安全 灵活、但有一定性能开销
简单示例:

// 正常方式:编译时就知道要使用Student类
Student s = new Student();
s.study();

// 反射方式:运行时才知道要使用哪个类
Class<?> clazz = Class.forName(“com.example.Student”);
Object obj = clazz.newInstance();
Method method = clazz.getMethod(“study”);
method.invoke(obj);

AI写代码
java
运行

二、为什么需要反射?

2.1 核心价值
反射机制使得Java程序从“静态”变为“动态”:

能力 说明
动态加载类 运行时才决定加载哪个类
动态创建对象 类名作为字符串传入,无需硬编码
动态调用方法 方法名作为字符串传入
访问私有成员 可以绕过访问权限检查
2.2 典型应用场景
场景 说明
IDE自动提示 编辑器通过反射获取类的方法和属性
Spring框架 IoC容器通过反射创建和管理Bean
JUnit框架 通过反射识别@Test注解的方法并执行
Tomcat等Web容器 动态加载Servlet类
JSON解析库 通过反射将JSON数据映射到Java对象
注解处理器 运行时读取注解信息
三、反射的核心API
3.1 Class类
Class类是反射的入口,每个Java类在运行时都会对应一个Class对象。

获取Class对象的三种方式:

// 方式1:通过类名.class
Class<?> clazz1 = String.class;

// 方式2:通过对象.getClass()
String str = “hello”;
Class<?> clazz2 = str.getClass();

// 方式3:通过Class.forName()(最常用,动态加载)
Class<?> clazz3 = Class.forName(“java.lang.String”);
AI写代码
java
运行
3.2 反射核心类

类名 作用
Class 代表一个类或接口
Field 代表类的成员变量
Method 代表类的方法
Constructor 代表类的构造方法
Modifier 访问修饰符的工具类
3.3 常用方法一览
Class类常用方法:

方法 说明
getName() 获取类全名(包名+类名)
getSimpleName() 获取类简单名称
getFields() 获取所有public成员变量
getDeclaredFields() 获取所有声明的成员变量(包括private)
getMethods() 获取所有public方法(包括继承的)
getDeclaredMethods() 获取所有声明的方法(不包括继承的)
getConstructors() 获取所有public构造方法
getDeclaredConstructors() 获取所有声明的构造方法
newInstance() 创建实例(调用无参构造)
四、反射的基本使用
4.1 准备工作:定义一个示例类
/**

  • 示例类:用于演示反射机制
    */
    public class Person {
    // 成员变量
    private String name;
    public int age;

    // 构造方法
    public Person() {
    System.out.println(“无参构造方法被调用”);
    }

    private Person(String name) {
    this.name = name;
    System.out.println(“私有构造方法被调用,name=” + name);
    }

    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    System.out.println(“有参构造方法被调用:” + name + “,” + age);
    }

    // 成员方法
    public void sayHello() {
    System.out.println(“Hello, I’m “ + name);
    }

    private void secret() {
    System.out.println(“这是一个私有方法”);
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    public String toString() {
    return “Person{name=’” + name + “‘, age=” + age + “}”;
    }

}
AI写代码
java
运行

4.2 获取类的信息

import java.lang.reflect.*;

public class ReflectionInfoDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 1. 获取Class对象
Class<?> clazz = Class.forName(“Person”);

    // 2. 获取类名
    System.out.println("类全名:" + clazz.getName());
    System.out.println("简单类名:" + clazz.getSimpleName());
    
    // 3. 获取所有public方法(包括继承的)
    System.out.println("\n=== 所有public方法 ===");
    Method[] methods = clazz.getMethods();
    for (Method m : methods) {
        System.out.println("  " + m.getName());
    }
    
    // 4. 获取所有声明的成员变量
    System.out.println("\n=== 所有声明的成员变量 ===");
    Field[] fields = clazz.getDeclaredFields();
    for (Field f : fields) {
        System.out.println("  " + Modifier.toString(f.getModifiers()) + " " + f.getName());
    }
    
    // 5. 获取所有构造方法
    System.out.println("\n=== 所有构造方法 ===");
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    for (Constructor<?> c : constructors) {
        System.out.println("  " + c);
    }
}

}
AI写代码
java
运行

4.3 通过反射创建对象

import java.lang.reflect.*;

public class ReflectionCreateDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName(“Person”);

    // 方式1:调用无参构造方法
    System.out.println("=== 方式1:无参构造 ===");
    Person p1 = (Person) clazz.newInstance();  // JDK9后可能过时
    p1.setName("张三");
    p1.sayHello();
    
    // 方式2:调用有参构造方法(推荐)
    System.out.println("\n=== 方式2:有参构造 ===");
    Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
    Person p2 = (Person) constructor.newInstance("李四", 20);
    p2.sayHello();
    
    // 方式3:调用私有构造方法
    System.out.println("\n=== 方式3:私有构造 ===");
    Constructor<?> privateCon = clazz.getDeclaredConstructor(String.class);
    privateCon.setAccessible(true);  // 突破私有访问限制
    Person p3 = (Person) privateCon.newInstance("王五");
    p3.sayHello();
}

}
AI写代码
java
运行

4.4 通过反射调用方法

import java.lang.reflect.*;

public class ReflectionMethodDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Class clazz = Class.forName("Person"); Constructor con = clazz.getConstructor(String.class, int.class);
Person person = (Person) con.newInstance(“小明”, 18);

    // 调用public方法
    System.out.println("=== 调用public方法 ===");
    Method sayHello = clazz.getMethod("sayHello");
    sayHello.invoke(person);
    
    // 调用带参数的方法
    Method setName = clazz.getMethod("setName", String.class);
    setName.invoke(person, "小红");
    sayHello.invoke(person);
    
    // 调用私有方法(需要setAccessible)
    System.out.println("\n=== 调用私有方法 ===");
    Method secret = clazz.getDeclaredMethod("secret");
    secret.setAccessible(true);  // 突破私有权限
    secret.invoke(person);
    
    // 调用有返回值的方法
    Method getName = clazz.getMethod("getName");
    String name = (String) getName.invoke(person);
    System.out.println("getName返回值:" + name);
}

}
AI写代码
java
运行

4.5 通过反射访问成员变量
以下是整理后的代码块格式:

import java.lang.reflect.*;

public class ReflectionFieldDemo {
public static void main(String[] args) throws Exception {
// 创建对象
Class clazz = Class.forName("Person"); Constructor con = clazz.getConstructor(String.class, int.class);
Person person = (Person) con.newInstance(“张三”, 25);

    System.out.println("原始对象:" + person);
    
    // 访问public成员变量
    System.out.println("\n=== 访问public成员变量 ===");
    Field ageField = clazz.getField("age");
    System.out.println("当前age:" + ageField.get(person));
    ageField.set(person, 30);
    System.out.println("修改后age:" + ageField.get(person));
    
    // 访问私有成员变量(需要setAccessible)
    System.out.println("\n=== 访问私有成员变量 ===");
    Field nameField = clazz.getDeclaredField("name");
    nameField.setAccessible(true);
    System.out.println("当前name:" + nameField.get(person));
    nameField.set(person, "李四");
    System.out.println("修改后name:" + nameField.get(person));
    System.out.println("最终对象:" + person);
}

}
AI写代码
java
运行

五、反射的实际应用示例

示例1:通用的toString方法
import java.lang.reflect.*;

class ObjectUtil {
public static String toString(Object obj) {
if (obj == null) return “null”;

    Class<?> clazz = obj.getClass();
    StringBuilder sb = new StringBuilder();
    sb.append(clazz.getSimpleName()).append(" { ");
    
    Field[] fields = clazz.getDeclaredFields();
    for (int i = 0; i < fields.length; i++) {
        fields[i].setAccessible(true);
        try {
            sb.append(fields[i].getName()).append("=");
            sb.append(fields[i].get(obj));
            if (i < fields.length - 1) {
                sb.append(", ");
            }
        } catch (IllegalAccessException e) {
            sb.append("?");
        }
    }
    sb.append(" }");
    return sb.toString();
}

}

class Student {
private String name;
private int score;

Student(String name, int score) {
    this.name = name;
    this.score = score;
}

}

public class GenericToStringDemo {
public static void main(String[] args) {
Student s = new Student(“张三”, 95);
System.out.println(ObjectUtil.toString(s));
}
}
AI写代码
java
运行

示例2:动态调用任意方法
import java.lang.reflect.*;

class DynamicInvoker {
public static Object invoke(Object obj, String methodName, Object… args) {
try {
Class clazz = obj.getClass(); Class[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
Method method = clazz.getMethod(methodName, paramTypes);
return method.invoke(obj, args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

class Calculator {
public int add(int a, int b) {
return a + b;
}

public String greet(String name) {
    return "Hello, " + name;
}

}

public class DynamicInvokeDemo {
public static void main(String[] args) {
Calculator calc = new Calculator();
Object result1 = DynamicInvoker.invoke(calc, “add”, 10, 20);
System.out.println(“add结果:” + result1);
Object result2 = DynamicInvoker.invoke(calc, “greet”, “张三”);
System.out.println(“greet结果:” + result2);
}
}
AI写代码
java
运行

示例3:模拟JUnit的@Test注解
import java.lang.annotation.;
import java.lang.reflect.
;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {}

class MyTestClass {
@Test
public void testMethod1() {
System.out.println(“执行测试方法1”);
}

@Test
public void testMethod2() {
    System.out.println("执行测试方法2");
}

public void normalMethod() {
    System.out.println("普通方法不会被自动执行");
}

}

class TestRunner {
public static void run(Class<?> testClass) throws Exception {
Object instance = testClass.newInstance();
Method[] methods = testClass.getDeclaredMethods();

    for (Method method : methods) {
        if (method.isAnnotationPresent(Test.class)) {
            System.out.print("正在执行:");
            method.invoke(instance);
        }
    }
}

}

public class AnnotationDemo {
public static void main(String[] args) throws Exception {
TestRunner.run(MyTestClass.class);
}
}
AI写代码
java
运行

运行结果
正在执行:执行测试方法1
正在执行:执行测试方法2
AI写代码
六、反射的优缺点
6.1 优点
优点 说明
动态性 运行时动态加载类,提高程序灵活性
通用性 可以编写通用的代码处理各种类型的对象
框架基础 几乎所有Java框架(Spring、Hibernate等)都依赖反射
绕过限制 可以访问私有成员,用于特殊场景(如调试工具)
6.2 缺点
缺点 说明
性能开销 反射调用比直接调用慢,因为需要动态解析
安全问题 可以绕过访问权限,破坏封装性
代码可读性 反射代码不如直接调用直观
编译时检查缺失 错误只能在运行时暴露
七、性能对比示例
以下是整理后的代码块格式:

/**

  • 对比直接调用与反射调用的性能
    */
    public class PerformanceCompare {
    public static void main(String[] args) throws Exception {
    Person person = new Person(“张三”, 20);

    // 1. 直接调用
    long start1 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    person.sayHello();
    }
    long end1 = System.nanoTime();
    System.out.println(“直接调用耗时:” + (end1 - start1) / 1000000 + “ ms”);

    // 2. 反射调用
    Method method = Person.class.getMethod(“sayHello”);
    long start2 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    method.invoke(person);
    }
    long end2 = System.nanoTime();
    System.out.println(“反射调用耗时:” + (end2 - start2) / 1000000 + “ ms”);

    // 3. 反射调用(启用访问检查关闭)
    method.setAccessible(true);
    long start3 = System.nanoTime();
    for (int i = 0; i < 1000000; i++) {
    method.invoke(person);
    }
    long end3 = System.nanoTime();
    System.out.println(“反射(setAccessible)耗时:” + (end3 - start3) / 1000000 + “ ms”);
    }

}
AI写代码
java
运行

代码结构说明:

完整保留了原始注释和逻辑结构
使用标准的Java代码块格式
保持了原有的三个测试部分(直接调用、反射调用、反射调用优化)
时间统计单位统一转换为毫秒(ms)
结论:反射调用通常比直接调用慢2-3倍,setAccessible(true)可以略微提升性能。

八、总结
8.1 核心知识点回顾
知识点 要点
反射的定义 运行时动态获取类信息并操作对象
获取Class对象 .class、getClass()、Class.forName()
核心API Class、Field、Method、Constructor
访问私有成员 setAccessible(true)
主要应用 Spring框架、JUnit、JSON解析等
8.2 使用建议
优先使用直接调用:除非确实需要动态性,否则不要滥用反射

缓存反射对象:如果需要频繁调用,缓存Method、Field等对象

注意安全性:反射可以破坏封装,谨慎使用

考虑性能影响:对性能敏感的代码避免使用反射

8.3 作业完成情况
理解反射机制的定义和作用

掌握获取Class对象的三种方式

能够通过反射创建对象、调用方法、访问属性

了解反射的实际应用场景

了解反射的优缺点

九、课后练习
编写代码,通过反射获取ArrayList类的所有方法名称

使用反射调用String类的toUpperCase()方法

通过反射创建一个HashMap对象,并调用put和get方法

尝试修改一个String对象的内容(利用反射突破不可变性)

Java作业:使用匿名内部类计算方法执行时间

一、作业需求

计算一个方法执行了多少秒(或毫秒),要求使用匿名内部类实现。

这是一个典型的应用场景:在不修改原有方法代码的情况下,为方法添加执行时间统计功能。

二、什么是匿名内部类?

2.1 定义

匿名内部类(Anonymous Inner Class)是没有名字的内部类,通常用于一次性使用的场景。

2.2 基本语法
匿名内部类 语法示例
匿名内部类的标准语法格式如下,适用于直接实现接口或继承父类并重写方法:

父类/接口 变量名 = new 父类/接口() {
@Override
返回类型 方法名(参数列表) {
// 方法实现
}
};
AI写代码
java
运行
具体实现案例
实现接口示例(Runnable):

Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(“匿名内部类执行任务”);
}
};
AI写代码
java
运行
继承抽象类示例(Thread):

Thread thread = new Thread() {
@Override
public void run() {
System.out.println(“重写Thread的run方法”);
}
};
AI写代码
java
运行
使用场景说明
适用于需要一次性使用的类实现,避免单独创建子类文件
常用于事件监听、线程实现等需要快速重写方法的场景
匿名内部类会隐式持有外部类的引用,需注意内存泄漏问题
语法要点
必须实现父类或接口的所有抽象方法
可以访问外部类的final变量或等效final变量
编译后会生成独立的.class文件,命名格式为外部类$数字.class
Lambda简化场景
当接口仅含单个抽象方法时(函数式接口),可用Lambda表达式替代:

Runnable task = () -> System.out.println(“Lambda替代匿名类”);
AI写代码
java
运行
2.3 特点
特点 说明
没有类名 创建时直接定义类体
一次性使用 通常只使用一次
简化代码 避免单独定义一个类
必须实现所有抽象方法 与普通类相同
三、设计思路
3.1 核心问题
如何在不修改原有方法代码的情况下,统计方法的执行时间?

3.2 解决方案
使用模板方法设计模式 + 匿名内部类:

定义一个接口或抽象类,包含需要执行的方法

在工具类中编写计时方法,接收接口/抽象类的实现

调用时使用匿名内部类传入具体逻辑

3.3 流程图
以下是基于文本描述的计时流程实现方案,采用Java代码示例和流程图说明:

代码实现方案
// 定义函数式接口,便于匿名内部类实现
@FunctionalInterface
interface TargetMethod {
void execute();
}

public class TimeRecorder {
public static void recordExecutionTime(TargetMethod method) {
long startTime = System.nanoTime();
method.execute();
long endTime = System.nanoTime();
long duration = endTime - startTime;
System.out.println(“Execution time: “ + duration + “ ns”);
}

public static void main(String[] args) {
    // 通过匿名内部类实现目标方法
    recordExecutionTime(new TargetMethod() {
        @Override
        public void execute() {
            // 替换为实际需要计时的代码
            int sum = 0;
            for (int i = 0; i < 1000000; i++) {
                sum += i;
            }
        }
    });
}

}
AI写代码
java
运行

流程图表示
text

开始计时
↓
记录开始时间 (startTime)
↓
执行目标方法 ←──── 匿名内部类提供具体实现
↓
记录结束时间 (endTime)
↓
计算耗时 = endTime - startTime
↓
输出结果
关键说明
时间精度:使用System.nanoTime()获取纳秒级时间戳,比currentTimeMillis()精度更高
函数式接口:通过接口设计使计时逻辑与业务逻辑解耦
Lambda优化:Java 8+可使用Lambda简化匿名内部类写法:
recordExecutionTime(() -> {
int sum = IntStream.range(0, 1000000).sum();
});
AI写代码
java
运行
注意事项
避免在测试方法中包含IO操作或随机因素
考虑JVM预热问题,正式测试前应先执行几次预热运行
对于微基准测试建议使用JMH等专业框架
四、代码实现
4.1 方式一:使用接口
以下是整理后的代码块格式:

/**

  • 定义一个接口,包含需要计时的方法
    */
    interface Task {
    void execute(); // 需要执行的任务
    }

/**

  • 计时器工具类
    /
    class TimerUtil {
    /
    *
    • 计算任务执行时间(毫秒)
    • @param task 要执行的任务
      */
      public static void measureTime(Task task) {
      long startTime = System.currentTimeMillis();
      task.execute();
      long endTime = System.currentTimeMillis();
      long duration = endTime - startTime;
      System.out.println(“方法执行耗时:” + duration + “ 毫秒”);
      System.out.println(“转换为秒:” + duration / 1000.0 + “ 秒”);
      }
      }

/**

  • 测试类
    */
    public class TimeTest {
    public static void main(String[] args) {
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    System.out.println(“开始执行任务…”);
    for (int i = 0; i < 100000000; i++) {
    Math.sqrt(i);
    }
    System.out.println(“任务执行完毕”);
    }
    });
    }
    }
    AI写代码
    java
    运行

运行结果:

开始执行任务…
任务执行完毕
方法执行耗时:12 毫秒
转换为秒:0.012 秒
AI写代码
text
4.2 方式二:使用抽象类
/**

  • 抽象类:需要计时的任务
    */
    abstract class AbstractTask {
    public abstract void run();
    }

/**

  • 计时器工具类(支持抽象类)
    */
    class TimerUtil2 {
    public static void measureTime(AbstractTask task) {
    long startTime = System.nanoTime(); // 纳秒,更精确

    task.run();

    long endTime = System.nanoTime();
    long duration = endTime - startTime;

    System.out.println(“执行耗时:” + duration + “ 纳秒”);
    System.out.println(“转换为毫秒:” + duration / 1000000.0 + “ 毫秒”);
    }

}

/**

  • 测试类
    */
    public class TimeTest2 {
    public static void main(String[] args) {
    // 使用匿名内部类继承AbstractTask
    TimerUtil2.measureTime(new AbstractTask() {
    @Override
    public void run() {
    // 需要计时的业务逻辑
    System.out.println(“正在计算100以内素数…”);
    for (int i = 2; i <= 100; i++) {
    boolean isPrime = true;
    for (int j = 2; j <= Math.sqrt(i); j++) {
    if (i % j == 0) {
    isPrime = false;
    break;
    }
    }
    if (isPrime) {
    System.out.print(i + “ “);
    }
    }
    System.out.println();
    }
    });
    }
    }

AI写代码
java
运行

4.3 方式三:带返回值的方法计时

如果需要统计的方法有返回值,可以使用泛型:

/**

  • 带返回值的任务接口
  • @param 返回值类型
    */
    interface CallableTask {
    T execute();
    }

/**

  • 计时器工具类(支持返回值)
    */
    class TimerUtil3 {
    public static T measureTime(CallableTask task, String taskName) {
    System.out.println(“========== “ + taskName + “ ==========”);

    long startTime = System.currentTimeMillis();
    T result = task.execute();
    long endTime = System.currentTimeMillis();
    long duration = endTime - startTime;

    System.out.println(“执行耗时:” + duration + “ 毫秒(” + duration / 1000.0 + “ 秒)”);
    System.out.println(“================================”);

    return result;
    }

}

/**

  • 测试类
    */
    public class TimeTest3 {
    public static void main(String[] args) {
    // 计算1到100的和,并返回结果
    Integer sum = TimerUtil3.measureTime(new CallableTask() {
    @Override
    public Integer execute() {
    int total = 0;
    for (int i = 1; i <= 100; i++) {
    total += i;
    }
    System.out.println(“计算结果:” + total);
    return total;
    }
    }, “计算1-100的和”);

    System.out.println(“返回结果:” + sum);

    // 计算斐波那契数列
    TimerUtil3.measureTime(new CallableTask() {
    @Override
    public Void execute() {
    System.out.println(“斐波那契数列前20项:”);
    long a = 0, b = 1;
    for (int i = 0; i < 20; i++) {
    System.out.print(a + “ “);
    long temp = a + b;
    a = b;
    b = temp;
    }
    System.out.println();
    return null;
    }
    }, “斐波那契数列”);
    }

}

AI写代码
java
运行

五、多种时间单位说明

方法 返回单位 说明
System.currentTimeMillis() 毫秒 从1970-01-01到现在的毫秒数
System.nanoTime() 纳秒 高精度计时,适合短时间测量
以下是将时间单位转换的Java代码示例:

毫秒转秒
long ms = 1234;
double seconds = ms / 1000.0; // 结果为1.234秒
AI写代码
java
运行
纳秒转毫秒
long ns = 12345678;
double ms = ns / 1000000.0; // 结果为12.345678毫秒
AI写代码
java
运行
秒转毫秒
double seconds = 1.5;
long ms = (long)(seconds * 1000); // 结果为1500毫秒
AI写代码
java
运行
毫秒转纳秒
long ms = 15;
long ns = ms * 1000000L; // 结果为15000000纳秒
AI写代码
java
运行
注意事项:

使用浮点数除法(1000.0)确保精度不丢失
大数值计算时使用L后缀防止整数溢出
从浮点转整型时需要进行显式类型转换
六、不使用匿名内部类的对比
6.1 传统方式(单独定义一个类)
// 需要单独定义一个类
class MyTask implements Task {
@Override
public void execute() {
// 业务逻辑
}
}

// 使用时
TimerUtil.measureTime(new MyTask());
AI写代码
java
运行

6.2 使用匿名内部类的方式

TimerUtil.measureTime(new Task() {
@Override
public void execute() {
// 业务逻辑
}
});
AI写代码
java
运行
代码说明
使用匿名内部类实现Task接口
measureTime方法接收Task实例并测量其execute方法的执行时间
业务逻辑需在execute方法中实现
格式要点
代码块使用三个反引号加语言类型标记
保持原代码缩进结构
注释保留在原有位置
6.3 Lambda表达式方式(Java 8+)
// 更简洁的Lambda写法(接口只有一个抽象方法时)
TimerUtil.measureTime(() -> {
// 业务逻辑
});
AI写代码
java
运行
代码说明
使用Lambda表达式简化了匿名内部类的写法
适用于函数式接口(只有一个抽象方法的接口)
大括号内可编写需要计时的业务逻辑代码
代码块格式便于在Markdown文档中突出显示
注意事项
确保TimerUtil类已正确导入
measureTime方法需接收Runnable或Callable等函数式接口参数
Lambda表达式中的业务逻辑不应过于复杂
方式 代码量 可读性 适用场景
单独定义类 多 一般 需要多处复用
匿名内部类 中 较好 一次性使用
Lambda 少 好 接口只有一个抽象方法
七、完整示例:多个方法计时
以下是转换后的代码块格式:

/**

  • 综合示例:统计多个排序算法的执行时间
    */
    public class SortTimeTest {
    public static void main(String[] args) {
    // 准备测试数据
    int[] arr1 = generateRandomArray(10000);
    int[] arr2 = arr1.clone();

    // 测试冒泡排序时间
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    bubbleSort(arr1.clone());
    System.out.println(“冒泡排序完成”);
    }
    });

    // 测试快速排序时间
    TimerUtil.measureTime(new Task() {
    @Override
    public void execute() {
    quickSort(arr2.clone(), 0, arr2.length - 1);
    System.out.println(“快速排序完成”);
    }
    });
    }

    /**

    • 生成随机数组
      */
      private static int[] generateRandomArray(int size) {
      int[] arr = new int[size];
      for (int i = 0; i < size; i++) {
      arr[i] = (int) (Math.random() * size);
      }
      return arr;
      }

    /**

    • 冒泡排序
      */
      private static void bubbleSort(int[] arr) {
      for (int i = 0; i < arr.length - 1; i++) {
      for (int j = 0; j < arr.length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
      int temp = arr[j];
      arr[j] = arr[j + 1];
      arr[j + 1] = temp;
      }
      }
      }
      }

    /**

    • 快速排序
      */
      private static void quickSort(int[] arr, int left, int right) {
      if (left >= right) return;
      int pivot = partition(arr, left, right);
      quickSort(arr, left, pivot - 1);
      quickSort(arr, pivot + 1, right);
      }

    private static int partition(int[] arr, int left, int right) {
    int pivot = arr[left];
    int i = left, j = right;
    while (i < j) {
    while (i < j && arr[j] >= pivot) j–;
    arr[i] = arr[j];
    while (i < j && arr[i] <= pivot) i++;
    arr[j] = arr[i];
    }
    arr[i] = pivot;
    return i;
    }

}
AI写代码
java
运行

八、总结

8.1 核心知识点
知识点 应用
匿名内部类 简化代码,一次性传入任务逻辑
接口/抽象类 定义任务规范
System.currentTimeMillis() 获取系统时间,计算耗时
模板方法模式 固定计时流程,任务逻辑可变
8.2 匿名内部类的使用场景
回调函数:如本作业的方法计时

事件监听:GUI编程中的按钮点击事件

线程创建:new Thread(new Runnable() {…})

集合排序:Collections.sort(list, new Comparator() {…})

8.3 作业完成情况
实现了方法执行时间计算功能

使用了匿名内部类

支持毫秒/纳秒两种精度

支持带返回值的场景

JAVA作业:形式化方法学习与《大象——Thinking in UML》阅读推荐

一、形式化方法概述
1.1 定义
形式化方法(Formal Methods)是一种基于数学的软件和硬件系统开发技术,采用严格的形式化规约语言描述系统,并通过数学推理 验证系统性质是否正确。

计算机科学家Edsger Dijkstra曾指出:“程序测试只能表明错误的存在,而不能表明错误的不存在。”这一论断揭示了传统测试方法的根本局限——测试无法穷尽所有可能的输入状态。形式化方法正是解决这一局限的重要途径。

1.2 软件工程 方法的分类
方法类型 特征 优势 局限性
非形式化方法 自然语言描述 通俗易懂,便于沟通 存在歧义,缺乏精确性
半形式化方法 图形化表示(如UML) 直观清晰,结构明确 语义不够精确
形式化方法 数学符号描述 精确无歧义,可验证 学习门槛高,开发成本高
1.3 形式化方法的核心构成
形式化方法由两个核心部分组成:

(1)形式化模型
具有严格数学定义的抽象模型,常见的有:

有穷自动机(Finite Automaton)

Petri网(Petri Net)

迁移系统(Transition System)

Z语言规范

(2)形式化分析
对模型进行数学验证的技术,主要包括:

技术 原理 特点
模型检查 遍历系统所有状态空间 自动化程度高,但存在状态爆炸问题
演绎证明 采用数学公理和推理规则证明 可处理无限状态,但需要人工引导
抽象解释 对程序行为进行静态近似分析 适用于大型程序,存在精度损失
1.4 主要应用领域
由于形式化方法具有较高的技术门槛和实施成本,其主要应用于对安全性、可靠性要求极高的关键系统:

航空航天:飞行控制系统、导航系统

轨道交通:列车信号控制系统

医疗设备:生命支持系统、放射治疗设备

核工业:核反应堆安全控制系统

金融交易:高频交易系统、清算系统

1.5 形式化方法的优势与局限
优势 局限
规约精确,无二义性 学习曲线陡峭
可进行数学正确性证明 开发周期较长
早期发现设计缺陷 需要专门的工具支持
提高系统可靠性和安全性 不适用于所有类型的项目
1.6 形式化方法与UML的互补关系
对比维度 形式化方法 UML
表达方式 数学符号 图形化语言
语义精确性 高(无歧义) 中等(存在解释空间)
可验证性 强(可数学证明) 弱(主要依赖评审)
学习门槛 高 中等
适用场景 安全关键系统 一般软件项目
主要用途 验证系统正确性 沟通系统设计
两者并非对立关系,而是互补关系:UML提供直观的系统视图用于团队沟通,形式化方法提供严格的语义和验证手段用于保证系统可靠性。在实践中,可将UML模型转换为形式化规约进行验证,实现优势互补。

二、《大象——Thinking in UML》阅读推荐
2.1 书籍基本信息
项目 内容
书名 《大象——Thinking in UML》
作者 谭云杰
出版社 中国水利水电出版社
版本 第2版
出版时间 2012年
豆瓣评分 8.1分
2.2 内容概述
本书并非一本简单的UML符号参考手册,而是一部系统讲解面向对象分析与设计思想的专业著作。

书名“大象”取意于“盲人摸象”的寓言——软件开发涉及需求、分析、设计、编码、测试等多个环节,参与者往往只了解局部而难以把握全貌。UML作为一种标准化 的建模语言,其价值在于帮助开发团队建立对软件系统的完整、一致的理解。

2.3 全书结构
部分 主题 核心内容
准备篇 面向对象基础 面向对象基本概念、建模原理
基础篇 UML核心元素 用例、类、接口、组件等及相互关系
进阶篇 统一开发过程 需求获取、系统分析、架构设计
总结篇 深入探讨 常见疑难问题与实践经验
2.4 核心价值
(1)培养面向对象思维方式

许多学习者虽然掌握了Java等语言的语法,但习惯于用面向对象 的语言编写面向过程的代码。本书从认知层面帮助读者理解:

何为抽象?

如何从业务领域中识别对象?

如何建立类之间的合理关系?

(2)建立用例驱动的开发流程

书中系统阐述了用例驱动的开发方法论:

用例作为分析单元

用例作为设计单元

用例作为开发单元

用例作为测试单元

(3)理论与实践相结合

区别于仅介绍UML符号的教材,本书以实际项目为背景,完整展示了从需求获取到架构设计 的过程,具有较强的实践指导意义。

(4)语言通俗,降低学习门槛

本书写作风格贴近开发者实际认知,避免了学术化的晦涩表达,适合初学者系统学习。

2.5 适用读者
读者群体 推荐程度
已完成Java基础学习的学生 ★★★★★ 强烈推荐
正在学习UML建模的读者 ★★★★★ 推荐
初次参与软件项目的开发者 ★★★★★ 推荐
希望提升设计能力的初级工程师 ★★★★☆ 推荐
2.6 阅读建议
建议按以下顺序阅读:

第一遍通读:把握全书框架,理解核心思想,不纠结技术细节

第二遍精读:重点研读用例图、类图、时序图等相关章节

边读边实践:选取一个小型项目(如图书管理系统),同步进行建模练习

2.7 关联参考
参考书目 推荐理由
《UML用户指南》(Grady Booch) UML创始人著作,经典权威
《形式化方法导论》(张广泉) 清华教材,系统学习形式化方法
《设计模式:可复用面向对象软件的基础》(GoF) 面向对象设计进阶读物
三、总结
本次作业的主要收获如下:

理解形式化方法:形式化方法以数学为理论基础,能够精确描述系统行为并验证其正确性,但实施成本较高,主要应用于安全关键领域。

UML的定位与价值:作为半形式化方法,UML通过图形化方式表达软件设计,在直观性与规范性之间取得平衡,是当前工业界主流的建模工具。

理论与实践的关系:形式化方法提供理论严谨性,UML提供实践可操作性,二者相互补充。

《大象——Thinking in UML》的阅读价值:该书有助于建立面向对象思维方式、掌握UML建模方法、提升软件设计能力,适合作为UML入门的首选读物。

第五章 继承与接口

一、本章知识回顾

知识点 说明
继承 子类继承父类的成员变量和方法
方法重写 子类重新定义父类的方法
super关键字 访问父类的成员变量、方法和构造方法
多态 父类引用指向子类对象
final关键字 防止继承、防止重写、定义常量
抽象类 包含抽象方法的类,不能实例化
接口 完全抽象的规范,支持多实现
二、为什么要继承?

2.1 问题引入

假设要设计学生和教师两个类:

学生类:姓名、年龄、学号、学习()

教师类:姓名、年龄、工号、教学()

你会发现,学生和教师都有相同的属性(姓名、年龄)。如果每个类都重复写一遍,代码会非常冗余。

继承的解决方案:把共同的特征提取到“人类”中,让学生和教师继承这个类。

2.2 继承的概念

  • 父类(超类):被继承的类
  • 子类(派生类):继承父类的类
  • 关键字:extends

// 父类
class Person {
String name;
int age;

void eat() {
    System.out.println(name + "正在吃饭");
}

}

// 子类继承父类
class Student extends Person {
String studentId; // 子类特有的属性

void study() {   // 子类特有的方法
    System.out.println(name + "正在学习");
}

}

AI写代码
java
运行

2.3 继承的好处

好处 说明
代码复用 子类自动拥有父类的属性和方法
易于维护 修改父类即可影响所有子类
体现is-a关系 Student is a Person(学生是一个人)
AI写代码
markdown
三、子类与父类

3.1 继承的基本语法

class 子类名 extends 父类名 {
// 子类特有的成员变量
// 子类特有的成员方法
}
AI写代码
java
运行
3.2 继承示例:银行账户

代码解析
/**

  • 父类:银行账户
    */
    class BankAccount {
    String accountNo; // 账号
    double balance; // 余额

    // 构造方法
    BankAccount(String accountNo, double balance) {
    this.accountNo = accountNo;
    this.balance = balance;
    }

    // 存款
    void deposit(double amount) {
    balance += amount;
    System.out.println(“存入 “ + amount + “ 元,余额:” + balance);
    }

    // 取款
    void withdraw(double amount) {
    if (balance >= amount) {
    balance -= amount;
    System.out.println(“取出 “ + amount + “ 元,余额:” + balance);
    } else {
    System.out.println(“余额不足!”);
    }
    }

    void showInfo() {
    System.out.println(“账号:” + accountNo + “,余额:” + balance);
    }

}

/**

  • 子类:储蓄账户(继承BankAccount)

  • 特点:有利息
    */
    class SavingsAccount extends BankAccount {
    double interestRate; // 子类特有的属性:利率

    SavingsAccount(String accountNo, double balance, double interestRate) {
    super(accountNo, balance); // 调用父类构造方法
    this.interestRate = interestRate;
    }

    // 子类特有的方法:计算利息
    void addInterest() {
    double interest = balance * interestRate;
    balance += interest;
    System.out.println(“增加利息:” + interest + “ 元,余额:” + balance);
    }

}

public class InheritanceDemo {
public static void main(String[] args) {
System.out.println(“=== 普通账户 ===”);
BankAccount acc1 = new BankAccount(“1001”, 1000);
acc1.deposit(500);
acc1.withdraw(200);

    System.out.println("\n=== 储蓄账户 ===");
    SavingsAccount acc2 = new SavingsAccount("2001", 2000, 0.03);
    acc2.deposit(300);
    acc2.addInterest();
}

}
AI写代码
java
运行

运行结果
=== 普通账户 ===
存入 500.0 元,余额:1500.0
取出 200.0 元,余额:1300.0

=== 储蓄账户 ===
存入 300.0 元,余额:2300.0
增加利息:69.0 元,余额:2369.0
AI写代码
关键点说明
父类 BankAccount 定义了银行账户的基本属性和方法,包括账号、余额、存款、取款和显示账户信息。

子类 SavingsAccount 继承了 BankAccount,并增加了特有的属性 interestRate 和方法 addInterest(),用于计算并添加利息。

在 main 方法中,分别创建了普通账户和储蓄账户的实例,演示了存款、取款和利息计算的功能。
四、方法重写(Override)

4.1 什么是方法重写?

子类可以重新定义(覆盖)父类中已有的方法,这叫做方法重写。

重写规则:

  • 方法名相同
  • 参数列表相同
  • 返回值类型相同(或是其子类)
  • 访问权限不能比父类更严格

4.2 示例:信用卡账户(重写取款方法)

信用卡账户实现代码
/**

  • 子类:信用卡账户(继承BankAccount)

  • 特点:可以透支
    */
    class CreditAccount extends BankAccount {
    double creditLimit; // 透支额度

    CreditAccount(String accountNo, double balance, double creditLimit) {
    super(accountNo, balance);
    this.creditLimit = creditLimit;
    }

    // 重写取款方法(支持透支)
    @Override
    void withdraw(double amount) {
    if (balance + creditLimit >= amount) {
    balance -= amount;
    System.out.println(“取出 “ + amount + “ 元,余额:” + balance);
    } else {
    System.out.println(“超过透支额度!”);
    }
    }

}

public class OverrideDemo {
public static void main(String[] args) {
CreditAccount acc = new CreditAccount(“3001”, 0, 5000);
acc.withdraw(2000); // 可以透支
acc.withdraw(4000); // 超出额度
}
}
AI写代码
java
运行

运行结果示例
取出 2000.0 元,余额:-2000.0
超过透支额度!
AI写代码
text
代码说明
信用卡账户类继承自银行账户基类,通过重写取款方法实现透支功能。当取款金额不超过账户余额与透支额度之和时允许交易,否则拒绝交易并提示超额。

构造方法接收账户号、初始余额和透支额度三个参数,初始化时需调用父类构造方法。透支额度作为信用卡账户特有的属性单独存储。

4.3 @Override注解

@Override是可选的,但推荐加上:

  • 让编译器 帮忙检查是否正确的重写了父类方法
  • 提高代码可读性

4.4 重写 vs 重载

方法重写(Override)与方法重载(Overload)对比
比较项 方法重写(Override) 方法重载(Overload)
发生位置 子类和父类之间 同一个类中
方法名 必须相同 必须相同
参数列表 必须相同 必须不同
返回值 相同或子类(协变返回类型) 无关
访问修饰符 不能比父类更严格(如父类为 protected,子类可为 public,但不能为 private) 无限制
异常抛出 不能抛出比父类更宽泛的检查异常 无限制
作用 实现多态,子类提供特定实现 提供同名方法的不同参数版本
关键区别说明
方法重写(Override)

发生在继承关系中,子类重新定义父类的方法。
方法签名(名称和参数列表)必须完全一致。
返回类型可以是原类型的子类型(协变返回)。
访问权限不能比父类方法更严格。
方法重载(Overload)

发生在同一类中,通过不同参数列表区分同名方法。
返回值类型、访问修饰符和异常声明可自由变化。
仅需参数列表不同(类型、顺序或数量)。
示例代码
方法重写示例
class Parent {
protected void show() {
System.out.println(“Parent’s show”);
}
}

class Child extends Parent {
@Override
public void show() { // 访问修饰符更宽松,参数列表相同
System.out.println(“Child’s show”);
}
}
AI写代码
java
运行

方法重载示例
class Calculator {
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {  // 参数类型不同
    return a + b;
}

int add(int a, int b, int c) {  // 参数数量不同
    return a + b + c;
}

}
AI写代码
java
运行

五、super关键字

5.1 super的作用

super用于在子类中访问父类的成员。

表格转换结果
用法 说明
super.成员变量 访问父类成员变量
super.成员方法() 调用父类成员方法
super(参数) 调用父类构造方法(必须在第一行)
注意事项
表格已按照原始内容格式转换,保持清晰对齐
每列内容准确对应原始数据
表格边框采用标准Markdown表格语法
列宽根据内容自动调整
5.2 示例:电脑

代码解析
/**

  • 父类:电脑
    */
    class Computer {
    String cpu;
    int ram;

    Computer(String cpu, int ram) {
    this.cpu = cpu;
    this.ram = ram;
    }

    void showSpec() {
    System.out.println(“CPU:” + cpu + “,内存:” + ram + “GB”);
    }

}

/**

  • 子类:游戏电脑
    */
    class GamingComputer extends Computer {
    String gpu; // 显卡

    GamingComputer(String cpu, int ram, String gpu) {
    super(cpu, ram); // 调用父类构造方法
    this.gpu = gpu;
    }

    @Override
    void showSpec() {
    super.showSpec(); // 调用父类的方法
    System.out.println(“显卡:” + gpu);
    }

}

public class SuperDemo {
public static void main(String[] args) {
GamingComputer gc = new GamingComputer(“i7-13700K”, 32, “RTX 4080”);
gc.showSpec();
}
}
AI写代码
java
运行

运行结果
CPU:i7-13700K,内存:32GB
显卡:RTX 4080
AI写代码
关键点说明
super关键字用于在子类中访问父类的成员变量或方法。在构造方法中,super()用于调用父类的构造方法,必须放在子类构造方法的第一行。

方法重写时,使用super.方法名()可以调用父类被重写的方法,实现功能的扩展而非完全覆盖。

继承关系中,子类可以继承父类的非私有成员,同时可以添加自己的特有成员,形成更具体的类。

5.3 注意事项

子类构造方法必须先调用父类构造方法

class Parent {
Parent(String name) { // 有参构造
System.out.println(“Parent: “ + name);
}
}

// 正确写法
class Child extends Parent {
Child(String name) {
super(name); // 必须手动调用父类有参构造
}
}
AI写代码
java
运行

六、多态

6.1 什么是多态?

多态:同一个方法调用,作用于不同的对象,产生不同的行为。

实现多态的三个条件:

  1. 有继承关系
  2. 子类重写父类的方法
  3. 父类引用指向子类对象6.2 多态示例

/*** 父类:动物*/
class Animal {
void sound() {
System.out.println(“动物发出声音”);
}
}

/*** 子类:狗*/
class Dog extends Animal {
@Override
void sound() {
System.out.println(“狗:汪汪汪!”);
}
}

/*** 子类:猫*/
class Cat extends Animal {
@Override
void sound() {
System.out.println(“猫:喵喵喵!”);
}
}

public class PolymorphismDemo {
public static void main(String[] args) {
// 多态:父类引用指向子类对象
Animal animal1 = new Dog();
Animal animal2 = new Cat();

    // 同一个方法调用,产生不同行为
    animal1.sound(); // 狗:汪汪汪!
    animal2.sound(); // 猫:喵喵喵!
    
    // 多态的方便之处:用一个数组存放多种动物
    Animal[] animals = {new Dog(), new Cat(), new Dog()};
    for (Animal animal : animals) {
        animal.sound(); // 不需要知道具体是哪种动物
    }
}

}

AI写代码
java
运行

6.3 对象类型转换

// 向上转型(自动,安全)
Animal animal = new Dog();

// 向下转型(强制,有风险)
Dog dog = (Dog) animal;

// instanceof 判断类型(避免转型异常)
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
}
AI写代码
java
运行

七、final关键字

7.1 final的三种用法

修饰符作用示例表
修饰符 作用 示例
final 类 不能被继承 final class String
final 方法 不能被重写 public final void print()
final 变量 只能赋值一次(常量) final double PI = 3.14
7.2 示例

// 1. final变量:常量
class Constants {
final double PI = 3.14159; // 声明时初始化
void test() {
// PI = 3.14; // 编译错误:不能修改
}
}

// 2. final方法:子类不能重写
class Parent {
final void finalMethod() {
System.out.println(“这个方法不能被重写”);
}
}

// 3. final类:不能有子类
final class FinalClass {
// 类体
}
// class SubClass extends FinalClass {} // 编译错误

AI写代码
java
运行

八、抽象类

8.1 什么是抽象类?

当一个父类只知道子类应该有什么方法,但不知道如何实现时,使用抽象类。

  • 用 abstract 关键字修饰
  • 抽象方法没有方法体
  • 抽象类不能实例化(不能 new)

8.2 示例:图形面积计算

/**

  • 抽象类:图形
    */
    abstract class Shape {
    String color;

    Shape(String color) {
    this.color = color;
    }

    // 抽象方法:只有声明,没有实现
    abstract double getArea();
    abstract double getPerimeter();

    // 普通方法(可以被子类继承)
    void showColor() {
    System.out.println(“颜色:” + color);
    }

}

/**

  • 子类:圆形
    */
    class Circle extends Shape {
    double radius;

    Circle(String color, double radius) {
    super(color);
    this.radius = radius;
    }

    @Override
    double getArea() {
    return Math.PI * radius * radius;
    }

    @Override
    double getPerimeter() {
    return 2 * Math.PI * radius;
    }

}

/**

  • 子类:矩形
    */
    class Rectangle extends Shape {
    double width;
    double height;

    Rectangle(String color, double width, double height) {
    super(color);
    this.width = width;
    this.height = height;
    }

    @Override
    double getArea() {
    return width * height;
    }

    @Override
    double getPerimeter() {
    return 2 * (width + height);
    }

}

public class AbstractDemo {
public static void main(String[] args) {
// Shape shape = new Shape(“红色”); // 错误:抽象类不能实例化

    Shape circle = new Circle("红色", 5);
    Shape rectangle = new Rectangle("蓝色", 4, 6);
    
    System.out.println("=== 圆形 ===");
    circle.showColor();
    System.out.println("面积:" + circle.getArea());
    System.out.println("周长:" + circle.getPerimeter());
    
    System.out.println("\n=== 矩形 ===");
    rectangle.showColor();
    System.out.println("面积:" + rectangle.getArea());
    System.out.println("周长:" + rectangle.getPerimeter());
}

}

AI写代码
java
运行

抽象类要点:

  • 含有抽象方法的类必须是抽象类
  • 抽象类可以没有抽象方法
  • 子类必须实现所有抽象方法,否则子类也是抽象类

九、接口

9.1 什么是接口?

接口是完全抽象的规范,定义了“能做什么”,但不关心“怎么做”。

  • 用 interface 关键字定义
  • 用 implements 关键字实现

9.2 接口的特性

成员类型与默认修饰符对照表
成员类型 默认修饰符 说明
常量 public static final 接口中定义的变量自动是常量
抽象方法 public abstract 接口中的方法自动是抽象的
9.3 示例:USB接口

代码解析
/**

  • USB接口规范
    */
    interface USB {
    // 常量(自动是 public static final)
    String STANDARD = “USB 3.0”;

    // 抽象方法(自动是 public abstract)
    void plugIn(); // 插入
    void work(); // 工作
    void plugOut(); // 拔出

}

/**

  • 鼠标实现USB接口
    */
    class Mouse implements USB {
    @Override
    public void plugIn() {
    System.out.println(“鼠标已插入”);
    }

    @Override
    public void work() {
    System.out.println(“鼠标:移动光标、点击”);
    }

    @Override
    public void plugOut() {
    System.out.println(“鼠标已拔出”);
    }

}

/**

  • 键盘实现USB接口
    */
    class Keyboard implements USB {
    @Override
    public void plugIn() {
    System.out.println(“键盘已插入”);
    }

    @Override
    public void work() {
    System.out.println(“键盘:输入文字”);
    }

    @Override
    public void plugOut() {
    System.out.println(“键盘已拔出”);
    }

}

public class InterfaceDemo {
public static void main(String[] args) {
USB mouse = new Mouse();
USB keyboard = new Keyboard();

    System.out.println("=== 鼠标 ===");
    mouse.plugIn();
    mouse.work();
    mouse.plugOut();
    
    System.out.println("\n=== 键盘 ===");
    keyboard.plugIn();
    keyboard.work();
    keyboard.plugOut();
    
    System.out.println("\nUSB标准:" + USB.STANDARD);
}

}
AI写代码
java
运行

运行结果
代码执行后的输出结果如下:

=== 鼠标 ===
鼠标已插入
鼠标:移动光标、点击
鼠标已拔出

=== 键盘 ===
键盘已插入
键盘:输入文字
键盘已拔出

USB标准:USB 3.0
AI写代码

关键点说明
接口USB定义了三个抽象方法和一个常量。Mouse和Keyboard类实现了USB接口,并提供了具体的方法实现。

在InterfaceDemo类的main方法中,创建了Mouse和Keyboard的实例,并通过接口类型引用调用各自的方法。最后打印了接口中定义的常量STANDARD。

该示例展示了Java接口的基本特性,包括抽象方法的定义、实现类的编写以及接口常量的使用。

9.4 接口的多实现

一个类可以实现多个接口,这是Java实现多继承的方式。

interface A {
void methodA();
}

interface B {
void methodB();
}

class MultipleImpl implements A, B {
@Override
public void methodA() {
System.out.println(“实现A的方法”);
}

@Override
public void methodB() {
System.out.println(“实现B的方法”);
}
}

AI写代码
java
运行

9.5 接口与抽象类的对比

抽象类与接口对比
比较项 抽象类 接口
关键字 abstract class interface
继承/实现 extends implements
多继承 不支持 支持
成员变量 可以有各种类型 只能是常量
构造方法 有 没有
普通方法 可以有 只有抽象方法(Java 8前)
使用场景 有共同代码的基类 定义规范/能力
注:Java 8及以后版本允许接口包含默认方法(default方法)和静态方法。

十、综合示例:教材“银行与利息”案例

/*** 抽象类:银行账户*/
abstract class BankAccount {
String accountNo;
double balance;

BankAccount(String accountNo, double balance) {
    this.accountNo = accountNo;
    this.balance = balance;
}

// 抽象方法:计算利息
abstract double calculateInterest();

void showInfo() {
    System.out.println("账号:" + accountNo + ",余额:" + balance);
    System.out.println("利息:" + calculateInterest());
}

}

/*** 定期账户*/
class FixedAccount extends BankAccount {
double rate; // 利率

FixedAccount(String accountNo, double balance, double rate) {
    super(accountNo, balance);
    this.rate = rate;
}

@Override
double calculateInterest() {
    return balance * rate;
}

}

/*** 活期账户*/
class CurrentAccount extends BankAccount {
double rate;
int days; // 存款天数

CurrentAccount(String accountNo, double balance, double rate, int days) {
    super(accountNo, balance);
    this.rate = rate;
    this.days = days;
}

@Override
double calculateInterest() {
    return balance * rate * days / 365;
}

}

public class BankDemo {
public static void main(String[] args) {
BankAccount account1 = new FixedAccount(“1001”, 10000, 0.035);
BankAccount account2 = new CurrentAccount(“2001”, 10000, 0.003, 180);

    System.out.println("=== 定期账户 ===");
    account1.showInfo();
    
    System.out.println("\n=== 活期账户 ===");
    account2.showInfo();
}

}

AI写代码
java
运行

十一、常见错误与注意点

常见Java继承与接口错误对照表
错误写法 正确写法 关键说明
class Child extends Parent1, Parent2 class Child implements Interface1, Interface2 Java单继承限制,类只能继承一个父类,但可实现多个接口
子类构造方法未调用super() 显式调用super(参数) 父类不存在默认构造器时,子类必须显式调用父类构造器
误用@Override注解 保持方法签名完全一致 包括方法名、参数列表、返回类型,否则触发编译错误
抽象类遗漏抽象方法声明 使用abstract修饰无实现的方法 抽象方法必须以分号结尾,不能包含方法体
接口实现类未声明public 显式添加public修饰符 接口方法默认public,但实现类必须显式声明为public
典型问题深度解析
多继承限制 Java通过单继承+多接口实现解决菱形继承问题。当需要复用多个父类功能时,应改用组合模式或接口默认方法。

构造器链规则 子类构造器必须调用父类构造器,若未显式调用且父类无默认构造器,编译器报错。调用语句必须作为构造器首行代码。

注解校验机制 @Override严格校验重写规范,包括协变返回类型等细节。现代IDE通常能实时提示签名不匹配问题。

抽象方法特征 抽象类可以包含具体方法,但抽象方法必须由子类实现。注意与接口方法的区别:抽象方法可受保护,接口方法始终公开。

访问控制一致性 接口方法默认公开且不可降低可见性。实现类若省略public修饰符会导致访问权限收缩,违反里氏替换原则。

十二、小结

本章学习了:

  • 继承:子类继承父类,实现代码复用
    -方法重写:子类重新定义父类的方法
  • super关键字:访问父类的成员
  • 多态:父类引用指向子类对象
  • final关键字:防止继承、防止重写、定义常量
  • 抽象类:包含抽象方法的类,不能实例化
  • 接口:完全抽象的规范,支持多实现
  • Copyrights © 2026 Shipanxs
  • 访问人数: | 浏览次数:

如果这篇文章对你有帮助,可以请我喝杯茶

支付宝

支付宝扫一扫