首页 / 资讯中心 / 文章详情

简易宠物商店Java源代码:从控制台到Swing的完整实现

简易宠物商店Java源代码:从控制台到Swing的完整实现 ★ FEATURED ARTICLE
简介这是一份面向Java初学者与编程练习者的简易宠物商店源代码项目以面向对象思想模拟宠物信息管理、库存控制与交易处理等核心业务适合用来巩固类与对象、继承封装多态等基础概念也可作为课程设计或面试练手素材。压缩包为rar格式共7个文件约8KB包含java源文件、编译后的class字节码、Eclipse工程配置project、classpath、prefs以及说明文本结构精简便于在Eclipse中直接导入运行与调试。目前已有827人学习下载。项目虽小却覆盖了Pet类属性与方法设计、JDBC数据库交互、Swing或JavaFX界面构建、异常处理与文件读写等可拓展方向读者可借此观察代码组织方式尝试重构并补充功能从而加深对Java工程实践的理解。1. 简易宠物商店Java源代码从课程设计到能跑通的最小闭环很多同学搜「简易宠物商店Java源代码」其实不是想抄一份交作业而是想找一个能跑起来、能看懂、能改的小系统把 Java 基础、集合、IO、Swing 或控制台交互串成一条线。我见过太多所谓「宠物商店源码」打开就是一堆空方法或者只有实体类没有业务逻辑跑起来直接报空指针。真正有用的版本应该是一个最小闭环能添加宠物、能查询、能下单、能保存到文件代码量控制在几百行新手能跟下来熟手能一眼看出扩展点在哪。这篇笔记就按这个目标来先讲清楚一个简易宠物商店该有哪些模块再给出一份可复现的控制台版实现然后拆参数、拆坑最后落到怎么把它改成带界面的版本。适合正在做课程设计、想练手 Java 集合与文件读写、或者需要一个干净小项目练重构的人。下面所有代码都是我自己会用的写法不依赖任何外部框架JDK 8 以上就能跑。2. 先定边界宠物商店到底要几个类为什么这么分2.1 最小可用模型宠物、客户、订单三件套一个简易宠物商店核心对象只有三个宠物Pet、客户Customer、订单Order。宠物有编号、名字、类别、价格、库存状态客户有编号、姓名、联系方式订单记录谁买了哪只宠物、什么时候、花了多少钱。很多源码一上来就搞七八个类结果每个类只有两三个字段反而看不清主线。我一般会先把这三个类写死再考虑要不要加「商店」这个管理类。为什么这么分因为宠物商店的本质是「库存 交易」。宠物是库存客户是交易对象订单是交易记录。把这三者理清后面加购物车、加会员折扣、加多店铺都是在它们上面挂东西。如果一开始就把「宠物类型」做成继承树比如 Dog、Cat、Bird 各一个类新手很容易陷进继承里出不来而实际业务里宠物的差异往往只是几个字段用枚举或字符串标签就够了。常见做法是Pet 用普通类类别用枚举 PetTypeCustomer 用普通类Order 里放一个 Pet 引用和一个 Customer 引用再加日期和实付金额。这样代码量小关系清晰后面要改成数据库也容易映射。2.2 控制台版还是 Swing 版先跑通再谈界面搜源码的人里有一半是想要带界面的觉得控制台「太 low」。但从落地角度我强烈建议先写控制台版。原因有三个第一控制台版能把业务逻辑和输入输出分离后面套 Swing 只是换一层壳第二调试方便不用处理事件分发线程第三代码量少容易在课程设计里讲清楚。如果你直接上 Swing很容易写成「按钮里塞所有逻辑」最后连自己都改不动。我一般会先写一个 PetShopService 类里面放 addPet、listPets、createOrder 这些方法控制台只负责读输入、调方法、打印结果。等这层稳定了再写一个 Swing 的 MainFrame按钮事件里只调 service。这样界面崩了不影响业务业务改了界面也不用大动。下面这张表是我建议的类职责划分你可以照着建包类名职责关键字段Pet宠物实体id, name, type, price, stockCustomer客户实体id, name, phoneOrder订单实体orderId, customer, pet, amount, createTimePetShopService业务逻辑添加、查询、下单、保存DataStore数据持久化读写文件Main控制台入口菜单循环这个划分不复杂但能让你在答辩时讲清楚「我为什么这么分层」。2.3 数据放内存还是放文件简易版的持久化选择简易宠物商店源码如果只把数据放内存关掉程序就没了这在实际使用中毫无意义。但上数据库又太重所以文件持久化是最合适的折中。我一般用两种格式CSV 或 Java 序列化。CSV 可读性好方便手动改序列化写起来简单但文件是二进制出问题不好查。对于课程设计我推荐 CSV。宠物存一个 pets.csv订单存一个 orders.csv每行一条记录字段用逗号分隔。读的时候按行 split写的时候拼字符串。注意价格和库存要转成数字别直接当字符串存否则后面排序会出错。文件路径不要写死成绝对路径用相对路径「data/pets.csv」并在程序启动时检查目录是否存在不存在就创建。提示CSV 里如果宠物名字带逗号简单 split 会错位。简易版可以约定名字不含逗号或者用分号做分隔符。这是最容易被忽略的坑。3. 动手写一份能直接跑的控制台版宠物商店3.1 实体类与枚举字段、构造、getter/setter先写 PetType 枚举和 Pet 类。枚举里放 DOG、CAT、BIRD、OTHER每个带一个中文描述方便打印。Pet 类字段用 private构造方法里做基本校验比如价格不能为负。getter/setter 用 IDE 生成就行但要注意 stock 的 setter 里别允许负数。public enum PetType { DOG(狗), CAT(猫), BIRD(鸟), OTHER(其他); private final String label; PetType(String label) { this.label label; } public String getLabel() { return label; } }public class Pet { private String id; private String name; private PetType type; private double price; private int stock; public Pet(String id, String name, PetType type, double price, int stock) { if (price 0) { throw new IllegalArgumentException(价格不能为负); } if (stock 0) { throw new IllegalArgumentException(库存不能为负); } this.id id; this.name name; this.type type; this.price price; this.stock stock; } // getter 和 setter 省略实际写的时候补全 public String getId() { return id; } public String getName() { return name; } public PetType getType() { return type; } public double getPrice() { return price; } public int getStock() { return stock; } public void setStock(int stock) { if (stock 0) { throw new IllegalArgumentException(库存不能为负); } this.stock stock; } Override public String toString() { return id | name | type.getLabel() | price | 库存: stock; } }这段代码的关键点是构造方法和 setStock 里的校验。很多源码不校验结果库存被减成负数后面下单逻辑就乱了。参数说明id 用字符串而不是 int是为了后面扩展成「P001」这种带前缀的编号price 用 double 在简易版够用如果要做金额计算建议换成 BigDecimal但课程设计里 double 加格式化输出也能接受。Customer 和 Order 类似Order 里加一个 LocalDateTime 字段记录下单时间。注意 Order 不要直接存 Pet 对象存 petId 和单价快照这样宠物信息改了历史订单不会变。这是业务系统的基本意识简易版也值得保留。3.2 业务层添加、查询、下单三个核心方法PetShopService 是整个系统的核心。我一般用 ArrayList 存宠物和订单用 HashMap 做 id 到宠物的索引查询时先查 map没有再遍历。下单方法要检查库存库存够就减一生成订单返回订单对象库存不够就抛异常或返回 null由调用方决定怎么提示。import java.time.LocalDateTime; import java.util.*; public class PetShopService { private final ListPet pets new ArrayList(); private final ListOrder orders new ArrayList(); private final MapString, Pet petIndex new HashMap(); public void addPet(Pet pet) { if (petIndex.containsKey(pet.getId())) { throw new IllegalArgumentException(宠物编号已存在: pet.getId()); } pets.add(pet); petIndex.put(pet.getId(), pet); } public ListPet listPets() { return Collections.unmodifiableList(pets); } public Pet findPet(String id) { return petIndex.get(id); } public Order createOrder(String customerId, String customerName, String petId) { Pet pet petIndex.get(petId); if (pet null) { throw new IllegalArgumentException(宠物不存在: petId); } if (pet.getStock() 0) { throw new IllegalStateException(库存不足: pet.getName()); } pet.setStock(pet.getStock() - 1); Order order new Order( UUID.randomUUID().toString().substring(0, 8), customerId, customerName, petId, pet.getPrice(), LocalDateTime.now() ); orders.add(order); return order; } public ListOrder listOrders() { return Collections.unmodifiableList(orders); } }逻辑说明addPet 先查重再放入避免同一个 id 出现两次listPets 返回不可修改的列表防止外部直接改内部集合createOrder 里先查宠物、再查库存、再减库存、最后生成订单顺序不能乱。参数说明customerId 和 customerName 直接传字符串是为了简化实际项目里应该先查客户表订单号用 UUID 截取 8 位够简易版用但要注意可能重复正式环境要用更可靠的生成策略。这里有个容易翻车的地方减库存和生成订单之间如果抛异常库存就白减了。简易版可以接受但你要知道这是事务问题的雏形。后面如果想改进可以把减库存放到订单生成成功之后或者加一个简单的回滚逻辑。3.3 文件读写把宠物和订单存成 CSVDataStore 类负责把 pets 和 orders 写到文件以及从文件读回来。我一般用 Files.readAllLines 和 Files.write代码短异常好处理。写的时候用 StringBuilder 拼行读的时候按逗号 split注意处理空行和格式错误。import java.io.IOException; import java.nio.file.*; import java.util.*; public class DataStore { private static final String DATA_DIR data; private static final String PET_FILE DATA_DIR /pets.csv; private static final String ORDER_FILE DATA_DIR /orders.csv; public static void ensureDir() throws IOException { Path dir Paths.get(DATA_DIR); if (!Files.exists(dir)) { Files.createDirectories(dir); } } public static void savePets(ListPet pets) throws IOException { ListString lines new ArrayList(); for (Pet p : pets) { lines.add(p.getId() , p.getName() , p.getType().name() , p.getPrice() , p.getStock()); } Files.write(Paths.get(PET_FILE), lines); } public static ListPet loadPets() throws IOException { ListPet pets new ArrayList(); Path path Paths.get(PET_FILE); if (!Files.exists(path)) { return pets; } for (String line : Files.readAllLines(path)) { if (line.trim().isEmpty()) continue; String[] parts line.split(,); if (parts.length 5) continue; Pet pet new Pet( parts[0], parts[1], PetType.valueOf(parts[2]), Double.parseDouble(parts[3]), Integer.parseInt(parts[4]) ); pets.add(pet); } return pets; } }逻辑说明ensureDir 在程序启动时调用保证 data 目录存在savePets 把每个宠物拼成一行字段顺序和 loadPets 的解析顺序必须一致loadPets 里跳过空行和字段不足的行避免一条坏数据导致整个程序起不来。参数说明PetType.valueOf 要求 CSV 里存的是枚举名比如 DOG不能存「狗」否则会抛异常。如果你想让文件更可读可以在保存时存 label读取时再根据 label 反查枚举但那样代码会多几行。订单的读写类似但要注意 LocalDateTime 的格式化。我一般用 DateTimeFormatter.ISO_LOCAL_DATE_TIME存成「2025-01-01T10:00:00」这种格式读的时候再 parse 回来。不要用默认的 toString不同 JDK 版本格式可能不一样。3.4 控制台菜单用 while 循环串起所有操作Main 类里用一个 while(true) 循环打印菜单读用户输入switch 分发。输入用 Scanner注意 nextInt 和 nextLine 混用会吞回车我一般统一用 nextLine 然后手动转数字这样不容易出玄学问题。import java.util.Scanner; public class Main { public static void main(String[] args) throws Exception { DataStore.ensureDir(); PetShopService service new PetShopService(); for (Pet p : DataStore.loadPets()) { service.addPet(p); } Scanner scanner new Scanner(System.in); while (true) { System.out.println(\n1. 添加宠物 2. 查看宠物 3. 下单 4. 查看订单 5. 保存并退出); System.out.print(请选择: ); String choice scanner.nextLine().trim(); switch (choice) { case 1: System.out.print(编号: ); String id scanner.nextLine().trim(); System.out.print(名字: ); String name scanner.nextLine().trim(); System.out.print(类别(DOG/CAT/BIRD/OTHER): ); PetType type PetType.valueOf(scanner.nextLine().trim().toUpperCase()); System.out.print(价格: ); double price Double.parseDouble(scanner.nextLine().trim()); System.out.print(库存: ); int stock Integer.parseInt(scanner.nextLine().trim()); service.addPet(new Pet(id, name, type, price, stock)); System.out.println(添加成功); break; case 2: for (Pet p : service.listPets()) { System.out.println(p); } break; case 3: System.out.print(客户编号: ); String cid scanner.nextLine().trim(); System.out.print(客户姓名: ); String cname scanner.nextLine().trim(); System.out.print(宠物编号: ); String pid scanner.nextLine().trim(); Order order service.createOrder(cid, cname, pid); System.out.println(下单成功: order.getOrderId()); break; case 4: for (Order o : service.listOrders()) { System.out.println(o); } break; case 5: DataStore.savePets(service.listPets()); System.out.println(已保存退出); return; default: System.out.println(无效选择); } } } }逻辑说明启动时先加载文件里的宠物再进入菜单循环每次操作后不自动保存退出时统一保存避免频繁写文件。参数说明类别输入用 toUpperCase 转成大写再 valueOf防止用户输入小写报错价格和库存用 parseDouble/parseInt如果用户输入非数字会抛异常简易版可以接受想友好一点可以加 try-catch 提示重新输入。这段代码跑起来后你就能添加宠物、查看列表、下单、看订单、保存退出。整个系统不到 300 行但已经具备了一个简易宠物商店的完整闭环。4. 避坑与排查简易宠物商店源码最常见的 5 个翻车点4.1 空指针集中在 findPet 和文件读取现象添加宠物后下单报 NullPointerException或者启动时读文件直接崩。原因通常是 petIndex 没有同步更新或者 CSV 里有一行格式不对split 后数组越界。解决addPet 里必须同时更新 list 和 maploadPets 里对每行做长度检查和 try-catch坏数据跳过并打印警告不要让整个程序挂掉。4.2 库存扣成负数订单却生成了现象同一只宠物被连续下单库存显示 -1。原因createOrder 里先减库存再判断或者判断和减库存之间没有原子性。解决先判断 stock 0再减再生成订单如果生成订单可能失败就把减库存放到最后。简易版单线程不会并发但逻辑顺序要对。4.3 CSV 字段顺序改了读回来全乱现象给 Pet 加了一个字段保存正常读取时价格变成库存。原因save 和 load 的字段顺序没有同步修改。解决把字段顺序写在一个常量数组里或者用带表头的 CSV读取时先解析表头映射列号。简易版至少要在两个方法旁边写注释提醒改一处要改两处。4.4 Scanner 混用 nextInt 和 nextLine 导致输入跳过现象输入价格后下一个「名字」直接跳过。原因nextInt 只读数字回车留在缓冲区nextLine 读到空字符串。解决统一用 nextLine 读字符串再转换或者每次 nextInt 后加一个 nextLine 吃掉回车。我一般直接全用 nextLine省心。4.5 文件路径写死换台机器就找不到现象在本机跑得好好的拷给别人就报 NoSuchFileException。原因用了绝对路径比如「/Users/xxx/data/pets.csv」。解决用相对路径「data/pets.csv」并在启动时 ensureDir 创建目录。如果要在任意目录运行可以用 System.getProperty(user.dir) 拼路径但简易版相对路径就够了。5. 从控制台到 Swing给宠物商店套一层界面的具体改法如果你已经跑通了上面的控制台版想把它改成带界面的简易宠物商店最稳的路径不是重写而是加一层。具体做法新建一个 PetShopFrame 继承 JFrame里面放一个 JTable 显示宠物列表下面放几个 JButton添加、下单、刷新。按钮的 ActionListener 里只调 PetShopService 的方法然后刷新表格模型。不要把所有逻辑塞进按钮里否则你后面加一个「按类别筛选」就要改一堆地方。表格模型我一般用 DefaultTableModel列名固定为「编号、名字、类别、价格、库存」。刷新方法里先 setRowCount(0)再遍历 service.listPets() 逐行 addRow。添加宠物弹一个 JDialog里面放 JTextField确定按钮里做和 Main 里 case 1 一样的校验和调用。下单弹一个输入宠物编号的对话框调 createOrder成功就弹 JOptionPane 提示失败就显示异常信息。这里有个血泪经验Swing 的更新必须在事件分发线程EDT里做。如果你在按钮里直接改表格模型一般没问题因为按钮事件本身就在 EDT。但如果你开了后台线程读文件读完要更新表格就必须用 SwingUtilities.invokeLater。简易版可以不开线程启动时直接读文件这样最省事。验证方法先跑控制台版确认添加、下单、保存、重启后数据还在再跑 Swing 版做同样的操作看表格和文件是否一致。如果 Swing 版表格不刷新检查是不是用了新的 service 实例或者表格模型没有重新设置。最后你可以把 DataStore 的保存改成每次操作后自动保存这样关窗口不会丢数据但要注意写文件频率简易版数据量小问题不大。我自己做这类小系统时习惯先把控制台版跑通再套界面最后才考虑加数据库。这样每一步都有可验证的产出不会写到一半发现底层逻辑是错的。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站