简介:用Java Swing开发的轻量级桌面图书管理程序,所有功能都在本地运行,不依赖数据库。支持添加、删除、修改和查询图书信息,同时管理用户账号和库存数量。数据全部保存在三个普通文本文件里:Books.txt存书名作者等基本信息,User1.txt存用户资料,BooksNum.txt记每本书的库存数,靠Java基础IO和集合类实现读写。界面简洁,带welcome.png启动图,事件响应逻辑清晰,适合刚学完Swing和文件操作的Java新手上手练习。项目结构完整,包含标准src源码目录、编译后的bin文件夹,还有Eclipse工程配置文件(.project、.classpath、.settings),直接拖进Eclipse或MyEclipse就能运行,不用额外配置环境。代码注释适中,重点练的是按钮点击响应、表格显示、文本文件解析和ArrayList增删改查这些核心技能。
1. 项目概述:为什么一个“纯文本+Swing”的图书管理器,反而更适合新手真正吃透Java?
你有没有试过学完Swing组件、事件监听、ArrayList和FileReader之后,想做个完整小项目练手,结果一搜全是“Spring Boot + MySQL + Vue”的大而全方案?界面炫酷,但光配环境就卡半天;代码量动辄上千行,核心逻辑被层层封装埋得看不见。我带过不少刚从《Java核心技术》卷一走出来的新手,他们最常问的一句话是:“老师,我写了按钮监听,也写了读文件,可它们怎么‘连起来’?数据从点击那一刻起,到底在内存里怎么跑的?又怎么稳稳落进硬盘?”——这个问题,恰恰是这个“Java Swing写的本地图书管理工具”存在的全部意义。
它不追求高并发、不搞分布式、不碰网络通信,甚至刻意回避数据库。它用三份普通文本文件(Books.txt、User1.txt、BooksNum.txt)作为全部数据载体,用Swing画出清晰的表格、输入框和按钮,用最朴素的BufferedReader逐行解析、用String.split(“\|”)切分字段、用ArrayList 在内存中实时维护状态、再用PrintWriter原样写回磁盘。整个过程像一条透明水管:用户点“添加”,数据从JTextField流进Book对象,塞进ArrayList,最后哗啦一声冲进Books.txt的末尾;点“查询”,程序不是发SQL,而是拿着关键词遍历整个ArrayList,匹配成功就高亮显示在JTable里。没有ORM映射的黑盒,没有连接池的抽象,没有事务隔离的迷雾——只有你亲手写的每一行IO操作、每一次集合增删、每一个ActionListener.onActionPerformed()的触发瞬间。
这正是它被设计成“Eclipse一键导入”的深层原因:不是为了省事,而是为了消除所有干扰项。当你双击.project文件,Eclipse自动识别为Java项目,src目录下的包结构(com.bookmanage.ui、com.bookmanage.model、com.bookmanage.dao)立刻展开,bin目录里已编译好的.class文件静静待命。你不需要查Maven坐标,不用改pom.xml依赖版本,更不必担心MySQL服务是否启动。你打开MainFrame.java,第一眼看到的就是new JFrame()、setLayout(new BorderLayout())、add(new BookPanel(), BorderLayout.CENTER)——这才是Swing最本真的模样。配套的welcome.png不是装饰,而是你第一次运行时看到的、实实在在的图形界面,它告诉你:“看,这就是你写的代码跑起来的样子。”对新手而言,这种“所见即所得”的确定性,比任何炫技都珍贵。它不教你如何造火箭,但它确保你亲手把第一枚火柴火箭,稳稳地、清清楚楚地,发射升空。
2. 整体架构与设计思路:为什么放弃数据库,坚持“文本即存储”?
2.1 核心设计哲学:用最简路径,暴露最本质的编程逻辑
这个项目的架构图,如果真要画出来,可能就一张A4纸都嫌大:UI层(Swing组件) ↔ 业务逻辑层(BookService、UserService) ↔ 数据访问层(TextFileDAO)。没有DAO接口抽象,没有Spring IoC容器,没有MyBatis的XML映射文件。TextFileDAO类里只有三个核心方法:loadBooks()、saveBooks(List )、loadUsers()、saveUsers(List )、loadStocks()、saveStocks(Map )。每个方法内部,就是几行标准的Java IO代码。这种“扁平化”设计绝非偷懒,而是精准锚定新手的学习瓶颈——当学生还在为“JButton.addActionListener(new ActionListener(){…})”的匿名内部类语法纠结时,引入JDBC驱动加载、Connection获取、PreparedStatement预编译,无异于让刚学会握笔的孩子直接临摹《兰亭序》。
选择纯文本而非数据库,背后有三层不可替代的教学价值:
- 第一层:IO操作的具象化。数据库的“insert into books values(…)”是一条抽象命令,而文本存储的“bw.write(book.getTitle() + “|” + book.getAuthor() + “|” + book.getIsbn() + “\n”)”是看得见、摸得着的动作。你能清晰感知到字符如何被编码、换行符如何被写入、缓冲区何时被flush。我在调试时,曾故意在saveBooks()里加一句Thread.sleep(1000),然后盯着Books.txt文件大小一秒一秒增长——这种“时间感”,是数据库事务日志永远给不了的。
- 第二层:数据结构与内存模型的强绑定。数据库里一条记录对应一行SQL结果集,而这里,ArrayList
里的每一个Book对象,就是Books.txt里被split(“\|”)切出来的那一整行。修改对象属性(book.setStock(5)),直接改变内存中的值;调用saveBooks(),才把整个List序列化回文本。这种“内存对象 ↔ 文本行”的一对一映射,让初学者彻底理解“持久化”的本质不是魔法,而是将运行时状态,以约定格式,刻录到硬盘上。
-
第三层:错误处理的现场教学。数据库抛出SQLException,新手往往只看到堆栈,不知所措。而文本IO会真实报出FileNotFoundException(文件被误删)、IOException(磁盘写满)、NumberFormatException(BooksNum.txt里某行库存数写成了“abc”)。这些异常信息直指问题根源,逼着你去读API文档,去加try-catch,去写日志输出——这才是工程化思维的起点。
2.2 文件格式设计:用“管道符”做分隔,为何比CSV或JSON更合适?
三份文本文件的格式,看似随意,实则经过反复推敲:
- Books.txt:每行格式为 书名|作者|ISBN|出版社|出版年份,例如 深入理解Java虚拟机|周志明|9787121346305|电子工业出版社|2019。
- User1.txt:每行格式为 用户名|密码|角色,例如 admin|123456|ADMIN 或 reader001|pass123|READER。
- BooksNum.txt:每行格式为 ISBN|库存数量,例如 9787121346305|12。
选择竖线“|”而非逗号“,”或制表符“\t”,是血泪教训后的选择。早期测试版用CSV,结果遇到一本叫《Java, the Good Parts》的书,作者栏填了“John Doe, Jane Smith”,用逗号分割直接导致解析错位——第3个字段本该是ISBN,却变成了“ Jane Smith”。而“|”在图书元数据中几乎不会出现,冲突概率趋近于零。至于不用JSON,理由更简单:新手还没学过Jackson或Gson,手写JSON序列化需要处理引号转义、嵌套对象,徒增复杂度。而“|”分隔的纯文本,用String.split(“\|”)一行搞定,结果是String数组,直接按索引取值:parts[0]是书名,parts[1]是作者……清晰、稳定、零学习成本。
文件编码统一采用UTF-8,这是硬性要求。我在Windows上用记事本另存为UTF-8时,务必勾选“UTF-8 无BOM”,否则BufferedReader读取时会在首行开头多出几个不可见字符(EF BB BF),导致split后parts[0]前面带乱码。这个细节,项目里没写注释,但你在实际运行时报错时,一定会撞上它——而这,恰恰是调试能力最好的磨刀石。
2.3 Eclipse工程结构:为什么“.project”和“.classpath”是新手的救命稻草?
项目根目录下那几个以点开头的隐藏文件,远不止是Eclipse的“户口本”。它们是新手跨越“写代码”和“跑起来”之间鸿沟的关键桥梁:
- .project:定义了这是一个“org.eclipse.jdt.core.javaproject”,指定了源码目录(<sourceEntries>指向src),以及构建输出目录(<outputEntries>指向bin)。这意味着,只要你把整个文件夹拖进Eclipse的Package Explorer,它立刻认出“这是个Java项目”,自动配置好编译路径。
- .classpath:最关键的配置在这里。它明确声明了<classpathentry kind="src" path="src"/>,告诉Eclipse所有Java源文件都在src下;<classpathentry kind="output" path="bin"/>指定编译结果放bin里;最妙的是<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>——它锁死了JRE版本为Java 8。很多新手环境里装了Java 11或17,但项目代码用了Java 8的语法(比如Arrays.asList()返回的List不支持remove()),如果没有这个锁定,Eclipse会默认用高版本JRE编译,运行时报UnsupportedClassVersionError,让人一头雾水。这个配置,相当于给你配了个“版本保险丝”。
而.settings/目录下的文件,则默默完成了更多事:org.eclipse.jdt.core.prefs设定了代码格式化规则(缩进用空格而非Tab),org.eclipse.jdt.ui.prefs保存了编辑器字体大小。当你第一次打开Book.java,发现所有缩进都整齐划一,括号自动换行,这不是巧合,是这些配置在后台工作。对新手而言,这意味着“我不用花半小时研究怎么配IDE,代码一打开就是舒服的样子”。
3. 核心模块解析与实操要点:从UI搭建到数据落地的完整链路
3.1 UI层:Swing布局的艺术——为什么用BorderLayout而非GridBagLayout?
MainFrame.java是整个应用的门面,它的布局策略决定了后续所有面板的嵌入逻辑。项目采用setLayout(new BorderLayout()),并将主功能面板(BookPanel)放在BorderLayout.CENTER,顶部欢迎图(JLabel with welcome.png)放在BorderLayout.NORTH,底部状态栏(JLabel显示当前用户、记录数)放在BorderLayout.SOUTH。这个选择绝非偶然。
初学者最容易陷入的误区,是盲目崇拜GridBagLayout——它功能强大,能实现像素级精确定位,但代价是代码量爆炸:一个简单的“添加”按钮,需要设置gridx、gridy、gridwidth、gridheight、weightx、weighty、fill、anchor……十几行配置。而BorderLayout只分五个区域(NORTH/SOUTH/EAST/WEST/CENTER),逻辑极度清晰。BookPanel本身是一个JPanel,内部再用BoxLayout.Y_AXIS垂直堆叠各个子面板(图书列表JTable、操作按钮组、信息录入表单)。这种“大框架用BorderLayout,局部用BoxLayout”的组合,既保证了整体结构的稳定性,又避免了过度复杂的约束配置。
实操中一个关键细节:JTable的滚动。图书列表用JTable展示,数据量一大就会超出窗口。很多人直接add(table),结果表格被拉伸变形。正确做法是:JScrollPane scrollPane = new JScrollPane(table); add(scrollPane, BorderLayout.CENTER);。JScrollPane会自动添加滚动条,并智能处理table的尺寸变化。我在第一次做时漏了这步,表格撑满整个CENTER区域,列宽被强制拉宽,中文显示全是方块——后来加了JScrollPane,一切恢复正常。这个教训告诉我:Swing里“容器”不是摆设,它是控制组件行为的中枢。
3.2 模型层:Book与User类的设计——为什么用public字段而非getter/setter?
翻开com.bookmanage.model.Book.java,你会惊讶地发现,它的字段是public String title; public String author; ...,而不是教科书式的private + public getter/setter。这看起来违背了面向对象的封装原则,但在此处,是深思熟虑的简化。
理由有二:
- 降低认知负荷:新手刚接触类,还在理解“什么是实例变量”、“this关键字怎么用”。如果每个字段都要写private String title; public void setTitle(String title){this.title = title;},光是模板代码就占去一半篇幅,挤占了真正重要的逻辑空间。而book.title = "Java编程思想";这种直白赋值,让他们能快速聚焦在“数据怎么流动”上。
- 与文本解析无缝对接:Books.txt的每一行split后得到String[] parts,book.title = parts[0]; book.author = parts[1]; 这种赋值方式,与文本解析的流程天然契合。如果用了getter/setter,就得写book.setTitle(parts[0]); book.setAuthor(parts[1]);,多出6个括号和6个单词,对初学者是额外的语法负担。等他们熟练掌握后,重构为private字段是分分钟的事;但入门阶段,流畅的代码流比教条的规范更重要。
当然,这不意味着鼓励全局滥用public字段。项目里User类同样如此,但StockManager(库存管理器)这类承担核心业务逻辑的类,依然严格使用private字段和public方法——它体现了“在哪该严,在哪可松”的工程权衡。
3.3 数据访问层:TextFileDAO的IO实现——BufferedReader的“坑”与救赎
TextFileDAO.java是整个项目的“心脏起搏器”,它的loadBooks()方法,是新手必须逐行读懂的范本:
public List<Book> loadBooks() {
List<Book> books = new ArrayList<>();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("Books.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim(); // 关键!去除首尾空格,避免空行干扰
if (line.isEmpty()) continue; // 跳过空行
String[] parts = line.split("\\|");
if (parts.length < 5) continue; // 字段数不足,跳过脏数据
Book book = new Book();
book.title = parts[0];
book.author = parts[1];
book.isbn = parts[2];
book.publisher = parts[3];
book.publishYear = parts[4];
books.add(book);
}
} catch (FileNotFoundException e) {
// 首次运行,Books.txt不存在,返回空列表,不报错
System.err.println("Books.txt not found, starting with empty list.");
} catch (IOException e) {
e.printStackTrace();
}
return books;
}
这段代码藏着三个新手必踩的“坑”,也是最佳实践:
- 坑一:编码陷阱。new InputStreamReader(new FileInputStream(...), StandardCharsets.UTF_8) 明确指定UTF-8,而非依赖系统默认编码。Windows默认是GBK,Linux是UTF-8,不指定就会在不同机器上读出乱码。我曾在同事的Mac上运行,中文全变问号,加了这句立刻解决。
- 坑二:空行与空格。line.trim() 和 if (line.isEmpty()) continue 是必备防护。文本编辑器保存时,最后一行常带空行;用户手动编辑Books.txt,可能在行首多打几个空格。不处理,split后parts[0]就是空字符串,book.title = "",后续查询永远找不到。
- 坑三:字段数校验。if (parts.length < 5) continue 是数据健壮性的底线。万一用户手抖,在Books.txt里少写了一个“|”,比如 Java编程思想|机械工业出版社|2013(缺作者和ISBN),split出来只有3个元素,parts[2]是出版社,parts[4]直接越界抛ArrayIndexOutOfBoundsException。跳过这行,至少保证程序不死,还能继续用。
saveBooks()同理,但多了个关键动作:先写入临时文件,再原子替换。代码里是File tempFile = new File("Books.txt.tmp");,写完后tempFile.renameTo(new File("Books.txt"))。为什么?因为如果程序在写入中途崩溃(断电、OOM),原Books.txt可能被截断成半截,数据全毁。而先写tmp,成功后再rename,是操作系统级别的原子操作,要么全成功,要么原文件完好无损。这个技巧,是我在生产环境里学到的,现在毫无保留地塞进了这个教学项目。
3.4 业务逻辑层:BookService的增删改查——事件驱动的完整闭环
BookService.java是连接UI和DAO的胶水。它的addBook(Book book)方法,展示了从用户点击到数据落盘的完整闭环:
public void addBook(Book book) {
// 1. 前置校验:ISBN不能重复
List<Book> existingBooks = dao.loadBooks();
for (Book b : existingBooks) {
if (b.isbn.equals(book.isbn)) {
throw new IllegalArgumentException("ISBN already exists: " + book.isbn);
}
}
// 2. 添加到内存列表
existingBooks.add(book);
// 3. 同时更新库存(BooksNum.txt)
Map<String, Integer> stocks = dao.loadStocks();
stocks.put(book.isbn, 1); // 新书默认库存1
// 4. 一次性持久化
dao.saveBooks(existingBooks);
dao.saveStocks(stocks);
}
这个流程揭示了桌面应用的核心模式:内存是主战场,文件是备份库。所有增删改查操作,首先在ArrayList或HashMap内存结构中完成,最后才批量刷回磁盘。这样做的好处是响应极快——用户点“添加”,毫秒级反馈;坏处是如果程序崩溃,最后一次save之前的操作会丢失。但对于本地单机工具,这是可接受的权衡。
其中,“ISBN唯一性校验”是典型业务规则。新手常犯的错误,是把校验逻辑写在UI层(JButton的actionPerformed里),导致同样的校验代码在“添加”、“导入”等多个入口重复。而BookService将其集中,UI层只需调用service.addBook(book),异常由上层捕获并提示用户。这种分层,是迈向工程化的重要一步。
另一个易忽略的细节:库存初始化。新书入库,默认库存为1。这个逻辑不在UI表单里让用户填(太麻烦),也不在DAO里硬编码,而是放在BookService的addBook()里——因为它属于业务规则,而非数据存储规则。这种职责划分,让代码更易维护。比如未来需求改成“新书默认库存5”,只需改这一行,无需动DAO或UI。
4. 实操过程详解:从Eclipse导入到功能验证的每一步
4.1 Eclipse一键导入:四步走,零配置启动
别被“一键”二字迷惑,实际操作需四步,但每一步都极其明确,无歧义:
-
解压与定位:将下载的ZIP包解压到任意目录,例如
D:\projects\bookmanager。确保目录结构完整:根目录下有.project、src/、bin/、Books.txt等。不要进入子文件夹,就停在bookmanager这一层。 -
Eclipse启动与导入:
- 打开Eclipse(推荐2021-12或更新版本,兼容Java 8)。
- 顶部菜单栏:
File→Import...→ 展开General→ 选择Existing Projects into Workspace→ 点击Next。 - 在
Select root directory旁,点击Browse...,导航到你解压的D:\projects\bookmanager目录。 - Eclipse会自动扫描到项目(名称通常为
bookmanager或类似),勾选它,确保Copy projects into workspace未勾选(我们希望直接使用原文件,方便后续查看文本文件变化)。 - 点击
Finish。等待几秒,项目图标出现在Package Explorer中,带有一个小“J”标识,表示是Java项目。
-
验证JRE配置(关键!):
- 右键项目名 →
Properties→ 左侧选择Java Build Path→ 切换到Libraries标签页。 - 展开
JRE System Library [JavaSE-1.8],确认其状态是Workspace default JRE或明确指向JavaSE-1.8。如果显示JRE System Library [JavaSE-11]或报错,说明你的Eclipse默认JRE太高。 - 解决方案:
Installed JREs...→Add...→Standard VM→Next→Directory选择你电脑上Java 8的安装路径(如C:\Program Files\Java\jdk1.8.0_301)→Finish→ 勾选新添加的JDK 8 →OK。回到Java Build Path,重新选择JavaSE-1.8。
- 右键项目名 →
-
运行主程序:
- 在Package Explorer中,展开
src→com.bookmanage.ui→ 找到MainFrame.java。 - 右键
MainFrame.java→Run As→Java Application。 - 如果一切顺利,一个带有welcome.png的窗口弹出,标题栏写着“本地图书管理系统”。下方是图书列表表格,初始为空(因为Books.txt是空的)。恭喜,你已成功启动!
- 在Package Explorer中,展开
提示:首次运行后,检查项目根目录下的
Books.txt、User1.txt、BooksNum.txt。它们应该已被创建(可能为空),证明IO路径完全通畅。这是验证环境配置成功的黄金标准。
4.2 功能验证实战:亲手添加一本书,见证数据流转
理论不如动手。让我们添加一本《算法导论》,全程跟踪数据如何从界面流入硬盘:
- UI操作:在MainFrame窗口中,找到“添加图书”按钮,点击。弹出
AddBookDialog对话框。 - 填写表单:在输入框中依次填入:
- 书名:
算法导论 - 作者:
Thomas H. Cormen - ISBN:
9787302130183 - 出版社:
清华大学出版社 - 出版年份:
2006
- 书名:
- 提交:点击“确定”按钮。
- 观察内存:对话框关闭,主窗口的图书列表JTable中,立刻新增了一行,显示刚才填的信息。这证明
BookService.addBook()成功执行,内存中的ArrayList已更新。 - 检查硬盘:立即打开项目根目录下的
Books.txt文件(用记事本或Notepad++)。你应该能看到新增的一行:
算法导论|Thomas H. Cormen|9787302130183|清华大学出版社|2006
同时,打开BooksNum.txt,应该有一行:
9787302130183|1
这证明dao.saveBooks()和dao.saveStocks()已将内存状态成功刷入磁盘。
注意:如果
Books.txt里出现了乱码(如“绠楁硶瀵兼簨”),一定是编码问题。请回到步骤3,严格检查JRE配置和文件读写时的StandardCharsets.UTF_8参数。这是新手最常见的卡点,解决它,你就掌握了Java跨平台文本处理的命门。
4.3 用户管理与权限验证:ADMIN与READER的差异体验
系统内置两个用户:admin/123456(管理员)和 reader001/pass123(普通读者)。它们的权限差异体现在UI上:
- 以admin登录:登录后,主窗口顶部状态栏显示“当前用户:admin (ADMIN)”,所有按钮(添加、删除、修改、导入、导出)均可用。你可以删除任何图书,修改任何信息。
- 以reader001登录:登录后,状态栏显示“当前用户:reader001 (READER)”,此时“添加”、“删除”、“修改”按钮变为灰色(
button.setEnabled(false)),只有“查询”和“刷新”可用。这证明UserService.login()返回的User对象,其role字段被正确传递给了UI层,触发了按钮的enable/disable逻辑。
这个设计教会新手两件事:一是用户状态如何在应用中全局传递(通常存在一个CurrentUserContext单例);二是UI控件的状态(enabled/disabled)如何根据业务状态动态响应。它不是静态的,而是活的、联动的。
5. 常见问题与排查技巧实录:那些让你抓狂又恍然大悟的瞬间
5.1 经典问题速查表
| 问题现象 | 可能原因 | 排查与解决步骤 |
|---|---|---|
运行时报错:Exception in thread "main" java.lang.UnsupportedClassVersionError: com/bookmanage/ui/MainFrame has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0 | Eclipse使用的JRE版本(Java 8, class file version 52)低于项目编译所需的版本(Java 17, class file version 61)。 | 1. 检查 .classpath 文件,确认 <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/...JavaSE-1.8"/> 存在且正确。2. 在Eclipse中 Window → Preferences → Java → Installed JREs,确保已添加并选中Java 8 JDK。3. 右键项目 → Properties → Java Build Path → Libraries,将JRE System Library切换为JavaSE-1.8。 |
| 图书列表JTable为空,但Books.txt里有数据 | TextFileDAO.loadBooks() 解析失败,常见于编码错误或空行。 | 1. 用Notepad++打开Books.txt,查看右下角编码是否为“UTF-8”(非“UTF-8-BOM”)。 2. 检查Books.txt是否有空行,或某行字段数不足5个(用 |分割后)。3. 在 loadBooks()方法中,System.out.println("Line: '" + line + "'"); 打印原始行,确认是否含不可见字符。 |
| 添加图书后,Books.txt内容变成乱码(如“绠楁硶瀵兼簨”) | 文件写入时未指定UTF-8编码,系统默认使用GBK。 | 1. 定位到TextFileDAO.saveBooks()方法。2. 确保 PrintWriter构造函数为 new PrintWriter(new OutputStreamWriter(new FileOutputStream("Books.txt", true), StandardCharsets.UTF_8))。3. 保存后,用支持UTF-8的编辑器(如Notepad++)重新打开验证。 |
| 点击“删除”按钮无反应,控制台无报错 | JTable 的 getSelectedRow() 返回-1(无选中行),代码中未做空检查,直接调用removeBook()导致静默失败。 | 1. 在deleteButton.addActionListener()中,添加 int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(null, "请先选择要删除的图书!"); return; }。2. 这是UI交互的黄金法则:任何依赖用户选择的操作,第一步永远是检查选择有效性。 |
| 修改图书信息后,JTable显示更新了,但Books.txt文件内容没变 | saveBooks() 方法未被调用,或调用时传入的是旧的List。 | 1. 在BookService.updateBook()方法末尾,添加 System.out.println("Saving updated books list with size: " + books.size());。2. 在 TextFileDAO.saveBooks()开头,添加 System.out.println("Writing " + books.size() + " books to file.");。3. 运行,观察控制台输出是否匹配。若不匹配,说明updateBook()里没有调用 dao.saveBooks(books),或传入了错误的引用。 |
5.2 独家避坑技巧:来自真实调试现场的经验
-
技巧一:“文件锁”陷阱。在Windows上,如果你用记事本打开了Books.txt,然后在Eclipse里运行程序,
saveBooks()会抛出IOException: The process cannot access the file because it is being used by another process。这是因为记事本独占了文件句柄。解决方案:永远用Notepad++或VS Code打开项目文本文件,它们不会加独占锁;或者,养成习惯,编辑完文件后立即关闭编辑器,再运行程序。这个坑,我踩了三次才记住。 -
技巧二:
JTable的fireTableDataChanged()必须调用。新手常以为table.setModel(newBookTableModel)就够了,但如果你只是修改了底层ArrayList<Book>,而没有通知JTable“数据变了”,表格视图永远不会刷新。正确的做法是:在BookService的addBook()、deleteBook()等方法修改完内存数据后,必须调用tableModel.fireTableDataChanged()(假设你用了自定义TableModel)。这是Swing MVC模式的铁律:Model变更,必须主动“广播”给View。 -
技巧三:
JOptionPane的线程安全。所有Swing组件(包括JOptionPane)必须在Event Dispatch Thread (EDT) 中创建和修改。项目里所有JOptionPane.showMessageDialog()都包裹在SwingUtilities.invokeLater()中,这是最佳实践。如果你自己写弹窗,忘了这句,偶尔会出现界面卡死或闪烁。记住口诀:“Swing操作,必走EDT”。 -
技巧四:
ArrayList的remove(int index)vsremove(Object o)。在deleteBook()中,删除逻辑是books.remove(selectedRow)。这里selectedRow是JTable.getSelectedRow()返回的索引(int),所以调用的是remove(int index)。但如果误写成books.remove(book),它会调用remove(Object o),试图在List中查找并移除那个book对象。由于Book类没有重写equals()和hashCode(),比较的是对象引用,永远找不到,导致删除失败。教训:对于基于索引的操作,务必确认方法签名;对于基于对象的操作,必须重写equals()。
6. 项目延伸与个人体会:从练手到实用的进化路径
这个项目的价值,远不止于“能跑起来”。它是一块坚实的跳板,支撑你向更广阔的Java世界跃进。我自己用它做过几次迭代,每一次都加深了对基础技术的理解:
第一次,我给它加了“模糊查询”。原来service.searchBooks(String keyword)是遍历ArrayList,用book.getTitle().contains(keyword)。我把它升级为正则表达式,支持title~"算法.*"这样的语法,顺便学会了Pattern和Matcher。代码量只增加了20行,但搜索体验天壤之别。
第二次,我实现了“图书借阅”。新增了BorrowRecord类和Borrows.txt文件,记录谁在什么时候借了哪本书。难点在于并发——如果两个用户同时借同一本书,库存数可能被覆盖。我引入了synchronized块,锁住stocks Map的更新操作。虽然简单,但这是我第一次亲手触摸到“线程安全”的实体。
第三次,也是最有意思的一次,我把Books.txt的文本存储,悄悄换成了SQLite。不是为了性能,而是为了对比。我新建了一个SQLiteDAO,实现了和TextFileDAO完全一样的接口。然后,只改了一行代码:BookService service = new BookService(new SQLiteDAO());。其他所有UI、Model、Service代码,一行没动。那一刻我豁然开朗:所谓“架构”,就是把变化的部分(数据存储)抽离出来,用接口隔离。文本和SQLite,不过是同一张契约下的两个不同实现者。
所以,如果你现在正看着这个项目,觉得它“太简单”,那太好了。简单,意味着没有遮蔽物,所有齿轮都裸露在外,你可以看清它们如何咬合、如何转动。不要急着去加Spring Boot,先把这三份文本文件读透、写熟、改烂。当你能闭着眼写出loadBooks()的每一行,当你能在BooksNum.txt里手动改一个数字,然后刷新界面看到库存立刻变化——你就已经拥有了比很多“高级工程师”更扎实的根基。因为真正的高手,不是会用多少框架,而是知道框架之下,那最朴素的字节,是如何被一行行Java代码,稳稳托起的。
简介:用Java Swing开发的轻量级桌面图书管理程序,所有功能都在本地运行,不依赖数据库。支持添加、删除、修改和查询图书信息,同时管理用户账号和库存数量。数据全部保存在三个普通文本文件里:Books.txt存书名作者等基本信息,User1.txt存用户资料,BooksNum.txt记每本书的库存数,靠Java基础IO和集合类实现读写。界面简洁,带welcome.png启动图,事件响应逻辑清晰,适合刚学完Swing和文件操作的Java新手上手练习。项目结构完整,包含标准src源码目录、编译后的bin文件夹,还有Eclipse工程配置文件(.project、.classpath、.settings),直接拖进Eclipse或MyEclipse就能运行,不用额外配置环境。代码注释适中,重点练的是按钮点击响应、表格显示、文本文件解析和ArrayList增删改查这些核心技能。

2811

被折叠的 条评论
为什么被折叠?



