本文还有配套的精品资源点击获取简介一个不依赖数据库和第三方框架的Java超市收银程序所有功能用JDK原生API实现。商品信息、用户账号、订单记录都通过ArrayList等集合类组织数据以明文文本格式保存在本地文件中靠FileInputStream和FileOutputStream完成读写。包含完整的模块分工Goods.java封装商品属性User.java和UserDatabase.java负责用户注册与密码校验ShopCar.java实现购物车增删改查和结账逻辑Tborders.java生成并存储交易单DBManager.java统一处理文件IO操作Supermark.java是程序入口Test.java提供基础测试用例。代码结构清晰关键位置有中文注释适合刚学完Java基础、集合、IO流的初学者动手编译运行快速理解面向对象设计在实际小系统中的落地方式。1. 项目概述为什么一个“纯Java”的收银工具值得你花30分钟跑起来你有没有试过学完Java基础、集合、IO流之后对着空荡荡的IDE发呆——知道ArrayList怎么addFileOutputStream怎么write但就是不知道这些“零件”拼在一起能干点啥实在事我带过不少刚入门的同学他们卡在的不是语法而是缺乏一个能从头到尾跑通、看得见摸得着、改几行就能验证效果的小系统。这个超市收银小工具就是我当年给学生搭的第一块“脚手架”。它不叫“系统”就叫“小工具”因为它的目标非常明确用JDK原生APIJava 8完全兼容不装MySQL、不配Tomcat、不引Spring Boot甚至不碰任何jar包只靠java.util.*和java.io.*两个包把商品管理、用户登录、购物车结算、订单生成这四个最核心的零售业务闭环跑通。所有数据——商品列表、用户账号密码、购物车临时状态、历史订单——全存在本地.txt文件里打开记事本就能看到明文内容删了重来也只要清空几个文本文件。关键词里说的“Java收银程序”“超市购物车”“本地文件存储”其实对应着三个关键设计选择第一用面向对象建模真实业务Goods类不是POJO是带getTotalPrice()方法的活对象第二购物车不是简单存个List而是要支持按条码查重、按数量动态更新总价、结算后清空并生成不可逆订单第三“本地文件存储”不是随便printWriter.println()糊弄而是设计了统一的DBManager做序列化/反序列化协议——比如商品数据用|分隔字段订单用换行分隔每笔交易避免JSON或XML引入额外依赖。它适合谁如果你刚写完“学生管理系统”课设但发现里面全是System.out.println(添加成功)这种假动作或者你正在啃《Java核心技术卷I》学到第9章IO流却还没见过BufferedWriter真正写进硬盘的瞬间又或者你是个转行者想确认自己写的代码能不能脱离教程独立运行——那这个项目就是为你准备的。它没有炫技的反射、没有复杂的线程同步但每一行都在回答一个问题“当用户按下‘结算’按钮时背后到底发生了什么”接下来我会带你一层层拆开这个看似简单的工具告诉你为什么UserDatabase.java里校验密码要用Arrays.equals()而不是为什么ShopCar.java的removeItem()方法必须先findIndex()再remove()以及——最关键的——当你双击Supermark.class运行时那些被写进goods.txt的字符串是如何一步步变成收银员屏幕上跳动的金额的。2. 整体架构与模块分工一张纸画清所有类的关系这个项目的结构之所以能让初学者快速上手核心在于它用最朴素的方式实现了“高内聚、低耦合”。没有MVC分层没有DAO/Service/Controller抽象但每个类都严格守着自己的边界。我把整个架构画成一张纸上的关系图文字版你对照代码目录看会特别清晰2.1 核心数据模型三类实体各自封装Goods.java商品实体。字段只有id(String)、name(String)、price(double)、stock(int)但重点在它的行为——getTotalPrice(int quantity)方法直接计算多件总价toString()重写为id|name|price|stock格式这是后续文件读写的协议基础。注意它没有无参构造器强制要求创建时传入完整信息避免出现null商品。User.java用户实体。字段username(String)、password(char[])——这里特意用char[]而非String存密码因为String在常量池中可能长期驻留内存而char[]用完可手动置零是Java安全编码的基本实践。equals()方法重写时密码比较用Arrays.equals(pwd1, pwd2)因为char[]的比的是引用地址equals()比的是数组对象本身只有Arrays.equals()才逐字符比较内容。Tborders.java订单实体。字段orderId(String用System.currentTimeMillis()生成毫秒级唯一ID)、username(String)、goodsList(List )、totalAmount(double)、timestamp(String格式化为yyyy-MM-dd HH:mm:ss)。关键点在于goodsList不是简单复制购物车而是深拷贝——新建Goods对象并赋值确保订单一旦生成后续修改购物车不影响历史记录。2.2 业务逻辑层各司其职拒绝越界UserDatabase.java用户数据库。它不继承任何父类就是一个纯工具类内部用static ArrayListUser存用户列表提供register(User user)和login(String username, char[] password)两个静态方法。注册时检查用户名是否已存在忽略大小写登录时先通过用户名查出User对象再用Arrays.equals()比对密码。这里有个易错点很多初学者会写if (user.getPassword() password)结果永远返回false因为比的是数组引用而每次输入密码都会新建一个char[]。ShopCar.java购物车。核心是ListGoods和ListInteger两个平行列表——前者存商品引用后者存对应数量。这样设计是为了避免在商品类里加quantity字段污染实体模型。addItem(Goods goods, int quantity)方法会先遍历查找是否已有同ID商品有则累加数量无则追加新项removeItem(String goodsId)同理必须先findIndexById(goodsId)获取索引再remove(index)否则直接remove(goods)会触发equals()比较而Goods的equals()默认是永远找不到。DBManager.java数据管家。这是整个项目的IO中枢所有文件读写都经它手。它不持有任何数据只提供静态方法saveGoods(ListGoods goodsList)将商品列表写入goods.txtloadGoods()从文件读取并构建新List同理处理users.txt和orders.txt。关键设计是统一异常处理——所有IO异常IOException都被捕获并打印堆栈但不向上抛出保证业务层调用时不需写try-catch降低初学者心智负担。2.3 入口与测试让代码真正活起来Supermark.java主程序入口。它是一个public static void main(String[] args)方法但不是简单调用几个函数。它实现了完整的命令行交互循环显示菜单1.商品管理 2.用户登录 3.购物车操作 4.结算 0.退出用Scanner读取用户输入根据数字进入不同分支。每个分支里它只做三件事调用对应业务类的方法、打印操作结果、调用DBManager持久化数据。比如结算分支先调用shopCar.checkout(username)生成订单再调用DBManager.saveOrder(order)写入文件最后清空购物车。Test.java测试用例。它不是JUnit而是最原始的手动测试——在main方法里new对象、调方法、System.out.println()输出结果。例如测试商品加载ListGoods goods DBManager.loadGoods(); System.out.println(加载商品数 goods.size());。这种“土法测试”对初学者极友好你能亲眼看到loadGoods()返回的List里每个Goods的name和price是否正确比看单元测试断言直观十倍。整个架构就像一家微型超市Goods是货架上的商品User是会员卡ShopCar是顾客手里的篮子Tborders是收银小票UserDatabase是前台登记本DBManager是仓库管理员负责把商品从货架搬到仓库/从仓库搬回货架Supermark是店长指挥全场Test是店长每天开店前的自查清单。没有一个角色越界去干别人的活所有协作都通过清晰的参数和返回值完成。3. 核心细节解析那些教科书不会告诉你的坑光知道类名和方法名远远不够。我在实际教学中发现初学者跑通这个项目最大的障碍往往藏在几个看似微不足道的细节里。下面这些都是我带着学生一行行调试、反复踩坑后总结出的“血泪经验”。3.1 文件路径与编码为什么你的中文商品名变成了乱码DBManager.java里所有FileWriter和FileReader的构造器都显式指定了UTF-8编码// 正确写法强制指定UTF-8 BufferedWriter writer new BufferedWriter(new OutputStreamWriter( new FileOutputStream(goods.txt), UTF-8)); // 错误写法依赖系统默认编码Windows是GBKMac/Linux是UTF-8 // FileWriter writer new FileWriter(goods.txt); // 危险为什么必须这样因为FileWriter的无参构造器会调用Charset.defaultCharset()而Windows系统的默认编码是GBK。当你在Goods.java里写new Goods(苹果, 5.5, 100)toString()生成001|苹果|5.5|100用GBK编码写入文件再用UTF-8读取就会变成001||. 这个坑我带过的学员90%都踩过解决方法就是死记硬背所有涉及中文的文件IO必须显式声明UTF-8。另一个坑是相对路径。Supermark.java里调用DBManager.loadGoods()时它期望goods.txt在当前工作目录下。但IDEA和Eclipse的默认工作目录不同IDEA是项目根目录Eclipse可能是bin目录。最稳妥的做法是在DBManager里用绝对路径或资源路径// 推荐用ClassLoader定位资源确保路径稳定 String filePath DBManager.class.getClassLoader().getResource(goods.txt).getPath(); // 或更简单直接放在项目根目录运行时确保IDE的Working directory设置为项目根目录3.2 集合操作的“隐形陷阱”ArrayList的remove()为什么总删错ShopCar.java的removeItem(String goodsId)方法初学者常写成// ❌ 错误示范试图直接remove一个Goods对象 for (Goods g : goodsList) { if (g.getId().equals(goodsId)) { goodsList.remove(g); // 这里会抛ConcurrentModificationException break; } }问题出在增强for循环foreach的本质是迭代器遍历而ArrayList.remove(Object)会修改集合结构触发迭代器的fail-fast机制。正确解法是用传统for循环配合索引// ✅ 正确先找索引再按索引删除 private int findIndexById(String id) { for (int i 0; i goodsList.size(); i) { if (goodsList.get(i).getId().equals(id)) { return i; } } return -1; // 未找到 } // 在removeItem中调用 int index findIndexById(goodsId); if (index ! -1) { goodsList.remove(index); quantityList.remove(index); }这个细节揭示了一个重要原理集合的遍历和修改是互斥操作必须分离。类似地addItem()里判断商品是否存在也不能用goodsList.contains(new Goods(id))因为Goods没重写equals()和hashCode()contains()默认用比较引用永远返回false。必须手动遍历getId()比较。3.3 密码安全的“最小实践”为什么用char[]而不是StringUser.java里密码字段定义为private char[] password;而不是private String password;。这不是为了炫技而是有切实的安全考量。Java的String是不可变对象一旦创建就在字符串常量池中驻留直到GC回收。如果密码以String形式存在它可能在内存中停留数分钟甚至更久期间若发生内存dump如JVM崩溃生成heap dump密码明文就可能被提取出来。而char[]是可变的用完可以立刻置零// UserDatabase.login()方法中验证后立即清理 if (Arrays.equals(user.getPassword(), inputPassword)) { // 登录成功... } else { // 登录失败立即清理输入密码 Arrays.fill(inputPassword, \0); // 将数组所有元素设为\0 }虽然这个小工具没有真正的生产环境威胁但养成这个习惯会让你在写真实系统时少一个致命漏洞。记住密码永远是char[]永远在用完后Arrays.fill()清零。3.4 订单生成的“原子性”为什么结算要分三步走ShopCar.checkout(String username)方法的逻辑是1. 创建新Tborders对象设置orderId、username、timestamp2. 遍历购物车goodsList对每个Goods新建一个Goods副本深拷贝加入order.goodsList3. 计算totalAmount调用DBManager.saveOrder(order)写入文件然后清空购物车为什么不能直接把购物车的goodsList赋值给订单因为goodsList是ListGoods引用如果订单直接持有它后续用户继续往购物车加商品订单里的商品列表也会跟着变——这违背了订单的“不可变性”原则。深拷贝确保订单快照是那一刻的真实状态。更关键的是第三步写入文件和清空购物车必须是原子操作。如果先清空购物车再写文件而写文件时抛出IOException如磁盘满用户就丢失了所有购物车商品且没生成订单体验极差。所以正确的顺序是先生成订单对象→写入文件成功则继续失败则抛异常→写入成功后再清空购物车。DBManager.saveOrder()方法里我用了try-with-resources确保BufferedWriter一定关闭但没做事务回滚因为文本文件没有事务所以业务层必须保证“写成功才清空”。4. 实操过程详解从零开始编译、运行、调试的完整链路现在我们把理论落地。假设你已经下载了源码包解压到D:\supermarket目录里面包含所有.java文件。下面是我手把手带你走一遍从编译到第一次成功结算的全过程包括每个步骤的预期输出和常见报错分析。4.1 环境准备确认JDK版本与路径首先打开命令行Windows按WinR输入cmdMac/Linux打开Terminal输入java -version javac -version确保输出类似java version 1.8.0_361 Java(TM) SE Runtime Environment (build 1.8.0_361-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.361-b09, mixed mode)如果提示java is not recognized说明JDK没配置环境变量。你需要- 找到JDK安装路径如C:\Program Files\Java\jdk1.8.0_361- 在系统环境变量PATH中添加%JAVA_HOME%\binWindows或$JAVA_HOME/binMac/Linux- 同时设置JAVA_HOME变量指向JDK根目录提示这个项目用Java 8完全兼容不必强求Java 17。很多学校机房还是Java 8反而更贴近真实学习场景。4.2 编译所有Java文件一次搞定拒绝单个编译切到项目根目录D:\supermarket执行javac -encoding UTF-8 *.java关键点--encoding UTF-8强制编译器用UTF-8读取源文件避免中文注释报错-*.java一次性编译所有文件让javac自动解析类依赖如Supermark.java用到ShopCarjavac会自动先编译ShopCar.java如果编译成功目录下会生成一堆.class文件Supermark.class,Goods.class等。如果报错最常见的有-error: unmappable character for encoding GBK源文件保存编码不是UTF-8。用记事本打开任意.java文件另存为→编码选UTF-8→保存。-error: cannot find symbol某个类名拼错了比如UserDatebase.java少了个a检查文件名和类名是否完全一致Java区分大小写。4.3 首次运行与初始化让文件“活”起来编译成功后运行主程序java Supermark首次运行时你会看到 超市收银系统 1. 商品管理 2. 用户登录 3. 购物车操作 4. 结算 0. 退出 请选择操作此时系统会尝试从goods.txt、users.txt、orders.txt读取数据。因为是第一次这些文件还不存在DBManager.loadGoods()会捕获FileNotFoundException并返回空ArrayList所以商品列表为空。这是正常现象。现在我们手动创建初始商品。选择1进入商品管理再选1添加商品请输入商品ID001 请输入商品名称苹果 请输入商品单价5.5 请输入商品库存100 添加成功这时DBManager.saveGoods()会被调用创建goods.txt文件并写入001|苹果|5.5|100同理添加香蕉002|香蕉|3.8|200、牛奶003|牛奶|12.0|50。每添加一个goods.txt就多一行。你可以用记事本打开它确认内容是否正确。实操心得我建议初学者在添加完商品后先退出程序按0再重新java Supermark运行。这样能验证DBManager.loadGoods()是否真的从文件读出了数据——如果再次进入商品管理看到“现有商品3个”就说明读写闭环成功了。4.4 用户注册与登录验证密码安全机制选择2用户登录再选1注册新用户请输入用户名zhangsan 请输入密码123456 注册成功此时users.txt被创建内容为zhangsan|123456注意密码是明文存储的这是为了简化教学。真实系统必须加盐哈希但这里的目标是理解流程。然后选2登录请输入用户名zhangsan 请输入密码123456 登录成功欢迎回来zhangsan如果输错密码会提示“用户名或密码错误”。你可以用记事本打开users.txt修改密码为654321再登录验证——这就是本地文件存储的最大优势数据完全透明修改即生效。4.5 购物车全流程从添加到结算的每一步登录成功后回到主菜单选3购物车操作- 选1添加商品输入商品ID001数量2→ 显示“已添加2个苹果到购物车”- 选2查看购物车显示“购物车商品苹果 x2总计11.0元”- 选3修改数量输入ID001新数量5→ 总价变为27.5- 选4删除商品输入ID001→ 购物车清空最后选4结算正在结算... 订单生成成功订单号1715678901234 订单详情 苹果 x5单价5.5小计27.5 总计27.5元 请保留此单据此时orders.txt被创建内容类似1715678901234|zhangsan|[Goods{id001, name苹果, price5.5, stock100}]|27.5|2024-05-15 14:30:01注意[Goods{...}]这部分是Tborders.toString()的输出实际项目中应该用更规范的格式如JSON但这里用toString()是为了避免引入额外依赖。关键验证点结算后购物车应为空。你再选3→2查看购物车应该显示“购物车为空”。同时goods.txt里的苹果库存没变——因为这个小工具没做库存扣减那是进阶功能但你可以轻松在ShopCar.checkout()里加上goods.setStock(goods.getStock() - quantity)逻辑。5. 常见问题与排查技巧实录那些让我熬夜调试的“幽灵Bug”即使严格按照上述步骤操作你也可能遇到一些让人抓狂的问题。下面是我整理的“高频故障速查表”每一条都来自真实教学现场附带一键修复方案。5.1 文件读写权限问题为什么goods.txt创建了却读不出数据现象添加商品后goods.txt文件生成了内容也正确但重启程序后loadGoods()返回空列表购物车始终为空。排查思路1. 检查DBManager.loadGoods()方法里BufferedReader读取时是否用了while ((line reader.readLine()) ! null)循环如果漏了while只会读第一行。2. 检查Goods的toString()和fromString(String line)是否严格对应。例如toString()输出001|苹果|5.5|100那么fromString()必须用line.split(\\|)分割且索引0、1、2、3分别对应id、name、price、stock。如果split()后数组长度不等于4会抛ArrayIndexOutOfBoundsException但被catch吞掉了导致静默失败。修复方案在DBManager.loadGoods()的catch块里加一句日志} catch (Exception e) { System.err.println(加载商品失败 e.getMessage()); e.printStackTrace(); // 打印完整堆栈暴露具体哪一行出错 return new ArrayList(); }运行后控制台会输出类似加载商品失败For input string: 苹果 java.lang.NumberFormatException: For input string: 苹果 at java.lang.Double.parseDouble(Double.java:540) at com.DBManager.loadGoods(DBManager.java:45) // 第45行Double.parseDouble(parts[2])这说明parts[2]不是数字而是苹果——因为split(\\|)没生效parts数组只有一个元素。根源是toString()里用了|但split()的参数是正则表达式|需要转义为\\|。5.2 中文乱码的“双重陷阱”控制台能显示文件里却是问号现象在IDEA里运行控制台打印“苹果”正常但打开goods.txt看到“苹?果”。原因分析这是IDEA的编码设置和文件编码不一致导致的。IDEA默认用UTF-8读取源文件但可能用系统编码GBK写文件。解决方案1. 在IDEA中File → Settings → Editor → File Encodings将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8。2. 在DBManager.java里所有FileWriter和FileReader必须显式指定UTF-8如前所述。3. 如果已生成乱码文件用记事本打开goods.txt另存为→编码选UTF-8→覆盖保存。5.3 “找不到符号”编译错误明明文件存在为什么javac不认识现象Supermark.java:10: error: cannot find symbol ShopCar cart new ShopCar(); ^ symbol: class ShopCar location: class Supermark根本原因javac找不到ShopCar.class通常是因为-ShopCar.java文件名和类名不一致如文件是shopcar.java类是public class ShopCarLinux/macOS区分大小写会失败- 当前目录不是项目根目录javac在错误路径下编译-ShopCar.java里有语法错误如少了个}导致编译失败没生成ShopCar.class排查步骤1.dir *.javaWindows或ls *.javaMac/Linux确认文件名是否全大写首字母ShopCar.java,User.java等。2.cd到项目根目录确保dir能看到所有.java文件。3. 单独编译ShopCar.javajavac ShopCar.java看是否报错。如果报错修复后再javac *.java。5.4 结算后购物车未清空用户抱怨“结完账商品还在篮子里”现象点击结算控制台显示“订单生成成功”但再进购物车查看商品还在。定位方法在Supermark.java的结算分支里找到调用shopCar.checkout(username)后的代码// ❌ 错误忘记清空购物车 Tborders order shopCar.checkout(username); DBManager.saveOrder(order); // 这里缺了shopCar.clear();修复在DBManager.saveOrder(order)后必须加shopCar.clear(); // ShopCar.java里实现clear()方法goodsList.clear(); quantityList.clear(); System.out.println(购物车已清空);独家技巧在ShopCar.clear()方法里加一句System.out.println(购物车清空goodsList大小 goodsList.size())运行时就能看到清空前后的size变化比猜强一百倍。5.5 用户登录无限循环输入错误密码后程序卡在“请输入密码”现象输错密码提示“错误”但没回到主菜单而是立刻又提示“请输入密码”形成死循环。原因Scanner的nextLine()和next()混用导致输入缓冲区残留。例如// ❌ 危险组合 System.out.print(用户名); String username scanner.next(); // 只读取到空格/回车前 System.out.print(密码); String password scanner.nextLine(); // 立刻读取到前面留下的回车password为空正确写法统一用nextLine()System.out.print(用户名); String username scanner.nextLine().trim(); // trim()去掉首尾空格 System.out.print(密码); String passwordStr scanner.nextLine().trim(); char[] password passwordStr.toCharArray(); // 转为char[]用于比较6. 进阶扩展与学习路径这个小工具还能怎么玩这个项目的价值远不止于“跑通”。它是一块优质的“乐高底板”你可以基于它无缝接入更高阶的知识点。下面是我给不同阶段学习者的定制化扩展建议每个都经过实测确保能在一个小时内完成。6.1 给Java新手加一个“库存预警”功能目标当商品库存低于10时在商品管理列表里标红显示。动手步骤1. 修改Goods.java加一个方法public boolean isLowStock() { return stock 10; }修改DBManager.loadGoods()加载后对每个Goods调用isLowStock()如果是打印时加[库存紧张]前缀。在Supermark.java的商品管理列表显示逻辑里加入判断for (Goods g : goodsList) { System.out.println(g.getId() g.getName() g.getPrice() 库存 g.getStock() (g.isLowStock() ? [库存紧张] : )); }收获理解方法封装、布尔逻辑、条件输出代码量增加不到10行成就感爆棚。6.2 给集合进阶者用HashMap优化商品查询现状ShopCar.addItem()每次都要遍历goodsList找IDO(n)复杂度。1000个商品时添加10次就要查10000次。升级方案1. 在DBManager里加一个静态MapString, Goods缓存private static MapString, Goods goodsCache new HashMap(); public static void loadGoodsToCache() { ListGoods list loadGoods(); goodsCache.clear(); for (Goods g : list) { goodsCache.put(g.getId(), g); } }修改ShopCar.addItem()用goodsCache.get(goodsId)代替遍历O(1)查询。注意goodsCache是静态的所以loadGoodsToCache()只需在程序启动时调用一次在Supermark.main()开头。6.3 给IO流深化者实现“备份文件”机制目标每次写入goods.txt前先自动备份为goods.txt.bak。核心代码在DBManager.saveGoods()开头File original new File(goods.txt); File backup new File(goods.txt.bak); if (original.exists()) { Files.copy(original.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING); } // 然后继续写入original需要导入java.nio.file.*这是Java 7的NIO.2 API比老IO更简洁安全。6.4 给面向对象实践者引入“折扣策略”接口目标支持会员价、满减、限时折扣等多种促销。设计1. 新建接口DiscountStrategy.javapublic interface DiscountStrategy { double calculateDiscount(double originalPrice, int quantity); }实现类MemberDiscount.java95折public class MemberDiscount implements DiscountStrategy { public double calculateDiscount(double price, int qty) { return price * qty * 0.05; // 返5% } }在Tborders里加DiscountStrategy strategy字段结算时调用strategy.calculateDiscount()。这个改动会逼你理解接口、多态、策略模式但代码结构依然清晰——所有折扣逻辑隔离在单独类里不影响购物车和订单主干。最后分享一个小技巧这个项目的所有.java文件我都刻意控制在200行以内。当你想加新功能时先问自己“这个逻辑应该属于哪个现有类的职责如果超过150行是不是该拆出一个新类”——这比死记UML图谱管用十倍。真正的面向对象不在图纸上而在你每次new一个对象时心里清楚它该做什么、不该做什么。本文还有配套的精品资源点击获取简介一个不依赖数据库和第三方框架的Java超市收银程序所有功能用JDK原生API实现。商品信息、用户账号、订单记录都通过ArrayList等集合类组织数据以明文文本格式保存在本地文件中靠FileInputStream和FileOutputStream完成读写。包含完整的模块分工Goods.java封装商品属性User.java和UserDatabase.java负责用户注册与密码校验ShopCar.java实现购物车增删改查和结账逻辑Tborders.java生成并存储交易单DBManager.java统一处理文件IO操作Supermark.java是程序入口Test.java提供基础测试用例。代码结构清晰关键位置有中文注释适合刚学完Java基础、集合、IO流的初学者动手编译运行快速理解面向对象设计在实际小系统中的落地方式。本文还有配套的精品资源点击获取
纯Java写的超市收银小工具:商品管理+用户登录+购物车结算,数据存本地文本
发布时间:2026/6/12 6:26:28
本文还有配套的精品资源点击获取简介一个不依赖数据库和第三方框架的Java超市收银程序所有功能用JDK原生API实现。商品信息、用户账号、订单记录都通过ArrayList等集合类组织数据以明文文本格式保存在本地文件中靠FileInputStream和FileOutputStream完成读写。包含完整的模块分工Goods.java封装商品属性User.java和UserDatabase.java负责用户注册与密码校验ShopCar.java实现购物车增删改查和结账逻辑Tborders.java生成并存储交易单DBManager.java统一处理文件IO操作Supermark.java是程序入口Test.java提供基础测试用例。代码结构清晰关键位置有中文注释适合刚学完Java基础、集合、IO流的初学者动手编译运行快速理解面向对象设计在实际小系统中的落地方式。1. 项目概述为什么一个“纯Java”的收银工具值得你花30分钟跑起来你有没有试过学完Java基础、集合、IO流之后对着空荡荡的IDE发呆——知道ArrayList怎么addFileOutputStream怎么write但就是不知道这些“零件”拼在一起能干点啥实在事我带过不少刚入门的同学他们卡在的不是语法而是缺乏一个能从头到尾跑通、看得见摸得着、改几行就能验证效果的小系统。这个超市收银小工具就是我当年给学生搭的第一块“脚手架”。它不叫“系统”就叫“小工具”因为它的目标非常明确用JDK原生APIJava 8完全兼容不装MySQL、不配Tomcat、不引Spring Boot甚至不碰任何jar包只靠java.util.*和java.io.*两个包把商品管理、用户登录、购物车结算、订单生成这四个最核心的零售业务闭环跑通。所有数据——商品列表、用户账号密码、购物车临时状态、历史订单——全存在本地.txt文件里打开记事本就能看到明文内容删了重来也只要清空几个文本文件。关键词里说的“Java收银程序”“超市购物车”“本地文件存储”其实对应着三个关键设计选择第一用面向对象建模真实业务Goods类不是POJO是带getTotalPrice()方法的活对象第二购物车不是简单存个List而是要支持按条码查重、按数量动态更新总价、结算后清空并生成不可逆订单第三“本地文件存储”不是随便printWriter.println()糊弄而是设计了统一的DBManager做序列化/反序列化协议——比如商品数据用|分隔字段订单用换行分隔每笔交易避免JSON或XML引入额外依赖。它适合谁如果你刚写完“学生管理系统”课设但发现里面全是System.out.println(添加成功)这种假动作或者你正在啃《Java核心技术卷I》学到第9章IO流却还没见过BufferedWriter真正写进硬盘的瞬间又或者你是个转行者想确认自己写的代码能不能脱离教程独立运行——那这个项目就是为你准备的。它没有炫技的反射、没有复杂的线程同步但每一行都在回答一个问题“当用户按下‘结算’按钮时背后到底发生了什么”接下来我会带你一层层拆开这个看似简单的工具告诉你为什么UserDatabase.java里校验密码要用Arrays.equals()而不是为什么ShopCar.java的removeItem()方法必须先findIndex()再remove()以及——最关键的——当你双击Supermark.class运行时那些被写进goods.txt的字符串是如何一步步变成收银员屏幕上跳动的金额的。2. 整体架构与模块分工一张纸画清所有类的关系这个项目的结构之所以能让初学者快速上手核心在于它用最朴素的方式实现了“高内聚、低耦合”。没有MVC分层没有DAO/Service/Controller抽象但每个类都严格守着自己的边界。我把整个架构画成一张纸上的关系图文字版你对照代码目录看会特别清晰2.1 核心数据模型三类实体各自封装Goods.java商品实体。字段只有id(String)、name(String)、price(double)、stock(int)但重点在它的行为——getTotalPrice(int quantity)方法直接计算多件总价toString()重写为id|name|price|stock格式这是后续文件读写的协议基础。注意它没有无参构造器强制要求创建时传入完整信息避免出现null商品。User.java用户实体。字段username(String)、password(char[])——这里特意用char[]而非String存密码因为String在常量池中可能长期驻留内存而char[]用完可手动置零是Java安全编码的基本实践。equals()方法重写时密码比较用Arrays.equals(pwd1, pwd2)因为char[]的比的是引用地址equals()比的是数组对象本身只有Arrays.equals()才逐字符比较内容。Tborders.java订单实体。字段orderId(String用System.currentTimeMillis()生成毫秒级唯一ID)、username(String)、goodsList(List )、totalAmount(double)、timestamp(String格式化为yyyy-MM-dd HH:mm:ss)。关键点在于goodsList不是简单复制购物车而是深拷贝——新建Goods对象并赋值确保订单一旦生成后续修改购物车不影响历史记录。2.2 业务逻辑层各司其职拒绝越界UserDatabase.java用户数据库。它不继承任何父类就是一个纯工具类内部用static ArrayListUser存用户列表提供register(User user)和login(String username, char[] password)两个静态方法。注册时检查用户名是否已存在忽略大小写登录时先通过用户名查出User对象再用Arrays.equals()比对密码。这里有个易错点很多初学者会写if (user.getPassword() password)结果永远返回false因为比的是数组引用而每次输入密码都会新建一个char[]。ShopCar.java购物车。核心是ListGoods和ListInteger两个平行列表——前者存商品引用后者存对应数量。这样设计是为了避免在商品类里加quantity字段污染实体模型。addItem(Goods goods, int quantity)方法会先遍历查找是否已有同ID商品有则累加数量无则追加新项removeItem(String goodsId)同理必须先findIndexById(goodsId)获取索引再remove(index)否则直接remove(goods)会触发equals()比较而Goods的equals()默认是永远找不到。DBManager.java数据管家。这是整个项目的IO中枢所有文件读写都经它手。它不持有任何数据只提供静态方法saveGoods(ListGoods goodsList)将商品列表写入goods.txtloadGoods()从文件读取并构建新List同理处理users.txt和orders.txt。关键设计是统一异常处理——所有IO异常IOException都被捕获并打印堆栈但不向上抛出保证业务层调用时不需写try-catch降低初学者心智负担。2.3 入口与测试让代码真正活起来Supermark.java主程序入口。它是一个public static void main(String[] args)方法但不是简单调用几个函数。它实现了完整的命令行交互循环显示菜单1.商品管理 2.用户登录 3.购物车操作 4.结算 0.退出用Scanner读取用户输入根据数字进入不同分支。每个分支里它只做三件事调用对应业务类的方法、打印操作结果、调用DBManager持久化数据。比如结算分支先调用shopCar.checkout(username)生成订单再调用DBManager.saveOrder(order)写入文件最后清空购物车。Test.java测试用例。它不是JUnit而是最原始的手动测试——在main方法里new对象、调方法、System.out.println()输出结果。例如测试商品加载ListGoods goods DBManager.loadGoods(); System.out.println(加载商品数 goods.size());。这种“土法测试”对初学者极友好你能亲眼看到loadGoods()返回的List里每个Goods的name和price是否正确比看单元测试断言直观十倍。整个架构就像一家微型超市Goods是货架上的商品User是会员卡ShopCar是顾客手里的篮子Tborders是收银小票UserDatabase是前台登记本DBManager是仓库管理员负责把商品从货架搬到仓库/从仓库搬回货架Supermark是店长指挥全场Test是店长每天开店前的自查清单。没有一个角色越界去干别人的活所有协作都通过清晰的参数和返回值完成。3. 核心细节解析那些教科书不会告诉你的坑光知道类名和方法名远远不够。我在实际教学中发现初学者跑通这个项目最大的障碍往往藏在几个看似微不足道的细节里。下面这些都是我带着学生一行行调试、反复踩坑后总结出的“血泪经验”。3.1 文件路径与编码为什么你的中文商品名变成了乱码DBManager.java里所有FileWriter和FileReader的构造器都显式指定了UTF-8编码// 正确写法强制指定UTF-8 BufferedWriter writer new BufferedWriter(new OutputStreamWriter( new FileOutputStream(goods.txt), UTF-8)); // 错误写法依赖系统默认编码Windows是GBKMac/Linux是UTF-8 // FileWriter writer new FileWriter(goods.txt); // 危险为什么必须这样因为FileWriter的无参构造器会调用Charset.defaultCharset()而Windows系统的默认编码是GBK。当你在Goods.java里写new Goods(苹果, 5.5, 100)toString()生成001|苹果|5.5|100用GBK编码写入文件再用UTF-8读取就会变成001||. 这个坑我带过的学员90%都踩过解决方法就是死记硬背所有涉及中文的文件IO必须显式声明UTF-8。另一个坑是相对路径。Supermark.java里调用DBManager.loadGoods()时它期望goods.txt在当前工作目录下。但IDEA和Eclipse的默认工作目录不同IDEA是项目根目录Eclipse可能是bin目录。最稳妥的做法是在DBManager里用绝对路径或资源路径// 推荐用ClassLoader定位资源确保路径稳定 String filePath DBManager.class.getClassLoader().getResource(goods.txt).getPath(); // 或更简单直接放在项目根目录运行时确保IDE的Working directory设置为项目根目录3.2 集合操作的“隐形陷阱”ArrayList的remove()为什么总删错ShopCar.java的removeItem(String goodsId)方法初学者常写成// ❌ 错误示范试图直接remove一个Goods对象 for (Goods g : goodsList) { if (g.getId().equals(goodsId)) { goodsList.remove(g); // 这里会抛ConcurrentModificationException break; } }问题出在增强for循环foreach的本质是迭代器遍历而ArrayList.remove(Object)会修改集合结构触发迭代器的fail-fast机制。正确解法是用传统for循环配合索引// ✅ 正确先找索引再按索引删除 private int findIndexById(String id) { for (int i 0; i goodsList.size(); i) { if (goodsList.get(i).getId().equals(id)) { return i; } } return -1; // 未找到 } // 在removeItem中调用 int index findIndexById(goodsId); if (index ! -1) { goodsList.remove(index); quantityList.remove(index); }这个细节揭示了一个重要原理集合的遍历和修改是互斥操作必须分离。类似地addItem()里判断商品是否存在也不能用goodsList.contains(new Goods(id))因为Goods没重写equals()和hashCode()contains()默认用比较引用永远返回false。必须手动遍历getId()比较。3.3 密码安全的“最小实践”为什么用char[]而不是StringUser.java里密码字段定义为private char[] password;而不是private String password;。这不是为了炫技而是有切实的安全考量。Java的String是不可变对象一旦创建就在字符串常量池中驻留直到GC回收。如果密码以String形式存在它可能在内存中停留数分钟甚至更久期间若发生内存dump如JVM崩溃生成heap dump密码明文就可能被提取出来。而char[]是可变的用完可以立刻置零// UserDatabase.login()方法中验证后立即清理 if (Arrays.equals(user.getPassword(), inputPassword)) { // 登录成功... } else { // 登录失败立即清理输入密码 Arrays.fill(inputPassword, \0); // 将数组所有元素设为\0 }虽然这个小工具没有真正的生产环境威胁但养成这个习惯会让你在写真实系统时少一个致命漏洞。记住密码永远是char[]永远在用完后Arrays.fill()清零。3.4 订单生成的“原子性”为什么结算要分三步走ShopCar.checkout(String username)方法的逻辑是1. 创建新Tborders对象设置orderId、username、timestamp2. 遍历购物车goodsList对每个Goods新建一个Goods副本深拷贝加入order.goodsList3. 计算totalAmount调用DBManager.saveOrder(order)写入文件然后清空购物车为什么不能直接把购物车的goodsList赋值给订单因为goodsList是ListGoods引用如果订单直接持有它后续用户继续往购物车加商品订单里的商品列表也会跟着变——这违背了订单的“不可变性”原则。深拷贝确保订单快照是那一刻的真实状态。更关键的是第三步写入文件和清空购物车必须是原子操作。如果先清空购物车再写文件而写文件时抛出IOException如磁盘满用户就丢失了所有购物车商品且没生成订单体验极差。所以正确的顺序是先生成订单对象→写入文件成功则继续失败则抛异常→写入成功后再清空购物车。DBManager.saveOrder()方法里我用了try-with-resources确保BufferedWriter一定关闭但没做事务回滚因为文本文件没有事务所以业务层必须保证“写成功才清空”。4. 实操过程详解从零开始编译、运行、调试的完整链路现在我们把理论落地。假设你已经下载了源码包解压到D:\supermarket目录里面包含所有.java文件。下面是我手把手带你走一遍从编译到第一次成功结算的全过程包括每个步骤的预期输出和常见报错分析。4.1 环境准备确认JDK版本与路径首先打开命令行Windows按WinR输入cmdMac/Linux打开Terminal输入java -version javac -version确保输出类似java version 1.8.0_361 Java(TM) SE Runtime Environment (build 1.8.0_361-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.361-b09, mixed mode)如果提示java is not recognized说明JDK没配置环境变量。你需要- 找到JDK安装路径如C:\Program Files\Java\jdk1.8.0_361- 在系统环境变量PATH中添加%JAVA_HOME%\binWindows或$JAVA_HOME/binMac/Linux- 同时设置JAVA_HOME变量指向JDK根目录提示这个项目用Java 8完全兼容不必强求Java 17。很多学校机房还是Java 8反而更贴近真实学习场景。4.2 编译所有Java文件一次搞定拒绝单个编译切到项目根目录D:\supermarket执行javac -encoding UTF-8 *.java关键点--encoding UTF-8强制编译器用UTF-8读取源文件避免中文注释报错-*.java一次性编译所有文件让javac自动解析类依赖如Supermark.java用到ShopCarjavac会自动先编译ShopCar.java如果编译成功目录下会生成一堆.class文件Supermark.class,Goods.class等。如果报错最常见的有-error: unmappable character for encoding GBK源文件保存编码不是UTF-8。用记事本打开任意.java文件另存为→编码选UTF-8→保存。-error: cannot find symbol某个类名拼错了比如UserDatebase.java少了个a检查文件名和类名是否完全一致Java区分大小写。4.3 首次运行与初始化让文件“活”起来编译成功后运行主程序java Supermark首次运行时你会看到 超市收银系统 1. 商品管理 2. 用户登录 3. 购物车操作 4. 结算 0. 退出 请选择操作此时系统会尝试从goods.txt、users.txt、orders.txt读取数据。因为是第一次这些文件还不存在DBManager.loadGoods()会捕获FileNotFoundException并返回空ArrayList所以商品列表为空。这是正常现象。现在我们手动创建初始商品。选择1进入商品管理再选1添加商品请输入商品ID001 请输入商品名称苹果 请输入商品单价5.5 请输入商品库存100 添加成功这时DBManager.saveGoods()会被调用创建goods.txt文件并写入001|苹果|5.5|100同理添加香蕉002|香蕉|3.8|200、牛奶003|牛奶|12.0|50。每添加一个goods.txt就多一行。你可以用记事本打开它确认内容是否正确。实操心得我建议初学者在添加完商品后先退出程序按0再重新java Supermark运行。这样能验证DBManager.loadGoods()是否真的从文件读出了数据——如果再次进入商品管理看到“现有商品3个”就说明读写闭环成功了。4.4 用户注册与登录验证密码安全机制选择2用户登录再选1注册新用户请输入用户名zhangsan 请输入密码123456 注册成功此时users.txt被创建内容为zhangsan|123456注意密码是明文存储的这是为了简化教学。真实系统必须加盐哈希但这里的目标是理解流程。然后选2登录请输入用户名zhangsan 请输入密码123456 登录成功欢迎回来zhangsan如果输错密码会提示“用户名或密码错误”。你可以用记事本打开users.txt修改密码为654321再登录验证——这就是本地文件存储的最大优势数据完全透明修改即生效。4.5 购物车全流程从添加到结算的每一步登录成功后回到主菜单选3购物车操作- 选1添加商品输入商品ID001数量2→ 显示“已添加2个苹果到购物车”- 选2查看购物车显示“购物车商品苹果 x2总计11.0元”- 选3修改数量输入ID001新数量5→ 总价变为27.5- 选4删除商品输入ID001→ 购物车清空最后选4结算正在结算... 订单生成成功订单号1715678901234 订单详情 苹果 x5单价5.5小计27.5 总计27.5元 请保留此单据此时orders.txt被创建内容类似1715678901234|zhangsan|[Goods{id001, name苹果, price5.5, stock100}]|27.5|2024-05-15 14:30:01注意[Goods{...}]这部分是Tborders.toString()的输出实际项目中应该用更规范的格式如JSON但这里用toString()是为了避免引入额外依赖。关键验证点结算后购物车应为空。你再选3→2查看购物车应该显示“购物车为空”。同时goods.txt里的苹果库存没变——因为这个小工具没做库存扣减那是进阶功能但你可以轻松在ShopCar.checkout()里加上goods.setStock(goods.getStock() - quantity)逻辑。5. 常见问题与排查技巧实录那些让我熬夜调试的“幽灵Bug”即使严格按照上述步骤操作你也可能遇到一些让人抓狂的问题。下面是我整理的“高频故障速查表”每一条都来自真实教学现场附带一键修复方案。5.1 文件读写权限问题为什么goods.txt创建了却读不出数据现象添加商品后goods.txt文件生成了内容也正确但重启程序后loadGoods()返回空列表购物车始终为空。排查思路1. 检查DBManager.loadGoods()方法里BufferedReader读取时是否用了while ((line reader.readLine()) ! null)循环如果漏了while只会读第一行。2. 检查Goods的toString()和fromString(String line)是否严格对应。例如toString()输出001|苹果|5.5|100那么fromString()必须用line.split(\\|)分割且索引0、1、2、3分别对应id、name、price、stock。如果split()后数组长度不等于4会抛ArrayIndexOutOfBoundsException但被catch吞掉了导致静默失败。修复方案在DBManager.loadGoods()的catch块里加一句日志} catch (Exception e) { System.err.println(加载商品失败 e.getMessage()); e.printStackTrace(); // 打印完整堆栈暴露具体哪一行出错 return new ArrayList(); }运行后控制台会输出类似加载商品失败For input string: 苹果 java.lang.NumberFormatException: For input string: 苹果 at java.lang.Double.parseDouble(Double.java:540) at com.DBManager.loadGoods(DBManager.java:45) // 第45行Double.parseDouble(parts[2])这说明parts[2]不是数字而是苹果——因为split(\\|)没生效parts数组只有一个元素。根源是toString()里用了|但split()的参数是正则表达式|需要转义为\\|。5.2 中文乱码的“双重陷阱”控制台能显示文件里却是问号现象在IDEA里运行控制台打印“苹果”正常但打开goods.txt看到“苹?果”。原因分析这是IDEA的编码设置和文件编码不一致导致的。IDEA默认用UTF-8读取源文件但可能用系统编码GBK写文件。解决方案1. 在IDEA中File → Settings → Editor → File Encodings将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8。2. 在DBManager.java里所有FileWriter和FileReader必须显式指定UTF-8如前所述。3. 如果已生成乱码文件用记事本打开goods.txt另存为→编码选UTF-8→覆盖保存。5.3 “找不到符号”编译错误明明文件存在为什么javac不认识现象Supermark.java:10: error: cannot find symbol ShopCar cart new ShopCar(); ^ symbol: class ShopCar location: class Supermark根本原因javac找不到ShopCar.class通常是因为-ShopCar.java文件名和类名不一致如文件是shopcar.java类是public class ShopCarLinux/macOS区分大小写会失败- 当前目录不是项目根目录javac在错误路径下编译-ShopCar.java里有语法错误如少了个}导致编译失败没生成ShopCar.class排查步骤1.dir *.javaWindows或ls *.javaMac/Linux确认文件名是否全大写首字母ShopCar.java,User.java等。2.cd到项目根目录确保dir能看到所有.java文件。3. 单独编译ShopCar.javajavac ShopCar.java看是否报错。如果报错修复后再javac *.java。5.4 结算后购物车未清空用户抱怨“结完账商品还在篮子里”现象点击结算控制台显示“订单生成成功”但再进购物车查看商品还在。定位方法在Supermark.java的结算分支里找到调用shopCar.checkout(username)后的代码// ❌ 错误忘记清空购物车 Tborders order shopCar.checkout(username); DBManager.saveOrder(order); // 这里缺了shopCar.clear();修复在DBManager.saveOrder(order)后必须加shopCar.clear(); // ShopCar.java里实现clear()方法goodsList.clear(); quantityList.clear(); System.out.println(购物车已清空);独家技巧在ShopCar.clear()方法里加一句System.out.println(购物车清空goodsList大小 goodsList.size())运行时就能看到清空前后的size变化比猜强一百倍。5.5 用户登录无限循环输入错误密码后程序卡在“请输入密码”现象输错密码提示“错误”但没回到主菜单而是立刻又提示“请输入密码”形成死循环。原因Scanner的nextLine()和next()混用导致输入缓冲区残留。例如// ❌ 危险组合 System.out.print(用户名); String username scanner.next(); // 只读取到空格/回车前 System.out.print(密码); String password scanner.nextLine(); // 立刻读取到前面留下的回车password为空正确写法统一用nextLine()System.out.print(用户名); String username scanner.nextLine().trim(); // trim()去掉首尾空格 System.out.print(密码); String passwordStr scanner.nextLine().trim(); char[] password passwordStr.toCharArray(); // 转为char[]用于比较6. 进阶扩展与学习路径这个小工具还能怎么玩这个项目的价值远不止于“跑通”。它是一块优质的“乐高底板”你可以基于它无缝接入更高阶的知识点。下面是我给不同阶段学习者的定制化扩展建议每个都经过实测确保能在一个小时内完成。6.1 给Java新手加一个“库存预警”功能目标当商品库存低于10时在商品管理列表里标红显示。动手步骤1. 修改Goods.java加一个方法public boolean isLowStock() { return stock 10; }修改DBManager.loadGoods()加载后对每个Goods调用isLowStock()如果是打印时加[库存紧张]前缀。在Supermark.java的商品管理列表显示逻辑里加入判断for (Goods g : goodsList) { System.out.println(g.getId() g.getName() g.getPrice() 库存 g.getStock() (g.isLowStock() ? [库存紧张] : )); }收获理解方法封装、布尔逻辑、条件输出代码量增加不到10行成就感爆棚。6.2 给集合进阶者用HashMap优化商品查询现状ShopCar.addItem()每次都要遍历goodsList找IDO(n)复杂度。1000个商品时添加10次就要查10000次。升级方案1. 在DBManager里加一个静态MapString, Goods缓存private static MapString, Goods goodsCache new HashMap(); public static void loadGoodsToCache() { ListGoods list loadGoods(); goodsCache.clear(); for (Goods g : list) { goodsCache.put(g.getId(), g); } }修改ShopCar.addItem()用goodsCache.get(goodsId)代替遍历O(1)查询。注意goodsCache是静态的所以loadGoodsToCache()只需在程序启动时调用一次在Supermark.main()开头。6.3 给IO流深化者实现“备份文件”机制目标每次写入goods.txt前先自动备份为goods.txt.bak。核心代码在DBManager.saveGoods()开头File original new File(goods.txt); File backup new File(goods.txt.bak); if (original.exists()) { Files.copy(original.toPath(), backup.toPath(), StandardCopyOption.REPLACE_EXISTING); } // 然后继续写入original需要导入java.nio.file.*这是Java 7的NIO.2 API比老IO更简洁安全。6.4 给面向对象实践者引入“折扣策略”接口目标支持会员价、满减、限时折扣等多种促销。设计1. 新建接口DiscountStrategy.javapublic interface DiscountStrategy { double calculateDiscount(double originalPrice, int quantity); }实现类MemberDiscount.java95折public class MemberDiscount implements DiscountStrategy { public double calculateDiscount(double price, int qty) { return price * qty * 0.05; // 返5% } }在Tborders里加DiscountStrategy strategy字段结算时调用strategy.calculateDiscount()。这个改动会逼你理解接口、多态、策略模式但代码结构依然清晰——所有折扣逻辑隔离在单独类里不影响购物车和订单主干。最后分享一个小技巧这个项目的所有.java文件我都刻意控制在200行以内。当你想加新功能时先问自己“这个逻辑应该属于哪个现有类的职责如果超过150行是不是该拆出一个新类”——这比死记UML图谱管用十倍。真正的面向对象不在图纸上而在你每次new一个对象时心里清楚它该做什么、不该做什么。本文还有配套的精品资源点击获取简介一个不依赖数据库和第三方框架的Java超市收银程序所有功能用JDK原生API实现。商品信息、用户账号、订单记录都通过ArrayList等集合类组织数据以明文文本格式保存在本地文件中靠FileInputStream和FileOutputStream完成读写。包含完整的模块分工Goods.java封装商品属性User.java和UserDatabase.java负责用户注册与密码校验ShopCar.java实现购物车增删改查和结账逻辑Tborders.java生成并存储交易单DBManager.java统一处理文件IO操作Supermark.java是程序入口Test.java提供基础测试用例。代码结构清晰关键位置有中文注释适合刚学完Java基础、集合、IO流的初学者动手编译运行快速理解面向对象设计在实际小系统中的落地方式。本文还有配套的精品资源点击获取