🐸 青蛙编程网
首页 / Java / 面向对象 / 5. 综合练习

综合练习:图书管理小程序

这是什么:图书管理小程序 = 一个能录入图书、管理书目的小项目,把面向对象四章的知识全部用上。

为什么用:类与对象、封装、继承与多态、接口与集合四章学完,可真写起程序来还是抓瞎:类怎么拆、字段怎么藏、接口怎么定,全靠一个个例子才串得起来。

如何用:本章先做一遍练习前自测,再跟着需求分析敲出完整代码,最后用代码走读把三处关键设计讲明白。

练习前自测

动手前先花两分钟对答案,四个问题都能脱口而出,再往下走;卡壳的就回对应章节补课:

  • new 一个对象时,哪个方法被自动调用?它和普通方法有什么不同?(类与对象)
  • 成员变量想"外面看得见、改不了",该用什么修饰符配什么方法?(封装)
  • Animal a = new Cat(...) 之后调 a.makeSound(),执行的是谁的版本?(继承与多态)
  • ArrayList<String> 的尖括号起什么作用?add、get、size 分别干什么?(接口与集合)

需求分析

  • Book 类:书名 + 是否已借出,重写 toString 用于展示
  • Library 类:用 ArrayList 管理图书,提供上架、借书、还书、查看全部方法
  • 借还都要校验:不存在的书、已借出的书,都要友好拒绝

先想清楚两个类的分工:Book 只管"一本书自己"的状态(在架还是借出),Library 管"整个书架"的增删查借。状态藏在 Book 身上,操作图书的入口开在 Library 身上——这就是面向对象的分工思维:每个类只干自己擅长的事。

把下面的代码保存为 Library.java,一条命令跑起来:java Library.java

完整代码

import java.util.ArrayList;

class Book {
    String title;
    boolean borrowed;   // false = 在架,true = 已借出

    Book(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return borrowed ? title + "(已借出)" : title + "(在架)";
    }
}

class Library {
    private ArrayList<Book> books = new ArrayList<>();   // 封装:书架藏起来

    void addBook(String title) {
        books.add(new Book(title));
        System.out.println("上架:" + title);
    }

    void borrow(String title) {
        for (Book b : books) {
            if (b.title.equals(title) && !b.borrowed) {
                b.borrowed = true;
                System.out.println("借出成功:" + title);
                return;
            }
        }
        System.out.println("借阅失败:" + title + "(不存在或已借出)");
    }

    void giveBack(String title) {
        for (Book b : books) {
            if (b.title.equals(title) && b.borrowed) {
                b.borrowed = false;
                System.out.println("归还成功:" + title);
                return;
            }
        }
        System.out.println("归还失败:" + title + "(没有借出记录)");
    }

    void showAll() {
        System.out.println("---- 当前书架 ----");
        for (Book b : books) {
            System.out.println(b);   // 自动调用重写后的 toString
        }
    }

    public static void main(String[] args) {
        Library library = new Library();
        library.addBook("西游记");
        library.addBook("Java 核心技术");
        library.addBook("三体");
        library.borrow("西游记");
        library.borrow("西游记");    // 已借出,被拒绝
        library.giveBack("西游记");
        library.borrow("三体");
        library.showAll();
    }
}

运行输出

一条命令跑起来,你会看到:

上架:西游记
上架:Java 核心技术
上架:三体
借出成功:西游记
借阅失败:西游记(不存在或已借出)
归还成功:西游记
借出成功:三体
---- 当前书架 ----
西游记(在架)
Java 核心技术(在架)
三体(已借出)

代码走读:三处值得多看一眼

这份不到百行的程序里,藏着三个值得琢磨的细节,看懂它们比多写十行代码更有用。

第一处:borrow 里的"找到就 return"。 循环里一旦借出成功,立刻 return 结束方法——后面的书不用再看了,也绝不会出现"同一本书被借两次"的 bug。如果循环正常走完还没 return,说明书架上压根没有可借的这本书,才落到最后那句失败提示。"成功即退场,失败走到底"是这类查找方法的标准骨架。

第二处:System.out.println(b) 为什么能打印出"西游记(在架)"? b 是个对象,直接打印默认只会得到 Book@1b6d3586 这样的地址串。是因为 Book 重写了 toString,println 发现对象时会自动调用它。这个小机关上一章继承与多态讲过——所有类都继承自 Object,重写 toString 就是在给"打印名片"换模板。

第三处:两个类都没写 public。 文件名叫 Library.java,按规则只有和文件同名的类才能是 public,所以 Book 乖乖不带 public。同文件里的类天生"互知底细",这是单文件小项目的惯用写法;拆成多个文件后,Book 就该有自己的 Book.java 了。

用到了哪些知识

知识点 在哪里用
类与对象 Book、Library 两张图纸,new 造出实例
封装 books 是 private,外面只能通过借/还方法操作书架
ArrayList 图书馆的"书架",随 add 伸缩
方法重写 toString println(b) 自动调用,输出友好的书目列表
this 与条件校验 构造方法赋值、借还时的状态检查,挡住非法操作

❓ 常见问题

Q1:为什么判断书名要用 b.title.equals(title),不能写 b.title == title?

== 比较引用类型时比的是地址:两个字面量"西游记"因为字符串常量池的优化可能恰好同址,但只要字符串是拼接、传参来的,就各是各的地址,== 会漏判。equals 比的是内容,才是你想要的。口诀:字符串比内容,一律 equals。

Q2:books 为什么声明成 private?改成 public 不是更省事?

省事是真的,翻车也是真的:public 之后外部能 library.books.clear() 一句清空书架,绕过所有校验。private 让书架只能通过 borrow / giveBack 这些"带安检的门"进出——这正是封装那一章的原则在实战里的样子。

Q3:借书失败时,怎么区分"书不存在"和"书已被借走"?

现在的代码把两种情况合并成一句提示,简洁但信息少。想区分也容易:循环里先记下"找到了这本书但已借出"(用一个 boolean 标记),循环结束后按标记分别提示。这正是下面练习 3 的进阶思路——提示信息越精确,用的人越少踩坑,写工具方法时永远站在调用方的角度多想一步。

动手改:分层练习

下面的练习按难度分级:⭐ 热身、⭐⭐ 进阶、⭐⭐⭐ 挑战。每题先自己动手,卡住了再看提示,写完再对参考答案——直接抄答案,肌肉记忆不会长。所有改动都在 Library.java 上进行,改完 java Library.java 验证。

建议按顺序刷:前四题是"照着葫芦画瓢",练的是手感;后四题开始要求自己设计方法签名、安排类与类的关系,练的是思路。每题控制在十五分钟以内,超时就看提示,别跟一道题死磕——卡住的地方,往往就是下一步该回炉的知识点。

练习 1 ⭐:只看在架图书

给 Library 加 showAvailable() 方法,只打印还没被借走的书。

提示:照抄 showAll 的结构,循环里多加一个 if (!b.borrowed) 的判断。

参考答案:

void showAvailable() {
    System.out.println("---- 在架图书 ----");
    for (Book b : books) {
        if (!b.borrowed) {
            System.out.println(b);
        }
    }
}

练习 2 ⭐:给书加作者

给 Book 加 String author 成员变量,上架时一起传入,toString 显示成"书名(作者)"。

提示:构造方法加一个参数;addBook 也跟着加参数;三处改动要同步。

参考答案:

class Book {
    String title;
    String author;
    boolean borrowed;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    @Override
    public String toString() {
        String state = borrowed ? "已借出" : "在架";
        return title + "(" + author + "," + state + ")";
    }
}

// Library 里相应改成:
void addBook(String title, String author) {
    books.add(new Book(title, author));
    System.out.println("上架:" + title);
}
// main 里调用改为:library.addBook("西游记", "吴承恩");

练习 3 ⭐⭐:模糊搜索

新增 search(String keyword):打印书名包含关键字的书;一本都没有就提示"查无此书"。

提示:String 的 contains 方法判断包含关系;用一个 boolean 记录"找没找到",循环结束再决定提不提示。

参考答案:

void search(String keyword) {
    boolean found = false;
    for (Book b : books) {
        if (b.title.contains(keyword)) {
            System.out.println(b);
            found = true;
        }
    }
    if (!found) {
        System.out.println("查无此书:" + keyword);
    }
}
// main 里试试:library.search("西游");  library.search("红楼梦");

练习 4 ⭐:统计在架数量

新增 countAvailable() 方法,返回 int(不是打印),在架有几本就返回几。

提示:定义计数器,循环里满足条件就 count++,最后 return count。

参考答案:

int countAvailable() {
    int count = 0;
    for (Book b : books) {
        if (!b.borrowed) {
            count++;
        }
    }
    return count;
}
// main 里:System.out.println("在架 " + library.countAvailable() + " 本");

多想一步:这题的返回值设计成 int 而不是直接打印,是有讲究的——数量交给调用方,是打印还是存起来还是再计算,随它用。方法负责算,调用方负责用,职责分清,方法才能被反复复用。

练习 5 ⭐⭐:下架图书

新增 removeBook(String title):把指定图书从书架删除,成功打印"下架成功",找不到打印"下架失败"。

提示:想在遍历 ArrayList 的同时删除元素,for-each 会报 ConcurrentModificationException(一边走一边拆桥)——改用普通 for 循环按下标遍历,找到就 books.remove(i) 再 return。这也是全篇唯一需要普通 for 的地方,因为只有它能拿到"当前下标 i"。

参考答案:

void removeBook(String title) {
    for (int i = 0; i < books.size(); i++) {
        if (books.get(i).title.equals(title)) {
            books.remove(i);
            System.out.println("下架成功:" + title);
            return;
        }
    }
    System.out.println("下架失败:" + title + "(查无此书)");
}

练习 6 ⭐⭐:借阅排行榜

用 HashMap<String, Integer> 统计每本书被借的次数,新增 showRank() 按书名打印"书名 × N 次"。

提示:Library 加一个 private Map<String, Integer> borrowCount = new HashMap<>();;borrow 成功时次数加一;打印时遍历 Map 的 keySet。

参考答案:

// 类里新增字段(记得 import java.util.HashMap; import java.util.Map;)
private Map<String, Integer> borrowCount = new HashMap<>();

// borrow 方法里,"借出成功"那句之前加:
borrowCount.put(title, borrowCount.getOrDefault(title, 0) + 1);

void showRank() {
    System.out.println("---- 借阅排行 ----");
    for (String title : borrowCount.keySet()) {
        System.out.println(title + " × " + borrowCount.get(title) + " 次");
    }
}

练习 7 ⭐⭐⭐:把封装补到位

现在 Book 的字段是裸的,Library 直接伸手改 b.borrowed。请把 Book 的字段全部改成 private,给 Book 加 borrow() / giveBack() 方法管理自己的状态,Library 只调用这些方法——状态只能由 Book 自己改,才算把封装学到位。

提示:Book 加 boolean isBorrowed() 给 Library 判断用;borrow / giveBack 里可以打印"这本书已借出"之类的拒绝信息,返回 boolean 告诉 Library 操作成没成功。

参考答案:

class Book {
    private String title;
    private boolean borrowed;

    Book(String title) {
        this.title = title;
    }

    boolean isBorrowed() {
        return borrowed;
    }

    String getTitle() {
        return title;
    }

    boolean borrow() {
        if (borrowed) {
            return false;   // 已借出,拒绝
        }
        borrowed = true;
        return true;
    }

    boolean giveBack() {
        if (!borrowed) {
            return false;   // 没借出去过,拒绝
        }
        borrowed = false;
        return true;
    }

    @Override
    public String toString() {
        return borrowed ? title + "(已借出)" : title + "(在架)";
    }
}

// Library 的 borrow 方法改为:
void borrow(String title) {
    for (Book b : books) {
        if (b.getTitle().equals(title)) {
            if (b.borrow()) {
                System.out.println("借出成功:" + title);
            } else {
                System.out.println("借阅失败:" + title + "(已借出)");
            }
            return;
        }
    }
    System.out.println("借阅失败:" + title + "(不存在)");
}

改完跑一遍,输出应和原来一模一样——外部行为不变、内部实现升级,这正是重构的味道。

练习 8 ⭐⭐⭐:借书上限

给读者立规矩:同一时刻最多借 2 本。Library 记一个 borrowedTotal(当前借出总数),达到上限后拒绝再借,还书时减回来。

提示:borrow 成功分支里先判断 borrowedTotal >= 2;giveBack 成功分支里 borrowedTotal--。想想:直接改 b.borrowed = true 的旧写法,这个计数该加在哪?

参考答案(以练习 7 改造后的 Book 为基础):

// Library 里新增字段
private int borrowedTotal = 0;
private static final int MAX_BORROW = 2;

// borrow 方法里,b.borrow() 返回 true 的分支中加:
if (borrowedTotal >= MAX_BORROW) {
    System.out.println("借阅失败:最多同时借 " + MAX_BORROW + " 本");
    return;   // 注意:这里 Book 的状态还没改,不算数
}
b.borrow();
borrowedTotal++;

// giveBack 成功分支里加:
borrowedTotal--;

做完这八题,类与对象、封装、集合、循环、条件判断就真正串成一条线了——不是"学过",而是"用过"。最后留一个开放挑战:把图书管理改成"电影管理",你会发现几乎只是把 Book、title 改名成 Movie、name——结构对了,换皮就是分钟级的事,这就是好设计的复利。

📋 毕业自查清单

对照下面的标准给自己打分,全中才算真的毕业:

  • 能不看书说出"类、对象、引用"三者的关系,并能画出 d3 = d1 时的内存示意
  • 写一个新类时,默认就把成员变量设为 private,而不是事后补救
  • 能解释 b.title.equals(title) 为什么不能换成 ==
  • 遍历 ArrayList 想删元素时,知道 for-each 会翻车、该换普通 for 循环
  • 看到一段"行为不变、内部重写"的代码,能说出这叫重构,并明白封装为什么让它变安全

四条以上达标,放心往下一站走;差几条就回去把对应练习再做一遍——代码这东西,做一遍胜过看三遍。

记忆口诀

类造对象,封装藏数据,继承重写传下去,接口定标准,集合装世界。

面向对象毕业 🎉 下一站 Spring Boot,让你的代码跑在网页上

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。