简介:针对备忘录管理信息化需求开发的毕业设计文档,面向计算机相关专业学生及需要快速理解SSM框架+MySQL开发流程的开发者。内容涵盖系统用户管理、备忘录管理、日志管理、登录与退出等核心模块的设计思路,并完整呈现绪论、技术介绍、系统分析、测试等章节,可作为课程设计或毕业设计的参考模板。资源包仅含1个docx文档,大小690KB,已有78人学习。文档基于Java语言,采用Spring、SpringMVC、MyBatis三层架构,结合Eclipse工具与MySQL数据库,详细说明了从需求分析到系统测试的全过程,对比传统管理模式突出了信息化管理在效率与经济成本上的优势,有助于读者快速掌握SSM框架在小型管理系统中的实际应用。
1. 为什么“课程设计级”的备忘录系统,反而值得认真拆解
很多人看到“基于Java的备忘录管理系统”这种题目,第一反应是“这不就是个增删改查吗,有什么好写的”。这个反应我太熟悉了,因为我自己带过不少新人,也评审过大量类似的课设、毕设项目。但我想说的是,正因为这类系统看起来简单,它才是一个绝佳的试金石——它能检验你对Java基础、面向对象设计、数据持久化、异常处理、用户交互这一整条链路是否真的吃透了。
备忘录管理系统的核心价值不在于“记事”本身,而在于它几乎覆盖了Java后台开发的全部基本功:JavaBean的封装、集合框架的使用、文件或数据库的读写、分层架构的拆解、时间日期的处理、界面与逻辑的分离。这些能力是通用的,今天你把它用在备忘录上,明天换成图书管理、日程管理、个人财务,架构思路完全一致。
我在实战中见过两种极端:一种是纯面向过程的写法,所有逻辑堆在一个类里,界面代码和业务代码纠缠在一起,改一个按钮就要翻几百行代码;另一种是过度设计,为了一个500行的系统引入了Spring Boot、MyBatis、Maven多模块,结果环境配置花了两天,还没写一行业务代码。这两种都很可惜。这篇博文我会按一个“刚好合适”的粒度来拆解——既保留分层设计带来的可维护性,又不至于用大炮打蚊子。适合正在做课程设计、毕业设计,或者想用一个小项目把Java基础串起来的朋友参考。
2. 系统的功能定位与核心需求分析
2.1 备忘录系统“应该”做成什么样
在动手写代码之前,先想清楚系统边界。我见过太多人一上来就写代码,写到一半发现需求含糊,又开始返工。备忘录系统的核心功能其实非常聚焦,就三类:
- 记事:新增、编辑、删除备忘录条目,记录标题、正文、创建时间、最后修改时间。
- 分类与检索:按类别或关键词筛选备忘录,否则记事越来越多之后根本无法使用。
- 提醒与状态管理:给备忘录设置提醒时间或优先级,把“已完成”“待办”状态区分开。
从用户角度来说,一个能用的备忘录系统不需要花哨,但一定要逻辑闭环。新增的记录能查到,改过的内容能反映,删除之前有确认,搜索时能命中。这些流程打通之后,系统才是“可用”的——很多课程设计恰恰就败在这上面,功能按钮摆了一排,但数据流是断的:新增完去列表页看不到,改完状态刷新还是旧值。
2.2 典型用户画像与使用场景
我建议在需求分析阶段做一次用户场景的推演。比如用户在早上创建一个“下午三点给客户回电”的备忘,设置优先级为高,类别为工作;下午处理完以后,把状态改为已完成;晚上查看时能过滤“只看未完成事项”。这整个流程就是系统设计的验收标准。
这样的场景推导看起来很基础,但它的作用非常大:它会决定你的类怎么切、方法怎么命名、异常怎么处理。例如你会发现需要一条“查询所有未完成备忘”的方法,那代码结构里就必须有findByStatus这类接口,而不是每次查出来再在内存里循环过滤——后者在小数据量下能跑,但做法是错的,而且一旦数据量上来就暴露出性能问题。
2.3 技术选型:Swing还是JavaFX,文件还是数据库
技术选型是每个Java桌面项目绕不开的问题,而很多人都选错了。
界面方面,Java Swing和JavaFX是目前主流的两条路线。对于课程设计,我更推荐 Swing,理由很实际:学习资料多、教材覆盖率极高、JDK自带无需额外配置、代码示例随便一搜就有。JavaFX 虽然界面更现代,但它从JDK 11开始被剥离出标准JDK,需要单独引入依赖,对于只想专注业务逻辑的初学者反而多了一道坎。
数据存储方面,我建议分阶段:先做文件存储(如TXT或CSV),把序列化和IO读写练熟;再进阶到SQLite或H2嵌入式数据库。不要一上来就装MySQL,桌面级备忘录工具用独立的数据库服务器太重了,而且部署和移植都麻烦。SQLite是单文件数据库,Java里通过JDBC驱动就能操作,体验和文件存储几乎一样简单,但已经能把SQL语句练上手了。
架构方面,不管数据层用什么,分层是最重要的:界面层只负责展示和收集输入,业务层处理规则和流转,数据层管持久化。这样分层之后,你把文件存储换成数据库,界面和业务代码几乎不用动。这个收益在写代码时感受不明显,等需求变化、出bug排查时你就知道分层的价值了。
3. 核心模块设计与关键代码实现
3.1 实体层设计:备忘录对象该怎么建模
实体类是整个系统的地基,地基没打好,后面的代码处处别扭。我的建议是使用一个Memo类来承载核心数据,字段设计如下:
public class Memo implements Serializable { private Integer id; // 唯一标识,自增 private String title; // 标题,必填 private String content; // 正文内容 private String category; // 分类:工作/生活/学习等 private Integer priority; // 优先级:1高 2中 3低 private Integer status; // 状态:0待办 1已完成 private LocalDateTime createdAt; // 创建时间 private LocalDateTime remindAt; // 提醒时间,可为空 private LocalDateTime updatedAt; // 最后修改时间 }有几个设计细节值得展开说。第一,id字段建议用包装类型Integer而不是基本类型int,因为后面从数据源读取时可能为空,自动拆箱容易抛NullPointerException。第二,时间字段统一使用LocalDateTime而不是Date,LocalDateTime是Java 8引入的新时间API,线程安全且操作方法丰富,格式化解析也更直观。第三,priority和status我建议用int而不是直接存字符串,这样查询排序和过滤时比较方便,显示层再根据数值映射成文字。
实体类要重写toString()方法,方便开发阶段打印日志排查问题;同时实现Serializable接口,如果第一版用文件存对象,这个接口是必须的。这是很多资料里不会强调,但实际开发中非常实用的细节。
3.2 数据访问层:从文件存储到JDBC的完整演进
数据访问层我建议按两条路线来写,不是二选一,而是循序渐进。
第一版:文件存储,练序列化和IO。
public class MemoFileDao { private static final String FILE_PATH = "memos.dat"; public void save(List<Memo> memos) throws IOException { try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(FILE_PATH))) { oos.writeObject(memos); } } @SuppressWarnings("unchecked") public List<Memo> load() throws IOException, ClassNotFoundException { File file = new File(FILE_PATH); if (!file.exists()) { return new ArrayList<>(); } try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream(file))) { return (List<Memo>) ois.readObject(); } } }这段代码看着简单,但这套思路很关键:try-with-resources语法保证流自动关闭,避免内存泄漏;先判断文件是否存在再读取,避免首次运行抛异常;用ArrayList作为默认返回值,避免上层拿到null再去判空。这些都是实战中非常常见的坑。
第二版:SQLite数据库,练JDBC和SQL。
public class MemoDao { private Connection getConnection() throws SQLException { return DriverManager.getConnection("jdbc:sqlite:memo.db"); } public void createTable() { String sql = "CREATE TABLE IF NOT EXISTS memo (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "title TEXT NOT NULL," + "content TEXT," + "category TEXT," + "priority INTEGER DEFAULT 2," + "status INTEGER DEFAULT 0," + "created_at TEXT," + "remind_at TEXT," + "updated_at TEXT)"; try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { stmt.execute(sql); } catch (SQLException e) { e.printStackTrace(); } } }这里有一个特别容易踩的坑:SQLite没有专门的日期时间类型,所以时间字段用TEXT字符串存储。那么在写入和读取时就必须统一格式。我建议在DAO层做转换,用DateTimeFormatter.ISO_LOCAL_DATE_TIME格式化后再存入数据库,取出时再解析回LocalDateTime。如果有人在写入时不格式化,直接拼接对象toString,取出时就解析不回来。
再强调一个点:JDBC操作里PreparedStatement是必须的,千万不要用字符串拼接SQL来传参。既防SQL注入,又不用手动处理引号转义,代码还更清晰:
public void insert(Memo memo) { String sql = "INSERT INTO memo(title, content, category, priority, status, created_at, remind_at, updated_at) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, memo.getTitle()); ps.setString(2, memo.getContent()); ps.setString(3, memo.getCategory()); ps.setInt(4, memo.getPriority()); ps.setInt(5, memo.getStatus()); ps.setString(6, memo.getCreatedAt() == null ? null : memo.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(7, memo.getRemindAt() == null ? null : memo.getRemindAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.setString(8, memo.getUpdatedAt() == null ? null : memo.getUpdatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }注意两个细节:setInt和setString之后,如果字段为null,要用setNull或者像上面这样提前判空,否则LocalDateTime.format会直接空指针;执行增删改用executeUpdate(),执行查询用executeQuery(),这两个方法绝对不能混用。
3.3 业务层设计:把“规则”从界面中剥离出来
业务层是很多课设忽略的,但恰恰是拉开水平差距的地方。比如“删除备忘录时需要二次确认”“已完成状态不能修改内容只允许删除”“统计不同分类的数量”——这些都是业务规则,处理它们的代码应该统一放在MemoService,而不是散落在各个窗口的监听器里。
public class MemoService { private MemoDao memoDao; public MemoService() { this.memoDao = new MemoDao(); } public boolean addMemo(String title, String content, String category, int priority, LocalDateTime remindAt) { if (title == null || title.trim().isEmpty()) { throw new IllegalArgumentException("标题不能为空"); } Memo memo = new Memo(); memo.setTitle(title.trim()); // 其余字段set... memo.setStatus(0); memo.setCreatedAt(LocalDateTime.now()); memo.setUpdatedAt(LocalDateTime.now()); memoDao.insert(memo); return true; } public List<Memo> findUnfinished() { return memoDao.findByStatus(0); } }业务层的好处体现在两个场景。第一是参数校验的统一收敛:标题为空这种错误,不管用户是在哪个窗口按下保存都触发同一套校验逻辑,不会出现“主窗口能存空标题、编辑窗口就不行”这种诡异现象。第二是扩展的便利性:以后想加一个“到期备忘录自动标记为超期”的规则,只需在Service里增加一个方法,界面层完全不用改。
3.4 界面层设计:Java Swing布局要点与事件绑定
Swing界面层很容易写成一坨乱麻,核心原因是布局管理器没有选对。我的建议是主界面用BorderLayout:北边放搜索和筛选工具栏,中间放JTable列表,南边放操作按钮。编辑界面用GridBagLayout或GroupLayout,虽然这两个布局写起来繁琐,但效果和伸缩性远超null布局。
我这里要严厉警告一点:绝对不要用setLayout(null)+绝对坐标定位。这样做的后果是窗口一旦拉伸,控件全部挤在一起;在不同分辨率的屏幕上效果还会不同;后期加一个按钮,所有坐标全部要重新算。我看到过太多课设代码就是这么写的,虽然能截图交差,但实际一运行全是问题。
表格的构建是界面层的核心,建议使用DefaultTableModel包装数据:
DefaultTableModel model = new DefaultTableModel( new Object[]{"ID", "标题", "分类", "优先级", "状态", "提醒时间"}, 0); public void refreshTable(List<Memo> memos) { model.setRowCount(0); for (Memo m : memos) { model.addRow(new Object[]{ m.getId(), m.getTitle(), m.getCategory(), priorityText(m.getPriority()), statusText(m.getStatus()), m.getRemindAt() == null ? "—" : m.getRemindAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) }); } }表格刷新有个常见问题:直接model.setRowCount(0)再重新addRow,会闪烁得很厉害。数据量小的时候看不出来,一旦超过几百条,体验就很糟糕。印象中比较高效的做法是使用TableModel的事件机制,或者直接重新setModel一个新的DefaultTableModel,这两者刷新效率都更好。
事件绑定方面,每个按钮的ActionListener里只做三件事:收集界面输入、调用Service方法、刷新表格。不要在监听器里写任何业务逻辑,比如“判断标题是否为空”这种逻辑必须放到Service层。这样监听器基本就是三行代码:
saveBtn.addActionListener(e -> { try { service.addMemo(titleField.getText(), contentArea.getText(), categoryBox.getSelectedItem().toString(), priorityBox.getSelectedIndex() + 1, remindTimeField.getText().isEmpty() ? null : LocalDateTime.parse(remindTimeField.getText(), formatter)); refreshTable(service.findAll()); clearInputs(); } catch (IllegalArgumentException ex) { JOptionPane.showMessageDialog(this, ex.getMessage(), "提示", JOptionPane.WARNING_MESSAGE); } });4. 关键功能实现:提醒机制与事务边界
4.1 提醒功能的实时性与轮询方案取舍
备忘录要“有点用”,提醒功能是加分项。Swing是单线程模型,所有界面操作都在**EDT(事件分发线程)**上执行,所以在EDT里写死循环等待就会卡死界面。最朴素的方案是用一个定时器轮询数据库,看看有没有到点的未提醒备忘。
我的做法是使用javax.swing.Timer而不是java.util.Timer,原因在于Swing版本的回调方法会在EDT线程上执行,可以直接安全地更新界面,而无须手动SwingUtilities.invokeLater。示例代码如下:
Timer reminderTimer = new Timer(1000 * 30, e -> checkReminders()); reminderTimer.start(); private void checkReminders() { List<Memo> dueMemos = service.findDueAndUnnotified(); for (Memo memo : dueMemos) { JOptionPane.showMessageDialog(mainFrame, "提醒:" + memo.getTitle(), "备忘录提醒", JOptionPane.INFORMATION_MESSAGE); service.markNotified(memo.getId()); } }这里的Timer构造函数第一个参数是毫秒间隔,30秒检查一次即可满足大多数场景。不要设成1秒一次,纯属浪费CPU,而且弹窗频率过密会严重干扰使用。这里有个设计细节:service.findDueAndUnnotified()在数据库层对应的SQL是WHERE remind_at <= ? AND notified = 0,所以实体类中还需要加一个notified字段,或者复用status字段来标记“已提醒”。我的经验是单独加notified字段更清晰,免得状态语义混淆。
4.2 时间比较的精度问题:从毫秒到分钟
时间提醒有一个典型陷阱——LocalDateTime存的是纳秒精度,而界面展示和用户认知通常精确到分钟。你在数据库里存了2025-06-08T15:30:00,用户看到的是15:30,但在程序内部比较时会带上纳秒部分,导致两个“看起来一样”的时间equals返回false。
所以在解析用户输入时,我建议统一做一次truncatedTo(ChronoUnit.MINUTES)处理,把秒和纳秒全部截断。这个动作既能让时间的相等比较符合直觉,还能避免数据库里存出一堆莫名其妙的秒级时间数据:
LocalDateTime remindAt = LocalDateTime.parse(text, formatter) .truncatedTo(ChronoUnit.MINUTES);另外,提醒时间是过去的时间该怎么办?用户在新增备忘时已经把提醒时间设在昨天,这种数据存进去没有任何意义,还可能导致启动程序时瞬间弹出无数个历史提醒窗口。所以Service的addMemo方法里必须有这个校验:remindAt.isBefore(LocalDateTime.now())就抛IllegalArgumentException("提醒时间不能早于当前时间")。
4.3 删除操作的二次确认与事务边界
删除备忘录时,弹一个确认框是对用户的基本尊重。但真正容易忽略的是删除关联数据的事务边界。如果你的系统有“备忘录和提醒记录是两张表”的关系,删除备忘录时必须同时删除它对应的提醒记录,这两个操作必须在一个事务里完成。
JDBC事务的写法要牢记:先conn.setAutoCommit(false),然后执行多个SQL,全部成功再commit(),任何一步失败都要rollback(),最后在finally里恢复autoCommit并关闭连接。很多新人只会写单个SQL,不知道Connection默认是自动提交的,结果总以为两条SQL之间天然就是原子的,一旦第二条执行失败,数据就出现了脏数据。
不过话又说回来,对于备忘录管理系统这种体量的项目,单表就能解决问题,不一定要设计成多表关联。表拆分得越多,代码复杂度上升越明显,即使有事务兜底也要考虑值不值。我的建议是:把提醒状态作为备忘录表的一个字段放在同一张表里,这样根本不需要事务,一条UPDATE就能解决,简单可靠。
5. 踩过的坑:从运行环境到代码细节的实战排查
5.1 JDK环境变量配置的经典问题
界面、业务、数据层都写完以后,最尴尬的事情往往发生在运行环境上。很多同学的代码在IDE里点“运行”完全正常,但双击Jar包就报错,或者命令行java -jar启动不了。排查下来十有八九是环境变量的问题。
我在热词里看到“java环境变量配置详细教程”“java安装教程详细”是高频搜索,说明这确实是拦路虎。环境变量配置的核心只有两件事:JAVA_HOME指向JDK安装目录,Path里加上%JAVA_HOME%\bin。配置完以后在命令行输入java -version和javac -version验证,两个都能正常输出版本号才算成功。如果安装了多个JDK版本,一定要把高版本的Path条目移到前面,否则命令行用的还是旧版本。
还有一个容易忽略的坑:JDK和JRE混用。现在JDK自带了JRE,但如果你的机器上单独装了JRE,并且Path里JRE的bin排在JDK前面,那么java命令用的是JRE。而JRE没有javac,所以就会出现“能运行不能编译”的怪象。这属于排查了半天都找不出原因、最后发现就是路径顺序问题的典型case。
5.2 编译期“软件包不存在”与Lombok冲突问题
有很多人在项目里用了Lombok,@Data注解写得很爽,结果换一台电脑或者用命令行编译时就报“程序包lombok不存在”。这个问题我在热词里也看到了:“you aren't using a compiler supported by lombok, so lombok will not work”。这说明编译环境和Lombok的适配出了问题。
对于备忘录管理系统这个体量,我的建议是放弃Lombok,手写getter/setter。虽然多打个十几行字,但它省掉的麻烦是实打实的:不用装IDE插件、不用配注解处理器、不用担心编译环境之间的兼容性、生成的class文件也绝对不会有问题。一个课程设计项目,把精力花在环境适配和奇怪报错上,太不值得了。
如果非要用Lombok,至少确认两件事:IDEA里安装Lombok插件并开启Enable annotation processing;项目的编译方式用Maven或Gradle管理依赖,而不是手动引入jar包。否则在别人电脑上拉下来代码,编译报错分分钟让人崩溃。
还有一个坑和编码相关:Windows环境下命令行编译Java源文件时,如果代码里有中文,而源文件是UTF-8编码,直接javac编译很可能报“未结束的字符串文字”或乱码。解决方案是编译时显式指定:javac -encoding UTF-8 *.java。这个参数在IDE里通常默认配好了,但命令行编译时非常容易踩到,尤其是从Windows的cmd窗口直接操作时。备忘录系统里到处都是中文字符串,编码问题可以说是必修课。
5.3 JVM内存不足与数组越界的高发场景
“java: outofmemoryerror: insufficient memory”这个报错,我在查看JVM类项目时没少见过。备忘录系统虽然是轻量级应用,但如果你一次性把整个文件读进来处理,或者表格里的数据不翻页全量加载,数据量到达一定级别时同样能撑爆堆内存。
我的经验是,做桌面工具时养成两个习惯:一是在主函数入口给JVM设合理的初始堆大小,java -Xms64m -Xmx256m -jar memo.jar,预留比实际需求稍高的空间,但也不要无脑给几个G;二是数据加载尽量分批,比如查询列表时默认只加载前500条,用户搜索时再加条件,减少单次内存压力。
数组越界异常也高发在界面层。比如表格选择行时没有判断getSelectedRow()的返回值是不是-1,用户没选中任何行就直接点删除,代码就会崩。这个异常在明明“不可能”的情况下就发生了,因为在快速点击时,表格的选中状态可能已经变化了。
我的防御性写法是:所有从界面控件取值的地方,都先判空或判断下标范围:
int selectedRow = table.getSelectedRow(); if (selectedRow < 0) { JOptionPane.showMessageDialog(this, "请先选择一条记录", "提示", JOptionPane.WARNING_MESSAGE); return; }这种处理看起来啰嗦,但正是这些防御性判断,让你的程序从“能跑”变成“不会莫名其妙崩”。一个无人维护的课设项目,最怕的就是用户在没有任何提示的情况下看到程序异常退出,这种体验非常差。
6. 让系统更“值钱”的三个进阶方向
6.1 从文件存储升级到数据库的完整迁移
如果第一版用文件存储,升级到SQLite其实非常顺滑,因为DAO接口的两个实现对外暴露的方法是一致的。操作步骤无非是:新写一个满足相同方法签名的MemoDao实现类,把内部逻辑换成JDBC;再做一个数据迁移方法,读取原文件数据批量写入数据库。界面层和业务层的代码完全不用动。这就是分层设计带来的核心优势——数据源替换对上层透明。
迁移时要注意主键的连续性。文件存储时你可能自己用一个AtomicInteger生成id,但SQLite的自增主键是由数据库管理的。迁移时要么显式插入旧id,要么重新分配id。如果备忘之间有关联关系(比如提醒记录引用备忘id),就一定要保留旧id,否则关联就断了。备忘录系统通常没有这种关系,但培养这种意识很重要。
6.2 导出功能与Java反射的初步结合
给系统加一个“导出为CSV”功能,是性价比很高的一个加分项。用户可以把备忘录列表导出成表格文件,用Excel打开。CSV本身就是一个纯文本格式,每一行是按逗号分隔的字段。生成的要点有两个:字段中含有逗号或换行时要加双引号包裹;文件写入时用UTF-8 BOM,否则Windows下的Excel打开中文会乱码。
代码非常简单:
public void exportCsv(List<Memo> memos, Path target) throws IOException { try (BufferedWriter writer = Files.newBufferedWriter(target, StandardCharsets.UTF_8)) { writer.write('\uFEFF'); // UTF-8 BOM,防止Excel中文乱码 writer.write("ID,标题,分类,优先级,状态,提醒时间\n"); for (Memo m : memos) { writer.write(String.join(",", String.valueOf(m.getId()), escapeCsv(m.getTitle()), m.getCategory(), String.valueOf(m.getPriority()), String.valueOf(m.getStatus()), m.getRemindAt() == null ? "" : m.getRemindAt().toString() )); writer.newLine(); } } }escapeCsv这个辅助方法的逻辑是:如果字段包含逗号、双引号或换行,就用双引号包起来,并把字段内部的双引号替换成两个双引号。这个规则是CSV格式的标准,不处理就会出现导出文件错位。这个小功能可以引导你了解一种轻量级的数据交换格式,也为以后对接别的系统打下基础。
6.3 面向对象思想的进阶场:代码重构
备忘录系统写完之后,一个很好的自我训练方式是“重构”。重构不是改bug,而是不改变外部功能的前提下,让代码结构更合理。比如你发现Memo类的方法越写越多,可以按职责拆出MemoValidator类统一管理字段校验;比如你发现MemoService里同时处理了业务逻辑和提醒时间解析,可以把事件解析工具类抽出来。
这些重构动作,本质上都是在练习“识别坏味道、找到更优设计”的肌肉记忆。以后你面对真实的后台系统时,面对一堆屎山代码才不至于手足无措——因为你在备忘录这种小项目上已经积累过“化繁为简”的经验了。
根据我个人经验来说,完成一个备忘录系统最好的方式,不是闷头一天写完,而是先花半小时把类和接口的关系画在纸上,再动手代码;写完核心流程以后运行一次,观察哪里交互不顺手,再迭代一版。这个“设计-实现-反思-重构”的循环,才是Java基础真正长在身上的过程。系统本身做得好不好是其次,你在整个过程中建立起来的工程直觉,才是未来能带走的财富。
本文还有配套的精品资源,点击获取