如果问一个写了十年 Java 的程序员,面向对象编程有哪几个核心概念,他多半会脱口而出:封装、继承、多态、抽象。但在面向对象思想的真正源头 Smalltalk 体系里,标准答案却是另一套:对象、消息、类,以及由“类之间存在一般与特殊(继承)的关系”推导出来的继承。这种答案上的差异,其实反映了两种完全不同的理解层次:一个是在语法层面看 OO,一个是在机制层面看 OO。这篇文章想聊的就是后者——把对象、消息、类这三个被很多教程匆匆带过的概念拆开揉碎,再顺着“一般与特殊”这句话,把隐含的第四个概念继承也讲清楚。适合刚学完面向对象语法、但总感觉没有真正理解的人,也适合写了很多年代码、想在设计层面重新梳理一遍的老开发。
1. 对象:状态、行为与身份,缺一不可的最小单元
1.1 对象到底由什么组成
我先说一个很多人没有仔细想过的点:对象不是一个简单的“数据 + 函数”的容器。如果只是数据加函数,那用一个结构体加一堆全局函数也能做到,C 语言早就实现了,根本不需要面向对象。对象真正的特殊性,在于它同时具备三样东西:状态、行为、身份。
状态是对象内部的数据,比如一个订单对象里的金额、状态、创建时间;行为是对象对外提供的操作,比如“取消订单”“确认支付”;身份则是最容易被忽略的——它是“这个对象”和“另一个看起来一模一样的对象”之间的区别。即便两个订单对象的状态完全一样,它们也仍然是两个不同的订单。这听起来像废话,但恰恰是“对象数组去重”这种看似简单的问题,背后最难的部分。
我见过不少人在做对象去重时,直接拿对象的某个业务字段(比如 ID)来比较,然后用这个 ID 作为身份。这种做法在大多数业务场景下没问题,但你要清楚:这是在用业务身份代替对象身份。真正的对象身份,是内存中的引用地址,是 JVM 中对象头的 mark word,是 Python 里 id() 返回的那个整数。业务 ID 只是状态的一部分。搞清楚这一点,你才能解释为什么两个内容完全相同的对象,放进 HashSet 里却是两个元素——因为它们的 equals 和 hashCode 没有被正确重写,默认逻辑看的是引用地址,也就是身份,而不是状态。
1.2 “万物皆对象”不是一句口号
“万物皆对象”这句话在每个 OO 教程里都会出现,但很多人的理解停留在“所有的东西都是对象”这个表面意思上。实际上,这句话是一个设计选择:它意味着语言把一切可操作的东西,都统一成“能接收消息、能持有状态、有身份”的个体。在纯面向对象语言里,整数是对象,字符串是对象,类是对象,连“对象的类”这个元信息也是对象。
这种设计带来的第一个好处是统一性。你不需要区分“值”和“对象”两套操作方式,所有东西都能调用方法、都能作为参数传递、都能被放入容器。第二个好处是表达能力变强了:因为类本身也是对象,所以你可以在运行时动态地创建类、修改类、给类增加方法——这在 Java 里需要靠反射绕一大圈,但在 Smalltalk 和 Python 里是天然支持的。
不过“纯 OO”和“实用 OO”之间是有取舍的。Java 保留了基本类型,后来才通过自动装箱机制让基本类型和对象类型互相转换;C++ 更是直接保留了完整的值语义。原因很简单:对象有身份、有引用,意味着有额外的内存开销和间接寻址,而纯值类型可以直接放在栈上,访问速度极快。所以不要一听到“万物皆对象”就觉得所有语言都应该这样做,它更像一个光谱,一端是 Smalltalk 那样的纯对象模型,另一端是 C 那样的纯值模型,Java、C++、Python 都在这条光谱上取了一个自己的位置。
1.3 对象的生命周期比你想的更值得关注
一个对象从创建到销毁,经历了:分配内存、初始化状态、参与消息传递、最终被回收。大多数教程会花大量篇幅讲构造函数、讲析构函数、讲垃圾回收,但很少讲“对象的边界”这件事。
对象边界的意思是:这个对象应该管理哪些数据、暴露哪些行为、隐藏哪些细节。边界划得好,对象就高内聚、低耦合;边界划得不好,就会出现那种“一个类几百行、全项目到处 new 它、改一个字段要连带改二十个地方”的灾难现场。我见过很多团队的技术债,本质上不是算法问题,也不是性能问题,而是对象边界从一开始就没划对。
这里有一个很实用的检查标准:如果
