免费java练习项目推荐:用图书管理系统巩固基础知识

AI提示词15分钟前发布 jdkjadmin
0 00
免费java练习项目推荐:用图书管理系统巩固基础知识

免费java练习项目推荐:用图书管理系统巩固基础知识

如果刚学完 Java 的变量、流程控制、类与对象、集合和异常处理,下一步往往不是再看一遍语法,而是把这些知识连成一个能运行的小程序。图书管理系统是适合入门的练习项目:需求容易理解,能够覆盖增删查改、数据校验和状态变化,又不必一开始就引入复杂框架。

本文从零搭建一个免费的命令行版本。项目只依赖 JDK,不需要数据库、账号或付费服务。代码可以直接运行,也便于继续扩展为文件存储、数据库应用或 Web 项目。需要先说明:本文实现的是用于学习的单机内存程序,不是可以直接部署到图书馆或企业环境的完整系统。

一、适合什么场景,不适合什么场景

这个项目适合已经掌握 Java 基本语法、希望练习综合应用的初学者,也适合作为课程作业或自学阶段的第一个小项目。你可以在实现过程中练习:

  • 用类表达图书和业务服务,理解字段、构造方法与封装。
  • 用集合保存数据,按编号查询、遍历和搜索。
  • 用条件判断维护库存状态,避免库存变成负数。
  • 用异常处理输入错误和业务规则冲突。
  • 把控制台交互与业务逻辑分开,为后续测试和扩展打基础。

它不适合直接管理真实馆藏、多人同时借阅、会员权限、罚款、预约、跨设备数据同步或审计记录。程序中的数据保存在内存里,程序退出后就会消失;也没有身份认证、授权、备份和并发事务。把它称为“图书管理系统”是练习项目的功能定位,不代表它具备生产级系统的完整能力。

二、先确定业务规则和边界

写代码前先把规则说清楚,能避免后面陷入“功能看起来有了,数据却对不上”的问题。本文采用以下简化规则:

  • 每本书有系统编号、书名、作者、ISBN、总册数和可借册数。
  • 新增图书时,册数必须大于零;ISBN 不能为空,且不能与现有图书重复。
  • 借阅时,可借册数必须大于零;归还时,必须存在未归还的借阅数量。
  • 系统只按图书累计借阅数量记录借出状态,不记录读者是谁、何时借出或应还日期。
  • 搜索只匹配书名,忽略英文字母大小写;不实现拼音、分词或模糊相似度。

最后两条尤其重要。由于没有读者和借阅记录,系统只能判断“这本书当前有未归还册数”,不能回答“哪位读者借了这本书”,也不能精确核对某位读者是否有权归还。这个限制是数据模型造成的,不是再加一条控制台提示就能补上的。后续若要支持读者维度,应新增读者实体和借阅记录实体。

三、创建项目并运行

准备好 JDK 8 或更高版本,在一个空目录中创建 LibraryApp.java,把下面的代码完整保存进去。代码没有使用第三方库,也没有依赖特定 IDE。

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

public class LibraryApp {
    private final LibraryService service = new LibraryService();
    private final Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        new LibraryApp().run();
    }

    private void run() {
        System.out.println("图书管理练习系统");
        System.out.println("提示:当前数据只保存在内存中,退出后不会保留。");

        boolean running = true;
        while (running) {
            printMenu();
            String choice = readLine("请选择:");

            try {
                switch (choice) {
                    case "1":
                        listBooks();
                        break;
                    case "2":
                        addBook();
                        break;
                    case "3":
                        searchBooks();
                        break;
                    case "4":
                        borrowBook();
                        break;
                    case "5":
                        returnBook();
                        break;
                    case "0":
                        running = false;
                        System.out.println("已退出。");
                        break;
                    default:
                        System.out.println("无效选项,请输入菜单中的编号。");
                }
            } catch (IllegalArgumentException | IllegalStateException e) {
                System.out.println("操作未完成:" + e.getMessage());
            }
        }

        scanner.close();
    }

    private void printMenu() {
        System.out.println();
        System.out.println("1. 查看全部图书");
        System.out.println("2. 新增图书");
        System.out.println("3. 按书名搜索");
        System.out.println("4. 借阅图书");
        System.out.println("5. 归还图书");
        System.out.println("0. 退出");
    }

    private void listBooks() {
        List<Book> books = service.listBooks();
        if (books.isEmpty()) {
            System.out.println("目前没有图书。");
            return;
        }

        for (Book book : books) {
            System.out.println(book);
        }
    }

    private void addBook() {
        String title = readLine("书名:");
        String author = readLine("作者:");
        String isbn = readLine("ISBN:");
        int copies = readPositiveInt("总册数:");

        Book book = service.addBook(title, author, isbn, copies);
        System.out.println("新增成功:" + book);
    }

    private void searchBooks() {
        String keyword = readLine("请输入书名关键词:");
        List<Book> results = service.searchByTitle(keyword);

        if (results.isEmpty()) {
            System.out.println("没有找到匹配的图书。");
            return;
        }

        for (Book book : results) {
            System.out.println(book);
        }
    }

    private void borrowBook() {
        String id = readLine("图书编号:");
        service.borrowBook(id);
        System.out.println("借阅成功。");
    }

    private void returnBook() {
        String id = readLine("图书编号:");
        service.returnBook(id);
        System.out.println("归还成功。");
    }

    private int readPositiveInt(String prompt) {
        while (true) {
            String value = readLine(prompt);
            try {
                int number = Integer.parseInt(value);
                if (number > 0) {
                    return number;
                }
            } catch (NumberFormatException e) {
                // 将输入错误转化为可重试的提示,不让程序退出。
            }
            System.out.println("请输入大于零的整数。");
        }
    }

    private String readLine(String prompt) {
        System.out.print(prompt);
        return scanner.nextLine().trim();
    }
}

final class Book {
    private final String id;
    private final String title;
    private final String author;
    private final String isbn;
    private final int totalCopies;
    private int availableCopies;

    Book(String id, String title, String author, String isbn, int totalCopies) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.totalCopies = totalCopies;
        this.availableCopies = totalCopies;
    }

    String getId() {
        return id;
    }

    String getTitle() {
        return title;
    }

    String getIsbn() {
        return isbn;
    }

    int getTotalCopies() {
        return totalCopies;
    }

    int getAvailableCopies() {
        return availableCopies;
    }

    void borrowOne() {
        if (availableCopies <= 0) {
            throw new IllegalStateException("该书没有可借册数。");
        }
        availableCopies--;
    }

    void returnOne() {
        if (availableCopies >= totalCopies) {
            throw new IllegalStateException("没有未归还的借阅册数。");
        }
        availableCopies++;
    }

    @Override
    public String toString() {
        return "编号:" + id
                + " | 书名:" + title
                + " | 作者:" + author
                + " | ISBN:" + isbn
                + " | 可借:" + availableCopies + "/" + totalCopies;
    }
}

final class LibraryService {
    private final Map<String, Book> books = new LinkedHashMap<>();
    private int nextId = 1;

    public synchronized Book addBook(
            String title, String author, String isbn, int totalCopies) {
        String cleanTitle = requireText(title, "书名");
        String cleanAuthor = requireText(author, "作者");
        String cleanIsbn = requireText(isbn, "ISBN");

        if (totalCopies <= 0) {
            throw new IllegalArgumentException("总册数必须大于零。");
        }
        if (findByIsbn(cleanIsbn) != null) {
            throw new IllegalArgumentException("该 ISBN 已存在。");
        }

        String id = String.format("B%04d", nextId++);
        Book book = new Book(id, cleanTitle, cleanAuthor, cleanIsbn, totalCopies);
        books.put(id, book);
        return book;
    }

    public synchronized List<Book> listBooks() {
        return new ArrayList<>(books.values());
    }

    public synchronized List<Book> searchByTitle(String keyword) {
        String cleanKeyword = requireText(keyword, "搜索关键词").toLowerCase();
        List<Book> results = new ArrayList<>();

        for (Book book : books.values()) {
            if (book.getTitle().toLowerCase().contains(cleanKeyword)) {
                results.add(book);
            }
        }
        return results;
    }

    public synchronized void borrowBook(String id) {
        Book book = findById(id);
        book.borrowOne();
    }

    public synchronized void returnBook(String id) {
        Book book = findById(id);
        book.returnOne();
    }

    private Book findById(String id) {
        String cleanId = requireText(id, "图书编号");
        Book book = books.get(cleanId);
        if (book == null) {
            throw new IllegalArgumentException("找不到编号为 " + cleanId + " 的图书。");
        }
        return book;
    }

    private Book findByIsbn(String isbn) {
        for (Book book : books.values()) {
            if (book.getIsbn().equalsIgnoreCase(isbn)) {
                return book;
            }
        }
        return null;
    }

    private String requireText(String value, String fieldName) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + "不能为空。");
        }
        return value.trim();
    }
}

在该目录打开终端,执行:

javac LibraryApp.java
java LibraryApp

如果系统安装了多个 JDK,可以用 java -version 和 javac -version 检查当前终端实际调用的版本。文件名必须是 LibraryApp.java,因为其中的公开类名是 LibraryApp。若使用 IDE,创建同名 Java 类后粘贴代码即可。

四、代码的职责划分

1. Book 表示图书状态

Book 保存图书的编号、书名、作者、ISBN 和册数。除可借册数外,其余字段声明为 final,表示创建后不允许随意改写。可借册数只能通过 borrowOne() 和 returnOne() 改变,并在改变前检查边界条件。这样比让外部代码直接执行 availableCopies-- 更容易维护不变量。

这里的关键不变量是:可借册数不能小于零,也不能大于总册数。借阅和归还方法负责维护这个条件。若将来新增“修改总册数”功能,也必须重新考虑在已有借阅未归还时如何调整库存。

2. LibraryService 管理业务规则

业务服务使用 LinkedHashMap 按编号保存图书。Map 适合根据编号快速定位对象,LinkedHashMap 则让遍历结果保持插入顺序,便于控制台查看。新增时自动生成编号,并检查 ISBN 是否重复;借阅和归还时先查找图书,再委托图书对象更新状态。

服务方法标记为 synchronized,使同一个服务实例上的单次方法调用不会同时交错执行。这对理解同步方法有帮助,但不能把它误认为完整的并发解决方案:该实现没有跨进程锁、数据库事务或持久化;如果以后把业务拆成多个步骤,多个方法之间也不自动构成一个原子事务。当前程序是单线程控制台程序,主要收益是让状态修改集中在服务层。

3. LibraryApp 负责交互

主程序负责显示菜单、读取输入和打印结果,不直接操作 Map。这样业务规则不会散落在每个菜单分支里。控制台读入的内容先按字符串处理,册数再通过整数解析和正数检查完成校验。用户输错数字时程序会提示重试,而不是因 NumberFormatException 直接退出。

五、逐项验证功能

运行后可以按下面的顺序手动测试:

  1. 选择“查看全部图书”,确认空系统会显示提示而非报错。
  2. 新增一本册数为 2 的书,记下输出的编号。
  3. 按书名关键词搜索,确认能找到该书;再用不存在的关键词测试空结果。
  4. 使用编号借阅两次,查看可借册数依次变为 1 和 0。
  5. 第三次借阅应提示没有可借册数,库存不能变成负数。
  6. 归还一次后,可借册数应恢复为 1;再多归还一次应被拒绝。
  7. 新增相同 ISBN 的图书应失败;输入空书名或零册数也应失败。
  8. 退出并重新运行,确认数据消失。这是内存存储的预期行为。

测试时不要只验证“正确输入能成功”。对每条业务规则,都应至少测试一个边界值和一个错误值,例如库存为零、编号不存在、空关键词和重复 ISBN。很多入门项目的主要缺陷并不是正常流程无法运行,而是错误输入破坏了数据状态。

六、常见错误及其原因

把总册数和可借册数混为一谈

总册数表示馆藏数量,可借册数表示当前可借数量。借阅时只减少可借册数,归还时只增加可借册数。如果直接减少总册数,系统就无法区分“已经借出”和“馆藏数量减少”;若归还时把总册数增加,也会凭空增加馆藏。

只检查借阅,不检查归还

仅仅在借书时防止负库存还不够。如果归还操作没有上限检查,可借数量可能超过总册数。本文还检查是否存在未归还册数,避免无中生有地增加库存。

用书名作为唯一标识

不同版本、不同作者可能有相同书名,同一本书也可能有不同版次。用书名查找并修改容易操作错对象。因此系统用生成的编号定位记录,ISBN 只用于新增时的重复检查。实际业务中的 ISBN 校验规则和不同版本的管理策略,需根据需求另外定义;本文没有验证 ISBN 的校验位或格式合法性。

菜单代码里直接修改集合

如果控制台分支随处添加、删除和修改图书,校验规则很容易遗漏,也难以单独测试。把规则放在 LibraryService 中,界面只负责调用服务,是本项目刻意保留的分层练习。再进一步可以为服务编写单元测试,不必通过模拟键盘输入测试每条业务规则。

用 Scanner 的 nextInt 和 nextLine 混读

nextInt() 通常不会消费行尾换行符,紧接着调用 nextLine() 时,可能读到空字符串,导致菜单或文本输入行为异常。本文统一使用 nextLine() 读取,再自行解析整数,避免这类常见问题,也更便于处理非法输入。

认为 synchronized 可以解决所有并发问题

同步方法保护的是当前服务实例的调用过程,不会自动处理多台机器、多个进程或数据库里的竞争更新。真实的借阅应用若有多人同时操作,需要在持久化层设计事务和并发控制,并针对超借等条件执行可靠的原子更新。不要因为加了关键字就声称系统支持高并发。

七、参数限制与安全用途说明

本程序对书名、作者、ISBN、编号和搜索关键词的限制是“去除首尾空白后不能为空”。册数通过 Java 的 int 解析,并要求大于零;超出 int 范围的输入会被视为无效整数并要求重新输入。代码没有设置书名长度、作者长度、ISBN 格式或总册数的业务上限,也没有验证 ISBN 校验位。若要用于实际业务,应根据数据规范制定明确限制,而不是把示例中的校验当成完整标准。

编号由程序自动生成,格式为字母 B 加至少四位数字,例如 B0001。这只是单次运行中的展示编号,不是跨重启永久唯一的主键;重新运行后计数会重新开始。当前因数据也同时清空,所以不会在这次运行中冲突;一旦加入持久化,就必须改用数据库生成键、可靠的序列或其他能跨运行保证唯一的方案。

在安全方面,这个练习程序没有网络接口,也没有用户账号和密码,因此不存在“通过加密就变安全”的结论。它不应直接作为线上服务使用。若把功能改成 Web 应用,需要按实际威胁模型增加身份认证、权限检查、输入校验、日志与错误处理,并使用参数化数据库操作避免 SQL 注入。显示用户输入时还要根据输出环境进行编码;HTML 页面里的编码规则与控制台输出不同。参数校验有助于数据一致性,但不能替代认证、授权或安全编码。

八、从基础版本继续扩展

建议一次只增加一个能力,并先补充对应业务规则。下面是比较自然的练习顺序:

  1. 删除图书:明确有未归还借阅时是否允许删除,不能只从 Map 移除就忽略借阅状态。
  2. 修改图书信息:明确哪些字段可改,ISBN 修改时仍须检查重复。
  3. 文件保存:退出时保存、启动时加载。需要考虑文件不存在、格式损坏、写入失败和数据版本变化,不要只在正常路径上测试。
  4. 数据库持久化:用 JDBC 学习连接、预编译语句和事务,再考虑表结构、唯一约束和并发更新。
  5. 读者与借阅记录:建立读者、图书和借阅记录之间的关系,记录借出时间、归还时间和状态。
  6. 自动化测试:为无库存借阅、重复 ISBN、重复归还和非法输入等边界编写测试。

练习项目可以继续演进为 Spring Boot 应用,但不建议一开始就叠加框架、登录系统、缓存和微服务。先确认业务规则正确,再更换存储方式或增加接口,排错范围会小得多。如果打算采用网上的开源图书管理项目作为参考,应先阅读其许可证和依赖信息;“代码可以下载”并不自动等于允许任意复制、修改或商用。

结语

图书管理系统的价值不在于菜单有多少项,而在于能否把数据状态和业务边界说清楚。这个零依赖版本已经覆盖类与对象、集合、输入处理、异常和基本库存规则,同时明确保留了读者记录、持久化和生产安全等边界。先照着运行,再尝试删除或改写一部分功能,最后为边界条件补测试,比一次性追求“功能齐全”更能巩固 Java 基础。

© 版权声明

相关文章