摘要:前端转 Java 后端时,最容易陷入一个误区:从变量、循环、数组、继承、多态一路背到 JVM,把 Java 基础学成一份语法清单。我的阶段目标不是成为 Java 语言专家,而是先能看懂业务代码、写出稳定接口、理解项目里的类、接口、集合、异常、泛型、注解和 Stream。本文记录一下我准备怎么学 Java 基础,以及哪些 Java 特性和真实业务开发最相关。
Start
上一篇记录了我为什么想从前端往后端走,以及大概的学习路线。
这篇就从第一阶段开始:Java 基础。
一开始看 Java 基础资料,很容易被内容量劝退。变量、数据类型、运算符、流程控制、数组、字符串、面向对象、接口、抽象类、集合、泛型、异常、IO、线程、JVM、反射、注解、Stream、枚举、日期时间......每个点都能展开一堆。
但如果目标是"前端转 Java 后端",我觉得第一阶段不应该把 Java 基础学成百科全书。
更现实的目标应该是:
- 能看懂公司后端项目里的业务代码
- 能理解 Controller、Service、Mapper 里为什么这样写
- 能写出类型清晰、异常明确、结构稳定的接口代码
- 能知道哪些 Java 特性是业务开发中高频出现的
- 能先跑通一个完整 CRUD,而不是在语法细节里耗太久
所以这篇不会堆一份 Java 基础知识清单,而是按业务开发视角,整理我认为最先要掌握的 Java 基础。
一、Java 基础不是从语法开始,而是从类型意识开始
前端写 JavaScript 久了,最明显的习惯是:很多事情可以先跑起来再说。
js
const user = {
id: 1,
name: "Tom"
};
console.log(user.age);
这段代码不会在写的时候拦住你,最多运行时发现 age 是 undefined。
但 Java 不一样。
java
public class User {
private Long id;
private String name;
}
在 Java 里,一个对象有哪些字段、字段是什么类型、方法返回什么、参数传什么,都会被提前写清楚。
这就是 Java 对前端思维的第一处改造:你要更早地把数据边界想明白。
后端业务代码里,类型不是形式主义。它会影响:
- 接口参数能不能被正确接收
- 数据库字段能不能正确映射
- 返回给前端的数据结构是否稳定
- 空值会不会引发运行时异常
- 业务状态是否可以被明确表达
比如用户状态,如果前端随手写字符串:
js
status: "enabled"
后面可能又出现:
js
status: "ENABLE"
status: "enable"
status: 1
Java 里更推荐用明确的类型收住它:
java
public enum UserStatus {
ENABLED,
DISABLED
}
这不是为了写得复杂,而是为了减少"大家各写各的"。
二、类和对象:业务模型的第一层表达
前端也有 class,但在很多前端业务里,我们更常用对象字面量、接口类型、函数组合。
Java 后端不一样,类是非常核心的组织单位。
比如一个用户:
java
public class User {
private Long id;
private String username;
private String email;
private UserStatus status;
}
它不只是一个数据袋子。放到后端项目里,它可能会对应数据库表,也可能会作为接口返回值,也可能只是业务逻辑中的一个中间对象。
真实项目里经常会看到不同类型的类:
text
Entity:数据库实体,通常和表结构接近
DTO:请求参数,前端传给后端
VO:响应对象,后端返回给前端
Service:业务服务
Controller:接口入口
Mapper / Repository:数据访问
刚开始学 Java 类的时候,不要只停留在"类有属性和方法"。
更重要的是理解:一个类在业务项目里承担什么角色。
比如创建用户时,前端传来的参数可能是:
java
public class CreateUserDTO {
private String username;
private String password;
private String email;
}
数据库里的用户实体可能是:
java
public class User {
private Long id;
private String username;
private String passwordHash;
private String email;
private UserStatus status;
private LocalDateTime createdAt;
}
返回给前端时,又不应该把密码相关字段返回:
java
public class UserVO {
private Long id;
private String username;
private String email;
private UserStatus status;
}
这就是后端里很常见的对象拆分。
前端可能习惯一个 User 类型到处用,但后端更强调边界:请求对象、数据库对象、响应对象不一定是同一个东西。
三、封装:不是 private 加 getter/setter 这么简单
学 Java 基础时,封装通常会被解释成:
java
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
这当然是封装的一部分,但业务里更重要的是:不要让外部随便破坏对象的状态。
比如订单状态:
java
public class Order {
private OrderStatus status;
public void pay() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("只有待支付订单可以支付");
}
this.status = OrderStatus.PAID;
}
}
这里的 pay() 比一个简单的 setStatus() 更有业务意义。
如果到处都能这样写:
java
order.setStatus(OrderStatus.PAID);
那谁都可以绕过业务规则直接改状态。
所以封装不是"把字段 private 掉再暴露 setter",而是把业务规则收进对象或服务里,让状态变化有入口、有校验、有语义。
这点对前端转后端很重要。后端系统最终要守住规则,而不是只把数据改成功。
四、接口:业务能力的抽象
前面聊过 extends 和 implements 的区别。真正写后端时,接口非常常见。
比如支付:
java
public interface PaymentService {
void pay(Long orderId);
}
支付宝实现:
java
public class AlipayPaymentService implements PaymentService {
@Override
public void pay(Long orderId) {
// 调用支付宝支付
}
}
微信实现:
java
public class WechatPaymentService implements PaymentService {
@Override
public void pay(Long orderId) {
// 调用微信支付
}
}
接口的价值不是让代码多一层,而是让调用方依赖"能力",不是依赖某个具体类。
业务代码只关心:
text
我需要一个能支付的东西
而不是:
text
我必须绑定支付宝支付这个类
在 Spring 项目里,你会经常看到:
java
private final UserService userService;
而不是:
java
private final UserServiceImpl userService;
这就是接口思维:面向能力编程,降低调用方和实现方的耦合。
五、集合框架:业务代码每天都在用
Java 集合是必须优先掌握的部分,因为业务开发几乎每天都在处理列表、映射、去重、分组。
最常见的几个:
text
List:有序列表,类似数组
Set:不重复集合
Map:键值映射,类似对象 / 字典
比如查询用户列表:
java
List<User> users = userMapper.selectList();
根据用户 ID 快速查用户:
java
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
去重:
java
Set<Long> userIds = new HashSet<>();
前端也会处理数组,但 Java 集合的类型更明确。
java
List<UserVO>
这表示列表里只能放 UserVO。
这种约束在后端很有价值,因为接口返回、数据库查询、中间转换都依赖稳定结构。
学集合时,我不会一上来死记所有实现类,而是先掌握几个业务里高频的选择:
ArrayList:最常用的列表HashSet:常用去重HashMap:常用键值映射LinkedHashMap:需要保留插入顺序时使用Collections/List.of:常用工具方法
等业务写多了,再补并发集合、队列、树结构这些。
六、泛型:让类型约束贯穿业务链路
泛型对前端同学不陌生,TypeScript 里也有:
ts
type ApiResponse<T> = {
code: number;
message: string;
data: T;
};
Java 里也会这么写:
java
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
}
这在后端接口里非常常见。
用户详情接口可以返回:
java
ApiResponse<UserVO>
用户列表接口可以返回:
java
ApiResponse<List<UserVO>>
分页接口可以返回:
java
ApiResponse<PageResult<UserVO>>
泛型的价值是:一套结构可以复用,但里面的数据类型仍然明确。
这比到处写 Object 稳很多。
java
private Object data;
如果什么都用 Object,编译器帮不上忙,调用方也不知道里面到底是什么。
所以泛型不是语法炫技,而是业务代码里非常实用的类型工具。
七、异常:后端不能只写正常流程
前端写业务时,也会处理异常,比如接口失败弹 toast、表单校验失败展示错误信息。
但后端的异常更靠近系统边界。
一个接口失败,可能是:
- 参数错误
- 用户未登录
- 用户无权限
- 数据不存在
- 当前状态不允许操作
- 数据库异常
- 第三方服务失败
如果后端随便抛异常,前端就只能拿到一个模糊的 500。
更好的方式是定义业务异常:
java
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
业务里明确抛出:
java
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用户不存在");
}
再通过统一异常处理返回给前端。
学 Java 异常时,我觉得重点不是背 checked exception 和 unchecked exception 的所有区别,而是先理解:
- 哪些错误是业务可预期的
- 哪些错误应该转换成明确错误码
- 哪些错误应该记录日志
- 哪些错误不能直接暴露内部细节
后端代码不能只写 happy path。
异常处理,是业务稳定性的一部分。
八、枚举:让业务状态更可控
枚举是 Java 里非常适合表达业务状态的特性。
比如订单状态:
java
public enum OrderStatus {
CREATED,
PAID,
SHIPPED,
FINISHED,
CANCELLED
}
比起到处写字符串:
java
"created"
"paid"
"cancelled"
枚举更稳定,也更容易被 IDE 和编译器检查。
实际项目里,枚举还可以带字段:
java
public enum OrderStatus {
CREATED("待支付"),
PAID("已支付"),
CANCELLED("已取消");
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
它适合表达那些固定范围的业务值:
- 用户状态
- 订单状态
- 支付渠道
- 审批状态
- 消息类型
- 任务执行状态
对前端来说,枚举也能减少联调时的口径不一致。
状态有哪些、每个状态叫什么、什么时候能流转,最好从后端模型里就清楚。
九、Lambda 和 Stream:业务数据转换高频出现
Java 8 之后,Lambda 和 Stream 是非常著名、也非常常用的特性。
前端同学可以把 Stream 先粗略理解成 Java 里的链式数组处理。
JavaScript:
js
const names = users
.filter(user => user.status === "ENABLED")
.map(user => user.name);
Java:
java
List<String> names = users.stream()
.filter(user -> user.getStatus() == UserStatus.ENABLED)
.map(User::getName)
.toList();
业务里常见场景很多:
筛选:
java
List<User> enabledUsers = users.stream()
.filter(user -> user.getStatus() == UserStatus.ENABLED)
.toList();
转换:
java
List<UserVO> userVOList = users.stream()
.map(this::toUserVO)
.toList();
提取 ID:
java
List<Long> userIds = users.stream()
.map(User::getId)
.toList();
转 Map:
java
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
分组:
java
Map<UserStatus, List<User>> groupedUsers = users.stream()
.collect(Collectors.groupingBy(User::getStatus));
Stream 很好用,但也不要滥用。
如果业务逻辑很复杂,强行写成一长串链式调用,反而不如普通循环清楚。
我的原则是:简单筛选、转换、分组可以用 Stream;复杂业务判断就老老实实拆方法。
十、注解:Java 后端项目里无处不在
学 Spring Boot 时,注解是绕不开的。
比如:
java
@RestController
@RequestMapping("/users")
public class UserController {
}
java
@Service
public class UserServiceImpl implements UserService {
}
java
@Autowired
private UserService userService;
或者构造器注入:
java
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
注解的作用可以先粗略理解成:给类、方法、字段加元信息,让框架知道它应该怎么处理。
比如:
@RestController:这是一个接口控制器@RequestMapping:接口路径@Service:这是一个业务服务@Transactional:这个方法需要事务@Valid:需要参数校验@Override:重写父类或接口方法
注解是 Java 后端的一个著名特性,尤其在 Spring 体系里非常关键。
但入门时不要只背注解名字,要理解它背后的含义:
text
框架为什么能找到这个类?
为什么请求能进入这个方法?
为什么事务能自动生效?
为什么参数校验能自动触发?
这些问题后面会牵涉到反射、代理、IoC、AOP。
第一阶段不需要全吃透,但至少要知道:注解不是魔法,它是框架读取元信息后做了额外处理。
十一、反射:先知道它为什么重要
反射这个词一开始会有点抽象。
简单说,反射就是程序在运行时也能知道一个类有哪些字段、方法、注解,并且可以动态操作它们。
很多框架能力都离不开反射。
比如 Spring 能扫描到:
java
@Service
public class UserServiceImpl {
}
然后创建对象、管理对象、注入依赖。
再比如 JSON 转对象:
json
{
"username": "Tom",
"email": "tom@example.com"
}
框架可以把它转换成:
java
public class CreateUserDTO {
private String username;
private String email;
}
这里背后也会用到类似反射的能力。
第一阶段我不会深挖反射 API,但会先建立一个认知:
text
Java 很多框架看起来自动完成的事情,背后都和注解、反射、代理有关。
这个认知对后面理解 Spring Boot 很重要。
十二、日期时间:业务里很容易出问题
日期时间看起来是小知识,但业务里非常高频。
用户注册时间、订单创建时间、支付时间、过期时间、统计时间范围、定时任务,都绕不开时间。
Java 里现在更推荐使用:
java
LocalDate
LocalTime
LocalDateTime
Instant
Duration
比如:
java
LocalDateTime now = LocalDateTime.now();
订单 30 分钟后过期:
java
LocalDateTime expireAt = now.plusMinutes(30);
判断是否过期:
java
if (LocalDateTime.now().isAfter(expireAt)) {
throw new BusinessException("ORDER_EXPIRED", "订单已过期");
}
前端转后端时,要尤其注意:
- 时间格式怎么传
- 时区怎么处理
- 数据库存什么时间
- 返回给前端是否需要格式化
- 查询时间范围时边界是否包含
很多线上问题都不是算法问题,而是时间边界没处理好。
十三、JVM 和 GC:第一阶段先了解,不急着深挖
Java 的另一个著名特性是运行在 JVM 上。
这也是 Java 和 JavaScript 很不一样的地方。Java 代码会编译成字节码,在 JVM 上运行。JVM 负责内存管理、类加载、垃圾回收等工作。
第一阶段我不会一上来深挖 JVM 参数、内存模型、GC 算法。
但至少要知道几个基础概念:
- Java 不是直接运行源码,而是运行编译后的字节码
- 对象创建后会占用内存
- 不再使用的对象会被 GC 回收
- 内存泄漏在 Java 里也会发生
- 线上服务的性能问题可能和内存、线程、GC 有关
为什么要先知道这些?
因为以后看到下面这些问题时,不至于完全没概念:
- 服务为什么突然变慢
- 为什么日志里出现 OutOfMemoryError
- 为什么频繁创建大对象会有风险
- 为什么连接、流、线程池要正确关闭
JVM 是进阶重点,但不是第一阶段的主战场。
第一阶段先做到:知道它重要,知道问题大概往哪里查。
十四、我准备怎么安排 Java 基础
为了避免学散,我会按业务相关程度排序。
第一优先级:
- 基本类型和引用类型
- 类和对象
- 封装
- 接口
- 集合框架
- 泛型
- 异常处理
- 枚举
- Lambda 和 Stream
- 日期时间
第二优先级:
- 注解
- 反射基础概念
- IO 基础
- JVM 基础概念
- 常用工具类
暂时不急着深挖:
- 多线程高级细节
- JVM 调优
- 复杂设计模式
- NIO 底层实现
- 并发容器源码
- 类加载机制细节
不是它们不重要,而是对"先写出业务接口"这个阶段来说,优先级可以后移。
十五、学习 Java 基础的正确打开方式
我现在更倾向于用业务场景驱动 Java 基础。
比如不要单独背枚举,而是用订单状态学枚举。
不要单独背泛型,而是用统一接口返回学泛型。
不要单独背异常,而是用"用户不存在""订单已支付不能取消"这种业务错误学异常。
不要单独背接口,而是用支付、通知、文件存储这种可替换实现学接口。
这样学出来的 Java 基础不会悬空。
因为后端代码最终不是写给考试看的,而是写给业务系统跑的。
结语
前端转 Java 后端,第一阶段学基础时最重要的不是"我看完了多少语法",而是"我能不能用 Java 的方式表达业务"。
Java 的强类型、面向对象、接口、泛型、异常、集合、枚举、注解、Stream,这些特性并不是孤立知识点。它们最终都会落到真实业务里:
- 用类型稳定接口结构
- 用类表达业务对象
- 用接口抽象可替换能力
- 用集合处理批量数据
- 用泛型复用通用结构
- 用异常表达失败原因
- 用枚举收住业务状态
- 用注解接入框架能力
- 用 Stream 做清晰的数据转换
所以这一步我不会把 Java 基础学成语法词典,而是先抓业务开发最常见、最有价值的部分。
下一步,就是基于这些基础去跑一个 Spring Boot 项目,把一个请求从 Controller 打到 Service,再从数据库查出来,最后返回给前端。
先把链路跑通,很多概念才会真正长到脑子里。