综合练习:图书管理小程序
这是什么:图书管理小程序 = 一个能录入图书、管理书目的小项目,把面向对象四章的知识全部用上。
为什么用:类与对象、封装、继承与多态、接口与集合四章学完,可真写起程序来还是抓瞎:类怎么拆、字段怎么藏、接口怎么定,全靠一个个例子才串得起来。
如何用:本章先做一遍练习前自测,再跟着需求分析敲出完整代码,最后用代码走读把三处关键设计讲明白。
练习前自测
动手前先花两分钟对答案,四个问题都能脱口而出,再往下走;卡壳的就回对应章节补课:
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,让你的代码跑在网页上