资讯动态

用Java写通信录:字段设计、文件存储与Swing界面避坑实战

发布时间:2026/10/14 16:16:58 来源:尧图企业网站定制
简介这是基于Java开发的通信录管理系统源码包面向Java初学者或课程设计人群完整实现联系人新增、删除、修改、查询等核心操作。项目结构清晰将数据库访问、业务逻辑与界面显示分层处理可帮助读者掌握JDBC数据库编程、MVC分层设计以及桌面端交互的常见写法。压缩包共42个文件以14个Java源文件、14个class文件、6个JSP页面为核心另含数据库SQL脚本、Eclipse工程配置及Web相关文件整体仅636KB便于下载与本地部署调试。已有145人学习下载适合需要参考完整项目实战或完成课设作业的开发者。通过学习这份代码可直观理解联系人管理系统的模块划分与数据流同时获得一套可直接运行或二次改造的示例工程。无论是课程设计、毕业设计还是入门级项目复现都能从中获得完整的工程化思路对提升Java全栈协作与排查编译部署问题均有帮助。1. 通信录 Java 到底在做什么一个把基础语法串成能跑系统的经典题目通信录 Java 这个标题乍看是入门练习实际是很多 Java 学习者第一道真正需要「组合」起来的题目把类、集合、异常、文件读写、事件驱动揉进一个能启动、能操作、能在程序重启后还不丢数据的程序。它解决的问题很明确怎么用 Java 做一个可靠的联系人管理小系统而不是写一堆互相不调用的语法 Demo。大多数教程会直接扔代码但我更愿意把它拆成「先定模型、再跑控制台、再上界面、最后拆层验证」四步走新手每一步都能看到运行结果熟手也能对照边界参数检查自己的实现。这篇文章适合正在做课程设计、准备简历小项目、或者想把 Java 基础串成完整工程的开发者。标题里没有提数据库、没有提框架所以正文从最省依赖的方案讲起懂 JDK 就能跑。演示场景需要界面时再升级到 Swing。整个过程的踩坑记录都集中在保存、刷新、编码这三类问题上这三类问题几乎覆盖了这个题目九成的翻车现场。2. 先定数据模型和存储方案通信录的地基别在写代码时现想2.1 联系人字段怎么设计别只存姓名和电话几乎每个做通信录 Java 的人都会在写到一半时把字段表推翻重来一开始只存姓名和电话做到查重时发现重名没法定位做到分组时发现没有分组字段做完又发现用户还要备注和邮箱。所以我的习惯是动手写代码前先把字段表定下来哪怕后头会改也比边写边加字段要强。字段类型说明idint唯一标识自增不对外展示nameString必填允许重名phoneString必填可存座机、短号、带分机groupNameString可选默认「默认分组」emailString可选做格式校验addressString可选remarkString可选createdAtlong创建时间戳存储用两个关键决策值得单独说。第一电话用 String 而不是 long因为电话号码会出现010-88886666、86 138 0013 8000这类带符号和空格的形态数值类型存不住也搜索不了String 才能保留原始格式。第二主键用自增 id 而不是姓名因为重名是常态按姓名删人会删错谁先录入谁拿小 id这个规则简单且不容易乱。字段约束上也别贪多姓名必填、电话必填且至少一个其他都能为空。这是通信录这类系统里成本最低的校验策略做太多强制反而会在演示的时候被评委随手输个空数据卡住。2.2 三种存储方案怎么选内存集合、文件序列化、嵌入式数据库选存储方案不需要纠结太久按数据量和项目诉求排优先级就行。方案上手难度适合数据量典型场景内存 List / Map最低运行期间练语法、写单元测试文件序列化低几百条课程设计、单机小工具轻量级嵌入式数据库中上千条以上求职作品、要演示 SQL 能力标题叫「通信录 Java」最稳的组合是「程序运行期间用内存集合保存数据每次写操作后立刻落盘到本地文件」。我不建议一上来就上数据库原因很实际通信录场景数据量小数据库会把 SQL、连接管理、驱动依赖这些噪音全部引进来反而冲淡了题目想考察的 Java 基础。用文件序列化时整个持久化代码能控制在 40 行以内演示机器上不需要装额外服务双击运行就能看效果。如果目标是简历作品那再考虑把存储层从文件换成嵌入式数据库这也是为什么我要强调在 2.3 里把 DAO 接口提前定义好。接口在前面立住后面切换存储实现不动上层代码这个好处在做完界面后会体会得很深。2.3 用接口把存储隔离开Contact 类和 ContactDao 的最小闭环先看实体类注意它实现了 Serializable因为后面的文件持久化依赖这个标记。import java.io.Serializable; public class Contact implements Serializable { private int id; private String name; private String phone; private String groupName; private String email; private String address; private String remark; public Contact(String name, String phone, String groupName) { this.name name; this.phone phone; this.groupName groupName; } public int getId() { return id; } public void setId(int id) { this.id id; } // 其余字段的 getter / setter 按同样方式补全 }Serializable 是最容易被漏掉的标记接口。漏掉之后程序在第一次执行保存动作时会直接抛 NotSerializableException属于「保存功能第一次真实使用时必然翻车」的问题。字段访问权限全部走 getter 和 setter别图省事直接设 public因为后面做手机号校验、姓名必填校验要改的是 setter 方法而不是所有调用点。然后定义 DAO 接口把「内存操作」和「存储细节」隔离开。import java.util.List; public interface ContactDao { boolean add(Contact contact); boolean delete(int id); boolean update(Contact contact); ListContact findAll(); Contact findById(int id); boolean save(); }add、delete、update 都返回 boolean 是故意的表示「不只是改内存文件也写成功了」这样上层能在失败时给用户明确提示而不是默默丢数据。save 单独暴露出来用于退出程序前做最后一次兜底落盘。这个接口设计的价值在于将来从文件存储换成嵌入式数据库实现菜单层和界面层的代码一行都不用改。3. 用控制台把流程跑通通信录 Java 的最小可运行版3.1 主循环和菜单结构程序要「活着」而不是跑一次就退出控制台版的第一个目标是让程序进入循环用户输入数字选择操作直到输入退出选项才结束。这个结构是所有后续版本的地基。import java.util.Scanner; public class ContactApp { public static void main(String[] args) { ContactDao dao new ContactFileDao(); Scanner scanner new Scanner(System.in, UTF-8); boolean running true; while (running) { System.out.println(1. 添加联系人); System.out.println(2. 删除联系人); System.out.println(3. 修改联系人); System.out.println(4. 浏览联系人); System.out.println(5. 退出); String input scanner.nextLine(); switch (input) { case 1: addContact(dao, scanner); break; case 2: deleteContact(dao, scanner); break; case 3: updateContact(dao, scanner); break; case 4: listAll(dao); break; case 5: running false; break; default: System.out.println(无效选项请重新输入); } } if (dao.save()) { System.out.println(通信录已保存再见); } else { System.out.println(保存失败请检查文件权限); } scanner.close(); } }两个容易被新手忽略的细节。第一这里用scanner.nextLine()统一读输入而不是nextInt()因为nextInt()读完后会在缓冲区留下换行符下一次nextLine()会直接读到空字符串表现为「菜单选项怎么输都不对」。第二new Scanner(System.in, UTF-8)显式指定编码能规避一部分中文环境下控制台乱码的问题。switch (input)用字符串做分支在 Java 7 之后就是标准写法兼容性最好。如果你的运行环境是 JDK 8 到 JDK 21 都没问题老项目里那种用nextInt()配合switch (choice)的写法我不推荐换行符问题太容易踩。3.2 添加联系人与号码查重增删改查里最容易漏的边界添加联系人逻辑看起来只是赋值后调用 dao.add实际有两个边界必须处理姓名和电话不能为空手机号不能重复。private static void addContact(ContactDao dao, Scanner scanner) { System.out.print(姓名); String name scanner.nextLine().trim(); System.out.print(手机号); String phone scanner.nextLine().trim(); if (name.isEmpty() || phone.isEmpty()) { System.out.println(姓名和手机号不能为空); return; } for (Contact c : dao.findAll()) { if (c.getPhone().equals(phone)) { System.out.println(该手机号已存在添加失败); return; } } Contact contact new Contact(name, phone, 默认分组); if (dao.add(contact)) { System.out.println(添加成功编号 contact.getId()); } else { System.out.println(添加失败请检查文件权限); } }查重策略上要注意姓名允许重复手机号不允许重复这是通信录产品的常见规则。手机号重复校验时理想情况应该先统一格式再去比对比如把138-0013-8000和13800138000视为同一个号码。上面的代码为了保持可读性没有做这个归一化实际使用时建议在比对前对 phone 做一次replace(-, ).replace( , )能把误判率降下来不少。还有一个性能边界这里每次添加都调用dao.findAll()全量扫描数据量五百条以内完全没问题上千条时会有轻微卡顿那是正常的。通信录场景不会出现十万条联系人所以别提前优化保持代码直白更重要。3.3 把内存数据写进本地文件序列化的保存与加载文件持久化这步是整个通信录 Java 是否「完整」的分水岭。只写在内存里的程序窗口一关数据就没了那不叫通信录叫练习。import java.io.*; import java.util.ArrayList; import java.util.List; public class ContactFileDao implements ContactDao { private static final String FILE_NAME contacts.dat; private final ListContact data new ArrayList(); private int nextId 1; public ContactFileDao() { load(); for (Contact c : data) { if (c.getId() nextId) { nextId c.getId() 1; } } } Override public boolean add(Contact contact) { contact.setId(nextId); data.add(contact); return save(); } // delete / update / findAll / findById 按接口实现操作 data 即可 Override public boolean save() { try (ObjectOutputStream out new ObjectOutputStream( new FileOutputStream(FILE_NAME))) { out.writeObject(data); return true; } catch (IOException e) { System.err.println(保存失败 e.getMessage()); return false; } } SuppressWarnings(unchecked) private void load() { File file new File(FILE_NAME); if (!file.exists()) { return; } try (ObjectInputStream in new ObjectInputStream( new FileInputStream(file))) { Object obj in.readObject(); if (obj instanceof List?) { data.addAll((ListContact) obj); } } catch (IOException | ClassNotFoundException e) { System.err.println(读取通信录失败 e.getMessage()); } } }这段代码里有三个参数和设计点需要说明。第一FILE_NAME固定为contacts.dat这个文件会生成在程序运行的当前工作目录下用.dat后缀是为了避免和源码混淆换成.bin或.data都可以注意别用中文文件名跨环境容易出编码问题。第二ObjectInputStream读取时先做instanceof List?判断再强转成ListContact不做这一步文件内容被篡改时可能抛 ClassCastException。第三nextId在构造时扫描已有数据取最大 id 加一作为下一个新联系人的编号否则删除最后一条记录再新增时新联系人的 id 会从 1 重新开始界面刷新时很容易出现显示错乱。load 方法遇到文件不存在时直接返回空集合不打印错误这是刻意的第一次运行本来就没有数据文件不该报错吓到用户。真正需要用户知道的是读取失败比如文件损坏这时打印错误信息但程序仍然能启动只是数据是空的。这个「程序不能因为数据文件坏了就启动不了」的思想是后面所有健壮性讨论的基础。4. 从黑框升级到 Swing 界面通信录 Java 能演示的第二种形态4.1 为什么演示场景普遍要界面Swing 是只靠 JDK 就能跑的选择课程设计或者做内部小工具时纯黑框程序的说服力确实弱。答辩现场评委不会盯着控制台看一行行文字他们更愿意看到一个表格里能点按钮操作。Swing 虽然不新但它有一个无法替代的优势只依赖 JDK不需要额外引入界面库复制到任何装了 JDK 的机器上都能运行。Swing 版通信录的核心组成是 JFrame主窗口、JTable联系人表格、DefaultTableModel表格数据模型、JButton 和事件监听。很多人第一次做界面时会把业务逻辑直接写进按钮监听器这会让代码越来越乱。正确做法是界面只负责两件事从 DAO 读数据展示、把用户输入传给 DAO 处理中间不夹带文件读写逻辑。4.2 表格模型与按钮事件联动JTable 的数据刷新别用错方法表格展示通信录列表最关键的是刷新逻辑。DefaultTableModel 维护着 JTable 显示的数据改数据时必须通过 model 操作直接改 JTable 的某个单元格只是视觉欺骗。private JTable table; private DefaultTableModel tableModel; private ContactDao dao; private void initTable() { String[] columns {编号, 姓名, 电话, 分组}; tableModel new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; table.setModel(tableModel); refreshTable(); } private void refreshTable() { tableModel.setRowCount(0); for (Contact c : dao.findAll()) { tableModel.addRow(new Object[]{ c.getId(), c.getName(), c.getPhone(), c.getGroupName() }); } }删除按钮的事件监听是另一个高频操作核心是先从表格里拿到选中的行再通过行数据拿到 id 去调 DAO。deleteButton.addActionListener(e - { int row table.getSelectedRow(); if (row 0) { showTip(请先选中要删除的行); return; } int id (int) tableModel.getValueAt(row, 0); if (dao.delete(id)) { refreshTable(); showTip(删除成功); } else { showTip(删除失败); } });这里要注意getValueAt(row, 0)拿到的是编号列类型是 Integer强转成 int 前要确保表格模型里这一列存的是整数。删除操作必须刷新表格否则内存里数据没了界面上还残留着那一行这就是后面避坑章节会展开的「界面和内存两份数据脱节」问题。整个事件监听里没有任何文件操作所有持久化都封装在 dao.delete 内部监听器只做「调用 DAO、按返回值刷新界面」两件事这是 Swing 程序不容易写乱的秘诀。4.3 事件线程里别做耗时 IO界面卡死是阻塞在事件线程上Swing 的所有界面刷新和事件回调都运行在同一个事件调度线程EDT上。如果在这个线程里执行文件读写或者网络请求整个界面会被冻住表现为窗口拖不动、按钮点了没反应严重时系统提示「程序未响应」。数据量小的时候保存一个几百条的联系人列表耗时在毫秒级用户感知不明显。但课程设计答辩现场如果评委快速点击保存按钮多次事件线程被连续占用依然可能出现卡顿。更合理的做法是把 IO 放到后台线程执行。saveButton.addActionListener(e - { saveButton.setEnabled(false); SwingWorkerVoid, Void worker new SwingWorker() { Override protected Void doInBackground() { dao.save(); return null; } Override protected void done() { saveButton.setEnabled(true); showTip(保存完成); } }; worker.execute(); });SwingWorker 的doInBackground运行在后台线程done会在事件线程上回调所以按钮恢复和提示信息可以放心操作界面。有人会问既然效率一样为什么还要多写一层线程包装因为这是给未来留后路一旦数据量涨到几千条或者存储层换成网络数据库后台保存就是刚需到时候再改事件监听器改动面会大得多。即使你决定在小数据量时不做线程包装也一定要把保存动作封装成一个独立方法比如 dao.save保证将来替换实现时不用改按钮监听代码。这个习惯能让界面层在换存储方案时做到无感知。5. 通信录 Java 的 5 个典型避坑点现象、根因、解决方案5.1 手机号正则写得太死固话和短号全被拦截现象用户输入010-88886666或者12345这种内部短号程序提示「手机号格式不正确」录入直接失败。原因校验逻辑写成了^1[3-9]\d{9}$把规则锁定在 11 位手机号。通信录的真实场景里固话、带分机的号码、短号都很常见一个只认手机号的正则把大量合法输入挡在门外。解决把校验拆成两级。第一级只检查空值和长度非零第二级如果需要严格校验按业务规则做多模式匹配允许手机号、固话、短号三种形态。课程设计场景下我建议第一级就够用别把校验做成「只有 11 位手机号能录进去」的极端状态。5.2 删除成功后表格不刷新界面和内存两份数据脱节现象点击删除按钮提示「删除成功」但表格里那条记录还显示着重启程序后又确实不在了。原因DAO 删的是内存 List删完后没有调用表格模型的刷新方法界面展示的还是旧数据。本质是界面层持有了一份「数据快照」而它没有同步机制。解决删除操作完成后无条件调用refreshTable()并且只以 DAO 的返回值为准。代码里最容易漏掉的细节是delete方法返回 false 时也要刷新因为内存可能已经发生部分变更界面必须回到真实状态。这个「操作后必刷新」的习惯能顺带解决修改联系人后界面不更新的各种变体问题。5.3 给类加了一个字段老数据文件就读不回来现象项目做完后给 Contact 类加了一个 email 字段程序启动时抛InvalidClassException之前保存的 contacts.dat 全部读不回来。原因Contact 类没有显式声明 serialVersionUIDJVM 在序列化时会根据类的结构自动生成一个版本号。加字段后类结构变了自动生成的版本号也跟着变反序列化时 JVM 认为版本不匹配直接拒绝读取。解决在 Contact 类里显式定义private static final long serialVersionUID 1L;。这样小改动加字段、加方法不会导致兼容性错误。如果是删除字段或修改字段类型这类破坏性变更文件格式本身就不再兼容常见的做法是程序启动时检测到加载失败先把旧文件改名备份再建一个新文件保证用户数据不会直接被覆盖丢失。5.4 中文乱码控制台黑框和文件编码各说各话现象在中文环境下运行控制台版菜单能显示中文但输入联系人姓名后列表页显示的姓名变成乱码有时连菜单都乱码。原因编译期的编码和运行期的控制台代码页不一致。代码文件是 UTF-8 保存的但编译时没指定编码中文环境默认用了 GBK 解码源码字符串在内存里就已经错了另一种情况是编译对了但控制台代码页不是 UTF-8输出到黑框时显示乱码。解决编译参数统一指定 UTF-8也就是每次执行javac -encoding UTF-8 ContactApp.java。如果使用 IDE把项目文件编码设为 UTF-8。运行端方面常见做法是在启动脚本里把控制台代码页切换到 UTF-8或者在程序里别依赖控制台切换直接用 Swing 界面做输入输出绕开黑框的编码问题。这个坑在 IDEA 里不明显因为 IDE 默认就是 UTF-8但交到环境没配置的机器上跑就会现形。5.5 看着保存成功了重启后数据却丢了一部分现象程序运行正常添加时也提示成功退出前也看到「通信录已保存」但重启后新增的数据不见了只剩旧数据。原因最典型的是 add 方法只往内存 List 里 add 了数据没有触发保存动作界面提示的「添加成功」只是内存操作成功。更隐蔽的一种是add 里调用了 save但 save 的 try-with-resources 没写对输出流没关数据只写进缓冲区没落盘程序正常退出时缓冲区被丢弃。解决让 DAO 内聚所有写操作在修改内存后立即调用 savesave 成功才算操作成功返回值一路传回界面层。save 方法的输出流必须用 try-with-resources保证无论成功失败都关闭流。另外建议保存时先写入临时文件再替换正式文件避免写入过程中程序崩溃导致原文件损坏File tmp new File(FILE_NAME .tmp); try (ObjectOutputStream out new ObjectOutputStream( new FileOutputStream(tmp))) { out.writeObject(data); } if (tmp.renameTo(new File(FILE_NAME))) { return true; } return false;tmp 文件写完后通过 renameTo 覆盖正式文件这样正式文件要么是旧数据、要么是新数据永远不会是写了一半的残废文件。这个「写临时文件再原子替换」的习惯在数据重要度高的场景里值得一直沿用。6. 进阶把通信录拆成三层用 jshell 十分钟验证数据层做到这一步通信录 Java 已经是一个能跑、能存、能展示的完整程序。接下来要做的是把散落在各个方法里的逻辑按职责拆开形成「界面层、业务层、数据层」三层结构。界面层只处理用户交互业务层做查重、校验、分组规则数据层只负责持久化。代码目录可以组织成下面这样contacts ├── model │ └── Contact.java ├── dao │ ├── ContactDao.java │ └── ContactFileDao.java └── app └── ContactApp.java这样的结构下验证数据层是否可靠根本不需要把整个界面跑起来。JDK 9 及以上自带的 jshell 可以直接对 DAO 做快速回归这个技巧在调整完存储代码之后特别好用。先编译全部源码到 out 目录javac -encoding UTF-8 -d out \ contacts/model/Contact.java \ contacts/dao/ContactDao.java \ contacts/dao/ContactFileDao.java然后启动 jshell 并指向编译输出目录jshell --class-path out进入 jshell 后直接操作数据层import contacts.model.Contact; import contacts.dao.ContactFileDao; var dao new ContactFileDao(); var c new Contact(测试联系人, 13800138000, 默认分组); dao.add(c); dao.findAll();做完这四步就能看到返回的 List 里有一条完整数据。这个验证方式的价值在于添加、删除、保存、加载这四条核心链路都可以不进界面、不写 main 方法几十秒内反复验证。尤其是改完 Contact 类字段、调整过保存策略之后用 jshell 先跑一遍比直接跑整个程序省太多时间。如果用的是 JDK 8没有 jshell就用一个临时的DaoTest.java写 main 方法做同样的事验证完删除即可本质上没有任何区别。我现在的习惯是无论项目多小都先把 DAO 接口列出来再谈界面和菜单。这个习惯让我少踩了很多保存和刷新的坑也让我在把纯控制台版升级成界面版时几乎没有改动数据层代码。通信录 Java 这个题目做一遍不难但把它做成「结构清晰、存储可靠、界面流畅」的样子才算是把 Java 基础真正串了起来。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑