前端转战后端:JavaScript 与 Java 对照学习指南(第五篇 —— 面向对象:类、接口与多态)

前言

在前几篇中,我们从 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层解耦,依赖倒置
多态 鸭子类型 (也就是动态类型) 严格基于继承/接口 插件化架构,策略模式
相关推荐
用户2190326527352 小时前
SpringBoot自动配置:为什么你的应用能“开箱即用
java·spring boot·后端
_膨胀的大雄_2 小时前
01-创建型模式
前端·设计模式
爱笑的眼睛112 小时前
TensorFlow Hub:解锁预训练模型的无限可能,超越基础分类任务
java·人工智能·python·ai
小林rush2 小时前
uni-app跨分包自定义组件引用解决方案
前端·javascript·vue.js
我的一行2 小时前
已有项目,接入pnpm + turbo
前端·vue.js
亮子AI2 小时前
【Svelte】怎样实现一个图片上传功能?
开发语言·前端·javascript·svelte
shehuiyuelaiyuehao2 小时前
7类和对象
java·开发语言
心.c2 小时前
为什么在 Vue 3 中 uni.createCanvasContext 画不出图?
前端·javascript·vue.js
咸鱼加辣2 小时前
【vue面试】ref和reactive
前端·javascript·vue.js