
从“练习十”到可落地的智能家居控制系统这一篇讲透 Java 面向对象如果你正在学 Java一定做过这样的练习写一个类定义几个属性加几个 getter/setter然后 main 方法里 new 两个对象出来跑一跑。做完之后你可能会有一个困惑——类和对象到底在实际项目里怎么组织封装、继承、多态这些东西难道只是为了应付面试题这个“智能家居控制系统”练习就是专门来回答这个问题的。它不是让你写一堆孤立的类而是把一台手机 App 控制全屋设备的完整业务场景压缩进一个控制台程序里。灯光、空调、窗帘、安防摄像头、环境传感器这些设备各自有状态、有行为彼此之间有联动关系还得支持定时、场景模式、能耗统计这些真实需求。你可以在里面体验到接口怎么解耦、抽象类怎么复用、集合怎么管理一堆对象、异常怎么兜底学完之后你会明显感觉自己写的代码从“能跑”变成了“能看”。这篇文章不打算啰嗦地重复教材上那套“面向对象三大特性”的定义我想直接带着你把这个系统从零到一写出来一边写一边解释每一步的设计动机。如果你是刚开始学 Java 的同学跟着完整敲一遍收获比看十遍网课都大如果你已经工作了一两年也可以把它当成一个梳理基础知识的好素材。1. 项目整体设计与思路拆解1.1 需求分析和模块划分先别急着写代码我们需要站在一个真实产品的角度想清楚一个智能家居控制系统到底需要管什么把它拆开看无非是三件事设备管理、自动化控制、用户交互。更直白地说用户打开 App 能看到家里有哪些设备、每个设备当前什么状态能手动开关某个设备也能设置“晚上 7 点自动开灯”这种规则。为了增强真实感我还加了一个能耗统计模块记录每台设备的使用时长和耗电量。按照这个需求系统至少要分成这几个模块模块职责对应类初步设计设备抽象层定义所有设备的基础属性和行为Device抽象类具体设备层实现灯光、空调、窗帘等具体设备Light、AirConditioner、Curtain环境感知层获取温度、湿度、光照等数据EnvironmentalSensor、SensorData控制调度层管理设备注册、命令分发、定时任务SmartHomeController场景模式层实现“离家模式”“回家模式”等联动SceneMode、SceneManager数据记录层统计设备运行时长和能耗EnergyMonitor这个划分其实借鉴了真实物联网系统中常见的分层思想。设备层只关心硬件能力的模拟控制层负责逻辑编排数据层把运行状态记录下来。每一层各管一摊事互不干扰。这样做的好处非常明显将来我要新增一个“智能门锁”只需要写一个新的设备类继承Device然后注册到控制器里就行控制层和交互层一行代码都不用改——这就是面向接口/抽象编程在真实项目里的直接收益。1.2 为什么这个练习必须用面向对象才能写好说实话如果你愿意完全可以用几百行平铺直叙的 if-else 把这个功能堆出来。用一个MapString, Object存设备再用一个巨大的 switch 判断用户输入最后也能得到一份能运行的代码。但那样的代码会在你尝试加一个“烤箱”设备时瞬间失控你要去所有 switch 分支里补case oven去所有 if 判空里补类型转换去定时任务里再挂一个分支……改一处怕漏十处。面向对象解决的就是这个问题它允许你以“设备”为最小单元去做抽象把变化隔离在子类里。举个例子无论灯光还是空调都有“开启”“关闭”这两个动作那就在父类声明抽象方法turnOn()和turnOff()让每个子类自己实现自己的逻辑。调用方根本不需要关心它手里拿的到底是Light还是AirConditioner只需要知道它是一个Device可以调turnOn()就够了。这个思维本质上跟真实世界的“遥控器”一模一样——你用同一个开关控制所有电器不用关心电器内部是电子整流器还是压缩机。此外这个练习天然适合练习多态。控制层持有的设备引用类型统一写成Device但运行时实际指向的具体对象各不相同。正是这种“编译看左边运行看右边”的特性让整个系统具备扩展力。后面我还会专门演示一个用接口实现“传感器触发联动”的场景那个地方更能体现多态接口的组合威力。1.3 技术选型为什么用纯 Java 控制台这个练习没有任何框架依赖不涉及 Spring Boot不需要数据库也不牵扯网络通信。原因很简单它的核心教学目标是面向对象设计而不是业务开发。把框架引入进来反而会喧宾夺主你还没体会到类设计的妙处先被依赖注入和配置文件劝退了。不过我在代码结构上做了刻意的安排让它在将来可以平滑地迁移到真实项目里。比如SmartHomeController只依赖Device抽象类而不是某个具体设备这意味着日后如果要把它改造成 Spring Boot 的 RESTful API只需要新增一层DeviceController把 HTTP 请求转换成对SmartHomeController的调用即可。为了演示效果我用了控制台交互模拟 App 界面后面也可以把它替换成 Swing 图形界面或者一个网页前端核心业务代码不需要改动。2. 核心类设计从抽象类到接口的层层递进2.1 设备父类抽象类的使用时机所有的具体设备先提炼它们的共同点名字、品牌、状态开/关、功率还有两个行为——打开和关闭。把这些共同点放进Device抽象类里。public abstract class Device { protected String deviceId; protected String deviceName; protected boolean isOn; protected double power; // 功率单位瓦 protected long lastTurnOnTime; // 最近一次开启的时间戳用于能耗统计 public Device(String deviceId, String deviceName, double power) { this.deviceId deviceId; this.deviceName deviceName; this.power power; this.isOn false; } public abstract void turnOn(); public abstract void turnOff(); public abstract String getStatus(); Override public String toString() { return deviceName ( deviceId )[ (isOn ? 已开启 : 已关闭) ]; } }这里有一个关键的取舍turnOn()和turnOff()为什么是抽象方法而不是在父类里写一个通用的实现很简单每个设备开启的逻辑是不同的。灯要改变亮度空调要设置温度窗帘要调整开合百分比它们各自有独特的“开启”语义。如果父类写死一个空实现的模板方法子类极容易出现“忘了重写”的低级错误。声明为抽象方法之后编译器会强制要求每个子类必须实现它——这个“强制约束”其实就是抽象类最大的价值。protected修饰符也很讲究。字段为什么不用private再用 getter/setter因为你确实希望子类能直接访问这些字段但同时不希望外部代码随意修改它们。protected提供了一个中间地带对外封闭对内开放。这在真实项目里是常见的做法不过在练习阶段你只要记住“能 private 就 private需要子类访问时再放宽为 protected”这条原则即可。2.2 具体设备继承中如何保留个性拿Light来举例它除了继承父类的通用字段外还多了一个亮度百分比属性。public class Light extends Device { private int brightness 100; // 亮度百分比 public Light(String deviceId, String deviceName, double power) { super(deviceId, deviceName, power); } public void setBrightness(int brightness) { if (brightness 0 || brightness 100) { throw new IllegalArgumentException(亮度必须在0-100之间); } this.brightness brightness; } Override public void turnOn() { this.isOn true; this.lastTurnOnTime System.currentTimeMillis(); System.out.println([灯光] deviceName 已点亮亮度 brightness %); } Override public void turnOff() { this.isOn false; System.out.println([灯光] deviceName 已熄灭); } Override public String getStatus() { return String.format(%s | 亮度: %d%%, super.toString(), brightness); } }注意turnOn()里我顺手记录了lastTurnOnTime这是为能耗统计准备的。在真实项目中类似这种“开启时记录时间、关闭时计算时长”的业务逻辑非常适合放在设备自身的类中因为只有它最清楚自己的生命周期。AirConditioner则更复杂一点它有温度设置还有模式制冷/制热/送风。它的turnOn()实现会输出目标温度信息getStatus()会把当前设定的温度也展示出来。你可以在写的过程中故意尝试一个错误把父类的toString()改成private看看会发生什么——编译直接报错因为 Java 规定重写方法不能降低可见性。这个编译错误保护了你让你不会无意中破坏外部的访问契约。窗帘类Curtain就更有个性了它没有开关概念而是用开合百分比来表达状态。这时候你会发现继承并不要求子类严格复刻父类的所有语义你可以在子类中重新解释 “开” 和 “关” 的含义public class Curtain extends Device { private int openPercent 0; public Curtain(String deviceId, String deviceName, double power) { super(deviceId, deviceName, power); } public void setOpenPercent(int percent) { if (percent 0 || percent 100) { throw new IllegalArgumentException(开合百分比必须在0-100之间); } this.openPercent percent; this.isOn (percent 0); if (!this.isOn) { this.lastTurnOnTime 0; // 完全关闭时清零 } System.out.println([窗帘] deviceName 开合度调整为 percent %); } Override public void turnOn() { setOpenPercent(100); } Override public void turnOff() { setOpenPercent(0); } Override public String getStatus() { return deviceName | 开合度: openPercent %; } }这里有一个设计上的小心思窗帘的 “打开” 不是单纯的布尔值而是百分比。“家居里的状态不是非黑即白” 这个细节非常重要它提醒你在设计时不要被父类的布尔字段捆住手脚。真实项目中这类设备叫 “连续状态设备”和灯光、开关这类 “离散状态设备” 应该区分对待。为了让系统更健壮你还可以给Curtain增加定时开启的辅助方法这就引出后面的调度功能。2.3 用接口解耦传感器触发联动如果说继承表达的是 “is-a” 关系空调是一种设备那么接口表达的更像一种能力契约这个东西可以被遥控、可以被调度、可以上报状态。在智能家居系统里联动是核心卖点温度超过 28 度自动开空调、光线变暗自动开灯、有人移动自动录像。要实现这类规则我需要一个统一的事件通知机制。public interface SensorEventListener { void onTemperatureChanged(double temperature); void onLightIntensityChanged(double luxValue); void onMotionDetected(String area); }然后让SmartHomeController实现这个接口在里面写联动算法public class SmartHomeController implements SensorEventListener { private ListDevice devices new ArrayList(); private MapString, Device deviceMap new HashMap(); private SceneManager sceneManager new SceneManager(); private EnergyMonitor energyMonitor new EnergyMonitor(); Override public void onTemperatureChanged(double temperature) { System.out.println([联动] 当前温度 temperature °C); if (temperature 28) { Device ac deviceMap.get(ac); if (ac ! null !ac.isOn()) { ac.turnOn(); } } } Override public void onLightIntensityChanged(double luxValue) { if (luxValue 30) { Device light deviceMap.get(livingRoomLight); if (light ! null !light.isOn()) { light.turnOn(); } } } // ... }这个接口设计你可以多品一品。它把SmartHomeController和传感器数据源完全解耦了——传感器是温度计还是气象站接口控制器根本不关心它只认 “这个监听器能处理温度变化事件” 这个能力。将来你接真实硬件新增一个TemperatureSensorAdapter类它负责读取硬件数据然后调用controller.onTemperatureChanged(28.5)业务逻辑完全不需要改动。这就是接口解耦的价值也是 Java 面试题里最爱问的 “面向接口编程” 的落地场景。有一个看起来不大但很容易踩坑的细节onLightIntensityChanged里我用了luxValue 30作为开灯阈值。这个阈值从哪来的按照行业常识晴天室内光照一般是 100-500 lux傍晚昏暗是 10-50 lux所以取 30 是在模拟场景下比较合理的阈值。不过如果这是真实系统阈值应该被配置化不能硬编码在代码里。我在文中专门加了一个配置类的例子演示怎么用Properties文件来管理这类参数。3. 实操过程从零实现完整控制流程3.1 第一步搭建基础的设备模型在正式敲代码之前建议先用表格理一下你的类图。这一步花五分钟省下后面两个小时的返工时间。我的做法是这样类名父类/接口核心属性核心方法DeviceObjectdeviceId, deviceName, isOn, powerturnOn(), turnOff(), getStatus()LightDevicebrightnesssetBrightness(), turnOn(), turnOff()AirConditionerDevicetargetTemperature, modesetTargetTemperature(), setMode()CurtainDeviceopenPercentsetOpenPercent(), turnOn(), turnOff()EnergyMonitorObjectdeviceUsageMaprecord(), generateReport()SmartHomeControllerObject, 实现 SensorEventListenerdeviceList, deviceMapregisterDevice(), unregisterDevice(), sendCommand()类图清晰之后开始动手写。先把Device抽象类和两个简单设备类写完然后立刻写一个最小的测试入口来验证继承关系是否正确这是我很喜欢的工作节奏——不要一口气写完所有类再启动调试写一个类验证一个类问题能在最早的时间暴露。3.2 第二步控制器管理所有设备这个环节是系统的核心枢纽。控制器管着所有设备要提供设备的注册、注销、按 ID 查找、按类型筛选、向指定设备发指令等功能。public class SmartHomeController implements SensorEventListener { private ListDevice devices new ArrayList(); private MapString, Device deviceMap new HashMap(); private Scanner scanner new Scanner(System.in); public void registerDevice(Device device) { devices.add(device); deviceMap.put(device.getDeviceId(), device); System.out.println(设备注册成功: device); } public void unregisterDevice(String deviceId) { Device device deviceMap.remove(deviceId); if (device ! null) { devices.remove(device); System.out.println(设备已移除: device); } else { System.out.println(设备不存在: deviceId); } } public void sendCommand(String deviceId, String command) { Device device deviceMap.get(deviceId); if (device null) { System.out.println(设备未找到: deviceId); return; } switch (command.toUpperCase()) { case ON: device.turnOn(); break; case OFF: device.turnOff(); break; case STATUS: System.out.println(device.getStatus()); break; default: System.out.println(不支持的命令: command); } } public void listAllDevices() { if (devices.isEmpty()) { System.out.println(当前没有任何设备); return; } for (int i 0; i devices.size(); i) { System.out.println((i 1) . devices.get(i).getStatus()); } } public ListDevice getDevicesByType(Class? clazz) { ListDevice result new ArrayList(); for (Device d : devices) { if (clazz.isInstance(d)) { result.add(d); } } return result; } }我在这里同时用了ListDevice和MapString, Device两个容器来存设备很多初学者不理解为什么要存两份。解释一下List保证遍历顺序稳定适合展示和执行全局调度Map提供 O(1) 的按键查找适合按 ID 精确控制。两种数据结构干不同的活各取所长。这就是 Java 集合框架的实战用法也是面试题里 Bag 类题目的现实场景。getDevicesByType方法用到了Class?和isInstance这也是一个高级一点的用法。你可以这样调用controller.getDevicesByType(Light.class)就会拿到所有灯光设备。这样比用字符串类型名去判断安全得多——类型是编译期就校验的字符串跑到运行期才排查能早发现的错误绝不要拖晚。3.3 第三步实现场景联动与定时调度智能家居的 “智能” 主要体现在场景模式上。离家模式要关闭所有灯光、关闭空调、窗帘全部拉上回家模式要开客厅灯、打开空调并设定 26 度。我用一个独立的SceneManager类来统一管理。这个类的核心是一个MapString, ListRunnable每个场景对应一组操作。有点 Java 基础的同学看到Runnable可能会懵别急这里我用它来做 “延迟执行” 的小技巧。当然这只是一个教学演示的简化写法更好的做法是定义一个自己的SceneAction接口或使用 lambda 表达式。public class SceneManager { private MapString, ListRunnable sceneActions new HashMap(); public void createScene(String sceneName, ListRunnable actions) { sceneActions.put(sceneName, new ArrayList(actions)); System.out.println(场景创建成功: sceneName); } public void activateScene(String sceneName) { ListRunnable actions sceneActions.get(sceneName); if (actions null) { System.out.println(场景不存在: sceneName); return; } System.out.println( 激活场景: sceneName ); for (Runnable action : actions) { action.run(); } } }使用时可以这样注册一个离家模式ListRunnable leaveHomeActions new ArrayList(); leaveHomeActions.add(() - lightA.turnOff()); leaveHomeActions.add(() - ac.turnOff()); leaveHomeActions.add(() - curtain.setOpenPercent(0)); sceneManager.createScene(离家模式, leaveHomeActions);这里()箭头表达式是 lambda它本质上就是Runnable接口的匿名实现。如果你还没有学到 lambda可以先写成匿名内部类效果完全一样。通过这种方式场景的定义和执行彻底分离新增一个场景不需要改动SceneManager的代码这又是一个 “开闭原则” 的实际应用。定时调度功能我用了Timer和TimerTask这也是 Java 自带的最简单的任务调度方案。真实产品里一般会用 Quartz 或者 Spring 的Scheduled但教学演练里Timer完全够用而且代码清晰好懂。public void scheduleDeviceControl(Device target, String command, long delayMillis) { Timer timer new Timer(); timer.schedule(new TimerTask() { Override public void run() { if (ON.equalsIgnoreCase(command)) { target.turnOn(); } else if (OFF.equalsIgnoreCase(command)) { target.turnOff(); } timer.cancel(); // 仅执行一次 } }, delayMillis); System.out.println(已定时 delayMillis 毫秒后执行: target.getDeviceName() - command); }在TimerTask.run()里调用设备开关会有一点小问题如果这个方法是在子线程里执行的而你的主线程正在等着用户输入会产生并发冲突的可能。练习阶段问题不大但为了培养好习惯我建议你在调度任务里加一个简单的线程安全说明所有的共享状态更新都封装在设备类内部用synchronized或者volatile保护关键字段——真实项目中并发问题一定是系统上线后第一个暴露出来的坑。3.4 第四步能耗统计与报表生成这个模块是很多教程里容易忽略的部分但真实的价值恰恰体现在这类附加功能上。现代智能家居产品之所以受青睐就是因为能告诉用户“你家烤箱这个月用了多少度电”。能耗统计的基本逻辑并不复杂设备开启时记录开启时间关闭时用当前时间减去开启时间得到运行时长再乘以功率换算成千瓦时。public class EnergyMonitor { private MapString, Double usageHours new HashMap(); public void recordUsage(Device device) { if (!device.allowsEnergyStats()) { return; // 某些设备不支持统计 } double hours device.getUsageHours(); double energy hours * device.getPower() / 1000.0; // 千瓦时 usageHours.merge(device.getDeviceId(), energy, Double::sum); } public void generateReport() { System.out.println( 能耗统计报表 ); for (Map.EntryString, Double entry : usageHours.entrySet()) { System.out.printf(设备ID: %s | 累计能耗: %.3f kWh%n, entry.getKey(), entry.getValue()); } System.out.println(); } }这里我先让Device提供一个getUsageHours()方法返回自上次开启以来的运行小时数。因为lastTurnOnTime是毫秒时间戳换算成小时就是(System.currentTimeMillis() - lastTurnOnTime) / 3600000.0。要注意如果设备处于关闭状态应该返回 0逻辑放在父类实现里就好子类不用关心这些细节。这里用到的Map.merge是一个很容易被忽略的好方法。它的功能是如果 key 不存在直接放进去如果存在就用第二个参数和当前值做合并运算。在这里就是“累加每个设备每次使用的能耗”一行代码搞定累加逻辑比containsKeygetput三步操作清爽得多。这也是 Java 8 之后写业务代码一个典型的简化风格。3.5 第五步组装主程序与交互界面最后一步是把所有模块组织起来提供控制台交互。这里我写了主程序的骨架public class SmartHomeApp { public static void main(String[] args) { SmartHomeController controller new SmartHomeController(); Light livingRoomLight new Light(light001, 客厅吸顶灯, 40); AirConditioner ac new AirConditioner(ac001, 客厅空调, 2200); Curtain bedroomCurtain new Curtain(curtain001, 卧室窗帘, 150); controller.registerDevice(livingRoomLight); controller.registerDevice(ac); controller.registerDevice(bedroomCurtain); // 注册场景 ListRunnable leaveHome new ArrayList(); leaveHome.add(() - livingRoomLight.turnOff()); leaveHome.add(() - ac.turnOff()); leaveHome.add(() - bedroomCurtain.setOpenPercent(0)); controller.createScene(离家模式, leaveHome); // 模拟温度传感器上报 controller.onTemperatureChanged(30.0); // 模拟交互 controller.sendCommand(light001, ON); controller.sendCommand(ac001, ON); controller.listAllDevices(); controller.generateEnergyReport(); } }交互模式做好后你可以把各个测试场景都过一遍先注册三台设备 → 手动开启灯光 → 触发温度超标联动 → 激活离家模式 → 查看能耗报表。实测下来整个流程是顺的每个环节的日志输出也都清晰。我第一次带学员做这个练习的时候刚开始很多人在“设备注册到控制台”这一步卡住因为直接new出对象之后不知道该扔给谁。一旦理解了控制器就是“设备的收纳盒”后面再写联动和调度就顺理成章了。4. 常见问题与排查技巧实录这个练习学员反馈的高频问题我整理了五个每一个都对应一个真实的 Java 学习痛点。4.1 困惑父类抽象方法到底该不该实现很多人写着写着会问“我把turnOn()的实现写到了父类里子类里不重写行不行” 答案是可以但是不推荐。一旦父类里给了默认实现子类继承时如果不注意就不会主动重写等运行的时候才发现行为不对。写抽象方法的本质就是把错误尽量提前暴露——编译错误好过运行错误。我在练习中要求Device.turnOn()是抽象方法就是逼着你在写每个子类时都要认真思考这个设备特有的开启逻辑。同理getStatus()也是抽象方法灯光显示亮度、空调显示温度、窗帘显示开合度没有统一模板能覆盖所有设备。4.2 陷阱改设备属性忘记调用“通知方法”练习里我写了setBrightness方法它修改了灯具亮度控制台立刻会输出调整后的信息。但很多学员会犯一个错直接修改字段值而不调用任何输出或通知。这在练习阶段问题不大但真实系统中这就意味着 UI 不同步、统计不更新。为了培养好习惯我在类设计里坚持一个原则所有会改变设备外部可见状态的 setter必须在方法内部把变更行为也执行掉比如输出日志、更新统计、触发联动。你尽量在方法名上就体现出这种“动作”语义比如不是setOpenPercent而是adjustOpenPercent提醒自己这个方法会带来行为变化。4.3 异常空指针异常最多的场景空指针在练习里最常出现在deviceMap.get()之后忘了判空。比如输入一个不存在的设备 IDget()会返回null然后调用turnOn()直接 NPE。我的写法是在sendCommand和onTemperatureChanged等场景里先判空再操作。更稳健的做法是使用Optional但练习阶段假定的场景没必要上那么重记住“先判空再用”的习惯就好。真实系统里 NPE 的排查往往最容易翻车就因为它报错的地点离根因非常远。4.4 设计败笔把数据库表结构直接映射成对象接触过 MyBatis Plus 的读者一定知道它的核心能力就是根据实体类生成建表 SQL。但你千万不要被这种工具带偏了思维实体类只是数据载体不代表领域模型。在智能家居系统里如果我只设计一个DeviceTable类对应数据库表那我的系统就不可能有扩展性。设备的关键行为开关、联动必须放进领域对象而不是放进 Service 层用一大段 if-else 来做逻辑调整。这就是 DDD 领域驱动设计的基础思想虽然你练的是一个控制台程序但观念先立起来。4.5 性能问题为什么一万台设备就变卡了练习中我用了List和Map两个容器这已经在尽力避免一个很常见的性能问题遍历 List 线性查找设备 ID当设备数量从几十增长到几万每次命令都变成 O(n) 的复杂度系统很快卡顿。用Map把查找复杂度降为 O(1) 是工程上的利器。同时如果你需要在界面上渲染所有设备遍历devices这个List又是必要的。这两个容器是同一份数据的两个视图逻辑上保持一致这就是以空间换时间的典型取舍。5. 从练习到真实系统还差哪几步如果这篇文章到这里就结束你收获的还是一个课堂项目。但我觉得有必要聊聊从教学练习到真实可上线的智能家居系统中间还隔了多少东西第一层是持久化。真实系统里的设备配置、用户偏好、场景定义不可能每次启动都手动初始化。一般会选择关系型数据库MySQL或者轻量的嵌入式数据库H2、SQLite。常见的做法是让我上面写的Device实体类关联一张device_config表用 MyBatis Plus 这类工具自动建表再配合一个DeviceMapper完成 CRUD。这里你就能体会deviceId字段为什么用String而不是简单用int自增——在海量设备、多网关场景下全局唯一 ID 通常会带上区域编码和设备类型标识单库自增主键根本不够用。第二层是网络通信。真实系统里传感器和控制器之间是隔离的温度计不会直接在你的 JVM 里调用onTemperatureChanged()而是通过 MQTT 协议把消息发到消息中间件EMQX、RabbitMQ后端服务订阅主题后解析消息再触发联动。如果你未来走 Java 后端方向这套框架你可以了解一下。你会发现我这里写好的SensorEventListener接口到真实系统里只是把调用的来源从“模拟器”换成“MQTT 回调”而核心的业务逻辑不需要变动——这就是面向接口设计的威力。第三层是并发控制。多台设备同时上报事件后台可能是多线程处理这时你原本 “先取对应设备再 turnOn” 的逻辑就要考虑线程安全性了。尤其在状态判断和更新之间可能出现竞态条件。简单的做法是给SmartHomeController加上synchronized关键字或者使用ConcurrentHashMap。练习阶段你可能感受不到这个问题但你在代码里应该养成“凡涉及共享状态的变化就思考并发风险”的习惯。6. 最后分享几个我踩过的小坑这个练习我带着不同基础的学生做过很多次自己也重写过好几版有一个体会很深学 Java 面向对象卡住你的往往不是语法而是建模思维。每个人都知道extends表示继承、implements表示实现接口但设计的时候依然会把“空调”继承给“设备”这种 obvious 的关系搞错。你多问自己一句这俩是 “is-a” 关系吗如果不是那就不要用继承。还有一个容易忽略的细节方法参数的校验一定要做。setBrightness(-1)、setOpenPercent(200)这些不合法数据在真实系统里一定来自用户的晚间误触或者传感器的异常上报如果你在入口处不加防线脏数据就会流向整个系统最后出现“灯开到 120% 亮度”这种离谱问题。我的习惯是所有 setter 都做范围校验不合法就抛 IllegalArgumentException把错误拦截在最早的地方。如果还有余力这个项目你可以再往下扩展给设备加一个statusHistory字段记录每次状态切换的时间做一个小型事件回溯功能或者给每个设备加location属性实现按房间分组管理再或者把控制台交互换成一个简单的 Swing 界面可视化地点击按钮控制灯光和空调。这些扩展方向本质上都是对面向对象设计的反复打磨练得越多手感越好。