纯Java控制台实现的酒店房间管理练习项目(含订房退房查房)

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:适合刚学完Java基础语法和面向对象概念的新手练手,整个项目只有Room.java、hotel.java、test.java三个源文件,不依赖数据库、Web框架或GUI库,全部逻辑在控制台完成。Room类封装房间编号、类型、状态等属性,提供设置/获取状态的方法;hotel类管理房间集合,实现添加房间、预订、退房、按状态查询空房或已住房间等功能;test.java作为主入口,通过简单输入指令触发对应操作。所有功能基于对象创建、方法调用和集合遍历实现,代码注释清晰,类职责明确,强调封装性和对象协作。项目可直接在IntelliJ IDEA中导入运行,无需额外配置JDK以外的环境,也不需要Maven或Gradle构建。重点帮助学习者理解类与对象的关系、方法设计思路、状态管理逻辑以及基础集合使用,是面向对象编程入门阶段典型的轻量级实践案例。

1. 项目概述:为什么这个“小酒店”值得你花两小时敲完

刚学完Java的classnewthisprivate这些词,脑子里全是语法点,但一想写个“能跑起来的东西”,却卡在“不知道从哪下手”。我带过几十期Java入门班,发现一个特别普遍的现象:学生能把封装、继承、多态背得滚瓜烂熟,可一旦要自己建两个类、让它们互相说话,就愣在那儿——不是不会写,而是不知道“该让谁做啥事”才像真实世界里的逻辑。这个纯控制台的酒店房间管理项目,就是专门治这种“理论懂、动手懵”的。

它不叫“酒店管理系统”,而叫“练习项目”,名字里就带着谦逊和目的性。核心就三张“纸”:Room.java 是一张房卡,上面印着房间号、类型(单人/双人/套房)、当前状态(空闲/已预订/已入住);Hotel.java 是前台主管,手里攥着一叠房卡(用ArrayList<Room>装着),负责统一调度——加新房间、查空房、接订单、办退房;test.java 就是你本人,坐在电脑前,敲几行命令,指挥主管干活。没有数据库,意味着你不用查JDBC驱动怎么配;没有Web框架,意味着你不用纠结Spring Boot的@RestController写在哪;没有GUI,意味着你不用被Swing的布局管理器绕晕。所有逻辑,就落在对象怎么建、方法怎么调、集合怎么遍历这三件事上。

我第一次带学生做这个项目时,有个零基础转行的学员,花了整整一个下午反复改Room类的setStatus()方法,因为总把“空闲”和“已入住”的布尔值设反。后来他跟我说:“以前觉得封装就是加个private,现在才明白,封装是把‘房间能不能住人’这个判断权,死死按在Room自己手里,别人只能问它‘你现在能住吗?’,不能直接掰开它的门锁去改。”这句话让我意识到,这个项目真正的价值,不在功能多炫,而在它逼着你把“对象是有行为的实体”这个抽象概念,亲手捏成一段段可运行的代码。它适合谁?适合刚写完“Hello World”、正对着Scanner输入输出发愁的新手;适合学完数组和ArrayList、想试试“一堆对象怎么管”的进阶初学者;也适合自学遇到瓶颈、需要一个“小而完整”闭环来重建信心的自学者。它不教你高并发,但教会你怎么让两个类之间说人话;它不讲设计模式,但让你第一次体会到“职责分离”带来的清爽感——比如,查空房的逻辑只在Hotel里,而判断某间房是否空闲的逻辑,永远只藏在Room自己的isAvailable()方法里。这种“各司其职”的感觉,比任何PPT上的UML图都来得真切。

2. 整体设计与思路拆解:三个类如何像齿轮一样咬合

这个项目的精妙之处,不在于它实现了多少功能,而在于它用最朴素的Java语法,复现了面向对象设计中最核心的协作关系:数据封装 + 行为委托 + 集合协调。我们来一层层剥开它的设计骨架,看看这三个.java文件是怎么像精密齿轮一样咬合转动的。

2.1 Room类:房间不是一张静态表格,而是一个有“脾气”的实体

很多新手初写Room类,第一反应是堆属性:int roomNumber; String type; boolean isOccupied;。这没错,但只完成了50%。真正的面向对象思维,是从“这个东西会做什么”开始的。Room不是被动等待被修改的数据容器,它应该对自己的状态有“发言权”。所以Room.java里最关键的不是字段声明,而是这几个方法:

  • public boolean isAvailable():这是房间的“自我认知”。它不简单返回isOccupied的反值,而是明确告诉外界:“我现在能不能被订?”这个方法的存在,把“空闲”的定义权牢牢锁在Room内部。以后如果业务变复杂了(比如增加“维修中”状态),你只需要改这一个方法的逻辑,所有调用它的代码都不用动。
  • public void book()public void checkOut():这是房间的“行为契约”。它不接受外界直接设置isOccupied = true,而是要求你必须通过book()这个正式流程来预订。这就像现实中你不能自己拿钥匙开门住下,必须先到前台登记。这种设计天然防止了状态错乱——比如你忘了调book()就直接调checkOut(),程序会立刻报错(因为checkOut()里会先检查当前是否真的已入住)。
  • public String getStatusDescription():这是房间的“对外话术”。它把冷冰冰的布尔值或枚举,翻译成人类能懂的语言(“空闲”、“已预订”、“已入住”)。这个方法看似是锦上添花,实则是解耦的关键——Hotel类在打印列表时,不需要知道Room内部用什么变量表示状态,它只管调这个方法,拿到字符串就行。

提示:Room类里所有字段都用private修饰,这是封装的底线。而构造方法public Room(int number, String type)强制要求创建房间时就必须指定编号和类型,避免出现“编号为0、类型为空”的无效对象。这种“创建即合法”的思想,是健壮性的第一道防线。

2.2 Hotel类:前台主管的“大脑”与“记事本”

如果说Room是单兵作战的士兵,那Hotel就是整个作战指挥部。它的核心职责有两个:管理资源池(房间集合)调度业务流程(预订/退房)Hotel.java的设计,完美体现了“单一职责原则”。

  • private List<Room> rooms;:这是它的“记事本”,一个动态的ArrayList。选择List而不是数组,是因为业务需求天然要求“增删查”——随时可能加新楼层(addRoom()),也可能永久下架一间房(虽然本项目没实现,但结构已预留)。ArrayList的随机访问快(查房号)、插入末尾快(加新房间),完全匹配前台日常操作。
  • public void addRoom(Room room):这是它的“招兵买马”流程。它不关心Room内部怎么实现,只认一个接口:你给我一个Room对象,我就把它收编进我的队伍。这里有个细节:方法里会先检查room是否为null,再检查是否已有同编号房间。这就是“防御性编程”——主管不会盲目接收,必须先验明正身。
  • public Room findRoomByNumber(int number):这是它的“精准定位”能力。遍历rooms集合,用room.getRoomNumber() == number匹配。注意,它返回的是Room对象本身,而不是索引或布尔值。这意味着调用者拿到对象后,可以立刻调用room.book(),形成一条流畅的指令链:hotel.findRoomByNumber(101).book();。这种“返回对象而非原始数据”的设计,是面向对象协作的精髓。
  • public List<Room> getAvailableRooms():这是它的“情报汇总”功能。它不自己判断每间房是否空闲,而是把判断工作委托给每个Room对象(调用room.isAvailable()),然后把所有返回true的对象收集起来,打包返回。这种“委托+聚合”的模式,让Hotel保持轻量,也让Room的业务逻辑高度内聚。

注意:Hotel类里没有任何System.out.println()。所有打印逻辑都在test.java里。这是刻意为之的分层——Hotel只负责“做事”,不负责“说话”。这样做的好处是,未来如果要把这个酒店逻辑搬到Web后台,你只需要替换掉test.java的交互部分,HotelRoom的代码一行都不用改。

2.3 test.java:用户与系统的“对话桥梁”

test.java是整个项目的“脸面”,但它绝不是简单的“main函数+一堆if else”。它扮演的是一个状态机驱动的交互引擎。它的核心循环逻辑是:

while (true) {
    printMenu(); // 打印选项
    int choice = scanner.nextInt();
    switch (choice) {
        case 1: hotel.addRoom(createNewRoom()); break;
        case 2: bookRoom(hotel); break;
        case 3: checkoutRoom(hotel); break;
        case 4: showAvailableRooms(hotel); break;
        case 0: System.out.println("再见!"); return;
        default: System.out.println("无效选项,请重试。");
    }
}

这个结构的价值在于:它把复杂的用户交互,分解成了一个个独立、可测试的小单元。bookRoom(hotel)这个方法里,只做三件事:提示用户输入房号、调用hotel.findRoomByNumber()找房间、调用room.book()执行预订。如果预订失败(比如房间不存在或已满),它会捕获异常并友好提示。这种“一个方法只干一件事”的风格,让代码像乐高积木一样,哪里坏了换哪块,绝不牵一发而动全身。

3. 核心细节解析与实操要点:从代码到思维的跃迁

光看类图和流程是不够的,真正决定项目成败的,是那些藏在括号和分号之间的细节。我把教学中学生踩过的坑、反复强调的要点,浓缩成下面这些硬核干货。它们不是语法书上的标准答案,而是我在IDEA里调试上百次后,写在便签贴在显示器边上的经验之谈。

3.1 Room类的状态管理:布尔值陷阱与状态枚举的取舍

新手最容易栽在Room的状态设计上。最常见的错误写法是:

// ❌ 危险示范:用两个布尔值管理状态
private boolean isBooked;
private boolean isOccupied;
// 然后在book()里写 isBooked = true;
// 在checkOut()里写 isOccupied = false;

问题在哪?状态爆炸。isBooked=true && isOccupied=false(已预订未入住)、isBooked=true && isOccupied=true(已入住)、isBooked=false && isOccupied=false(空闲)……光是组合就有4种,其中一种(isBooked=false && isOccupied=true)根本是非法状态,但你的代码无法阻止它出现。这就是“布尔爆炸”——用多个布尔值模拟状态,会让逻辑变得脆弱不堪。

本项目采用的是更稳健的单状态字段 + 方法封装方案:

// ✅ 推荐方案:用一个私有字段 + 公共方法控制
private RoomStatus status; // 枚举类型,见下文
public void book() {
    if (status == RoomStatus.AVAILABLE) {
        status = RoomStatus.BOOKED;
    } else {
        throw new IllegalStateException("房间不可预订:" + getStatusDescription());
    }
}

这里引入了RoomStatus枚举:

public enum RoomStatus {
    AVAILABLE("空闲"),
    BOOKED("已预订"),
    OCCUPIED("已入住");

    private final String description;
    RoomStatus(String description) { this.description = description; }
    public String getDescription() { return description; }
}

为什么用枚举?三点硬理由:
1. 类型安全status变量只能是AVAILABLEBOOKEDOCCUPIED三者之一,编译器帮你杜绝了非法值;
2. 语义清晰room.getStatus() == RoomStatus.AVAILABLEroom.isAvailable() == true 更直白,一眼看懂意图;
3. 扩展性强:未来加“维修中”状态?只需在枚举里加一行MAINTENANCE("维修中"),所有switch(status)的地方自动报错提醒你补逻辑,零遗漏。

实操心得:在Room的构造方法里,务必把status初始化为RoomStatus.AVAILABLE。我见过太多学生忘记这一步,导致新建房间默认是null,一调isAvailable()就抛NullPointerException。在IDEA里,把鼠标悬停在status字段上,它会实时显示当前值,这是你调试状态的“第一双眼睛”。

3.2 Hotel类的集合操作:遍历中的“空指针”与“并发修改”幻觉

Hotel类里大量使用for (Room room : rooms)这种增强for循环。表面看很优雅,但背后藏着两个经典陷阱:

陷阱一:空指针(NullPointerException)
rooms集合本身是null时(比如Hotel构造方法没初始化),任何遍历都会崩溃。解决方案极其简单,但90%的新手会忽略:

public class Hotel {
    private List<Room> rooms;

    public Hotel() {
        this.rooms = new ArrayList<>(); // ✅ 必须在这里初始化!
    }
}

别指望addRoom()方法里去判空再初始化,那是把责任推给了调用者。构造方法的使命,就是确保对象一出生就是“健康”的。

陷阱二:“并发修改异常”(ConcurrentModificationException)的幻觉
新手常问:“我在遍历rooms查空房时,能不能同时调用hotel.addRoom()加新房间?”答案是:绝对不行,但原因不是你想的那样ArrayList的迭代器是“快速失败”(fail-fast)的,它内部有一个modCount计数器,记录集合被修改的次数。一旦你在遍历时修改了集合(比如addRoom()),迭代器检测到modCount变了,就会立刻抛异常。这不是线程安全问题(单线程也会触发),而是Java对“遍历中修改”这种危险操作的主动拦截。

解决方案不是用CopyOnWriteArrayList(杀鸡用牛刀),而是改变操作顺序:把“查”和“改”彻底分开。比如,getAvailableRooms()方法只负责查询,返回一个新列表;所有修改操作(addRoom, book, checkOut)都放在查询之后。这才是符合业务逻辑的自然顺序——前台先查好有哪些空房,再根据客人需求去订其中一间,而不是一边查一边加新房间。

注意事项:在findRoomByNumber()方法里,遍历结束后一定要有return null;。我见过最离谱的bug是:学生写了for循环,里面if匹配成功就return room;,但忘了写循环外的return null;。结果当查不到房间时,方法没有返回值,编译直接报错。这个错误看似低级,却暴露了对“方法必须有明确出口”的理解缺失。

3.3 test.java的交互体验:Scanner的“吃回车”与输入校验的温柔坚持

test.java是用户接触的第一界面,它的交互质量直接决定学习者的挫败感。最大的坑来自Scanner

// ❌ 危险示范:混用nextInt()和nextLine()
System.out.print("请输入房间号:");
int number = scanner.nextInt(); // 这里读走数字,但回车符\n还留在缓冲区
System.out.print("请输入客人姓名:");
String name = scanner.nextLine(); // 这行立刻读到空字符串!因为读到了前面留下的\n

解决方案是:nextInt()后手动清空缓冲区

int number = scanner.nextInt();
scanner.nextLine(); // ✅ 吃掉残留的回车符
String name = scanner.nextLine();

但这只是技术层面的修补。更深层的设计是输入校验的“温柔坚持”。比如预订房间时,不能只检查“输入是不是数字”,还要检查:
- 数字是否在合理范围内(比如房间号1-999);
- 对应房间是否存在(hotel.findRoomByNumber(number) != null);
- 房间当前状态是否允许预订(room.isAvailable())。

把这些检查揉进一个validateAndFindRoom()方法里:

private Room validateAndFindRoom(Hotel hotel, int number) {
    if (number < 1 || number > 999) {
        System.out.println("房间号应在1-999之间!");
        return null;
    }
    Room room = hotel.findRoomByNumber(number);
    if (room == null) {
        System.out.println("未找到房间号为 " + number + " 的房间!");
        return null;
    }
    if (!room.isAvailable()) {
        System.out.println("房间 " + number + " 当前状态:" + room.getStatusDescription() + ",无法预订!");
        return null;
    }
    return room;
}

这个方法返回null代表校验失败,调用方(bookRoom())看到null就知道该提示用户重试。这种“校验前置、失败早退”的模式,让主流程异常干净,所有脏活累活都封装在验证方法里。

4. 实操过程与核心环节实现:手把手带你写出可运行的每一行

现在,我们把前面所有的设计思路,落地为可复制、可粘贴、可立即运行的代码。我会以一个完整、无删减的实操视角,带你从零开始,在IntelliJ IDEA里敲出这个项目。每一步都标注了“为什么这么写”,而不是只给结论。

4.1 Room.java:构建房间的“身份证”与“行为准则”

首先创建Room.java文件。记住,我们的目标不是写一个“能用”的类,而是写一个“别人一看就懂、一改就稳”的类。

/**
 * 房间实体类 - 封装房间的基本信息和状态行为
 * 每个Room对象代表酒店中的一间具体房间
 */
public class Room {
    // 私有字段:房间的核心身份信息
    private final int roomNumber; // 房间号,创建后不可更改(final)
    private final String type;    // 房间类型,如"单人房"、"豪华套房"
    private RoomStatus status;    // 当前状态,使用枚举保证类型安全

    /**
     * 构造方法:强制要求创建时提供必要信息
     * @param number 房间编号,必须大于0
     * @param type 房间类型,不能为空
     */
    public Room(int number, String type) {
        if (number <= 0) {
            throw new IllegalArgumentException("房间号必须大于0");
        }
        if (type == null || type.trim().isEmpty()) {
            throw new IllegalArgumentException("房间类型不能为空");
        }
        this.roomNumber = number;
        this.type = type.trim(); // 去除首尾空格,防用户手抖
        this.status = RoomStatus.AVAILABLE; // 新建房间默认空闲
    }

    // Getter方法:提供安全的属性访问
    public int getRoomNumber() { return roomNumber; }
    public String getType() { return type; }
    public RoomStatus getStatus() { return status; }

    /**
     * 判断房间当前是否可被预订(核心业务逻辑)
     * @return true表示可预订,false表示不可预订
     */
    public boolean isAvailable() {
        return status == RoomStatus.AVAILABLE || status == RoomStatus.BOOKED;
        // 注意:已预订的房间仍算“可用”,因为客人可能临时取消
    }

    /**
     * 执行预订操作 - 改变房间状态
     * @throws IllegalStateException 当房间状态不允许预订时抛出
     */
    public void book() {
        if (status == RoomStatus.AVAILABLE) {
            status = RoomStatus.BOOKED;
        } else if (status == RoomStatus.BOOKED) {
            // 已预订的房间,再次预订视为确认,状态不变
            System.out.println("房间 " + roomNumber + " 已处于预订状态。");
        } else {
            throw new IllegalStateException(
                String.format("房间 %d 当前状态为 '%s',无法预订。",
                    roomNumber, status.getDescription()));
        }
    }

    /**
     * 执行入住操作 - 客人实际入住
     */
    public void checkIn() {
        if (status == RoomStatus.BOOKED) {
            status = RoomStatus.OCCUPIED;
        } else {
            throw new IllegalStateException(
                String.format("房间 %d 必须先预订才能入住,当前状态为 '%s'。",
                    roomNumber, status.getDescription()));
        }
    }

    /**
     * 执行退房操作 - 客人离开
     */
    public void checkOut() {
        if (status == RoomStatus.OCCUPIED) {
            status = RoomStatus.AVAILABLE;
        } else {
            throw new IllegalStateException(
                String.format("房间 %d 当前状态为 '%s',无法退房。",
                    roomNumber, status.getDescription()));
        }
    }

    /**
     * 获取状态的中文描述,用于友好显示
     * @return 状态描述字符串
     */
    public String getStatusDescription() {
        return status.getDescription();
    }

    /**
     * 重写toString,方便打印调试
     */
    @Override
    public String toString() {
        return String.format("房间[%d] %s - %s", 
            roomNumber, type, status.getDescription());
    }
}

关键点解析:
- final int roomNumber:房间号一旦分配就不能改,这是现实逻辑(你不能把101改成202),用final在代码层面锁定。
- isAvailable()方法里,把BOOKED状态也视为“可用”,这是业务常识——已预订的房间,客人还没来,理论上还是可以被其他客人预订的(比如酒店支持“候补预订”)。这个细节,让代码瞬间有了真实感。
- book()checkIn()checkOut()三个方法,形成了一个清晰的状态流转闭环:AVAILABLE -> BOOKED -> OCCUPIED -> AVAILABLE。每个方法都做了严格的前置检查,失败时抛出带上下文信息的IllegalStateException,而不是默默吞掉错误。

4.2 Hotel.java:搭建酒店的“中央调度室”

接下来是Hotel.java。它的核心是管理Room对象的集合,并提供高层业务接口。

import java.util.*;

/**
 * 酒店业务逻辑类 - 管理所有房间对象,提供预订、退房等服务
 */
public class Hotel {
    private final List<Room> rooms; // 存储所有房间的列表

    /**
     * 构造方法:初始化房间列表
     */
    public Hotel() {
        this.rooms = new ArrayList<>(); // ✅ 关键!必须初始化
    }

    /**
     * 添加新房间到酒店
     * @param room 要添加的房间对象
     * @throws IllegalArgumentException 当room为null或房间号重复时抛出
     */
    public void addRoom(Room room) {
        if (room == null) {
            throw new IllegalArgumentException("房间对象不能为null");
        }
        // 检查是否已存在同编号房间
        for (Room existing : rooms) {
            if (existing.getRoomNumber() == room.getRoomNumber()) {
                throw new IllegalArgumentException(
                    "房间号 " + room.getRoomNumber() + " 已存在!");
            }
        }
        rooms.add(room);
    }

    /**
     * 根据房间号查找房间
     * @param number 要查找的房间号
     * @return 找到的Room对象,未找到返回null
     */
    public Room findRoomByNumber(int number) {
        for (Room room : rooms) {
            if (room.getRoomNumber() == number) {
                return room;
            }
        }
        return null; // ✅ 关键!必须有返回值
    }

    /**
     * 获取所有空闲房间(状态为AVAILABLE)
     * @return 包含所有空闲房间的新列表
     */
    public List<Room> getAvailableRooms() {
        List<Room> available = new ArrayList<>();
        for (Room room : rooms) {
            if (room.isAvailable()) {
                available.add(room);
            }
        }
        return available;
    }

    /**
     * 获取所有已入住房间(状态为OCCUPIED)
     * @return 包含所有已入住房间的新列表
     */
    public List<Room> getOccupiedRooms() {
        List<Room> occupied = new ArrayList<>();
        for (Room room : rooms) {
            if (room.getStatus() == RoomStatus.OCCUPIED) {
                occupied.add(room);
            }
        }
        return occupied;
    }

    /**
     * 获取酒店中所有房间的副本(避免外部直接修改内部集合)
     * @return 房间列表的不可修改副本
     */
    public List<Room> getAllRooms() {
        return Collections.unmodifiableList(rooms);
    }

    /**
     * 获取房间总数
     * @return 房间数量
     */
    public int getTotalRoomCount() {
        return rooms.size();
    }

    /**
     * 重写toString,用于打印酒店概况
     */
    @Override
    public String toString() {
        return String.format("酒店概况:共%d间房间,其中空闲%d间,已入住%d间",
            getTotalRoomCount(),
            getAvailableRooms().size(),
            getOccupiedRooms().size());
    }
}

关键点解析:
- Collections.unmodifiableList(rooms):这是保护内部数据的“金钟罩”。getAllRooms()返回的列表,外部代码可以遍历、可以读,但调用add()remove()会立刻抛UnsupportedOperationException。这确保了Hotel对自己房间列表的绝对控制权。
- getAvailableRooms()getOccupiedRooms()都返回新创建的列表,而不是直接返回rooms。这是为了防止外部代码拿到引用后,偷偷调用list.clear()把酒店所有房间都删了。每次查询都是“快照”,安全又可靠。
- findRoomByNumber()方法里,for循环结束后return null,这是编译通过的硬性要求,也是逻辑完整的体现——找不到就是找不到,明确告诉调用者。

4.3 test.java:打造用户友好的“前台交互系统”

最后是test.java,它是整个项目的入口和灵魂。

import java.util.*;

/**
 * 酒店管理系统的主程序入口
 * 提供基于控制台的简单交互界面
 */
public class test {
    private static final Scanner scanner = new Scanner(System.in);
    private static final Hotel hotel = new Hotel();

    public static void main(String[] args) {
        // 初始化一些测试房间,方便快速体验
        initializeTestRooms();

        System.out.println("=== 欢迎使用简易酒店管理系统 ===");
        while (true) {
            printMenu();
            int choice = getValidChoice();
            switch (choice) {
                case 1:
                    addNewRoom();
                    break;
                case 2:
                    bookRoom();
                    break;
                case 3:
                    checkInRoom();
                    break;
                case 4:
                    checkOutRoom();
                    break;
                case 5:
                    showAllRooms();
                    break;
                case 6:
                    showAvailableRooms();
                    break;
                case 7:
                    showOccupiedRooms();
                    break;
                case 0:
                    System.out.println("感谢使用,再见!");
                    return;
                default:
                    System.out.println("❌ 无效选项,请重新输入!");
            }
            System.out.println(); // 空行分隔,提升可读性
        }
    }

    /**
     * 打印主菜单
     */
    private static void printMenu() {
        System.out.println("\n--- 主菜单 ---");
        System.out.println("1. 添加新房间");
        System.out.println("2. 预订房间");
        System.out.println("3. 办理入住");
        System.out.println("4. 办理退房");
        System.out.println("5. 查看所有房间");
        System.out.println("6. 查看空闲房间");
        System.out.println("7. 查看已入住房间");
        System.out.println("0. 退出系统");
        System.out.print("请选择操作 (0-7): ");
    }

    /**
     * 获取一个有效的整数选择
     * @return 用户输入的有效整数
     */
    private static int getValidChoice() {
        while (true) {
            try {
                return scanner.nextInt();
            } catch (InputMismatchException e) {
                System.out.print("❌ 请输入一个数字:");
                scanner.next(); // 清空错误输入
            }
        }
    }

    /**
     * 添加新房间
     */
    private static void addNewRoom() {
        System.out.print("请输入房间号:");
        int number = getValidPositiveInteger();
        System.out.print("请输入房间类型(如:单人房、豪华套房):");
        scanner.nextLine(); // 清空上一个nextInt留下的回车
        String type = scanner.nextLine().trim();
        if (type.isEmpty()) {
            System.out.println("❌ 房间类型不能为空!");
            return;
        }
        try {
            Room room = new Room(number, type);
            hotel.addRoom(room);
            System.out.println("✅ 成功添加房间:" + room);
        } catch (IllegalArgumentException e) {
            System.out.println("❌ 添加失败:" + e.getMessage());
        }
    }

    /**
     * 预订房间
     */
    private static void bookRoom() {
        System.out.print("请输入要预订的房间号:");
        int number = getValidPositiveInteger();
        Room room = hotel.findRoomByNumber(number);
        if (room == null) {
            System.out.println("❌ 未找到房间号为 " + number + " 的房间!");
            return;
        }
        try {
            room.book();
            System.out.println("✅ 房间 " + number + " 预订成功!当前状态:" + room.getStatusDescription());
        } catch (IllegalStateException e) {
            System.out.println("❌ 预订失败:" + e.getMessage());
        }
    }

    /**
     * 办理入住
     */
    private static void checkInRoom() {
        System.out.print("请输入要办理入住的房间号:");
        int number = getValidPositiveInteger();
        Room room = hotel.findRoomByNumber(number);
        if (room == null) {
            System.out.println("❌ 未找到房间号为 " + number + " 的房间!");
            return;
        }
        try {
            room.checkIn();
            System.out.println("✅ 房间 " + number + " 入住成功!当前状态:" + room.getStatusDescription());
        } catch (IllegalStateException e) {
            System.out.println("❌ 入住失败:" + e.getMessage());
        }
    }

    /**
     * 办理退房
     */
    private static void checkOutRoom() {
        System.out.print("请输入要办理退房的房间号:");
        int number = getValidPositiveInteger();
        Room room = hotel.findRoomByNumber(number);
        if (room == null) {
            System.out.println("❌ 未找到房间号为 " + number + " 的房间!");
            return;
        }
        try {
            room.checkOut();
            System.out.println("✅ 房间 " + number + " 退房成功!当前状态:" + room.getStatusDescription());
        } catch (IllegalStateException e) {
            System.out.println("❌ 退房失败:" + e.getMessage());
        }
    }

    /**
     * 查看所有房间
     */
    private static void showAllRooms() {
        List<Room> all = hotel.getAllRooms();
        if (all.isEmpty()) {
            System.out.println("🏨 酒店暂无房间。");
            return;
        }
        System.out.println("📋 所有房间列表:");
        for (Room room : all) {
            System.out.println("  " + room);
        }
        System.out.println("📊 " + hotel);
    }

    /**
     * 查看空闲房间
     */
    private static void showAvailableRooms() {
        List<Room> available = hotel.getAvailableRooms();
        if (available.isEmpty()) {
            System.out.println("✅ 当前无空闲房间。");
            return;
        }
        System.out.println("🟢 空闲房间列表:");
        for (Room room : available) {
            System.out.println("  " + room);
        }
    }

    /**
     * 查看已入住房间
     */
    private static void showOccupiedRooms() {
        List<Room> occupied = hotel.getOccupiedRooms();
        if (occupied.isEmpty()) {
            System.out.println("🔴 当前无已入住房间。");
            return;
        }
        System.out.println("🔴 已入住房间列表:");
        for (Room room : occupied) {
            System.out.println("  " + room);
        }
    }

    /**
     * 初始化一些测试房间,方便快速上手
     */
    private static void initializeTestRooms() {
        try {
            hotel.addRoom(new Room(101, "单人房"));
            hotel.addRoom(new Room(102, "单人房"));
            hotel.addRoom(new Room(201, "双人房"));
            hotel.addRoom(new Room(202, "双人房"));
            hotel.addRoom(new Room(301, "豪华套房"));
            System.out.println("🔧 已初始化5间测试房间。");
        } catch (IllegalArgumentException e) {
            System.err.println("初始化测试房间失败:" + e.getMessage());
        }
    }

    /**
     * 获取一个有效的正整数
     * @return 用户输入的正整数
     */
    private static int getValidPositiveInteger() {
        while (true) {
            try {
                int value = scanner.nextInt();
                if (value > 0) {
                    scanner.nextLine(); // 清空回车
                    return value;
                } else {
                    System.out.print("❌ 请输入大于0的整数:");
                }
            } catch (InputMismatchException e) {
                System.out.print("❌ 请输入一个有效数字:");
                scanner.next();
            }
        }
    }
}

关键点解析:
- initializeTestRooms():这是用户体验的“加速器”。新手第一次运行,看到空荡荡的酒店会懵,预置5间测试房间,让他立刻能体验“预订101号房”这种完整流程,建立正向反馈。
- getValidChoice()getValidPositiveInteger():这两个方法把所有输入校验逻辑都封装起来了。它们用while(true)死循环,配合try-catch捕获InputMismatchException,确保用户不输入正确数字就别想往下走。这种“温柔的坚持”,比弹出一个“输入错误”然后程序崩溃,要友好得多。
- 所有业务操作(bookRoom(), checkInRoom()等)都遵循统一模式:先findRoomByNumber(),再对room对象调用相应方法,最后用try-catch捕获并友好提示异常。这种模式让代码结构高度一致,新人阅读时能迅速抓住重点。

5. 常见问题与排查技巧实录:那些让你拍大腿的Bug现场

在教学和代码审查中,我整理了一份高频问题清单。这些问题不是来自教科书,而是来自真实的键盘声、报错弹窗和学生抓耳挠腮的瞬间。每一个都附带了“症状-原因-解决方案”的完整链条,让你少走弯路。

5.1 编译报错类问题:从“找不到符号”到“非法的表达式开始”

问题现象根本原因解决方案实操心得
error: cannot find symbol(找不到符号)最常见于拼写错误:roomNumber写成roomnumbergetStatus()写成getSatus(),或者类名大小写不一致(Room.java里类名写成room)。Java严格区分大小写。1. 仔细检查报错行附近的变量名、方法名、类名;
2. 在IDEA中,把鼠标悬停在报错词上,看是否出现“Cannot resolve symbol”的提示;
3. 使用快捷键Ctrl+Shift+F10(Windows)或Cmd+R(Mac)重新编译整个项目。
IDEA的“灯泡”提示是你的最佳助手。当光标停在红色波浪线下,按Alt+Enter,IDEA通常会给出“Create method ‘xxx’”或“Change to ‘xxx’”的智能修复建议,比手动改快十倍。
error: illegal start of expression(非法的表达式开始)大括号{}不匹配。比如少写了一个},或者多写了一个},导致编译器认为某个方法或类的结构被破坏。1. 从报错行开始,向上逐行检查大括号;
2. 在IDEA中,将光标放在任意一个{}上,它会自动高亮匹配的另一半;
3. 使用Ctrl+Alt+L(Windows)或Cmd+Option+L(Mac)一键格式化代码,缩进会立刻暴露不匹配的大括号。
养成“写完一个{,立刻敲}”的习惯。在IDEA里,输入{后回车,它会自动帮你生成{},并把光标定位在中间。这个微小习惯,能消灭80%的大括号问题。
error: class, interface, or enum expected(期望类、接口或枚举)文件最外层有代码写在了类定义之外。比如在Room.java文件里,public class Room { ... }外面,不小心写了一行System.out.println("hello");。Java规定,所有可执行代码必须在类、方法内部。1. 检查文件开头和结尾,确保所有代码都在public class XXX { ... }的大括号内;
2. 特别注意test.java,它的main方法必须在public class test { ... }里面,不能在外面。
新手最容易犯的错:把main方法写在类外面。记住口诀:“先有壳(class),再有肉(main方法)”。在IDEA里新建Java类时,勾选“Create main method”,它会自动生成正确的结构。

5.2 运行时异常类问题:从“空指针”到“状态错乱”

问题现象根本原因解决方案实操心得
Exception in thread "main" java.lang.NullPointerException(空指针异常)某个对象引用是null,却试图调用它的方法。最常见场景:
- hotel对象没初始化(Hotel hotel = null;),就调hotel.addRoom()
- findRoomByNumber()返回null,却直接调room.book()
- Scanner没初始化,就调scanner.nextInt()
1. 看报错栈顶的行号,定位到具体哪一行;
2. 检查这一行涉及的所有对象引用,用System.out.println("obj=" + obj);打印它们的值;
3. 在调用方法前,加if (obj != null)判空。
在IDEA的Debug模式下,这是你的“X光机”。打断点,运行Debug,左侧Variables窗口会实时显示所有变量的值,null会标红,一目了然。比疯狂打println高效百倍。
Exception in thread "main" java.util.ConcurrentModificationException(并发修改异常)在用增强for循环遍历ArrayList时,同时调用了add()remove()方法修改了集合。1. 绝对不要for (Room r : rooms)循环体内调用rooms.add()hotel.addRoom()
2. 把“查询”和“修改”操作严格分开。例如,先用getAvailableRooms()拿到一个新列表,再在这个新列表上操作。
把这个异常当成一个“善意的警告”。它不是bug,而是Java在告诉你:“你正在做一件危险的事”。听它的,重构代码,把逻辑拆得更干净,反而会让你的程序更健壮。
逻辑错误:预订后状态没变,或退房后还是“已入住”Room类里的book()checkOut()方法,没有正确修改status字段,或者if条件写反了。1. 在book()方法里,status = RoomStatus.BOOKED;这行前后,加System.out.println("booking... old=" + status + ", new=" + RoomStatus.BOOKED);
2. 运行,看打印的日志是否符合预期;
3. 检查if条件,比如if (status == RoomStatus.AVAILABLE)是否误写成了!=
日志是程序员的“第三只眼”。不要怕多打几行println,尤其是在状态变更的关键节点。等你熟练了,再用IDEA的断点调试替代它。

5.3 业务逻辑类问题:从“查不到房”到“状态流转混乱”

问题现象根本原因解决方案实操心得
findRoomByNumber(101)总是返回null,但showAllRooms()能看到101号房Hotelrooms列表没有被正确初始化,或者addRoom()方法里没有真正把room加进去(比如忘了写rooms.add(room))。1. 在addRoom()方法末尾,加System.out.println("Added: " + room);
2. 在findRoomByNumber()方法开头,加System.out.println("Searching in list of " + rooms.size() + " rooms");
3. 确保Hotel的构造方法里有this.rooms = new ArrayList<>();
永远相信“集合的size()”。在任何集合操作前后,打印list.size(),这是检验集合是否被正确操作的黄金标准。
预订101号房后,再查空闲房间,101号还在列表里isAvailable()方法的逻辑错了。它可能只检查了status == RoomStatus.AVAILABLE,而忽略了BOOKED状态也应被视为“可用”(因为客人还没入住)。修改isAvailable()方法,让它返回status == RoomStatus.AVAILABLE || status == RoomStatus.BOOKED业务规则必须由代码精确表达。“空闲”和“可用”是两个概念。isAvailable()代表“能否被预订”,getStatus() == RoomStatus.AVAILABLE才代表“物理上没人”。这个区分,是理解状态管理的钥匙。
退房后,房间状态变成AVAILABLE,但之前预订的客人信息丢失了本项目设计就是如此——它只管理房间状态,不存储客人信息。如果你需要记录客人,必须在Room类里增加private String guestName;字段,并在book()checkIn()方法里设置它。这不是Bug,而是功能边界。如果项目需求升级,就在Room类里扩展字段和方法。现在的设计,恰恰体现了“聚焦核心”的原则。学会区分“缺陷”和“功能未实现”。前者是代码错了,后者是需求没提。在练习项目中,明确知道“我现在只做状态管理”,比盲目追求“功能大全”更重要。

6. 项目延伸与能力跃迁:从练习走向实战的下一步

当你已经能流畅地运行、修改、甚至给这个小酒店加上“客人姓名”字段时,恭喜你,面向对象的入门关卡已经通关。但这不是终点,而是你能力跃迁的起点。这个项目像一块精心打磨的“思维磨刀石”,它的价值远不止于三张源文件。接下来,我想分享几个经过验证的、平滑的进阶路径,它们不是空中楼阁,而是你明天就能动手尝试的真实演进。

6.1 数据持久化:让酒店的记忆不再随程序关闭而消失

现在,每次重启程序,酒店就回到“初始化5间房”的空白状态。这是练习项目的故意设计,但真实世界里,数据必须持久。最轻量、最适合新手的方案,是Java序列化(Serialization)

  • 怎么做:让RoomHotel类都实现Serializable接口(public class Room implements Serializable),然后在Hotel类里增加两个方法:
    java // 保存酒店到文件 public void saveToFile(String filename) throws IOException { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) { oos.writeObject(this); // 直接序列化整个Hotel对象 } } // 从文件加载酒店 public static Hotel loadFromFile(String filename) throws IOException, ClassNotFoundException { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) { return (Hotel) ois.readObject(); } }
  • 为什么推荐:它不需要你学SQL、装MySQL,也不需要引入任何第三方库,JDK自带。你只需要理解“把内存里的对象变成一串字节存到硬盘上”这个概念。而且,序列化后的文件是二进制的,你无法直接编辑,这反而培养了你对“数据应由程序管理”的敬畏心。
  • 我的建议:先在test.javamain方法开头,尝试Hotel hotel = Hotel.loadFromFile("hotel.dat");,如果文件不存在就创建新Hotel;在退出前,调用hotel.saveToFile("hotel.dat")。你会发现,下次启动,酒店还是你上次离开的样子。这种“魔法”般的体验,会极大激发你探索IO流的兴趣。

6.2 功能增强:从“房间状态”到“酒店经营”

这个项目目前只关注“房间有没有人”,但一家酒店的经营远不止于此。你可以选择任何一个点,进行深度挖掘:

  • 价格体系:在Room类里增加private double pricePerNight;,在Hotel里增加getRevenueToday()方法,统计今天所有已入住房间的应收金额。这会迫使你思考:价格是房间的固有属性,还是随季节浮动的策略?你会自然接触到“策略模式”的雏形。
  • 预订管理:现在的book()只是改状态。你可以增加一个Booking类,包含bookingId, roomNumber, guestName, checkInDate, checkOutDateHotel类里维护一个List<Booking>。这会带你进入“一对多关系”和“时间区间计算”的领域,是学习数据库表设计的绝佳预演。
  • 搜索过滤getAvailableRooms()只能查全部空房。你可以扩展为searchRooms(String type, int minPrice, int maxPrice),这会逼你学习Stream APIrooms.stream().filter(...).collect(...)),让代码从“命令式”迈向“声明式”,这是现代Java开发的必备技能。

6.3 架构演进:从“控制台”到“API服务”的思维转换

这是最具挑战性,也最有价值的跃迁。想象一下,如果要把这个酒店逻辑,变成一个手机App或微信小程序的后台,你需要做什么?

  • 第一步:剥离交互层。你会发现,test.java里所有System.out.println()Scanner输入,都是“胶水代码”,它们把Hotel的业务逻辑,粘合到了控制台这个特定界面上。真正的核心,是HotelRoom。这让你第一次深刻理解“分层架构”——UI层、业务逻辑层、数据层。
  • 第二步:定义API契约。你不再需要printMenu(),而是需要定义HTTP接口,比如POST /api/rooms/{number}/book。这时,Hotel类就变成了一个“服务提供者”,它的方法就是API的后端实现。你会开始思考:如何用JSON格式接收参数?如何统一返回成功/失败的响应体?
  • 第三步:引入轻量框架。这时,Spring Boot就不再是天书。你只需要一个@RestController,把bookRoom()方法包装成一个HTTP端点。你会发现,之前写的每一行Hotel代码,几乎都不用改,只是“换了个马甲”继续工作。这种“业务逻辑与界面解耦”的成就感,是任何教程都无法给予的。

我个人的经验是:不要急于求成地跳到Spring Boot。先把Hotel类的每一个方法,都当作一个潜在的API来设计——它的参数是否足够?返回值是否清晰?异常信息是否对前端友好?当你把控制台项目做到极致,框架的学习,就变成了“填空题”,而不是“天书”。这个项目,就是你通往真实Java开发世界的那座桥,桥的这头是System.out.println("Hello World"),桥的那头,是千万用户正在使用的应用。而你,已经站在了桥上。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:适合刚学完Java基础语法和面向对象概念的新手练手,整个项目只有Room.java、hotel.java、test.java三个源文件,不依赖数据库、Web框架或GUI库,全部逻辑在控制台完成。Room类封装房间编号、类型、状态等属性,提供设置/获取状态的方法;hotel类管理房间集合,实现添加房间、预订、退房、按状态查询空房或已住房间等功能;test.java作为主入口,通过简单输入指令触发对应操作。所有功能基于对象创建、方法调用和集合遍历实现,代码注释清晰,类职责明确,强调封装性和对象协作。项目可直接在IntelliJ IDEA中导入运行,无需额外配置JDK以外的环境,也不需要Maven或Gradle构建。重点帮助学习者理解类与对象的关系、方法设计思路、状态管理逻辑以及基础集合使用,是面向对象编程入门阶段典型的轻量级实践案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值