再学面向对象
背景:
老师问,当年学习java看的谁的视频?第一节课讲的什么?为什么学他的视频?我:啊?啊?啊?
经过同学一起回忆,终于唤醒沉睡的记忆:马士兵的课程,讲了面向对象,因为马士兵老师讲课实在风趣幽默,会举各种通俗易懂的例子。我重新又看了视频,形成今天的文章。
导图如下:
一、开车去新疆
如果用程序实现“开车去新疆”,可以有几种设计方法?至少可以想到两种:面向过程,面向对象。
用面向过程实现:车启动→踩油门→张家口→内蒙古→甘肃→新疆→熄火;以我为主语,如果想复用,是复用具体的方法。
用面向对象实现:车.go(),实现细节在车的go()方法里。我如果想去新疆,直接执行go()方法,具体要如何操作车,要经过哪里只有车知道。
看起来,用面向对象实现“开车去新疆”,我需要知道的信息更少,具体细节都隐藏在了车里。如果有其他人也想去新疆,看起来只用调用车.go()就行,而不用知道发动机在哪,油门在哪,。。。
那让人有个go()方法可以吗?也可以。但面向对象中,一般让合适的方法出现在合适的类。车启动、熄火等变化车最清楚,所以封装到车中,外部不需要知道。
二、面向对象
前面我们用开车去新疆的例子简单对比了面向过程和面向对象的差别,那面向对象思想具体是怎样的?我们该如何遵循面向对象的原则解决问题呢?面向对象最基本的概念是类、对象,其他概念都是基于类和对象展开的研究。
1、类和对象的概念
1、对象,是用计算机语言对问题域中事物的描述,对象通过"属性(attribute)"和"方法(method)"来分别对应事物所具有的静态属性和动态属性。
2、类,是用于描述同一类型的对象的抽象,类中定义了这一类对象所因具有的静态和动态属性。
3、类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
举个例子:
用车举例:
类是对一类事物的抽象,比如,车是类,丰田和大众是对象是对车类的实例化。我们指的某一辆车是指的对象,比如:嘿,路上那辆大众跑的真快。而我们指的某一类车是指的类,比如:车按动力补给方式可以分为燃油车和新能源车。只有指具体某一个时,才是指对象。
从动态和静态的角度认识类
还用上面的车的例子
从动态和静态的角度认识类的属性和方法,是这次再学习中印象深刻、感觉很有意思的点。为什么觉得有意思呢?想想之前在类中都写了哪些方法?getter,setter,toString。业务中涉及到处理属性值的方法,基本都是面向过程的写法,用到哪写到哪,一个方法,洋洋洒洒几千行代码,复用性极差。我认为从动态和静态的角度分析类的属性和方法,是面向对象的起点。因为这样才是从宏观去分析业务,分析当前的类,把类里有什么都分析全了。类的内涵和外延更加明确,能更好的被复用,更好的和其他类建立关系。
在Java中,属性对应成员变量的概念。
听马士兵老师讲完这段,我觉得收获了一个很有意义的认识类的方法,觉得很兴奋,之前也看过,但这次印象特别深刻,真是经典永流传。
2、类与类的关系
在2.1中我们从动态和静态的角度认识了类的内部,在2.2中,我们通过UML中的6种关系认识类间的关系。认识了这6种关系,对应到我们的业务中就知道该如何给类合理的建立关联。(合理很重要,理-是科学依据,是前人总结的伟大经验,不能仅仅是研发个人的经验,更不能是一拍脑门)
UML 类间 6 大关系完整对照表(耦合强度从强→弱排序):
| 关系名称 | 中文别名 | 核心语义 | 记忆句式 | 生命周期特点 | 代码特征示例 | UML 图示 |
|---|---|---|---|---|---|---|
| 泛化 Generalization | 继承 | 子类是父类的特殊种类 | is-a(是一种) | 父子同生命周期,子类复用父类 | class Sedan extends Car | 空心三角实线,指向父类 |
| 实现 Realization | 接口实现 | 类遵守接口规范、履行契约 | 遵循 XX 规范 | 接口仅定义,类独立实现 | class Car implements Drive | 空心三角虚线,指向接口 |
| 组合 Composition | 强组合、包含 | 整体由部件构成,部件不能脱离整体 | contains-a(专属包含) | 整体销毁,部件同步销毁;部件无法单独复用 | 类内部构造器new部件对象 | 实心菱形实线,菱形在整体侧 |
| 聚合 Aggregation | 弱聚合、拥有 | 整体持有部件,部件可拆分复用 | has-a(拥有) | 整体、部件生命周期相互独立,部件可更换 / 转移 | 通过 set / 构造传入外部已存在对象 | 空心菱形实线,菱形在整体侧 |
| 关联 Association | 普通关联、引用 | 两个类互相持有对方引用,长期存在联系 | has-reference(持有引用) | 双方生命周期互不绑定,平等关系 | 作为成员变量定义(无菱形) | 普通实线,可标多重度 |
| 依赖 Dependency | 临时依赖、使用 | 一个类方法内临时使用另一个类,用完即无关 | use-a(临时使用) | 仅方法局部临时生效,不存成员变量 | 方法入参 / 局部变量 / 方法内 new 对象 | 虚线箭头,指向被依赖类 |
| 举个栗子: |
- 泛化:小米 SU7
extends汽车(小米 SU7 是一种汽车) - 实现:汽车
implements可行驶接口(汽车实现行驶规范) - 组合:汽车包含发动机(车报废,发动机随之报废,专属不可拆分)
- 聚合:汽车拥有轮胎(轮胎可拆下换到另一台车)
- 关联:司机 ↔ 汽车(司机长期持有车辆引用,平等双向关联)
- 依赖:年检工具类方法传入 Car 对象(仅年检时临时使用车辆)
3、用面向对象分析问题
2.1和2.2都是在讲方法,马士兵老师把使用面向对象分析问题也总结出了三点,也让我非常非常印象深刻:
1、找对象:找出问题中涉及的所有对象;技巧:找名词;
2、分析类/对象:用动态和静态视角确定属性和方法;
3、确定对象之间的关系;这里就用到了2.2中的UML的6种关系。
我们用这三步试一下”开车去新疆“:
1)找对象(找名词)
场景名词:司机 (Driver)、汽车 (Car)、路线 (Route) → 三个类
2)区分静态属性、动态行为
- 静态(名词描述、特征):品牌、颜色、姓名、途经城市等,全部定义为私有成员变量,配套 get/set
- 动态(动词动作):启动车辆、开车、规划路线、展示城市,定义为类的普通方法
3)确定类之间 UML 关系
- 关联:Driver 内部有
Car myCar成员变量,司机长期持有车辆,双向稳定联系; - 依赖:
planRoute(Route route)仅方法参数使用 Route,只规划路线时临时依赖,用完无绑定;
代码:
1. Route 路线类(依赖被使用对象)
import java.util.List; /** * 路线类:自驾新疆路线 */ public class Route { // 静态属性 private String routeName; // 路线名称 private List<String> cityList; // 途经城市 // 构造 public Route(String routeName, List<String> cityList) { this.routeName = routeName; this.cityList = cityList; } // getter setter public String getRouteName() { return routeName; } public void setRouteName(String routeName) { this.routeName = routeName; } public List<String> getCityList() { return cityList; } public void setCityList(List<String> cityList) { this.cityList = cityList; } // 动态方法:打印路线途经城市 public void showCities() { System.out.println("路线【" + routeName + "】途经城市:"); for (String city : cityList) { System.out.print(city + " "); } System.out.println(); } }2. Car 汽车类(关联对象)
/** * 汽车类 */ public class Car { // 静态属性 private String brand; // 品牌 private String model; // 型号 private String color; // 颜色 private String produceTime;// 出厂时间 private int seatNum; // 座位数 // 构造 public Car(String brand, String model, String color, String produceTime, int seatNum) { this.brand = brand; this.model = model; this.color = color; this.produceTime = produceTime; this.seatNum = seatNum; } // getter & setter public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String getProduceTime() { return produceTime; } public void setProduceTime(String produceTime) { this.produceTime = produceTime; } public int getSeatNum() { return seatNum; } public void setSeatNum(int seatNum) { this.seatNum = seatNum; } // 动态方法 public void start() { System.out.println(brand + model + " 车辆启动"); } public void drive() { System.out.println("正在沿高速行驶,前往新疆"); } public void addOil() { System.out.println("中途服务区加满油"); } }3. Driver 司机类(核心类,体现关联 + 依赖)
/** * 司机类 * 关系说明: * 1. 关联:持有Car成员变量,长期拥有车辆 * 2. 依赖:planRoute方法参数是Route,方法内临时使用路线对象 */ public class Driver { // 静态属性 private String name; private int driveYear; // 关联关系:成员变量持有Car private Car myCar; public Driver(String name, int driveYear, Car myCar) { this.name = name; this.driveYear = driveYear; this.myCar = myCar; } // getter setter public String getName() { return name; } public void setName(String name) { this.name = name; } public int getDriveYear() { return driveYear; } public void setDriveYear(int driveYear) { this.driveYear = driveYear; } public Car getMyCar() { return myCar; } public void setMyCar(Car myCar) { this.myCar = myCar; } // 动态方法:开车自驾新疆 public void driveToXinJiang() { System.out.println("司机:" + name + ",驾龄" + driveYear + "年,准备自驾新疆"); myCar.start(); myCar.drive(); myCar.addOil(); } // 依赖关系:方法入参传入Route,临时使用路线对象 public void planRoute(Route route) { System.out.println(name + "正在规划自驾路线:"); route.showCities(); } }4. 测试主类 Main,完整运行演示
import java.util.List; public class Main { public static void main(String[] args) { // 1. 创建汽车对象 Car su7 = new Car("小米", "SU7 Pro", "海湾蓝", "2026-03", 5); // 2. 创建司机对象(关联绑定车辆) Driver driver = new Driver("小张", 6, su7); // 3. 创建北京到新疆G7路线对象 List<String> cityList = List.of("北京", "张家口", "呼和浩特", "额济纳旗", "哈密", "乌鲁木齐"); Route g7Route = new Route("G7京新高速", cityList); // 4. 调用依赖方法,传入路线对象 driver.planRoute(g7Route); System.out.println("======================"); // 5. 自驾出发 driver.driveToXinJiang(); } }运行输出结果:
小张正在规划自驾路线:
路线【G7京新高速】途经城市:
北京 张家口 呼和浩特 额济纳旗 哈密 乌鲁木齐
司机:小张,驾龄6年,准备自驾新疆
小米SU7 Pro 车辆启动
正在沿高速行驶,前往新疆
中途服务区加满油
到这里,我们又系统的认识了面向对象思想,学习了动静结合去认识类,用UML的6种关系建立类之间的关系,并总结出了3个步骤处理实际的问题。之前模糊的点都找到了章法,而不仅仅是个人经验了。
三、为什么选择面向对象?
在第二节,我们花了很大的功夫研究面向对象,学习使用面向对象分析实际问题。为什么呢?为什么选择使用面向对象呢?
首先,因为面向对象思想能帮我们大大提升软件的复用性。软件会变化(Lehman)(只适合长期演化系统)→ 需要频繁修改 → 复杂性爆炸 →复用能减少重复修改的工作量。面向对象是复用整个对象,包括对象的数据和方法;相比之下,面向过程的函数库只能复用"行为"(方法),数据结构和行为是分离的,复用时需要自己重新组装——这就是面向对象复用颗粒度更粗、更"整块"的独有优势。
其次,使用面向对象思想帮我们更好解耦。高内聚、低耦合是面向对象的核心设计原则(Parnas, 1972)。每个类的内聚性高(职责单一),彼此之间通过 UML 的几种关系管理依赖,尽可能松耦合 →修改时影响范围可控 → 降低修改成本。
所以,对于需要频繁修改的软件,在设计时使用面向对象思想,一方面通过数据+行为绑定的独有优势提升复用性,另一方面通过高内聚、低耦合的设计原则降低修改成本,两方面共同减少开发和维护的工作量。
面向对象思想的局限(个人经验):
面向对象思想也有局限,并不是能解决所有问题的银弹。
1、OOP只适合"会变化的软件",不适合一次性脚本 / 性能极端敏感场景;
2、OOP增加了系统复杂度(类多、间接调用多),小项目反而过度设计;
3、OOP不适合"无状态 / 纯计算"场景,函数式编程反而更自然。
总结:
通过这次学习,我深刻体会到了面向对象与面向过程在复用上的巨大差异,更加明确了如何使用面向对象思想编程。面向对象复用性是对象级别,复用整个对象的数据和结构,复用粒度更大。面向过程的复用性是方法级别,更适合变化频率低,系统简单的场景。