前言
在前几篇中,我们从 int/String 聊到了 List/Map,解决了数据的"存储"问题。现在,我们要解决代码的"组织"问题。
对于前端开发者,class 并不陌生。但 JavaScript 的类更像是一个灵活的模具 (基于原型,随时可改),而 Java 的类则是一张严格的工程蓝图(编译期确定,不可篡改)。在后端开发中,类不仅仅是数据的集合,更是业务逻辑的载体、安全控制的关卡以及系统解耦的钥匙。
本篇将深入剖析 Java 面向对象(OOP)的核心,带你跨越从"写脚本"到"架构系统"的鸿沟。
1. 类的本质:动态 vs 静态
1.1 字段(Field)的严格定义
在 JS 中,我们习惯在 constructor 里直接挂载属性,甚至在对象实例化后随手添加属性。但在 Java 中,这是绝对禁止的。
JavaScript (自由的灵魂):
JavaScript
class User {
constructor(name) {
this.name = name; // 直接定义
}
}
const u = new User('F2B');
u.age = 18; // 随时添加新属性,JS 觉得没问题
Java (严谨的蓝图):
Java 必须在类级别显式声明所有字段。这不仅是为了类型安全,更是为了内存布局的确定性。
Java
public class User {
// 1. 必须先声明,才能使用
private String name;
private Integer age;
public User(String name) {
this.name = name;
// this.gender = "Male"; // ❌ 报错!类定义中没有 gender 字段
}
// ... getter and setter
}
1.2 方法重载 (Overloading) ------ JS 没有的特性
这是前端同学最容易忽略的特性。在 JS 中,如果定义两个同名函数,后面的会覆盖前面的。但在 Java 中,只要参数列表不同(类型或数量不同),方法名可以相同。
这在后端非常常用,用于提供"默认参数"的效果(Java 不支持 func(a=1) 这种默认值语法,通常用重载实现)。
Java
public class SearchService {
// 场景1:只按名字搜
public List<User> search(String name) {
return search(name, 18); // 转发调用给全量方法,默认 age 18
}
// 场景2:按名字和年龄搜
public List<User> search(String name, int minAge) {
// 执行具体的数据库查询逻辑
return db.find(name, minAge);
}
}
核心差异: Java 是根据参数签名 来区分方法的,而 JS 仅根据函数名。
2. 封装:不仅仅是 private
在后端开发中,封装(Encapsulation)不是为了"隐藏",而是为了保护数据的一致性。
2.1 为什么要写烦人的 Getter/Setter?
前端同学常问:"为什么不直接把变量设为 public?"
看这个例子:
Java
public class BankAccount {
private double balance; // 余额私有化,禁止外部直接修改
// 只提供存款方法,可以在这里加逻辑
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("存款金额必须大于0");
}
this.balance += amount;
}
// 只提供读取,不提供 setBalance,防止外部随意篡改余额
public double getBalance() {
return balance;
}
}
如果 balance 是 public 的,任何代码都能写 account.balance = -9999,这将导致严重的业务事故。Java 的封装强制你通过方法来操作数据,从而在方法里构筑防线。
2.2 开发神器:Lombok
为了解决 Java"样板代码"过多的问题(写一堆 getter/setter 手很累),后端开发标配插件 Lombok。
实际开发中的写法:
Java
import lombok.Data;
@Data // 一个注解,编译时自动生成 get、set、toString、equals、hashCode
public class UserDTO {
private String username;
private String password;
private Integer age;
}
注:转战后端,请务必尽早学会配置 Lombok,它能让你的 Java 代码像 JS 一样简洁。
3. 继承的高级形态:抽象类 (Abstract Class)
JS 的 extends 大家都懂,但 Java 有一个中间态------抽象类。
它位于"普通类"和"接口"之间。它不能被实例化,可以包含具体实现,也可以包含强制子类实现的"抽象方法"。
场景:电商支付系统
不管是支付宝、微信还是银联,支付流程(校验 -> 扣款 -> 记录日志)是相似的,只有"扣款"动作不同。
Java
// 抽象父类:定义模板
public abstract class BasePayment {
// 1. 公共逻辑:所有支付方式都一样的校验逻辑
public boolean validate(double amount) {
return amount > 0;
}
// 2. 抽象方法:留给子类必须去实现的(JS没有这个强制约束)
protected abstract void doDeduct(double amount);
// 3. 核心流程(模板方法模式)
public void pay(double amount) {
if (validate(amount)) {
doDeduct(amount); // 调用子类的具体实现
System.out.println("支付日志已记录");
}
}
}
// 具体子类
public class AliPay extends BasePayment {
@Override
protected void doDeduct(double amount) {
System.out.println("调用支付宝API扣款: " + amount);
}
}
后端思维: 抽象类用于复用代码。如果你发现几个类有大量重复逻辑,就应该提取一个抽象父类。
4. 接口 (Interface):后端的"契约精神"
这是 Java 最重要的概念,也是 TypeScript 极力模仿的对象。
在 Java 后端(特别是 Spring 框架)中,我们遵循**"面向接口编程"**。
4.1 接口 vs 抽象类
- 抽象类:是 "is-a" 关系(它是某种东西)。如:猫是动物。
- 接口:是 "can-do" 关系(它具备某种能力)。如:猫能跑(Runnable),车也能跑(Runnable)。
4.2 实际业务场景:多态与解耦
假设你需要把用户信息存起来,现在用 MySQL,以后可能换 Redis。
定义接口(契约):
Java
public interface UserRepository {
void save(User user);
User findById(String id);
}
实现 A(MySQL版本):
Java
public class MySQLUserRepository implements UserRepository {
public void save(User user) {
System.out.println("写入 MySQL 表...");
}
// ...
}
业务层调用(关键点):
Java
public class UserService {
// 这里声明的是接口类型,而不是具体的 MySQLUserRepository
private UserRepository userRepo;
// 构造函数注入实现类
public UserService(UserRepository repo) {
this.userRepo = repo;
}
public void register(User user) {
// 业务层完全不知道底层是 MySQL 还是 Redis,只知道 userRepo 能 save
userRepo.save(user);
}
}
好处: 如果明天老板说换 MongoDB,你只需要写一个 MongoUserRepository 实现接口,然后在启动配置里改一下注入,UserService 的代码一行都不用动 。这就是解耦。
5. 静态(Static):属于类的领地
在 JS 中,函数是一等公民,我们可以直接 export 一个函数。
但在 Java 中,一切皆对象。如果你想提供一个"工具函数"(不依赖具体对象状态),必须使用 static。
Java
public class DateUtils {
// 静态常量
public static final String FORMAT = "yyyy-MM-dd";
// 静态方法:通过 DateUtils.now() 直接调用,不需要 new DateUtils()
public static String now() {
return LocalDate.now().toString();
}
}
内存区别:
- 实例变量(非Static):
new一个对象,堆内存就分配一份。100个对象有100份name。 - 静态变量(Static): 全局只有一份,属于类,所有对象共享。慎用可变静态变量,会引发线程安全问题!
6. 总结与对照表
| 特性 | JavaScript (ES6+) | Java | 后端应用场景 |
|---|---|---|---|
| 属性定义 | 动态,构造函数中赋值 | 静态,类体内显式声明 | DTO、Entity 数据模型 |
| 方法重载 | 不支持 (后覆盖前) | 支持 (看参数签名) | 提供多种参数的查询接口 |
| 访问修饰 | #私有 (新), _约定 |
private/protected/public |
保护核心业务数据不被污染 |
| 抽象类 | 无 (普通类模拟) | abstract class |
提取公共业务逻辑 (模板模式) |
| 接口 | 无 (TS 有 interface) | interface |
核心! Service层解耦,依赖倒置 |
| 多态 | 鸭子类型 (也就是动态类型) | 严格基于继承/接口 | 插件化架构,策略模式 |