本文还有配套的精品资源点击获取简介提供Text.java和TextTool.java两个可直接运行的Java源文件实现基础记事本功能文本输入、显示、复制、粘贴、剪切、清空、字体设置等常见操作基于Swing构建轻量级图形界面代码结构清晰、关键逻辑配有中文注释适配高校Java面向对象编程与GUI组件教学场景支持JDK 8及以上版本编译运行方便学生完成实验调试、功能验证及Swing事件处理机制理解。我带过好几届西南科大的Java实验课也帮不少同学调试过这个记事本项目。说实话光看目录里那两个.java文件名——Text.java 和 TextTool.java很多学生第一反应是“不就是个简单 Swing 窗口加个 JTextArea 吗抄完交作业得了。”结果一跑起来就卡在菜单栏不响应、复制粘贴失效、字体设置后文字乱码、甚至清空按钮点了没反应……最后发现问题根本不在“会不会写”而在于没真正理解 Swing 的事件分发机制、组件生命周期和线程安全边界。这个项目表面是做个记事本实则是西南科大 Java 实验教学中一个精心设计的“认知脚手架”它用最小可行代码覆盖了 GUI 编程四大核心断层——组件组装逻辑、事件监听绑定、模型-视图分离雏形、以及 Swing 线程约束下的状态同步。关键词里写的“Java记事本”“Swing GUI”“文本编辑实验”说白了就是三把钥匙第一把打开面向对象封装实践TextTool 是工具类Text 是主窗口职责必须分明第二把解锁 GUI 交互本质不是“点一下就执行”而是“事件触发→监听器响应→模型更新→视图刷新”的闭环第三把验证教学目标达成度能否独立补全“另存为”“打开文件”“行号显示”等扩展功能才是真掌握。我见过太多学生直接javac Text.java java Text就以为完成了结果连 CtrlC 都用不了——因为没搞懂 JTextArea 的默认快捷键绑定依赖于 ActionMap/InputMap 的预设配置而 TextTool 里自定义的 copy() 方法如果没正确调用textArea.copy()而是手动读取再写入剪贴板就会绕过 Swing 内置的焦点管理导致后续粘贴失效。这恰恰是实验课想让学生踩的“合理坑”不是为了难住你而是逼你去翻 Javadoc 查getSelectedText()和replaceSelection()的区别去理解为什么setText()和selectAll(); replaceSelection()在清空操作中行为不同。所以这篇分享不讲“怎么抄代码”而是带你从西南科大实验指导书第3页那个不起眼的要求出发——“要求使用内部类实现 ActionListener并在 TextTool 中封装所有编辑逻辑”——一层层拆开它的设计意图、实操陷阱和调试心法。无论你是刚学完继承多态、第一次碰 GUI 的大一新生还是被 Swing 线程模型搞晕、想补基础的重修同学这里给的都不是标准答案而是我在机房陪你们调通凌晨两点的代码时记在实验报告边角的真实笔记。1. 项目整体设计思路与教学意图解构1.1 为什么是 Text.java TextTool.java 的双文件结构西南科大实验大纲明确要求“体现面向对象职责分离思想”这个双文件结构绝非随意拆分而是对应着 MVC 模式中最简化的ViewText.java Controller/UtilityTextTool.java分离。我们先看典型错误做法把所有按钮监听、文本处理、菜单响应全塞进 Text 类里用一堆if (e.getSource() saveBtn)判断——这看似省事但完全违背实验核心目标。Text.java 的定位非常清晰纯界面容器与生命周期管理者。它只做四件事- 创建 JFrame 及其基础布局菜单栏、工具栏、主文本区、状态栏- 初始化所有 Swing 组件JMenuBar、JTextArea、JScrollPane 等- 注册监听器但监听器本身不写业务逻辑- 处理窗口关闭事件setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)而 TextTool.java 才是真正的“大脑”。它不继承任何 Swing 类也不持有 JFrame 引用而是通过构造函数接收 JTextArea 实例所有编辑操作都围绕这个“文本模型”展开。比如copy()方法签名一定是public void copy(JTextArea area)而不是public void copy()。这种设计强制学生思考操作对象是谁状态保存在哪方法调用是否破坏了封装性提示你在 Text.java 的initMenu()方法里看到new JMenuItem(复制)后紧接着item.addActionListener(new ActionListener(){...})这个匿名内部类里的copy()调用实际执行的是 TextTool 实例的方法而非 Text 自己的方法。这就是“控制反转”的初级实践——View 不决定怎么做只告诉 Controller “用户点了复制”。1.2 Swing GUI 构建中的教学关键点选择逻辑对比 NetBeans 或 IntelliJ 的 GUI Designer 自动生成的臃肿代码这个项目坚持手写 Swing是因为实验课要训练三个不可替代的能力第一组件嵌套关系的物理直觉。很多学生拖拽设计器时以为JScrollPane包裹JTextArea是“自动适配”其实必须显式调用scrollPane.setViewportView(textArea)。Text.java 里这段代码JScrollPane scrollPane new JScrollPane(textArea); contentPane.add(scrollPane, BorderLayout.CENTER);就是在强化“容器Container与内容Component的父子关系由代码显式建立”的概念。删掉setViewportView这一行运行后文本区就是空白的——这个报错不告诉你哪里错了只让你盯着空白界面发呆逼你查 API 文档确认JScrollPane的构造逻辑。第二事件监听的粒度控制意识。实验要求实现“CtrlC/V/X”快捷键但没要求你重写整个键盘事件链。正确做法是利用 Swing 内置的 InputMap/ActionMap 机制InputMap inputMap textArea.getInputMap(JComponent.WHEN_FOCUSED); ActionMap actionMap textArea.getActionMap(); inputMap.put(KeyStroke.getKeyStroke(control C), copy); actionMap.put(copy, new AbstractAction() { public void actionPerformed(ActionEvent e) { tool.copy(textArea); // 调用 TextTool 的方法 } });这里的关键教学点在于快捷键绑定必须作用于获得焦点的组件WHEN_FOCUSED且 Action 名称必须与 InputMap 中的 key 严格一致。学生常犯的错是把KeyStroke.getKeyStroke(ctrl C)写成CtrlC或漏掉WHEN_FOCUSED参数导致快捷键全局失效——这不是代码 bug而是对 Swing 事件作用域的理解偏差。第三字体设置的跨平台渲染一致性。setFont(new Font(微软雅黑, Font.PLAIN, 14))看似简单但实验环境Windows 10 教学机和学生本地Mac/Linux字体库不同。TextTool.java 里实际采用的是更鲁棒的方案GraphicsEnvironment ge GraphicsEnvironment.getLocalGraphicsEnvironment(); String[] fontNames ge.getAvailableFontFamilyNames(); // 优先尝试Microsoft YaHei, SimSun, Arial, Dialog Font baseFont new Font(Microsoft YaHei, Font.PLAIN, 14); if (!Arrays.asList(fontNames).contains(Microsoft YaHei)) { baseFont new Font(SimSun, Font.PLAIN, 14); } textArea.setFont(baseFont);这段代码出现在setFont()方法中目的就是让学生意识到GUI 开发不是写死参数而是要做运行时环境探测。这也是为什么实验报告里专门有一栏要求填写“在不同系统下字体显示效果差异及原因分析”。1.3 为什么刻意回避 JFileChooser 和文件 I/O实验指导书第5页明确写着“本次实验聚焦 GUI 交互逻辑与文本编辑状态管理文件持久化功能留待后续 IO 实验拓展。” 这个取舍极具教学智慧。如果过早引入JFileChooser.showOpenDialog()学生会陷入路径解析、字符编码GBK vs UTF-8、异常捕获FileNotFoundException等 IO 细节反而冲淡对 Swing 事件流主线的理解。我观察到当学生强行在 Text.java 里加openFile()方法时90% 的人会写出这样的代码// ❌ 错误示范在事件监听器里直接读文件并 setText JFileChooser fc new JFileChooser(); if (fc.showOpenDialog(this) JFileChooser.APPROVE_OPTION) { String path fc.getSelectedFile().getPath(); String content Files.readString(Paths.get(path)); // JDK11 textArea.setText(content); // 此处可能阻塞 UI 线程 }问题在于Files.readString()是同步阻塞调用若文件较大或磁盘慢整个 GUI 会卡死。而实验课此时还没讲SwingWorker这就是故意埋的伏笔——让你在“能跑”和“能用”之间感受到差距为下一次课的“后台任务与 UI 更新”主题做认知铺垫。2. 核心文件逐行解析与关键逻辑深挖2.1 Text.java窗口骨架与事件注册的精密组装Text.java 的核心价值不在功能而在结构范式。我们以 JDK 8 兼容性为前提逐段拆解其不可简化的部分构造函数中的线程安全初始化public Text() { SwingUtilities.invokeLater(() - { initUI(); setVisible(true); }); }这是几乎所有合格 Swing 应用的起点。SwingUtilities.invokeLater()确保所有组件创建和显示都在 Event Dispatch ThreadEDT上执行。学生常忽略这点直接在 main 方法里 new Text()结果在某些 JDK 版本下菜单栏渲染异常。西南科大机房用的 OpenJDK 8u292这个问题尤为明显——因为 EDT 未启动时JMenuBar的add()方法可能静默失败。initUI() 方法的布局契约private void initUI() { setTitle(西南科大Java实验记事本); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { if (tool.hasUnsavedChanges(textArea)) { int result JOptionPane.showConfirmDialog( Text.this, 文档未保存是否退出, 确认退出, JOptionPane.YES_NO_CANCEL_OPTION ); if (result JOptionPane.YES_OPTION) { System.exit(0); } else if (result JOptionPane.CANCEL_OPTION) { return; // 取消退出 } } else { System.exit(0); } } }); setLayout(new BorderLayout()); // ... 后续组件添加 }注意setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)这一行。它关闭了 Swing 默认的退出策略强制你手动处理窗口关闭逻辑。这是为了训练“资源清理意识”——虽然本实验没涉及文件句柄但hasUnsavedChanges()方法已在 TextTool 中预留接口为后续实验埋下扩展钩子。JOptionPane的模态对话框调用也必须在 EDT 中执行否则可能出现界面假死。菜单栏构建中的 Action 抽象private void initMenu() { JMenuBar menuBar new JMenuBar(); JMenu fileMenu new JMenu(文件); JMenuItem newMenuItem new JMenuItem(新建); newMenuItem.setAccelerator(KeyStroke.getKeyStroke(control N)); newMenuItem.addActionListener(e - tool.newDocument(textArea)); fileMenu.add(newMenuItem); // ... 其他菜单项 menuBar.add(fileMenu); setJMenuBar(menuBar); }这里有两个易错点1.setAccelerator()必须在addActionListener()之前调用否则快捷键注册失败2.tool.newDocument(textArea)的参数传递体现了“数据驱动 UI”的思想——TextTool 不知道窗口存在只负责修改传入的 JTextArea 实例的状态。2.2 TextTool.java文本编辑逻辑的原子化封装TextTool.java 是整个项目的“算法内核”所有方法都遵循单一职责 无副作用原则。我们重点剖析四个高频出错方法copy() 方法的剪贴板协议实现public void copy(JTextArea area) { if (area.getSelectedText() ! null !area.getSelectedText().trim().isEmpty()) { StringSelection selection new StringSelection(area.getSelectedText()); Clipboard clipboard Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(selection, null); } }关键细节- 必须检查getSelectedText() ! null因为未选中文本时返回 null直接传给StringSelection会抛NullPointerException-clipboard.setContents(selection, null)的第二个参数是ClipboardOwner实验中设为null表示不监听剪贴板内容变更符合教学简化需求- 学生常误用area.getText()全文复制这违反“所见即所得”原则——用户只选中了3个字不该复制整篇。paste() 方法的焦点与插入点协同public void paste(JTextArea area) { Clipboard clipboard Toolkit.getDefaultToolkit().getSystemClipboard(); try { Transferable transferable clipboard.getContents(null); if (transferable ! null transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) { String content (String) transferable.getTransferData(DataFlavor.stringFlavor); int caretPos area.getCaretPosition(); area.insert(content, caretPos); } } catch (Exception e) { JOptionPane.showMessageDialog(null, 粘贴失败 e.getMessage()); } }这里area.insert(content, caretPos)比area.replaceSelection(content)更精确前者在光标位置插入后者替换当前选中区域。当用户未选中任何文本时replaceSelection会插入到光标处行为一致但当有选中文本时insert会把新内容插在选中区域前而replaceSelection会覆盖选中区域——实验要求“粘贴即替换选中内容”所以此处应为replaceSelection。这个细节正是实验报告里“分析 insert() 与 replaceSelection() 行为差异”的来源。setFont() 方法的字体族回退机制public void setFont(JTextArea area, String fontFamily, int size) { GraphicsEnvironment ge GraphicsEnvironment.getLocalGraphicsEnvironment(); String[] availableFonts ge.getAvailableFontFamilyNames(); // 优先级列表中文字体 英文字体 通用字体 String targetFont Dialog; if (Arrays.asList(availableFonts).contains(fontFamily)) { targetFont fontFamily; } else if (fontFamily.equals(微软雅黑) Arrays.asList(availableFonts).contains(Microsoft YaHei)) { targetFont Microsoft YaHei; } else if (fontFamily.equals(宋体) Arrays.asList(availableFonts).contains(SimSun)) { targetFont SimSun; } Font font new Font(targetFont, Font.PLAIN, size); area.setFont(font); }这段代码解决了 Windows 教学机与学生 Mac 笔记本的字体兼容问题。GraphicsEnvironment的getAvailableFontFamilyNames()返回的是运行时真实可用的字体名而非字符串字面量。学生常直接写new Font(微软雅黑, ...)在 Mac 上必然 fallback 到Dialog字体导致中文显示为方块——这正是实验要求“记录不同平台字体渲染效果”的实践依据。hasUnsavedChanges() 方法的状态快照设计private String lastSavedContent ; public void markAsSaved(JTextArea area) { lastSavedContent area.getText(); } public boolean hasUnsavedChanges(JTextArea area) { String current area.getText(); return !Objects.equals(current, lastSavedContent); }这个设计巧妙避开了复杂的文本差异比对。markAsSaved()在“新建”“打开”“保存”后调用将当前文本快照存入lastSavedContenthasUnsavedChanges()仅做字符串引用比较Objects.equals处理 null 安全。虽然对超大文本效率不高但完全满足实验场景通常 10KB。更重要的是它让学生直观理解“状态快照”概念——后续学 Git 时这个lastSavedContent就是工作区与暂存区的朴素映射。3. 实操过程详解从编译到功能验证的完整链路3.1 环境准备与 JDK 兼容性实测西南科大实验环境统一为OpenJDK 8u292Windows 10 x64但学生本地可能是 JDK 11/17/21。我们必须验证跨版本兼容性编译阶段# ✅ 推荐命令显式指定源码版本 javac -source 8 -target 8 Text.java TextTool.java # ❌ 危险操作依赖默认版本 javac Text.java TextTool.java # 若本地是 JDK 17可能生成 class 文件版本 61无法在 JDK 8 运行-source 8 -target 8参数强制编译器生成 JDK 8 兼容字节码。实测发现若用 JDK 17 编译但不加此参数生成的.class文件在机房 JDK 8 环境下会报UnsupportedClassVersionError——这是学生提交作业后机房运行失败的头号原因。运行阶段# ✅ 安全启动避免中文路径乱码 java -Dfile.encodingUTF-8 Text # ❌ 隐患启动Windows 默认 GBK可能导致 JOptionPane 中文乱码 java Text-Dfile.encodingUTF-8设置 JVM 默认字符集。西南科大机房的系统 locale 是中文GBK但实验代码中所有字符串字面量都是 UTF-8 编码若不显式指定JOptionPane的消息框可能出现“????”乱码。这个参数必须写在实验报告“运行环境说明”栏。3.2 功能验证 checklist 与调试技巧按实验指导书要求需完成以下 8 项基础功能验证。我整理了每个功能的必测场景、常见失败现象、底层原因及快速修复法功能必测场景常见失败现象底层原因修复要点文本输入输入含中文、英文、数字、符号的混合字符串输入中文时卡顿、光标跳动异常JTextArea 默认字体不支持中文fallback 到等宽字体导致渲染延迟在initUI()中textArea.setFont(...)显式设置中文字体复制CtrlC选中部分文本后按 CtrlC再按 CtrlV粘贴内容为空或乱码copy()方法未检查getSelectedText()是否为 null或剪贴板内容类型不匹配在copy()中增加if (selected ! null !selected.isEmpty())判断剪切CtrlX选中文本后剪切再在另一位置粘贴剪切后原位置未清空cut()方法调用了copy()但遗漏replaceSelection()cut()必须包含copy(area); area.replaceSelection();两步清空菜单栏点击“编辑→清空”菜单项界面无反应文本仍在监听器未正确绑定到菜单项或actionPerformed中未调用tool.clear(area)检查initMenu()中clearItem.addActionListener(...)是否指向正确方法字体设置在字体对话框选择“微软雅黑”字号 16中文显示为方块英文正常系统未安装“微软雅黑”new Font(微软雅黑,...)创建失败fallback 字体不支持中文改用GraphicsEnvironment动态探测可用字体见 2.2 节代码窗口关闭修改文本后直接点右上角 ×无提示直接退出setDefaultCloseOperation()设为EXIT_ON_CLOSE未注册WindowListener必须设为DO_NOTHING_ON_CLOSE并手动处理windowClosing事件新建文档点击“文件→新建”窗口标题不变文本区未清空tool.newDocument(area)方法未调用area.setText()和tool.markAsSaved(area)newDocument()必须重置文本内容并更新保存状态快照状态栏更新输入任意字符状态栏字数统计不变化DocumentListener未正确添加到textArea.getDocument()在initUI()中textArea.getDocument().addDocumentListener(...)调试技巧实时监控文本状态变化很多学生抱怨“不知道文本内容有没有真的变”推荐在 Text.java 中加入简易状态监控// 在 initUI() 末尾添加 textArea.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { updateStatus(); } public void insertUpdate(DocumentEvent e) { updateStatus(); } public void removeUpdate(DocumentEvent e) { updateStatus(); } private void updateStatus() { int length textArea.getText().length(); int lines textArea.getLineCount(); statusLabel.setText(String.format(字符数%d 行数%d, length, lines)); } });这段代码让状态栏实时显示字数和行数既是调试利器也是理解DocumentListener事件机制的活教材。3.3 实验报告关键项撰写指南西南科大实验报告有三项强制内容学生常因理解偏差丢分“面向对象设计分析”栏不能只写“Text 是窗口TextTool 是工具类”。必须指出- Text 类的tool成员变量是依赖注入Dependency Injection的体现而非在内部 new TextTool- TextTool 的所有方法都接受 JTextArea 作为参数实现了策略模式Strategy Pattern的雏形——未来可轻松替换为 RichTextTool 或 MarkdownTool-hasUnsavedChanges()方法通过lastSavedContent字符串快照模拟了Memento 模式的轻量实现。“Swing 事件处理机制理解”栏需结合代码说明三层响应链1.硬件层键盘按下 CtrlC → 系统生成 KeyEvent2.Swing 层JTextArea 的 InputMap 将 KeyEvent 映射为 Action 名如 “copy”→ ActionMap 执行对应 AbstractAction3.应用层AbstractAction 的actionPerformed()调用 TextTool.copy(area)完成业务逻辑。强调SwingUtilities.invokeLater()保证了第2、3层始终在 EDT 执行避免多线程并发修改 UI 组件。“扩展功能设计思路”栏加分项推荐三个低侵入性扩展-行号显示在 JScrollPane 左侧添加 JList监听DocumentListener的insertUpdate事件动态更新行号列表-查找替换新增 JDialog使用textArea.getText().indexOf(searchText)实现基础查找注意处理大小写和全字匹配-语法高亮为 JTextArea 设置StyledDocument在DocumentListener中对特定关键词如public、class应用不同颜色 AttributeSet。4. 常见问题与排查技巧实录4.1 编译与运行期高频问题速查表问题现象错误日志关键词根本原因解决方案验证方式运行时报UnsupportedClassVersionErrorUnsupported major.minor version 61.0用 JDK 17 编译生成了 class 文件版本 61但目标环境是 JDK 8版本 52用javac -source 8 -target 8 *.java重新编译javap -verbose Text.class \| grep major查看 class 版本号菜单栏显示为方块或空白无异常日志界面渲染异常系统字体缺失new Font(微软雅黑,...)创建失败fallback 字体不支持中文在setFont()方法中加入GraphicsEnvironment探测逻辑见 2.2 节在setFont()中添加System.out.println(实际使用字体 area.getFont().getFamily());CtrlC/V 快捷键完全无效无日志操作无响应setAccelerator()调用顺序错误或 InputMap 未绑定到WHEN_FOCUSED确保item.setAccelerator(...)在addActionListener(...)之前检查getInputMap(JComponent.WHEN_FOCUSED)在actionPerformed中加System.out.println(触发了复制)日志粘贴后文本乱码如“涓枃”无异常显示异常字符JVM 默认编码与系统 locale 不一致JTextArea读取剪贴板时编码错误启动时加-Dfile.encodingUTF-8参数在paste()方法中System.out.println(粘贴内容长度 content.length())点击“清空”菜单无反应无日志界面无变化clearItem.addActionListener()绑定的监听器未正确调用tool.clear(area)检查initMenu()中clearItem.addActionListener(e - tool.clear(textArea))是否存在在tool.clear()方法首行加System.out.println(正在清空)4.2 GUI 渲染异常的底层诊断法当遇到“界面部分组件不显示”“按钮点击无反馈”等玄学问题不要急着重启 IDE按以下步骤逐层排查第一步确认 EDT 执行上下文在initUI()开头加入System.out.println(initUI 执行线程 Thread.currentThread().getName()); System.out.println(是否为 EDT SwingUtilities.isEventDispatchThread());如果输出false说明你没用invokeLater()包裹必须重构入口。第二步验证组件可见性链路Swing 组件显示需满足三个条件1. 组件自身setVisible(true)2. 所有父容器setVisible(true)3. 组件已添加到某个Container且该容器已validate()。在initUI()末尾加System.out.println(textArea 可见 textArea.isVisible()); System.out.println(scrollPane 可见 scrollPane.isVisible()); System.out.println(contentPane 是否有效 getContentPane().isValid());第三步检查布局管理器冲突JTextArea必须放在JScrollPane中且JScrollPane必须用BorderLayout.CENTER添加到contentPane。常见错误是// ❌ 错误用 FlowLayout 导致 JTextArea 被压缩为 0x0 contentPane.setLayout(new FlowLayout()); contentPane.add(scrollPane); // ✅ 正确BorderLayout 是 JFrame contentPane 的默认布局 contentPane.add(scrollPane, BorderLayout.CENTER);4.3 学生实操中踩过的 5 个典型坑坑1在actionPerformed()中直接System.exit(0)现象点击“退出”菜单后程序终止但未执行windowClosing中的保存提示。原因System.exit(0)是暴力终止绕过了WindowListener的回调。正解在windowClosing中处理退出逻辑菜单项的监听器应调用dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING))触发标准流程。坑2setFont()后文本区内容消失现象设置字体后原有文本不见了。原因JTextArea.setFont()会重置Document的默认属性若未显式设置Document的字体渲染引擎可能丢失样式。正解在setFont()后立即调用textArea.repaint()强制重绘或改用textArea.setComponentPopupMenu(...)等不影响 Document 的方式。坑3JOptionPane对话框不弹出现象调用showMessageDialog()后界面卡住无对话框。原因JOptionPane必须在 EDT 中调用若在非 EDT 线程如Timer回调中调用会静默失败。正解包装为SwingUtilities.invokeLater(() - JOptionPane.showMessageDialog(...))。坑4复制粘贴后光标位置错乱现象粘贴后光标跳到文本开头或结尾。原因textArea.insert(content, caretPos)插入后光标位置未同步更新。正解插入后手动设置光标textArea.setCaretPosition(caretPos content.length())。坑5hasUnsavedChanges()始终返回 true现象新建文档后关闭提示仍说“未保存”。原因markAsSaved()未在newDocument()后调用lastSavedContent仍是空字符串而textArea.getText()返回空字符串Objects.equals(, )为 true —— 等等这应该返回 false真相newDocument()中area.setText()后area.getText()返回lastSavedContent也是所以equals返回 truehasUnsavedChanges返回 false。但如果学生在newDocument()中忘了tool.markAsSaved(area)lastSavedContent仍是旧值就会永远返回 true。正解在newDocument()方法末尾强制调用tool.markAsSaved(area)。5. 教学延伸与能力迁移建议这个记事本项目真正的价值不在于它能做什么而在于它如何成为你后续学习的“认知支点”。我在带实验时会引导学生做三件事第一把 TextTool.java 当作“可插拔引擎”来替换。试着写一个MarkdownTextTool让它在setText()时自动将**粗体**渲染为加粗文本。你会发现只需修改setText()方法其他所有菜单项复制、粘贴、清空依然可用——这就是面向接口编程的力量。Text.java 完全不知道自己在显示普通文本还是 Markdown它只认JTextArea这个契约。第二在 Text.java 中注入日志框架。用java.util.logging.Logger替代System.out.println在每个actionPerformed()开头加logger.info(用户触发复制操作)。然后配置logging.properties让所有操作日志输出到notepad.log文件。这一步把 GUI 项目带入了工程化开发语境为后续学 Spring Boot 的 AOP 埋下伏笔。第三用 JUnit 为 TextTool.java 写单元测试。别被“GUI 不能测试”吓住。TextTool 的所有方法都不依赖 Swing 组件实例只依赖JTextArea的文本内容。你可以用 Mockito 模拟一个JTextAreaTest public void testCopyWhenTextSelected() { JTextArea mockArea mock(JTextArea.class); when(mockArea.getSelectedText()).thenReturn(Hello); tool.copy(mockArea); // 验证剪贴板是否设置了正确内容需用 PowerMockito 拦截静态方法 }这教会你真正的可测试性来自逻辑与 UI 的彻底解耦——而这正是 TextTool.java 存在的根本意义。最后分享个小技巧下次调试时别只盯着代码。打开 Windows 任务管理器切换到“性能”选项卡观察“Java(TM) Platform SE binary”进程的 CPU 和内存曲线。当你疯狂点击“清空”按钮时如果内存持续上涨不回落说明DocumentListener没有被正确移除存在内存泄漏。这个视角会让你真正理解“GUI 编程不只是画界面更是资源生命周期管理”。这个记事本是你和 Java GUI 的第一次深度握手。它不华丽但每行代码都在教你一件事好的设计是让复杂性沉在水下浮出水面的只有清晰的接口与可靠的契约。本文还有配套的精品资源点击获取简介提供Text.java和TextTool.java两个可直接运行的Java源文件实现基础记事本功能文本输入、显示、复制、粘贴、剪切、清空、字体设置等常见操作基于Swing构建轻量级图形界面代码结构清晰、关键逻辑配有中文注释适配高校Java面向对象编程与GUI组件教学场景支持JDK 8及以上版本编译运行方便学生完成实验调试、功能验证及Swing事件处理机制理解。本文还有配套的精品资源点击获取