简介:一份面向高校Java课程设计的通讯录管理系统完整源码,采用Java Swing、JDBC与MySQL技术,实现用户注册登录和通讯录信息浏览、查找、增删改等功能,适合学习Java面向对象编程、数据库连接及桌面应用开发的读者参考实践。压缩包共5个文件,含4个Java源文件与1个SQL数据库脚本:Java文件覆盖登录窗体、注册窗体、通讯录主界面及数据库工具类,SQL脚本可直接导入MySQL完成建表,整套资源仅8KB,便于快速部署到主流IDE中运行。目前已有1562人学习下载,通过源码可直观学习窗口组件布局、事件监听与响应、参数化查询等关键技巧,并体会模块化拆分与复用思想。对照项目代码和SQL语句,还能深入理解用户表、联系人表的设计方法,掌握密码校验、联系人增删改查的完整业务闭环,并可在现有基础上扩展分组管理、模糊搜索等功能,是一份能助力课程设计快速落地、也能支撑答辩讲解的实用资料。
1. 需求是先立住的:通讯录管理系统到底要解决什么问题
很多同学拿到「java课程设计:通讯录管理系统」这个题,第一反应是「不就是个增删改查吗」,然后照着教材把 JTable、文本框、按钮拼在一起,数据库里建一张表,能跑起来就去交差了。结果答辩时老师随口问一句「你的联系人资料怎么防止输错手机号」「连续录一千条数据界面会不会卡死」,就卡住了。这套方案把通讯录管理系统做成一份能完整交付的 Java 课程设计,从需求分析、数据库建模、三层架构拆分,到 Swing 界面落地、模糊搜索、vCard 导入导出,再到验收用例和答辩问题,全部对得上,适合正在做 Java 课设、不想只交「玩具 Demo」的同学直接照着搭。
2. 总体架构设计:三层结构与核心数据模块的职责划分
2.1 技术选型:界面用 Swing,数据用 MySQL,为什么不纠结
Java 课程设计最常见的界面技术就三类:Swing、JavaFX、纯控制台。我的建议很直接:默认选 Swing。原因是课程里教的、教材里讲的、老师答辩时问的,基本都是 Swing 那套组件体系,你上手最快,环境配起来也最简单,哪怕实验室机器没装额外运行时也能跑。JavaFX 不是不行,但它引入的 scene、controller、FXML 概念,在一个课设项目里讲解成本高,而且很多课程大纲根本没提过,答辩时反而容易暴露你没吃透的点。
数据层我建议主方案用 MySQL,而不是直接用文件存储或者 SQLite。虽然 SQLite 零配置、拷走就能跑,但多数课程设计明确要求「使用数据库」,老师想看的其实是 JDBC 连接、建库建表、SQL 语句、事务控制这些数据库课里的知识点。用 MySQL 能把这些得分点全部覆盖。备一个 SQLite 版本作为保底也行,但如果时间只够做一个,优先 MySQL。
| 选型维度 | Swing + MySQL(推荐) | JavaFX + SQLite | 纯文件存储 |
|---|---|---|---|
| 学习成本 | 低,教材全覆盖 | 中,需要学 FXML | 最低 |
| 答辩得分点 | JDBC、SQL、事务都能讲 | 界面技术新颖 | 基本没有 |
| 部署环境 | 需要本机 MySQL 服务 | 依赖 JDK 较高版本 | 无依赖 |
| 演示风险 | 数据库服务没启动会翻车 | 配置路径容易踩坑 | 数据文件损坏全丢 |
选型上和同学做区分的一个点是:大多数人的通讯录课设只有一张联系人表,我建议把「分组」抽成独立表,再预留一张「操作日志」表。这会在答辩时让架构显得更完整,也更接近真实项目的建模思路。
2.2 包结构设计:界面层、业务层、数据层各管一件事
我见过不少课设把全部代码塞进一个文件里,听着能跑,答辩时老师问「改个电话号码格式校验要去哪个文件改」,你只能现场 Ctrl+F 搜半天。正确做法是分层分包,哪怕项目小,也要让「界面代码」和「数据库代码」不混在一个类里。下面是我一般会用的包结构:
com.contact.demo ├── ui/ // 界面层:主窗体、添加/编辑对话框、表格模型 │ ├── MainFrame.java │ ├── ContactDialog.java │ └── ContactTableModel.java ├── service/ // 业务层:校验逻辑、分组管理、导入导出 │ ├── ContactService.java │ └── VCardHelper.java ├── dao/ // 数据层:JDBC 操作 │ ├── ContactDAO.java │ ├── ContactDAOImpl.java │ └── GroupDAO.java ├── entity/ // 实体类 │ ├── Contact.java │ └── ContactGroup.java ├── util/ │ └── DBHelper.java // 数据库连接工具 └── Main.java // 入口为什么要分层?不是为了炫技,是为了让你在被问到「某个功能怎么实现的」时能直接说出「点按钮触发了 service 层的某方法,service 调用 dao 层的接口,dao 里写具体 SQL」这种清晰的调用链。答辩老师判断你有没有做过「带结构」的项目,就看你分不分组、接口和实现类分不分。DAO 写成接口 + 实现类也是同样的道理:实现类你写 JDBC,如果哪天想换成 JPA 「假实现」,接口不用动。
2.3 数据库设计:两张核心表加一张日志表的字段设计
通讯录系统里和「人」相关的字段不难想:姓名、电话、邮箱、单位、备注。关键是「分组」怎么建表。我的做法是单独建 contact_group 表,联系人表里只存 group_id,不存分组名的字符串,这样分组的改名、合并都只需要操作一条分组记录,联系人数据不用批量更新。这是课程里讲过的范式设计的实际落地,答辩时能顺带讲一句「冗余的分组名字符串会导致数据不一致」。
CREATE DATABASE contact_db DEFAULT CHARACTER SET utf8mb4; USE contact_db; CREATE TABLE contact_group ( id INT PRIMARY KEY AUTO_INCREMENT, group_name VARCHAR(50) NOT NULL UNIQUE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE contact ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100), work_unit VARCHAR(100), group_id INT, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_contact_group FOREIGN KEY (group_id) REFERENCES contact_group(id) ) ENGINE=InnoDB;数据库建在 utf8mb4 下,是从源头解决中文乱码的关键一步,后面表格里存「张伟」这样的名字才不会变成问号。phone 字段我定义成 VARCHAR(20),别用 BIGINT,因为手机号可能带「+86」前缀,也可能出现座机号里的短横线,纯数字类型会把这些合法格式都弄丢。create_time 和 update_time 是给演示用的:排序时按 create_time 倒序就能展示「最近添加的联系人」,这也是一个答辩时可以演示的实用功能。
3. 核心业务层实现:增删改查与关键字搜索的完整链路
3.1 实体类与 DAO 接口:先把数据模型钉死
实体类就是一张表的 Java 映射。Contact 类里每个成员变量对应 contact 表的一个字段,类型对得上。注意 create_time 和 update_time 在数据库是 DATETIME 类型,Java 侧用 java.time.LocalDateTime 来接,别再用老的 java.util.Date,前者处理格式化更顺手,也能顺便体现你的 JDK 版本不是停留在教材年代。
DAO 接口先把方法签约定下来:新增、按 ID 删除、更新、按 ID 查询、按关键字查列表、查全部分组。这样 service 层依赖的是接口而不是具体实现,后面如果要把 JDBC 换成别的持久化方式,service 层完全不用动。
public interface ContactDAO { int insert(Contact contact); int update(Contact contact); int deleteById(Integer id); Contact findById(Integer id); List<Contact> search(String keyword); List<Contact> findAll(); }接口定完再写实现类 ContactDAOImpl,在实现类里统一通过 DBHelper 拿连接、PreparedStatement 执行 SQL、处理结果集。为什么连接放在 util 包里单独管理?因为增删改查每个方法都要拿连接、关连接,抽一个工具类出来能省大量重复代码,以后换连接池也只需要改这一个类。
3.2 增删改查落地:PreparedStatement 的常规写法和防注入细节
增删改查的「查」写起来最讲究。很多人图省事用 Statement 拼字符串:"SELECT * FROM contact WHERE name = '" + keyword + "'"。这在课设答辩时属于送命题——老师只要问「如果 keyword 里传一个' OR '1'='1会怎样」,就暴露了 SQL 注入的漏洞。正确做法是全程用 PreparedStatement,参数用占位符传进去,数据库驱动会处理转义。
public List<Contact> search(String keyword) { String sql = "SELECT * FROM contact WHERE name LIKE ? " + "OR phone LIKE ? OR work_unit LIKE ? ORDER BY update_time DESC"; List<Contact> list = new ArrayList<>(); try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这段代码里有三个细节值得记住。一是 try-with-resources,连接、PreparedStatement、ResultSet 都实现了 AutoCloseable,自动关闭,不会出现连接没释放把数据库连接池耗尽的翻车现场。二是%通配符放在参数里而不是拼在 SQL 里,这样数据库看到的永远是一条参数化的语句。三是 mapRow 这个方法单独抽出来负责「结果集一行 → Contact 对象」的映射,它复用率高,后面 findAll 和 findById 都靠它。
增删改的写法同理,deleteById 用DELETE FROM contact WHERE id = ?,update 用UPDATE contact SET name = ?, phone = ?, group_id = ? WHERE id = ?,注意 set 的索引从 1 开始,顺序必须和 SQL 里的占位符一致。这里有一半人第一次写容易索引越界,报错时先检查占位符位置对不对。
3.3 模糊搜索:一个坑在于 LIKE 的索引失效
搜索接口设计的核心是「多个字段同时模糊匹配」。只搜姓名过于呆板,正常的通讯录使用场景是:用户只记一个片段,「王」可能出现在姓名里,也可能出现在单位名「王庄卫生院」里。所以 search 方法里我用 OR 把三个字段串起来,覆盖姓名、电话、单位,演示时随便输一个「13」能搜出一堆号码含 13 的人,效果非常直观。
代价是模糊搜索的性能。LIKE '%关键词%'这种前导通配符写法,数据库索引基本用不上,会做全表扫描。数据量小(几千条)无所谓,但如果验收时的测试数据插了一万条,搜索响应会有可感知的延迟。我的处理方式是:课设阶段不做过度优化,在答辩时说清楚「这个查询数据量临界点大概在十万条级别,超过之后可以考虑改造成前缀匹配或者引入全文索引」,这段话比闷头把 SQL 改复杂更得分,因为它体现了你知道这个写法的边界在哪。
3.4 导入导出:vCard 格式让方案多一个可演示的加分点
大多数人的课设做到增删改查就停了,我在方案里加了 vCard 导入导出,理由很实际:通讯录系统最典型的迁移场景就是「把旧手机里的联系人导进来」。vCard 是通用的联系人交换格式,以BEGIN:VCARD开头、END:VCARD结尾,里面的行是FN:(姓名)、TEL:(电话),字段简单、纯文本、可解释性强,比导出 Excel 更「真」。
public static void exportToVCard(List<Contact> contacts, File file) throws IOException { try (BufferedWriter writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { for (Contact c : contacts) { writer.write("BEGIN:VCARD"); writer.newLine(); writer.write("VERSION:3.0"); writer.newLine(); writer.write("FN:" + c.getName()); writer.newLine(); writer.write("TEL;TYPE=CELL:" + c.getPhone()); writer.newLine(); if (c.getEmail() != null && !c.getEmail().isEmpty()) { writer.write("EMAIL:" + c.getEmail()); writer.newLine(); } writer.write("END:VCARD"); writer.newLine(); } } }导出时编码必须指定 UTF-8,否则 Windows 下用记事本打开 vCard 中文全是乱码,这个坑我在第一次做的时候踩过。导入是导出的逆操作:按行读,遇到BEGIN:VCARD开始累积字段,遇到END:VCARD就封装成一个 Contact 对象并调用 DAO 的 insert。解析时不要用 String.split(":") 粗暴切分,因为TEL;TYPE=CELL:这种行里有两个冒号,正确姿势是用 indexOf(":") 找到第一个冒号的位置再截取。
4. 界面层与交互细节:Swing 布局和表格渲染的落地
4.1 主窗体布局:BorderLayout 五区分区
Swing 布局讲「分区」,主窗体我用 BorderLayout 分成五个区:北区放搜索栏,西区放分组树,中心放联系人表格,南区放状态栏,工具栏和菜单栏挂在窗体最顶层。这样一个界面上把「搜索、分组、列表、操作入口」四个核心功能全部排开,演示时不用来回切窗口。
分组树用 JTree 实现,数据从 contact_group 表查询后填充。点击分组节点时,触发事件重新调用 ContactService.searchByGroup 方法。这里有个细节:分组树和表格是联动的,改分组、删分组之后必须同时刷新两个组件,我封装了一个 refreshAll() 方法统一处理,避免只刷新一半导致界面和数据库状态对不上的迷惑行为。
主窗体的构建代码比较固定,但有一个习惯可以从一开始就养好:所有窗体的默认关闭操作写成setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE),如果你写的是DO_NOTHING_ON_CLOSE,演示时点右上角叉子界面没反应,现场还挺尴尬的。
4.2 表格模型:为什么不直接用 JTable 的默认模型
JTable 直接塞二维数组能显示数据,但更新数据时会面临「整个表重建」还是「增量刷新」的选择。正确做法是自定义一个 ContactTableModel 继承 AbstractTableModel,把列名、取行数、取单元格值这三个方法实现好。
class ContactTableModel extends AbstractTableModel { private final String[] columns = {"ID", "姓名", "电话", "邮箱", "单位", "分组", "备注"}; private List<Contact> data = new ArrayList<>(); public void setData(List<Contact> contacts) { this.data = contacts; fireTableDataChanged(); } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Contact c = data.get(rowIndex); switch (columnIndex) { case 0: return c.getId(); case 1: return c.getName(); case 2: return c.getPhone(); case 3: return c.getEmail(); case 4: return c.getWorkUnit(); case 5: return c.getGroupName() == null ? "" : c.getGroupName(); case 6: return c.getRemark(); default: return ""; } } }关键在 setData 方法尾部必须调用fireTableDataChanged()。没有这行,界面模型不会感知数据变化,你往数据库里插了记录,表格界面「看起来完全没反应」,这是 Swing 初学者最容易误以为「代码没生效」的黑匣子。改成自定义模型之后,所有通过模型刷新表格的操作都走同一个方法,逻辑也收敛了。
4.3 表单校验:把「保存失败」变成「哪一项填错了」
添加和编辑联系人的表单我统一用一个 ContactDialog 对话框承载,上面排列姓名、电话、邮箱、分组、备注等字段。保存按钮的监听器里先跑一遍校验,不合格就弹 JOptionPane 提示,合格再调 service 层入库。校验属于业务规则,我把校验方法放在 service 层而不是界面层,这样以后从 Swing 换成网页前端也能复用。
public boolean validateContact(Contact contact, StringBuilder errorMsg) { if (contact.getName() == null || contact.getName().trim().isEmpty()) { errorMsg.append("姓名不能为空;"); return false; } String phone = contact.getPhone(); if (phone == null || !phone.matches("^1[3-9]\\d{9}$")) { errorMsg.append("手机号格式不正确;"); return false; } String email = contact.getEmail(); if (email != null && !email.isEmpty() && !email.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { errorMsg.append("邮箱格式不正确;"); return false; } return true; }正则这里有个小讲究:手机号写^1[3-9]\d{9}$就够了,不用追求把所有号段列全,因为运营商号段随时会变,校验的目的是挡住明显错误,而不是当号码字典。邮箱正则是课程里常见的简化版,能挡住「没有@」「没有点」的明显错误,足够应付课设。提示信息我用 StringBuilder 把多个错误拼在一起一次性弹出,比一次只提示一个问题体验好得多。
4.4 长耗时操作与界面卡顿:导入数据时界面会不会「假死」
这个点很多人的课设从来没遇到,因为测试数据量太小。一旦演示时从 vCard 导入两千条联系人,Swing 是单线程模型,主线程忙着解析文件,界面刷新就被卡住,窗口变成「未响应」,老师要是开着任务管理器看你,这段就翻车了。解决办法是导入导出操作扔到后台线程执行,用 SwingWorker 在完成后回到事件分发线程刷新界面。
SwingWorker<Void, Void> worker = new SwingWorker<>() { @Override protected Void doInBackground() { // 这里执行 VCardHelper.importFromFile(file),不碰界面组件 importedCount = VCardHelper.importFromFile(file, contactService); return null; } @Override protected void done() { // 回到界面线程,刷新表格,弹提示 refreshTable(); JOptionPane.showMessageDialog(frame, "导入完成,共 " + importedCount + " 条"); } }; worker.execute();注意 SwingWorker 里的硬性规则:doInBackground 里碰任何 Swing 组件都是禁忌,状态栏控件也不行。你需要进度反馈就在 done 方法里一次性刷新,这才是正确的线程分工。这个设计在答辩时很加分,因为多数同学根本不知道 Swing 的 EDT(事件分发线程)机制,你能讲清楚,说明真的去研究过组件库的运行机制。
5. 避坑与排查:通讯录课设最容易翻车的五个点
5.1 中文乱码:数据库里全是问号
现象:界面上输入「张三」,保存到数据库后变成「???」,查询出来也是问号,或者从数据库读出来显示成乱码。
原因:三层编码不一致。数据库字符集不是 utf8mb4,JDBC 连接 URL 没指定 characterEncoding,以及 Java 源文件本身不是 UTF-8 编码,这三层只要有一层是旧的默认编码,中文就走样。
解决:建库语句加上DEFAULT CHARACTER SET utf8mb4(参见第二章的建表 SQL);JDBC URL 里拼上?useUnicode=true&characterEncoding=utf8;IDE 里把项目文件编码统一设置为 UTF-8。这三件事做完基本不会再有乱码问题。
5.2 驱动加载失败:ClassNotFoundException 和连接超时
现象:点运行直接报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者报Communications link failure。
原因:前者是 mysql-connector-java 的 jar 包没放进项目的类路径里,常见的是你手动拷贝了 jar 到项目目录但没有在 IDE 里「Add to Build Path」。后者是 MySQL 服务没启动,或者连接 URL 里的端口号写错(默认 3306),或者用户名密码不对。
解决:在 IDE 的模块设置里确认 jar 已被引用;连接 URL 核对三要素——主机名、端口、数据库名,本机演示就用jdbc:mysql://localhost:3306/contact_db;MySQL 服务在 Windows 服务里确认是「正在运行」。我一般会在 DBHelper 里把连接失败的异常信息打完整,第一眼就能看到是驱动、连接、还是认证的问题。
5.3 点了保存没反应,控制台也没报错
现象:界面不弹任何提示,数据库里也没新增记录,看起来像「按钮失效」。
原因:最常见的是用了事务但忘了提交。如果你的代码写了conn.setAutoCommit(false),又没有调用conn.commit(),数据会被 MySQL 回滚掉,而程序又没抛异常,所以你「什么都没看到」。另一种情况是 group_id 填了一个不存在的分组 ID,外键约束把插入直接拦截了,异常被 catch 掉没打出来。
解决:所有写操作统一在 try 块里最后 commit,catch 里第一个动作是调用conn.rollback(),第二个动作是打印异常堆栈而不是e.printStackTrace()之后装作无事发生。给控制台打印加上System.err.println,异常信息才会从红色通道打出来,不容易被刷掉。
5.4 联系人重复录入:同一个人录了七八遍
现象:通讯录里同一个手机号出现多条记录,姓名一样,只有备注略有差别,看起来像脏数据。
原因:业务层没有查重校验,数据库表也没有唯一索引,「重复」被当成合法数据入库了。实际使用场景里用户想找「小王」的号码,结果跳出来五条疑似记录,这就是通讯录质量下降的开始。
解决:双保险。业务层在 insert 之前先按手机号查一次,存在就弹「该号码已存在于联系人列表」;数据库层面给 phone 字段加唯一索引UNIQUE KEY uk_phone (phone),从源头拦截。唯一索引还能挡住「两个同学同时往库里面录」的并发场景,虽然单机课设用不上,但答辩能多讲一句设计意图。
5.5 编辑后表格没刷新:数据库变了,界面还是老的
现象:在对话框里改了联系人手机号,点保存,数据库确认更新了,但主界面表格显示的还是旧号码。
原因:回想第四章的内容——改了数据之后,表格模型没有收到新数据,或者你new了一个新的 TableModel 但 JTable 还引用着旧对象。刷新机制断了,界面就成了黑匣子。
解决:所有「数据变更 → 刷新界面」的操作统一走model.setData(newList)+model.fireTableDataChanged()这一条链路。不要在增删改查每个方法里各写一套刷新逻辑,否则一定会漏。我通常把刷新封装成refreshAll(),在对话框关闭、导入完成、分组切换三个入口各调一次,保证界面永远跟着数据库走。
6. 验收与答辩:拿得出手的演示流程与老师提问的准备
验收环节最怕的不是功能缺,而是功能都在但你演示顺序混乱,或者准备了但没演出来。我建议你按一条固定的演示脚本走:第一步启动程序,展示主窗体分区,说明这是一个三层架构的系统;第二步点「新增」,故意先不填手机号,让校验拦截,再填完整数据保存,展示「正确数据能存、错误数据被拦」;第三步搜索「13」看模糊搜索结果,再按分组切一遍;第四步添加两条联系人和一个分组,证明更新逻辑有效;第五步删除一条记录,顺带说明删除后分组数据不受影响;第六步导出 vCard 到桌面,用记事本打开展示格式,再清空列表导入回来,证明往返一致。每一步都要说一句「这个动作调用了哪一层的方法」,这是答辩控场的关键。
答辩提问一般集中在四个方向:为什么用 Swing 不用 JavaFX、LIKE 模糊查询会不会慢、删除分组时联系人怎么处理、事务怎么保证一致性。前两个本文已经讲过,第三个建议的答案是「删除分组时把该分组下联系人的 group_id 置为 NULL,保留联系人本身,而不是级联删除联系人」,因为通讯录的语义是分组没了人还在。第四个答「所有写操作都在同一个事务里,任何一个步骤失败都回滚,不会出现半条数据」。
演示前花十分钟造一组像样的测试数据:五个分组,五十个联系人,姓名覆盖常见姓氏和生僻字,电话号段和邮箱域名都安排上,备注写一到两句有真实感的说明。数据太像编的,老师一眼就看出来你从没真实使用过这个系统。
这套完整方案从评审表到数据库 SQL、从源码到演示脚本都已整理好,做 Java 课程设计通讯录系统可以直接拿去做骨架再改自己的功能点。
从那以后我做任何课设,演示前都强制自己按验收脚本完整走一遍,尤其是「先拒绝、再成功」的校验演示和「导入导出往返」这种容易临时出岔子的环节。你把这套流程走完,答辩时手里有货,心里就不慌。希望帮到你。
本文还有配套的精品资源,点击获取