ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java通信录管理系统实战:Swing界面与CSV存储的轻量级方案

Java通信录管理系统实战:Swing界面与CSV存储的轻量级方案

简介:通信录Java是一份基于Java开发的通信录管理系统完整项目,面向正在学习Java Web开发的学生或初级开发者,演示联系人增删改查、数据库持久化与MVC分层等核心技能。压缩包共42个文件,主体为14个Java源文件和对应的class字节码,配合6个JSP页面、1个SQL建库脚本、1个依赖JAR包及MyEclipse工程配置,整体仅636KB,可直接导入开发工具查看运行。项目采用Servlet+JSP+JavaBean的分层结构,按database、bean、servlets、filter等包划分,并附有数据库初始化脚本;JSP页面覆盖添加、修改、搜索、显示等联系人操作界面,能帮助读者理解表单提交、请求处理与数据库更新之间的完整链路。从工程配置到SQL脚本一应俱全,便于学习者按模块对照研读。已有145人学习下载,适合希望借助完整案例快速上手Java Web开发、掌握JDBC操作与页面交互的读者,作为课程设计或项目实战参考。

1. 通信录管理系统:把几百个联系人从手机里解放出来

当通信录里存了三百多个联系人,真正麻烦的不是存储空间,而是找一个号码要翻十几屏,换手机时导出的联系人文件格式对不上,就会丢掉几个再也找不回的号码。这套基于Java的通信录管理系统,把联系人数据完整收回到本地桌面:用Swing搭建界面,数据存成本地CSV文件,支持增删改查、关键字搜索、分组管理和导入导出。做Java课程设计的人在找能跑通、能讲清楚的项目,从业者想要一个不依赖云端的轻量联系人工具,这套代码都覆盖得到。系统本身不复杂,但细节相当多,下面从选型逻辑、实现套路到踩坑记录逐层拆开讲。

2. 技术选型:界面用Swing、存储用CSV的理由与替代方案

2.1 界面框架:Java课设和小工具场景,Swing仍然是最稳选择

通信录这种量级的桌面工具,界面无非是表格加表单加按钮,可选的技术方向其实有三个:Swing、JavaFX、或者用Web技术套一个本地壳。JavaFX的视觉效果确实更好,支持CSS皮肤和动画,但环境成本比Swing高一个量级:JDK 8之后JavaFX从JDK中分离出去,要么单独引入依赖模块,要么换用特定发行版,换一台机器跑经常因为模块缺失直接启动失败。至于Web套壳方案,虽然界面能做到很漂亮,但引入前端构建链路以后,整个项目从纯Java变成了前后端混合工程,对课程设计和轻量工具来说维护成本不划算。

Swing是JDK自带组件,任何装了JDK的机器都能直接跑,存量的教程和参考代码也最多,出问题时搜索解决速度快得多。交互上Swing虽然老,但JTable、JDialog、GridBagLayout这些组件做好之后稳定得可以用十年,不像前端框架隔一两年就换一波API。对通信录管理系统来说,交互复杂度不高,数据量也不大,Swing的性能和内存占用都没有压力。我见过不少JavaFX项目在答辩或演示现场因为缺库启动不了,最后只能临时换机器;反而Swing项目只要JDK在就一定能跑。如果需求文档没有明确要求JavaFX,我一般直接选Swing,把省下来的时间放在业务逻辑和数据安全上。

2.2 存储层选型:CSV、SQLite、对象序列化三选一

存储层是通信录系统的核心决策,三种常用方案的适用场景差异很大。SQLite用起来最“正规”,支持事务、索引、SQL查询,但对这个项目而言要引入JDBC和SQL,代码量直接上一个台阶,驱动包还需要单独下载,在无网环境里打包部署会多出不少麻烦。对象序列化实现最快,一个ObjectOutputStream就能把整个List写进文件,但生成的是二进制文件,人眼无法查看,Excel打不开,数据一旦损坏几乎没有抢救手段。CSV介于两者之间:文本格式肉眼可读,Excel和记事本都能直接打开编辑,导入导出天然支持,用Java标准库的字符流就能读写,不引入任何外部依赖。

方案可读性外部依赖适合场景主要风险
CSV好,文本可编辑无千条以内,需要导入导出并发写、格式不规范
SQLite差,需工具查看JDBC驱动数据量大、关系复杂代码复杂度高
对象序列化差,二进制无快速原型、无外部需求数据损坏不可恢复

通信录管理系统数据规模通常在几百到几千条,CSV一次全量读入内存完全无压力,不需要索引和事务机制,代码可读性反而最好。后续做联系人导入导出时,CSV本身就是通用交换格式,不涉及格式转换。即便未来数据量真的大到需要数据库,只要数据访问层用DAO接口对外提供服务,把实现从CSV换成JDBC版本即可,界面代码一行都不用改。这就是把数据访问封装成独立层的价值。还有一个方案是Java自带的Properties文件,key-value结构简单,但这个格式天生不适合存列表数据,每个联系人都要编号key,读取时还要自己组装对象,等于实现了一个半吊子序列化协议。CSV在这个场景下确实综合成本最低,可读性、可编辑性、跨平台性都兜得住。

2.3 开发环境与项目目录怎么搭

开发环境用JDK 8或JDK 11都可以,Swing的API在这两个版本上几乎没有差异。IDE用常见的IDEA或Eclipse都行,IDEA的窗体设计器可以直接拖控件生成界面类,但自动生成的代码可读性一般,代码和布局逻辑混在一起,后面改样式比较费劲。我习惯手写布局管理器,用GridBagLayout或GroupLayout排表单,一开始多花半小时,后续维护省下的是到处找控件变量名的时间。

项目按功能拆包,别把所有类堆在默认包下,否则后期维护和答辩讲代码都会很痛苦:

contact-book/ ├── src/ │ └── com/example/contact/ │ ├── model/Contact.java │ ├── dao/ContactDao.java │ ├── util/CsvUtil.java │ └── ui/MainFrame.java ├── data/ │ └── contacts.csv └── README.md

包结构分model、dao、util、ui四层:model放实体,dao放数据操作,util放通用工具,ui放窗口和事件处理。依赖关系是单向的,ui依赖dao和model,dao依赖model和util,不会出现循环依赖。data目录和源码分开,备份时只拷这个目录就能带走全部数据。程序启动时如果发现data目录或CSV文件不存在,自动创建空文件并写入表头,避免首次保存因文件缺失报错。这里有一个容易被忽略的点:代码里所有路径不要写死绝对路径,统一用相对路径或者从配置文件读取,保证项目换个目录、换台机器还能正常跑。

3. 功能设计与数据建模:字段、分组和交互流程先定好

3.1 联系人实体类:字段取舍比想的重要

实体字段的取舍直接影响后续所有数据的操作代码。常见联系人系统候选字段有姓名、电话、邮箱、公司、职位、地址、生日、备注、分组、头像照片等,如果全塞进一个类,表单录入会变得冗长,CSV每一行的列数也会爆炸,读写的兼容性变差。反过来字段太少又不够用,后补字段要动CSV表头和数据访问层。平衡下来,选了六个核心字段,如下表:

字段类型说明
idint自增主键,用于定位、删除和更新
nameString必填,显示和搜索的主要依据
phoneString必填,支持多号码用分号分隔
emailString选填,做格式校验防止录入错误
groupNameString选填,默认归入“默认分组”
remarkString选填,记录地址、公司等额外信息

电话用String而不用long,是这里最容易踩的坑。电话号码可能包含区号、分机号、前导零,如果把字段声明成long,前导零直接丢失,比如“010-12345678”解析后就变成一套完全不同的数字。邮箱、备注用String没有争议,groupId是否要用自增int也是可以讨论的,但这里用了groupName字符串,理由是运行时允许用户直接创建新分组,后面会专门讲。

Contact类的代码不复杂:

public class Contact { private int id; private String name; private String phone; private String email; private String groupName; private String remark; public Contact() {} public Contact(int id, String name, String phone, String email, String groupName, String remark) { this.id = id; this.name = name; this.phone = phone; this.email = email; this.groupName = groupName; this.remark = remark; } // getter 和 setter 略,IDE 自动生成 }

id的setter要控制访问权限:新增时id由数据层分配,编辑时id不允许修改,实际工程里可以把它改成protected或者直接在数据层用构造方法赋值,避免业务层误改id导致更新错行。name和phone的setter里可以做空值拦截,但更好的做法是提供model层的校验静态方法,界面和数据层共用,保证校验规则不写两遍。

3.2 分组设计:字符串字段比枚举更灵活

groupName用String而不是Java枚举,是有意做的取舍。枚举在代码里写死分组集合,新增“同事”“家人”“客户”这类分组要改代码重新编译;String则支持运行时创建新分组,用户可以在界面上直接输入分组名,数据层不需要感知新分组的存在。对通信录这类轻交互工具,运行时可扩展比编译期类型安全更重要。

String当然也有风险。比如“同事”和“同事 ”带尾随空格会被当成两个分组,录入时统一做trim能解决大部分问题;再比如大小写不一致,“Family”和“family”在统计时会被算成两组。这些问题的通用解法是规范化函数:任何分组名进入模型层之前先trim,再按统一规则处理空串为“默认分组”。分组统计用Map来聚合,聚合结果的key就是规范化之后的分组名,这样展示给用户的时候不会出现同义分组的奇怪情况。

分组在界面上的作用不只是数据归类。左侧分组列表的点击事件会调用分组过滤查询,把当前选中的分组传给数据访问层,由内部按groupName精确匹配。加了这个交互之后,搜索框和分组之间的关系要处理好:我采用的约定是如果搜索框有内容,分组过滤暂时失效,因为用户明确在做关键字检索;搜索框清空后恢复分组过滤。否则两个条件叠加,用户容易搞不清当前到底在看哪一层数据。

3.3 界面布局与交互流程

主窗口布局采用经典左树右表结构:左侧是JList或JTree显示分组列表,每个分组后面带联系人数量;右侧是JTable显示联系人信息,展示姓名、电话、邮箱和备注;顶部是搜索框和“添加”“编辑”“删除”三个按钮;底部状态栏显示总人数和当前搜索命中数。这个布局对通信录来说最高效,也是用户心智中最熟悉的样式。

新增和编辑共用同一个联系人对话框,对话框用GridBagLayout排两列:左列放标签,右列放输入框。GridBagLayout是Swing里最灵活的布局器,虽然写起来啰嗦,但窗口拉伸时控件能按权重均匀扩展,不像BorderLayout或FlowLayout那样容易出现控件堆叠或间距失真的问题。

交互流程定为:添加按钮打开空表单,保存后刷新表格;双击表格行打开编辑表单,预填当前行数据;删除按钮先弹确认框,确认后再执行删除。整个流程单向清晰,每个操作都有可预期的反馈。保存前的校验顺序也有讲究:先校验姓名非空,再校验电话格式,再校验邮箱格式,任何一个不通过都弹提示并停留在对话框,不执行写入。校验失败时的焦点要落在具体输入框上,比如电话格式错了,光标直接定位到电话输入框,比单纯弹一条错误信息体验好很多。对话框的保存按钮绑定了Enter键,取消按钮绑定了Esc键,键盘操作能覆盖大部分使用场景,不用鼠标也能完成一条联系人的增改。

4. 核心代码实现:CSV读写、增删改查和搜索可以直接复用

4.1 CsvUtil:带引号转义的CSV读写工具

CSV最容易被忽略的是转义规则。一个电话号如果是“123,456”这种带逗号的字符串,直接按逗号切分会把字段拆成两半;备注里如果包含换行,也会把一行记录拆成两行。标准做法是对包含特殊字符的字段做双引号包裹,字段内部的引号用双引号转义,这样生成的CSV才能让Excel正确解析。

public class CsvUtil { // 读取整个CSV文件,每行返回一个String数组 public static List<String[]> read(File file) throws IOException { List<String[]> rows = new ArrayList<>(); BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); String line; while ((line = reader.readLine()) != null) { rows.add(parseLine(line)); } reader.close(); return rows; } // 把所有行写入CSV文件,自动处理转义 public static void write(File file, List<String[]> rows) throws IOException { BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); for (String[] row : rows) { writer.write(formatLine(row)); writer.newLine(); } writer.close(); } // 写出时对含逗号/引号/换行的字段加双引号 private static String formatLine(String[] fields) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < fields.length; i++) { if (i > 0) sb.append(','); String field = fields[i] == null ? "" : fields[i]; if (field.contains(",") || field.contains("\"") || field.contains("\n")) { field = field.replace("\"", "\"\""); sb.append('"').append(field).append('"'); } else { sb.append(field); } } return sb.toString(); } // 逐字符解析,正确处理引号内的逗号和换行 private static String[] parseLine(String line) { List<String> fields = new ArrayList<>(); StringBuilder cur = new StringBuilder(); boolean inQuotes = false; for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); if (inQuotes) { if (c == '"') { if (i + 1 < line.length() && line.charAt(i + 1) == '"') { cur.append('"'); // 双引号转义 i++; } else { inQuotes = false; } } else { cur.append(c); } } else if (c == '"') { inQuotes = true; // 进入引号包裹区 } else if (c == ',') { fields.add(cur.toString()); cur.setLength(0); } else { cur.append(c); } } fields.add(cur.toString()); return fields.toArray(new String[0]); } }

逻辑说明:formatLine负责写出,对包含逗号、引号或换行的字段加双引号包裹,字段内部的引号用双引号转义,这样Excel打开不会乱列。parseLine是手动状态机,逐字符扫描,只有正确跳过引号对,才能解析带换行的多行备注。如果不用这段解析而是直接line.split(","),一条备注含逗号就会把字段拆开,后续所有列都会错位。

参数说明:read返回值是List<String[]>,每个String数组对应CSV一行,索引对应列顺序;write接收的List<String[]>里某个元素为null时按空串处理。字符集固定用UTF_8,保证跨平台一致。文件不存在时read抛出IOException,调用处要捕获并返回空列表,不能让程序启动崩溃。

4.2 ContactDao:自增id与增删改查

数据访问层负责把Contact对象和CSV文件互相转换,同时维护自增id。每次启动时扫描全表计算最大id,后续新增基于这个最大值递增,这样即使重启应用,id也不会从1重新开始导致覆盖已有记录。

public class ContactDao { private final File file; private final List<Contact> contacts = new ArrayList<>(); private int nextId = 1; public ContactDao(File file) { this.file = file; load(); } // 启动时加载CSV全部联系人 private void load() { contacts.clear(); if (!file.exists()) return; try { for (String[] row : CsvUtil.read(file)) { if (row.length < 3) continue; Contact c = new Contact(); c.setId(Integer.parseInt(row[0])); c.setName(row[1]); c.setPhone(row[2]); c.setEmail(row.length > 3 ? row[3] : ""); c.setGroupName(row.length > 4 ? row[4] : "默认分组"); c.setRemark(row.length > 5 ? row[5] : ""); contacts.add(c); if (c.getId() >= nextId) nextId = c.getId() + 1; } } catch (IOException e) { // 文件损坏时保留空列表,不让程序启动失败 } } public List<Contact> findAll() { return new ArrayList<>(contacts); } // 新增联系人,id由这里分配 public boolean add(Contact c) { c.setId(nextId++); contacts.add(c); return save(); } // 按id更新联系人 public boolean update(Contact c) { for (int i = 0; i < contacts.size(); i++) { if (contacts.get(i).getId() == c.getId()) { contacts.set(i, c); return save(); } } return false; } // 按id删除联系人 public boolean delete(int id) { Iterator<Contact> it = contacts.iterator(); while (it.hasNext()) { if (it.next().getId() == id) { it.remove(); return save(); } } return false; } // 全量写回CSV,先写表头再写数据行 private boolean save() { List<String[]> rows = new ArrayList<>(); rows.add(new String[]{"id", "name", "phone", "email", "groupName", "remark"}); for (Contact c : contacts) { rows.add(new String[]{ String.valueOf(c.getId()), c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark() }); } try { CsvUtil.write(file, rows); return true; } catch (IOException e) { return false; } } }

逻辑说明:load在构造时调用,把CSV全量读进内存;add分配自增id后追加到列表并落盘;update按id找位置再替换,不改变联系人原有顺序;delete用迭代器安全移除。save每次全量写文件,数据量在几千条以内性能完全够用,不需要做增量写入。

参数说明:findAll返回的是新ArrayList副本,外部拿到的集合无论如何修改都不会影响内部列表,避免界面层绕开DAO直接操作数据;save先写表头再写数据行,所以CSV第一行是字段名,外部用Excel编辑时也看得懂;nextId的初始化逻辑保证删除最后一个联系人之后,新加入的联系人id从删除前的最大值继续递增,不会复用旧id,界面上不会有两条记录因为id相同而在更新时互相覆盖。

4.3 新增与编辑逻辑:区分两个分支

界面新增和编辑通常共用同一个表单,但提交逻辑必须分开:新增走add,编辑走update。常见翻车是把编辑实现成“先删后加”,导致id变化,历史引用和排序全部错乱。

public boolean saveContact(Contact c, boolean isNew) { if (isNew) { c.setId(0); // 让DAO分配新id return dao.add(c); } else { return dao.update(c); } }

isNew来源于表单打开模式,新增时初始化表单传入true,编辑时传入false,不依赖id是否为0判断,避免用户手动填了一个已存在的id导致新增覆盖旧数据。setId(0)是保险动作,防止界面层误传记忆过的id。

4.4 关键字搜索:一次匹配多个字段

搜索需求比表面看起来复杂:用户搜“张三”可能想找姓名,搜“139”可能想找电话号码,搜“研发”可能想找分组或备注。所以搜索不能只匹配name字段,而是把name、phone、email、remark、groupName五个字段拼成一个大字符串,统一做不区分大小写的包含匹配。

public List<Contact> search(String keyword) { if (keyword == null || keyword.trim().isEmpty()) { return findAll(); } String k = keyword.trim().toLowerCase(); List<Contact> result = new ArrayList<>(); for (Contact c : contacts) { String haystack = String.join(" ", c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark()).toLowerCase(); if (haystack.contains(k)) { result.add(c); } } return result; }

逻辑说明:String.join把五个字段拼成一整行,toLowerCase统一大小写,contains做包含匹配。搜索“139”能命中手机号,“研发”能命中分组或备注,比只匹配姓名实用得多。

参数说明:空关键字返回全部联系人,调用方可以直接复用这个结果刷新表格,不用单独做“无搜索词”分支。这里没有用正则,是因为包含匹配已经满足九成场景,正则要处理特殊字符转义问题,反而容易引入新的边界错误。

5. 常见问题与避坑记录:通信录开发中五个真实的翻车现场

5.1 中文乱码:CSV文件在Excel里打开全变问号

现象:程序内部显示联系人姓名正常,导出CSV后用Excel打开,所有中文字段变成乱码或问号,但英文和数字正常。

原因:Java按UTF-8写文件,而Excel对CSV默认按ANSI编码解析,两边编码不一致,中文自然乱。

解决:在CsvUtil写文件时改用GBK可以立刻解决,但会导致程序自己再读文件时也要同步改编码,否则两边不一致。更可靠的方法是写带BOM的UTF-8文件,也就是文件开头写入EF BB BF三个字节,Excel识别后按UTF-8解析,Java侧读文件仍按UTF-8处理,互不影响。

提示:如果CSV文件还要在Linux或macOS下用脚本处理,BOM会被当成特殊字符混进第一行,这时改用普通UTF-8更合适。建议在工具类里加一个布尔常量控制是否输出BOM。

从那以后,凡是涉及CSV文件,我都会先确认目标用户用什么工具打开,再决定要不要BOM,而不是默认UTF-8就完事。

5.2 删除联系人没有后悔药:误删了整条记录

现象:点击删除后记录直接消失,再想找回只能重新录入,只记得姓名但记不清号码时基本等于彻底丢失。

原因:删除操作直接执行移除并落盘,没有确认提示,也没有任何恢复路径。

解决:第一层是删除前弹确认框,把“我确认删除”做成按钮文案而不是只有“确定”,降低误点概率。第二层是软删除:Contact增加deleted字段,删除时把deleted置为true,查询默认过滤掉已删除记录,界面上提供“显示已删除”的开关,勾选后能从界面直接恢复。CSV里多一列,兼容性完全不受影响。这个设计花不了多少代码,但能把“误删找不回”这个最大的痛点直接消掉。

5.3 电话号码校验太松:录进去一堆不完整号码

现象:电话字段什么都能保存,“abc”“123”也进得去,到按号码找人时发现大量脏数据。

原因:保存逻辑只判断非空,没有对电话号码格式做约束,键盘上误触的字符全部被当成合法数据写入。

解决:在模型层加静态校验方法,手机号和座机号分开匹配。手机号按国内习惯匹配1开头的11位数字,座机号匹配区号加号码,区号和号码之间允许一个连字符。界面保存前先调这个方法,数据层新增更新前再调一次,双重保险。

public static boolean isValidPhone(String phone) { if (phone == null || phone.trim().isEmpty()) return false; return phone.matches("^1[3-9]\\d{9}$") || phone.matches("^0\\d{2,3}-?\\d{7,8}$"); }

正则中^1[3-9]\d{9}$覆盖手机号,^0\d{2,3}-?\d{7,8}$覆盖带区号的座机号,-?表示兼容用户是否输入了连字符。如果你的场景涉及国际号码,把规则放宽为“以+开头后跟数字和空格”即可,不要直接删掉校验,否则脏数据会卷土重来。

5.4 表格刷新不及时:修改后界面没变化

现象:新增、删除或更新联系人之后,表格还是原来的数据,要重启程序才能看到最新结果。

原因:JTable显示的是TableModel里的数据,直接修改内部ArrayList不会自动触发界面重绘,这是Swing的经典翻车点。

解决:数据变更操作完成后,重新刷新表格模型。简单做法是setRowCount(0)清空,再逐行addRow;或者保留原有行对象,更新完成后调用fireTableDataChanged()通知视图重新渲染。

public void refreshTable(List<Contact> data) { model.setRowCount(0); for (Contact c : data) { model.addRow(new Object[]{ c.getId(), c.getName(), c.getPhone(), c.getEmail(), c.getGroupName(), c.getRemark() }); } }

把刷新封装成一个方法后,添加、删除、搜索、分组切换全部调用同一个入口,表格展示的一致性由这个方法统一保证,不会出现某个操作忘记刷新导致界面状态不一致的问题。

5.5 文件读写卡住界面:数据量大时窗口无响应

现象:联系人到几千条时,点保存按钮窗口卡顿几秒,严重时松开鼠标还要过一会儿才响应。

原因:save方法在事件分发线程(EDT)里执行全量文件写入,文件越大阻塞时间越长,期间界面所有事件都处理不了。

解决:把文件写入放到SwingWorker的doInBackground里执行,done回调里再做界面刷新,这样写文件期间界面仍然可以响应点击。几百条数据时感受不明显,但几千条就能明显感觉到窗口冻结,不处理会被用户定性为“程序卡死”。

SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() { @Override protected Void doInBackground() { dao.flush(); return null; } @Override protected void done() { refreshTable(dao.findAll()); } }; worker.execute();

doInBackground里只做数据持久化,done里访问Swing组件,这是SwingWorker的标准用法。不要用普通Thread在后台直接更新JTable,Swing组件不是线程安全的,跨线程更新会出现界面状态错乱甚至抛出异常。

6. 进阶用法:vCard导出、批量导入和一份联系人数据的统计价值

6.1 导出为vCard格式,实现在多设备间流转

CSV适合自己备份,vCard才是手机和邮箱客户端通用的联系人交换格式,手机导入、蓝牙传输、邮件导入都认它。导出单条联系人的vCard代码如下:

public static String toVCard(Contact c) { StringBuilder sb = new StringBuilder(); sb.append("BEGIN:VCARD\n"); sb.append("VERSION:3.0\n"); sb.append("FN:").append(c.getName()).append("\n"); sb.append("TEL:").append(c.getPhone()).append("\n"); if (c.getEmail() != null && !c.getEmail().isEmpty()) { sb.append("EMAIL:").append(c.getEmail()).append("\n"); } sb.append("END:VCARD\n"); return sb.toString(); }

vCard格式有两个容易翻车的细节:换行符要用\r\n,有些手机客户端只认这个;中文字段最好在文件头标注编码,否则导入后姓名可能乱码。单条能导出,全部导出就把循环外包一层,弹一个保存对话框让用户选目录,循环写入。有了这个功能,这个通信录就可以当作个人数据的备份中枢,在多个设备之间流转而不被某个平台锁住。

6.2 批量导入:把Excel整理好的联系人一次加载

批量导入是CsvUtil最直接的应用:读取CSV时跳过第一行表头,逐行构建Contact对象,先做格式校验再入库。批量导入最怕“成功一半失败一半”,处理策略是先校验所有行,把错误行收集起来统一返回给用户,全部合法才执行写入;不能边读边写遇到错误就中断,否则用户不知道哪些进去了哪些没进去,数据处于半新半旧状态。

6.3 从联系人数据里挖出统计价值

联系人数据存得越久越有价值。按分组统计人数是最实用的一项,用Map聚合一遍就能输出“同事45人、家人12人、客户27人”这样的结果。数据量继续变大时,还可以按月份统计新增联系人趋势,看出自己的社交关系是扩张还是收缩。这些功能代码量都不大,却能体现对数据结构的理解,是课程设计答辩里很自然的加分项。

public Map<String, Integer> countByGroup() { Map<String, Integer> counts = new LinkedHashMap<>(); for (Contact c : contacts) { String g = c.getGroupName() == null ? "默认分组" : c.getGroupName(); counts.put(g, counts.getOrDefault(g, 0) + 1); } return counts; }

统计结果用JTable里的两列表格展示即可,不需要引入图表库。如果还想更直观,把统计结果按数值降序排列,前几个分组就是当前联系人关系的主要构成,这份数据的可读性会再上一个台阶。

做这个通信录项目时,我吃过最多的亏就是只把功能跑通算完,在编码和误删恢复上反复摔跤。从那以后,凡是落盘的数据项目,我都会强制走一遍“备份入口、可恢复路径、编码统一”三个检查点,再去看功能菜单。这套通信录管理系统从选型到避坑都按这个思路整过一遍,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进