Java通信录项目实战:基于SQLite与JDBC的增删改查实现

发布时间:2026/10/23 3:13:42

Java通信录项目实战:基于SQLite与JDBC的增删改查实现 简介基于Java开发的通信录管理系统完整源码适合Java初学者、在校学生用于课程设计或毕业设计参考。系统支持联系人信息的增加、删除、修改与查询并通过JDBC访问MySQL数据库实现数据的持久化存储整体采用MVC分层设计将模型、视图与控制逻辑清晰分离便于维护和扩展。资源共42个文件包含14个Java源码、14个class编译文件、6个JSP页面以及SQL初始化脚本、JAR依赖库和Eclipse工程配置文件压缩包约636KB结构清晰可直接导入开发环境运行和二次修改。目前已有145人学习下载。通过研读该项目可以掌握Java Web开发中的数据库增删改查、前后端数据传递、异常处理与文件导入导出等实用技能也能理解一个完整小型系统从界面到数据层的组织方式对提升工程化编码能力很有帮助。1. 通信录 Java 项目实战一个不依赖任何重量级框架的联系人增删改查某开发者从旧硬盘里翻出一个通信录 Java 项目时第一反应是“怎么还有文件名带 final 的代码”但读完后觉得这个项目把核心知识落得很扎实实体类、DAO、服务层、SQLite 持久化、控制台交互一样不少没有任何框架包袱IDE 里打开就能跑。它解决的问题很直接不靠 MySQL不靠 Redis只用一个嵌入式数据库文件就能完成联系人的添加、删除、修改、查询并且重启程序数据依然在。它适合三类人刚学完 Java 基础和 JDBC 的新手想快速补一个增删改查项目的课程设计选手以及需要在真实项目里抄一段 SQLite 持久化逻辑的开发者。2. 技术栈与工程结构SQLite 为什么够用Maven 依赖怎么选2.1 源码包拆解一个典型的 Maven 目录能理出哪些责任拿到源码包后先把目录树拉出来看项目结构非常规矩没有多余文件通信录Java/ ├── pom.xml ├── src/main/java/ │ └── com/example/addressbook/ │ ├── App.java # 程序入口 │ ├── model/ │ │ └── Contact.java # 联系人实体 │ ├── dao/ │ │ └── ContactDao.java # SQLite 数据访问层 │ ├── service/ │ │ └── ContactService.java # 业务校验与转发 │ └── cli/ │ └── MenuHandler.java # 控制台菜单交互 ├── src/main/resources/ │ └── addressbook.db # SQLite 数据库文件 └── src/test/java/ └── com/example/addressbook/ ├── ContactServiceTest.java # 服务层单元测试 └── ContactDaoTest.java # DAO 层集成测试这种分层的妙处在于每一层只干一件事。model里放纯数据不掺任何 JDBC 代码dao只管 SQL 和结果集的转换service负责判断“姓名不能为空”“手机号格式对不对”cli则把用户输入的字符串翻译成 service 调用。后续想改成 Web 接口只需要写一个新的 Controller复用 service 和 dao。这里有一个容易被新手忽略的细节addressbook.db放在resources下并不是让程序从 classpath 里读而是让 Maven 打包时把空文件带到根目录防止首次运行时因为找不到数据库路径而怀疑人生。真正连接时用的是相对路径jdbc:sqlite:addressbook.db工作目录在哪就在哪创建库文件。2.2 依赖选型sqlite-jdbc、JUnit 5、Lombok 各自承担什么pom.xml里的依赖很少可以一眼看清项目的地基project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdaddressbook/artifactId version1.0.0/version properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties dependencies dependency groupIdorg.xerial/groupId artifactIdsqlite-jdbc/artifactId version3.42.0.0/version /dependency dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.10.0/version scopetest/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.28/version scopeprovided/scope /dependency /dependencies /projectsqlite-jdbc一个包搞定驱动和 SQLite 原生库不需要额外安装数据库服务连接方式就是DriverManager.getConnection(jdbc:sqlite:数据库路径)这一行。值得说明的是这里的scope没有写provided是compile默认级别意味着最终 jar 包会内含驱动类避免打包后单独再找 lib。JUnit 5 的范围是test所以生产 jar 不会带测试框架。Lombok 在这类小项目里属于可选项我用它主要是省掉getter/setter的样板代码但代码生成是编译期完成的一旦 IDE 没开启注解处理编译就会一脸懵。如果团队里有人习惯不装 Lombok 插件干脆不用它手动写 getter 也不多。这个依赖组合反映了一个非常重要的选型直觉通信录场景的数据量顶天几千条SQLite 的单文件事务机制完全兜得住。如果一开始拍脑袋上 MySQL不但要安装服务还要配置账号密码学习和运维成本直接把“先跑通”门槛抬高了。项目的用户价值是联系人的增删改查不是高并发所以 SQLite 是这里的最优解不是妥协。3. 核心模块拆解一个联系人从输入到落盘的完整链路3.1 实体层 Contactid 要不要单独设计DEFAULT 值如何取舍Contact.java是这个项目里最基础的一块砖。它不需要继承任何父类也不实现任何接口就是一个 POJO。package com.example.addressbook.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; Data NoArgsConstructor AllArgsConstructor public class Contact { private Integer id; private String name; private String phone; private String email; }为什么 id 用Integer而不是int因为新增联系人时 id 是空的DAO 层执行完 INSERT 后要拿自增主键回填到这个对象上用null来表示“还未落库”会让逻辑很干净。SQLite 的主键AUTOINCREMENT从 1 开始递增删除掉最后一条再新插id 也会继续增长不会复用旧号。这样的好处是 id 的语义永远是“唯一标识”不会因为删除了某条记录就产生歧义。字段校验没有用一堆注解而是放在 Service 层手动判断。因为实体类要尽量纯净不该依赖jakarta.validation那套运行时注解万一以后换成 Web 项目这些注解又会和 Spring 的校验机制混淆。真正的校验逻辑在后面 service 层这样实体类既可用于数据库映射也能直接序列化到 JSON。3.2 持久层 ContactDaoPreparedStatement 参数绑定与自增主键回填DAO 层是整个项目的黑匣子枢纽。这一层写得好不好直接决定程序换一台机器跑还会不会出问题。下面是核心增删改查实现package com.example.addressbook.dao; import com.example.addressbook.model.Contact; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ContactDao { private final Connection conn; public ContactDao() throws SQLException { String url jdbc:sqlite:addressbook.db; conn DriverManager.getConnection(url); try (Statement stmt conn.createStatement()) { stmt.execute( CREATE TABLE IF NOT EXISTS contact ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, email TEXT ) ); } } public int add(Contact c) throws SQLException { String sql INSERT INTO contact(name, phone, email) VALUES(?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, c.getName()); ps.setString(2, c.getPhone()); ps.setString(3, c.getEmail()); int affected ps.executeUpdate(); try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { c.setId(keys.getInt(1)); } } return affected; } } public int update(Contact c) throws SQLException { String sql UPDATE contact SET name ?, phone ?, email ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, c.getName()); ps.setString(2, c.getPhone()); ps.setString(3, c.getEmail()); ps.setInt(4, c.getId()); return ps.executeUpdate(); } } public int delete(int id) throws SQLException { String sql DELETE FROM contact WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate(); } } public ListContact list() throws SQLException { String sql SELECT id, name, phone, email FROM contact ORDER BY id; try (Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { ListContact contacts new ArrayList(); while (rs.next()) { contacts.add(mapRow(rs)); } return contacts; } } private Contact mapRow(ResultSet rs) throws SQLException { Contact c new Contact(); c.setId(rs.getInt(id)); c.setName(rs.getString(name)); c.setPhone(rs.getString(phone)); c.setEmail(rs.getString(email)); return c; } }这里最关键的习惯是所有拼接 SQL 的地方一律用PreparedStatement的?占位符绝不把用户输入字符串直接拼进 SQL。通信录项目虽然看起来没有风险但一旦用户输入张三; DROP TABLE contact; --这种名字拼 SQL 就会让整张表瞬间消失。用参数绑定后SQLite 驱动会把整个字符串当作一个普通字段值处理注入方案直接失效。RETURN_GENERATED_KEYS是自增主键回填的常规做法。注意getGeneratedKeys()必须紧跟在executeUpdate()之后调用中间不能再执行其他 SQL否则这个结果集可能被重置。如果你在写某个功能时需要同时插入多条联系人和关联表这个机制同样适用只是要把GeneratedKeys的结果遍历完整。DAO 层没有关闭连接因为这里用的是单例连接生命周期归整个程序所有。控制台程序退出时 JVM 停止会自动断开连接。但如果是 Web 应用这种写法要改成连接池管理否则并发请求会互相干扰。这也是初学者把 DAO 抄进 Spring 项目时最容易踩的坑。3.3 服务层与菜单层校验规则、循环状态和异常转译Service 层把 DAO 的SQLException转成语义明确的业务异常同时把菜单层可能误输入的脏数据拦在门口。package com.example.addressbook.service; import com.example.addressbook.dao.ContactDao; import com.example.addressbook.model.Contact; public class ContactService { private final ContactDao dao; public ContactService() throws Exception { dao new ContactDao(); } public int add(Contact c) throws Exception { if (c.getName() null || c.getName().isBlank()) { throw new IllegalArgumentException(姓名不能为空); } if (c.getPhone() ! null !c.getPhone().matches(\\d{3,15})) { throw new IllegalArgumentException(电话只能包含数字长度需在3到15位之间); } return dao.add(c); } public int update(Contact c) throws Exception { if (c.getId() null) { throw new IllegalArgumentException(修改时 id 不能为空); } return dao.update(c); } public ListContact listAll() throws Exception { return dao.list(); } public int delete(int id) throws Exception { if (id 0) { throw new IllegalArgumentException(id 必须为正数); } return dao.delete(id); } }业务校验放在服务层而不是 DAO 层是为了让 DAO 专注于“执行 SQL”这一件事。比如未来加一个“批量导入联系人”的功能服务层仍然要逐条走同样的校验逻辑不会漏掉。控制台交互层MenuHandler的循环设计有点小心机package com.example.addressbook.cli; import com.example.addressbook.model.Contact; import com.example.addressbook.service.ContactService; import java.util.List; import java.util.Scanner; public class MenuHandler { private final ContactService service; public MenuHandler() throws Exception { service new ContactService(); } public void run() { Scanner scanner new Scanner(System.in); boolean running true; while (running) { try { System.out.println(请选择1.添加 2.全部联系人 3.修改 4.删除 5.退出); String command scanner.nextLine(); switch (command) { case 1 - addContact(scanner); case 2 - listContacts(); case 3 - updateContact(scanner); case 4 - deleteContact(scanner); case 5 - running false; default - System.out.println(无效选项请重新输入); } } catch (Exception e) { System.out.println(操作失败 e.getMessage()); } } scanner.close(); } private void addContact(Scanner scanner) { System.out.print(姓名); String name scanner.nextLine(); System.out.print(电话); String phone scanner.nextLine(); System.out.print(邮箱); String email scanner.nextLine(); Contact c new Contact(null, name, phone, email); try { int rows service.add(c); System.out.println(添加成功影响行数 rows 生成 id c.getId()); } catch (Exception e) { System.out.println(添加失败 e.getMessage()); } } }run()方法把整个循环体包在 try/catch 里是个看起来很糙但极其实用的做法。控制台程序本身就是一次性的短会话与其在每一个方法里单独 catch 异常不如在最外层统一拦截并显示e.getMessage()既能保证程序不因一个小错误崩溃退出又能让用户看到具体原因。通信录这种工具型项目稳定输出比花哨的错误处理更有价值。switch (command)使用 Java 14 之后的箭头语法每个分支不再需要break执行完自动跳出。菜单命令用字符串而不是数字可以避免Scanner.nextInt()与后续nextLine()混用时的那种换行符陷阱后面避坑章节会详细展开。4. 从零到跑通Maven 构建、SQLite 初始化与三种启动姿势4.1 一条命令链先 clean再 test最后 package打开终端进入源码根目录优先执行mvn clean test这条命令会把之前编译过的旧 class 清掉然后跑一遍单元测试。通信录项目的测试不多但ContactServiceTest里有一个非常关键的用例验证空格姓名会被拦截。只要这个测试通过说明 Service 层的校验真的生效了。接着打包mvn package -DskipTests跳过测试是为了快速出 jar省去二次验证。打完包后在target/下生成addressbook-1.0.0.jar运行java -jar target/addressbook-1.0.0.jar如果你用的是 IDEA可以直接打开App.java右键“运行”也能启动。这背后隐含着一条规矩项目里不该有任何自定义启动脚本让标准的 Maven 生命周期保证“本地能跑命令行也能跑”。4.2 第一次启动数据库文件在哪里生成建表语句何时执行首次启动时ContactDao构造方法里的CREATE TABLE IF NOT EXISTS会自动创建表结构。表不存在才创建存在就跳过所以哪怕你把contact表手动删掉再启动程序也能自愈这是往里写业务前必须先做好的底层保障。此时注意观察工作目录如果你用java -jar在项目根目录运行就会在根目录下看到addressbook.db文件。如果你在 IDEA 里运行则取决于 working directory 的配置。有些同学设置错了工作目录导致程序重新连到了另一个空库文件上看起来数据全没了其实数据还在原来的位置。一个稳妥的验证方式是看日志或加一行输出。DAO 构造里可以把conn.getMetaData().getURL()打印出来这样能立刻知道当前连接到了哪个路径。4.3 持久性验证用 sqlite3 命令直接查表程序添加几个联系人并退出后执行sqlite3 addressbook.db .tables SELECT * FROM contact;如果能看到刚插入的记录说明数据没有只停留在内存里。再去修改和删除一条记录重复同样查询会发现 UPDATE 和 DELETE 确实落盘了。这一步跑完才算真正验证了“持久化”而不是只看了代码就以为没问题。需要注意SQLite 的读锁与写锁会互相阻塞如果你在程序运行的同时打开 SQLite 命令行执行写操作可能收到database is locked提示。这是因为程序持有的连接还没释放。通信录这种单用户程序一般不会遇到但如果你把这段代码搬到一个长连接常驻的服务进程里就要考虑连接是否被正确关闭。5. 避坑指南这类项目最容易翻车的几个点5.1 现象重启后联系人全没了代码看着也没错原因SQLite 默认处于自动提交关闭状态你执行INSERT/UPDATE/DELETE后事务没有提交。程序正常退出时未提交的事务被回滚所以数据消失。解决在 DAO 层执行写操作后统一调用conn.commit()。简洁的做法是让Connection开启自动提交conn.setAutoCommit(true);很多教程里会写一句话“JDBC 默认自动提交”这句话在 MySQL 驱动上是真的但在 SQLite JDBC 上默认行为会坑人。我一般会在构造方法里显式设置不让默认行为跑偏。5.2 现象控制台中文乱码姓名变成问号原因Windows 控制台默认编码是 GBK而源码和项目配置是 UTF-8。Scanner读入时按平台默认编码解析中文就裂了。解决启动时附加-Dfile.encodingUTF-8参数或者把 IDE 的 console output encoding 改成 UTF-8。在 IDEA 里是“Help - Edit Custom VM Options”加上-Dfile.encodingUTF-8。如果是在纯命令行跑 jar就写成java -Dfile.encodingUTF-8 -jar target/addressbook-1.0.0.jar5.3 现象SQL 执行报错 no such table: contact原因Connection连接成功但建表语句没执行成功或者当前连接的是另一个路径的库文件。最常见的是把url写成了jdbc:sqlite:src/main/resources/addressbook.db但打包后运行时没有src/main/resources这层路径。解决统一使用相对路径jdbc:sqlite:addressbook.db并在 DAO 构造里先执行建表语句再开始增删改查确保顺序正确。5.4 现象输入 5 想退出程序却直接跳过或死循环原因如果菜单用了Scanner.nextInt()用户输入数字后会残留一个换行符之后nextLine()会把空字符串读取为下一次命令导致分支匹配失败。解决永远用scanner.nextLine()读取整行然后把字符串解析成命令。对应本项目里的switch (command)方案。这个坑几乎是控制台项目的通病我遇到过的翻车现场比 SQL 报错还多。5.5 现象打包出的 jar 运行提示 ClassNotFoundException: org.sqlite.JDBC原因sqlite-jdbc依赖没有打进 jar。如果你把 Maven 打包插件配置成了maven-assembly-plugin但没指定shadeMainJar或者误把依赖 scope 改成provided就会出现这种鬼问题。解决直接用maven-shade-plugin配置includes或者把依赖的scope保持在默认状态。推荐最简单的方式plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals /execution /executions /plugin配置后重新mvn clean package再查看生成的 jar 大小如果从几十 KB 变成几 MB就说明驱动被打进去了。6. 二次开发实战从控制台升级为 Swing GUI 的最小改动方案6.1 只替换交互层把 Scanner 换成 JFrame 事件监听控制台菜单层是唯一依赖java.util.Scanner的地方而 Service 和 Dao 都不关心用户如何输入。因此升级 GUI 时只需新增一个view/ContactFrame.java不动其它任何类。写一个 200 行的 Swing 面板表格用JTable展示service.listAll()按钮监听触发service.add/update/delete。这里最需要小心的是Swing 的 UI 线程不能执行耗时操作SQLite 的执行速度极快可以忽略但如果在其他项目里换成了网络数据库务必用SwingWorker包一层避免忘掉这个边界。6.2 增加“标签”字段并支持按标签筛选数据库、实体、菜单三层联动第一步在表里加字段ALTER TABLE contact ADD COLUMN tag TEXT DEFAULT 朋友;第二步在Contact实体加private String tag;同时更新mapRow、PreparedStatement和INSERT/UPDATESQL。第三步在服务层搜索方法public ListContact searchByTag(String tag) throws Exception { return dao.searchByTag(tag); }DAO 层里写public ListContact searchByTag(String tag) throws SQLException { String sql SELECT id, name, phone, email, tag FROM contact WHERE tag ? ORDER BY id; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, tag); try (ResultSet rs ps.executeQuery()) { ListContact result new ArrayList(); while (rs.next()) { result.add(mapRow(rs)); } return result; } } }这样改动后旧的控制台菜单里多加一个6.按标签筛选分支就能测试Swing 版也可以在搜索框里复用同一个 service 方法。6.3 用 JUnit 钉死回归先写测试再改代码二次开发最怕的是改完字段把原来的查询逻辑带崩。我的习惯是先补一个回归测试再动代码。比如验证新增联系人后listAll()能查到带tag字段的完整对象以及按标签筛选时空列表不会抛异常。等红测用例出现再补实现最后全绿收工。这比手工点菜单快得多也能避免改到一半把 DDL 都忘了。从那次以后我每次拿到一个新的 Java 项目都会强制走一遍流程先看 pom.xml 理清依赖再跑一遍测试确认红绿状态然后用一条最简单的 CRUD 链路打断点最后才敢动手改代码。这种习惯帮我省掉了无数次“明明我没动这里怎么就坏了”的自我怀疑。希望你也能用这个通信录项目少走几步我曾踩过的冤枉路早日把整套流程跑顺。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/23 3:13:42

KubeEdge实战:云原生边缘计算架构与最小集群部署

如果你正在做物联网平台、工业互联网、车联网或者任何“设备在远端、服务在云端”的系统,大概率遇到过这样一个两难:设备端需要容器化和弹性调度,但设备端网络又经常不稳定;云端 Kubernetes 集群很成熟,却没法把能力平…

2026/10/23 3:13:42

WorkBuddy开源工作台:LLM Agent调用分析实践指南

WorkBuddy 零基础从入门到精通:一个开源 LLM Agent 调用分析工作台的完整实践指南如果你最近在做 Agent 应用,一定绕不开一个尴尬场景:模型在普通对话里表现很好,一旦让它调用工具,就开始“自由发挥”——该传的参数不…

2026/10/23 3:13:42

AI辅助论文写作:五大类工具与合规使用指南

1. 2025届论文季,AI辅助论文助手的定位早就变了每年到了2月底,我都能看到一批2025届的同学开始晒进度:有的人已经在改第三稿,有的人还在跟导师确认开题方向,还有人偷偷把整章丢给AI,结果被导师一句“这段话…

2026/10/23 4:13:51

随心撤销重做:visual-json History类实现undo/redo机制源码走读

【免费下载链接】visual-json The Visual JSON Editor. Schema-aware, embeddable, extensible. 项目地址: https://gitcode.com/gh_mirrors/vis/visual-json 点击查看 免费下载 visual-json 是一款支持 Schema 感知、可嵌入、可扩展的可视化 JSON 编辑器&#xff…

2026/10/23 4:13:51

Spirula Studio 入门指南:一个二进制跑通 3D 高斯泼溅训练

Spirula Studio 入门指南:一个二进制跑通 3D 高斯泼溅训练 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Spi…

2026/10/23 4:08:51

霞鹜文楷:2 万余字开源楷体字体免费商用

霞鹜文楷:2 万余字开源楷体字体免费商用 【免费下载链接】LxgwWenKai An open-source Chinese font derived from Fontworks Klee One. 一款开源中文字体,基于 FONTWORKS 出品字体 Klee One 衍生。 项目地址: https://gitcode.com/GitHub_Trending/lx…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/23 0:02:26

Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战

说实话,这几年接触了不少想入门FPGA的朋友,如果你问哪个项目能一口气把Verilog的核心基本功串起来,我通常会推荐电子密码锁。这个项目看着不起眼,却把矩阵键盘扫描、按键消抖、状态机设计、动态显示刷新、外设控制这些数字逻辑里的…

2026/10/23 0:02:26

数据服务监控运维实战:从指标体系到告警治理与故障排查

周末凌晨两点,手机震了。第一反应是“数据服务挂了”。打开电脑看了下,果然,某张核心报表的数据产出时间已经超过SLA两小时,下游三个业务方连着发来问询。这种场景,做过大数据运维的人都不会陌生。数据服务的监控与运维…

2026/10/23 0:02:26

FreeRTOS任务通知实战详解:内核原理、API与避坑指南

裸机编程切到RTOS之后,任务之间的通信和同步永远是躲不开的坎。前面聊过队列、信号量、互斥锁、事件组,这次把FreeRTOS任务通知(Task Notification)单独拎出来讲透。原因很简单:任务通知是FreeRTOS里最容易用错、也最容…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑