Appearance
面向对象、泛型与异常
这篇笔记聚焦 Java 语言层最容易“会写但没讲透”的几条主线:
- 面向对象
- 对象比较与相等性判断
- 泛型
- 异常机制
它们看起来像基础语法,但真正影响的是:代码怎么组织、类型边界怎么表达、错误怎么被处理和传递。
1. 面向对象的具体释义是什么
面向对象 可以看成 用对象来组织程序,把数据和行为放在一起,再通过对象之间的协作完成业务。
这里最容易被误解的点是:
面向对象 不是“会写 class”,也不是“把函数包进类里”。
更具体一点,它是一种:建模和组织代码的方式。
所谓 建模,可以理解成 把现实业务里的角色、状态、行为和关系,转换成程序中的对象、属性、方法和协作关系。
例如在订单系统里,常见会有:
- 用户
- 商品
- 订单
- 支付
这些都不是简单的一组变量,而是可以进一步抽象成对象。
对象里通常会有两类东西:
属性:它有什么,例如订单号、金额、状态行为:它能做什么,例如创建订单、取消订单、支付订单
所以面向对象真正强调的是:先想系统里有哪些对象,它们各自负责什么,再想这些对象如何协作。
2. 面向对象到底在解决什么问题
如果没有面向对象,代码很容易退化成:
- 数据散在各处
- 处理逻辑散在各处
- 谁能改什么边界不清楚
- 系统一变大就越来越难维护
面向对象主要在解决这些问题:
- 业务概念如何抽象成代码结构
- 数据和行为如何绑定到一起
- 职责应该交给哪个对象负责
- 模块之间如何降低耦合
- 修改和扩展时如何减少牵一发动全身
可以用一句更直白的话理解:它是在回答:这个事情到底应该由哪个对象负责,而不是把所有逻辑堆成一条大流程。
3. 为什么叫“面向对象”
因为它思考问题的起点,不是“先执行第 1 步、第 2 步、第 3 步”,而是先问:
- 系统里有哪些对象
- 每个对象掌握什么状态
- 每个对象应该负责什么行为
- 对象之间如何互相配合
这和 面向过程 的思路不太一样。
3.1 面向过程更关注什么
面向过程 更偏:先做什么,再做什么,最后做什么。
它的重点在流程步骤本身。
3.2 面向对象更关注什么
面向对象 更偏:这个动作应该交给哪个对象来完成。
它的重点在职责划分和对象协作。
例如“下单”这个场景:
- 订单对象负责订单自身状态
- 库存对象负责扣减库存
- 支付对象负责支付行为
这种写法的好处是:业务职责不会都堆在一个超长方法里。
4. 对象、类和抽象分别是什么意思
4.1 对象是什么
对象 可以理解成 现实世界或业务世界中某个具体事物在程序里的表示。
例如:
- 一个具体用户
- 一笔具体订单
- 一个具体商品
这些都可以是对象。
4.2 类是什么
类 可以理解成 创建对象的模板或蓝图。
它描述的是:
- 这一类对象通常有哪些属性
- 这一类对象通常有哪些行为
所以可以这样记:
类:模板对象:具体实例
4.3 抽象是什么
抽象 可以理解成 从很多具体事物里提取出共性,只保留当前问题真正关心的部分。
例如“支付”这个概念里,微信支付和支付宝支付都不一样,但都可以抽象成:支付能力
这就是为什么面向对象很强调:先抽象,再实现。
5. 封装、继承、多态到底该怎么理解
这三个词常被称为面向对象的核心特征,但如果只是背定义,很容易变成空话。
更好的理解方式是:
封装解决“对象内部该怎么藏、外部该怎么看”继承解决“已有能力怎么复用”多态解决“面向抽象时,不同实现怎么替换”
5.1 封装
封装 可以理解成 把对象的状态和行为放在一起,并且控制哪些细节对外暴露。
它解决的是:
- 降低外部直接操作内部状态的风险
- 让对象自己维护自己的不变量
- 给内部实现留出修改空间
这里的 不变量 可以理解成 对象在任何正常状态下都应该成立的约束。
例如账户余额不能随便变成负数,这就不应该由外部直接随意改字段。
5.2 继承
继承 可以理解成 子类复用父类已有能力,并在此基础上扩展或改写行为。
它适合表达:一种明确的“是一个”关系。
但它不是“代码复用的默认首选”。
要注意的是:继承会建立更强的结构耦合。
所以工程里很常强调:组合优于继承
也就是更倾向于通过对象组合来复用能力,而不是层层继承。
5.3 多态
多态 可以理解成 同一个抽象类型,在不同实现对象上表现出不同实际行为。
它解决的是:
- 面向抽象编程
- 提升扩展性
- 降低调用方对具体实现的依赖
例如你用接口类型接收对象时,调用方更关心“它会不会支付”,而不是“它具体是微信支付还是支付宝支付”。
6. interface 和 abstract class 有什么区别
这是 Java 面向对象里最常见的边界问题之一。
| 对比项 | interface | abstract class |
|---|---|---|
| 更偏什么 | 抽象能力约定 | 抽象父类骨架 |
| 是否能有状态 | 一般不持有实例状态 | 可以持有成员变量和共用实现 |
| 设计意图 | 定义能做什么 | 定义是什么,以及部分共性实现 |
| 更常见场景 | 能力契约、解耦 | 复用公共逻辑 |
可以这样记:
interface 更像能力接口,abstract class 更像半成品父类。
7. equals 和 hashCode 到底在解决什么问题
这是 Java 对象模型里非常重要、也非常容易“会写代码但讲不清”的一组方法。
很多人第一次接触它们,只会记住:
equals用来比较对象hashCode和HashMap有关
这样记太薄了。
更具体一点:
equals负责定义对象在逻辑意义上是否相等hashCode负责支撑对象在哈希结构中的高效定位
它们一起解决的是:对象应该如何被判断为“同一个逻辑对象”,以及这种判断如何在 HashMap、HashSet 这类结构里高效工作。
7.1 == 和 equals 不是一回事
对引用类型来说:
==比较的是两个引用是否指向同一个对象equals比较的是两个对象在逻辑意义上是否相等
这里的“逻辑意义上相等”,可以理解成 虽然它们不一定是同一个对象实例,但它们代表的业务内容相同。
例如:
java
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true这段代码里:
a和b不是同一个对象- 但它们表示的字符串内容相同
7.2 Object 默认的 equals 做了什么
equals 方法定义在 Object 类中。
默认情况下,它的行为更接近:比较两个引用是不是同一个对象。
也就是说,如果你不重写 equals,那很多自定义对象的“相等”判断,本质上还是在看:
是不是同一个实例。
但很多业务对象更关心的往往不是“是不是同一个实例”,而是:关键字段是否相同。
这就是为什么很多领域对象、值对象会重写 equals。
7.3 hashCode 是什么
hashCode 是 Object 里的另一个方法,它会返回一个 int 类型的哈希值。
这个哈希值不是用来直接判断两个对象是否相等的。
它更像是:对象进入哈希结构时的一个快速定位线索。
例如在 HashMap 或 HashSet 里,通常不是上来就把所有元素一个个拿出来做 equals 比较,而是:
- 先计算
hashCode - 根据哈希值决定大概落在哪个桶
- 再在那个桶里用
equals做精确判断
所以:
hashCode负责先缩小查找范围equals负责最终确认是否真的相等
7.4 为什么重写 equals 时通常必须重写 hashCode
因为 Java 对这两个方法有一个非常重要的约定:如果两个对象 equals 为 true,那么它们的 hashCode 必须相同。
也就是:
java
if (a.equals(b)) {
a.hashCode() == b.hashCode();
}这条规则不能反过来理解。
也就是说:hashCode 相同,不代表 equals 一定为 true。
这叫 哈希冲突,是允许发生的。
真正容易出问题的是:
- 你重写了
equals - 却没有重写
hashCode
这样对象放进 HashSet 或作为 HashMap 的 key 时,就会出现逻辑不一致。
7.5 一个最典型的错误场景
java
class User {
private String name;
public User(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(name, user.name);
}
}如果此时:
java
User u1 = new User("Tom");
User u2 = new User("Tom");那么:
java
u1.equals(u2)会得到 true。
但如果把它们放进 HashSet:
java
Set<User> set = new HashSet<>();
set.add(u1);
set.add(u2);结果却可能仍然保留两份。
原因不是 HashSet 出错了,而是:
HashSet 先依赖 hashCode 做定位,如果逻辑相等的对象没有给出一致的 hashCode,它们就可能进入不同桶,根本不会走到你预期的 equals 语义。
7.6 equals 应该满足哪些约定
一个合理的 equals,通常应该满足这些性质:
- 自反性:
x.equals(x)必须是true - 对称性:如果
x.equals(y)为true,那么y.equals(x)也必须为true - 传递性:如果
x.equals(y)和y.equals(z)都为true,那么x.equals(z)也应该为true - 一致性:如果参与比较的状态没变,多次调用结果应该一致
- 对
null比较时必须返回false
这些规则不是为了背定义,而是为了保证:对象相等性的判断不会在不同场景下前后矛盾。
7.7 hashCode 应该满足哪些约定
至少要记住这两条:
- 同一个对象,在参与计算的状态不变时,多次调用
hashCode结果应该一致 - 如果两个对象
equals为true,那么它们的hashCode必须相同
但反过来并不成立:两个对象 hashCode 一样,不代表它们一定 equals。
7.8 一个更标准的重写方式
java
import java.util.Objects;
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}这段代码里最重要的不是模板本身,而是背后的原则:
- 用哪些字段定义“逻辑相等”,就让哪些字段同时参与
equals - 这些字段也必须同时参与
hashCode
7.9 getClass() 和 instanceof 怎么选
这是重写 equals 时经常会碰到的边界问题。
用 getClass()
表示:只有完全相同的类,才允许参与相等判断。
这种方式更严格。
用 instanceof
表示:只要类型兼容,就允许继续比较。
这种方式更宽松,但在继承体系里更容易引发对称性和传递性问题。
所以很多业务对象里,更常见的稳妥写法是:优先使用 getClass() 保持相等性边界明确。
7.10 工程上最常见的几个坑
1. 只重写 equals,不重写 hashCode
这是最典型的问题。
2. 把可变字段用于 equals / hashCode
例如对象已经放进 HashSet,你又修改了参与哈希计算的字段,就可能出现:
- 查找不到
- 删除异常
- 逻辑行为混乱
3. 在继承体系里随意扩展 equals
很容易破坏:
- 对称性
- 传递性
- 类边界一致性
7.11 一句话总结
equals 和 hashCode 这一组方法,本质上是在解决:对象怎样被定义为逻辑相等,以及这种相等性如何在哈希结构中以正确且高效的方式工作。
8. 泛型到底在解决什么问题
泛型 可以理解成 把类型信息参数化,让同一套逻辑可以在不同具体类型上复用,同时保留类型检查能力。
如果没有泛型,集合这类容器会非常依赖强制类型转换,代码既不安全,也不清晰。
例如:
java
List<String> names = new ArrayList<>();这里的 String 就是在告诉编译器:这个列表里应该放的就是 String。
9. 泛型里的几个高频名词是什么意思
5.1 类型参数
T、E、K、V 这类写法,通常叫 类型参数。
它们不是固定有特殊魔法,只是约定俗成的命名方式。
例如:
T:TypeE:ElementK:KeyV:Value
5.2 泛型擦除
泛型擦除 可以理解成 Java 的泛型主要在编译期发挥作用,编译后很多具体泛型信息不会完整保留到运行期。
它解决的是历史兼容问题,但也带来了某些限制。
例如你不能直接:
new T()T.classnew List<String>[10]
这些限制的背后,很多都和泛型擦除有关。
5.3 上界和下界
例如:
<? extends Number><? super Integer>
可以这样记:
extends:上界,表示某个类型或其子类super:下界,表示某个类型或其父类
它们主要解决的是:在类型复用和类型安全之间做更细的边界控制。
10. 异常机制到底在解决什么问题
异常 可以理解成 程序运行过程中出现异常情况时,用来中断当前正常流程并携带错误信息的一套机制。
它解决的是:
- 错误如何被显式传递
- 出问题时程序如何停止当前流程
- 调用方如何决定继续处理、转换还是向上抛出
11. checked exception 和 unchecked exception 有什么区别
7.1 checked exception
checked exception 可以理解成 编译器要求你显式处理或声明抛出的异常。
例如常见的 IOException。
7.2 unchecked exception
unchecked exception 通常指运行时异常,也就是 RuntimeException 及其子类。
它可以理解成 编译器不强制你显式处理,但运行时仍然可能抛出的异常。
例如:
NullPointerExceptionIllegalArgumentExceptionIndexOutOfBoundsException
12. try-catch-finally 和 throw / throws 分别在干什么
8.1 try-catch-finally
它解决的是:
- 哪段代码可能出问题
- 出问题后怎么处理
- 是否有无论是否异常都要执行的收尾逻辑
8.2 throw
throw 表示:在当前代码位置主动抛出一个异常对象。
8.3 throws
throws 表示:声明当前方法可能把某类异常继续往上交给调用方处理。
13. 一个更像工程实践的理解
在项目里,异常机制真正要关注的不是“语法会不会写”,而是:
- 哪些异常应该就地处理
- 哪些异常应该向上抛
- 哪些是业务异常
- 哪些是系统异常
- 日志应该在哪里记
如果所有地方都随手 catch(Exception e),最后通常会把错误边界弄得很乱。
14. 工程上最常见的几个误区
14.1 误区一:继承天然比组合好
不一定。
继承方便,但也更容易把类层次耦合得越来越重。
14.2 误区二:泛型只是为了少写强转
这只是表面作用。
更重要的是它把类型边界显式表达出来了。
14.3 误区三:异常就是“报错信息”
异常真正重要的是:它定义了错误传播和处理的边界。
14.4 误区四:equals 只要写了就行,hashCode 可有可无
不对。
一旦对象可能进入 HashMap、HashSet 这类结构,equals 和 hashCode 就必须协同设计。
11. 一句话总结
面向对象、泛型与异常这条主线,本质上是在解决:
代码如何被抽象组织、类型边界如何被安全表达,以及错误在系统里应该如何被传递和处理。