减少Java样板代码

Java 去样板代码 Cheatsheet

面向 Java 后端开发的 modern Java / engineering tricks。目标不是炫技,而是把那些“必须写、但信息密度很低”的样板代码压掉,把注意力留给业务本身。


目录

  1. record:干掉 DTO / VO / Request 样板
  2. var:减少局部变量类型噪音
  3. Optional:减少 null if-else
  4. Stream API:声明式集合处理
  5. 方法引用 :::让 lambda 更干净
  6. switch 表达式:替代老式 switch
  7. Text Block:多行字符串不再丑
  8. Lombok:自动生成常见样板
  9. Bean Validation:参数校验声明式化
  10. MapStruct:对象转换自动化
  11. MyBatis-Plus:减少简单 CRUD / 条件查询代码
  12. 枚举承载行为:替代散落的 if/switch
  13. Builder:解决长构造器 / 多可选参数
  14. 泛型统一返回体:减少重复响应结构
  15. 策略模式:把“分支型业务”从 if-else 中解放出来
  16. 建议学习顺序
  17. 高频组合拳

1. record:干掉 DTO / VO / Request 样板

替代什么

替代那种只负责装数据的 POJO:字段、构造器、getter、equals/hashCode/toString

传统写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LoginRequest {
private String username;
private String password;

public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}
}

record 写法

1
2
public record LoginRequest(String username, String password) {
}

使用方式

1
2
3
LoginRequest request = new LoginRequest("Gilbert", "123456");
System.out.println(request.username());
System.out.println(request.password());

适用场景

  • Request DTO
  • Response VO
  • MQ 消息体
  • 查询结果投影
  • 只表达“某一刻数据事实”的对象

坑点

  • record 默认更偏 immutable,不能随手 set
  • 不适合“流程中不断补字段”的对象
  • 访问器不是 getXxx(),而是 xxx()

2. var:减少局部变量类型噪音

替代什么

替代那些右侧类型已经非常明显、但左侧又写一遍很啰嗦的局部变量声明。

传统写法

1
2
Map<Long, List<User>> userMap = users.stream()
.collect(Collectors.groupingBy(User::getDeptId));

var 写法

1
2
var userMap = users.stream()
.collect(Collectors.groupingBy(User::getDeptId));

适用场景

  • 泛型很长
  • 右边类型很明显
  • 局部临时变量

不推荐场景

1
var x = getSomething();

右边看不出类型时,会伤害可读性。

经验

var 是为了让视觉重心落到逻辑,而不是为了显得高级。


3. Optional:减少 null if-else

替代什么

替代多层 null 判断和默认值处理。

传统写法

1
2
3
4
5
6
7
8
User user = userService.getById(id);
if (user != null) {
String name = user.getName();
if (name != null) {
return name.trim();
}
}
return "unknown";

Optional 写法

1
2
3
4
return Optional.ofNullable(userService.getById(id))
.map(User::getName)
.map(String::trim)
.orElse("unknown");

高频写法

1
2
3
4
Optional.ofNullable(obj)
.map(...)
.filter(...)
.orElse(defaultValue);

适用场景

  • 可能为空的返回值
  • 多层取值
  • 默认值 fallback
  • 查不到则抛异常

例子:查不到抛异常

1
2
User user = Optional.ofNullable(userService.getById(id))
.orElseThrow(() -> new RuntimeException("用户不存在"));

坑点

  • 不要把 Optional 当成实体类 / DTO 字段类型到处用
  • 不要写成复杂 monad 表演赛,超过可读性就收手

4. Stream API:声明式集合处理

替代什么

替代“遍历 + 判断 + 转换 + 收集”的大量 for 循环。

传统写法

1
2
3
4
5
6
List<Long> ids = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
ids.add(user.getId());
}
}

Stream 写法

1
2
3
4
List<Long> ids = users.stream()
.filter(user -> user.getAge() >= 18)
.map(User::getId)
.toList();

高频模板

1
2
3
4
5
6
list.stream()
.filter(...)
.map(...)
.distinct()
.sorted()
.toList();

高频收集器

1
2
3
4
5
6
7
Collectors.toList()
Collectors.toSet()
Collectors.toMap(...)
Collectors.groupingBy(...)
Collectors.partitioningBy(...)
Collectors.joining(",")
Collectors.counting()

例子:按部门分组

1
2
Map<Long, List<User>> map = users.stream()
.collect(Collectors.groupingBy(User::getDeptId));

例子:转成 Map

1
2
Map<Long, String> map = users.stream()
.collect(Collectors.toMap(User::getId, User::getName));

坑点

  • 链太长时可读性会下降
  • 涉及副作用、复杂流程时,普通 for 有时更清楚
  • toMap 要注意 key 冲突

5. 方法引用 :::让 lambda 更干净

替代什么

替代那些“只是调用现成方法”的 lambda。

传统 lambda

1
list.stream().map(user -> user.getName()).toList();

方法引用

1
list.stream().map(User::getName).toList();

常见形式

1
2
3
4
User::getId
String::trim
System.out::println
HashMap::new

适用场景

  • map
  • filter
  • forEach
  • 构造对象

坑点

如果方法引用反而让上下文更绕,就用普通 lambda。


6. switch 表达式:替代老式 switch

替代什么

替代 switch + break + 先定义变量后赋值 的老写法。

传统写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String desc;
switch (status) {
case 1:
desc = "待支付";
break;
case 2:
desc = "已支付";
break;
case 3:
desc = "已取消";
break;
default:
desc = "未知";
}

新写法

1
2
3
4
5
6
String desc = switch (status) {
case 1 -> "待支付";
case 2 -> "已支付";
case 3 -> "已取消";
default -> "未知";
};

多语句分支

1
2
3
4
5
6
7
String result = switch (status) {
case 1 -> {
log.info("待支付状态");
yield "待支付";
}
default -> "未知";
};

适用场景

  • 状态码映射
  • 类型选择
  • 策略分发的轻量版

7. Text Block:多行字符串不再丑

替代什么

替代 "...\n" + "...\n" + ... 这种多行字符串拼接。

传统写法

1
2
3
String sql = "select * \n" +
"from tb_user \n" +
"where id = ?";

Text Block 写法

1
2
3
4
5
String sql = """
select *
from tb_user
where id = ?
""";

适用场景

  • SQL
  • JSON mock
  • HTML 片段
  • prompt 文本
  • 长说明字符串

8. Lombok:自动生成常见样板

替代什么

替代 getter / setter / 构造器 / builder / 日志对象 等常见样板。

常用注解

1
2
3
4
5
6
7
8
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Slf4j

例子

1
2
3
4
5
6
7
8
9
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private Long id;
private String name;
private Integer age;
}

Builder 用法

1
2
3
4
5
UserDTO user = UserDTO.builder()
.id(1L)
.name("Gilbert")
.age(20)
.build();

适用场景

  • entity
  • command / dto
  • 配置对象
  • 参数多的对象创建

坑点

  • @Data 不要无脑乱贴到所有类
  • 对实体类要注意 equals/hashCode 范围
  • 团队需要统一风格,否则会出现“有人爱 Lombok,有人不让用”的冲突

经验

  • 简单数据载体:record
  • 可变对象 / entity:Lombok 往往更顺手

9. Bean Validation:参数校验声明式化

替代什么

替代 Controller / Service 里一堆手写 if 参数校验。

传统写法

1
2
3
4
5
6
if (request.getUsername() == null || request.getUsername().isBlank()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (request.getAge() == null || request.getAge() < 0) {
throw new IllegalArgumentException("年龄不合法");
}

Bean Validation 写法

1
2
3
4
5
6
public record RegisterRequest(
@NotBlank String username,
@NotNull @Min(0) Integer age,
@Email String email
) {
}
1
2
3
4
@PostMapping("/register")
public String register(@Valid @RequestBody RegisterRequest request) {
return "ok";
}

常见注解

1
2
3
4
5
6
7
8
@NotNull
@NotBlank
@NotEmpty
@Min
@Max
@Size
@Email
@Pattern

适用场景

  • Controller 入参
  • 表单校验
  • 接口参数约束

坑点

  • 记得配合 @Valid
  • 复杂跨字段校验可能要自定义注解
  • 最好统一异常处理,别让错误响应乱飞

10. MapStruct:对象转换自动化

替代什么

替代“一个字段一个字段搬运”的低价值代码。

传统写法

1
2
3
4
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
vo.setAge(user.getAge());

MapStruct 写法

1
2
3
4
@Mapper(componentModel = "spring")
public interface UserMapper {
UserVO toVO(User user);
}

使用

1
UserVO vo = userMapper.toVO(user);

适用场景

  • Entity -> VO
  • Request -> Command
  • DO -> DTO
  • Page 数据转换

高收益点

在企业项目里,对象转换代码 often 非常多。MapStruct 能明显降低重复劳动。

坑点

  • 需要编译期生成代码,IDE / Maven 配置要对
  • 特殊字段映射要额外写配置

11. MyBatis-Plus:减少简单 CRUD / 条件查询代码

替代什么

替代简单 CRUD XML 和大量重复查询样板。

例子:链式查询

1
2
3
4
lambdaQuery()
.eq(User::getStatus, 1)
.orderByDesc(User::getCreateTime)
.list();

例子:字符串字段版

1
2
3
query().eq("status", 1)
.orderByDesc("create_time")
.list();

推荐优先级

优先使用 lambda 风格:

1
2
3
4
lambdaQuery()
lambdaUpdate()
Wrappers.lambdaQuery()
Wrappers.lambdaUpdate()

适用场景

  • 单表 CRUD
  • 简单分页
  • 常规条件查询
  • 快速开发 demo / 中后台业务

坑点

  • 复杂 SQL 仍然建议回到 MyBatis XML / 注解 SQL
  • 不要为了链式而牺牲可读性
  • 字符串字段名容易写错,能 lambda 就 lambda

12. 枚举承载行为:替代散落的 if/switch

替代什么

替代“状态码 everywhere + 到处 if/switch 做映射”。

普通枚举

1
2
3
4
5
public enum OrderStatus {
UNPAID,
PAID,
CANCELED
}

更实用的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum OrderStatus {
UNPAID(1, "待支付"),
PAID(2, "已支付"),
CANCELED(3, "已取消");

private final int code;
private final String desc;

OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}

public int getCode() {
return code;
}

public String getDesc() {
return desc;
}
}

使用

1
String desc = OrderStatus.PAID.getDesc();

进一步:枚举里放行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum PayType {
ALIPAY {
@Override
public void pay() {
System.out.println("支付宝支付");
}
},
WECHAT {
@Override
public void pay() {
System.out.println("微信支付");
}
};

public abstract void pay();
}

适用场景

  • 状态码
  • 类型映射
  • 小型策略分发

13. Builder:解决长构造器 / 多可选参数

替代什么

替代“字段多时难读又容易错位的构造器调用”。

难读写法

1
User user = new User(1L, "Gilbert", 20, "male", "Guangzhou", null, null);

Builder 写法

1
2
3
4
5
6
User user = User.builder()
.id(1L)
.name("Gilbert")
.age(20)
.city("Guangzhou")
.build();

适用场景

  • 字段很多
  • 可选参数很多
  • 对象创建复杂

常见来源

  • Lombok @Builder
  • 手写 Builder

14. 泛型统一返回体:减少重复响应结构

替代什么

替代每个接口都手搓一套相似响应结构。

例子

1
2
3
4
5
6
7
8
9
10
public record Result<T>(boolean success, String message, T data) {

public static <T> Result<T> ok(T data) {
return new Result<>(true, "ok", data);
}

public static <T> Result<T> fail(String message) {
return new Result<>(false, message, null);
}
}

使用

1
2
return Result.ok(userVO);
return Result.fail("用户不存在");

收益

  • 统一响应格式
  • 静态工厂简化调用
  • 泛型避免重复定义 UserResult / OrderResult

15. 策略模式:把“分支型业务”从 if-else 中解放出来

替代什么

替代那种“分支越来越多”的支付、登录、优惠、消息发送逻辑。

传统 if-else

1
2
3
4
5
6
7
if (type.equals("ALI")) {
// 支付宝逻辑
} else if (type.equals("WX")) {
// 微信逻辑
} else if (type.equals("CARD")) {
// 银行卡逻辑
}

策略接口

1
2
3
public interface PayStrategy {
void pay(Order order);
}

具体实现

1
2
3
4
5
6
public class AliPayStrategy implements PayStrategy {
@Override
public void pay(Order order) {
System.out.println("支付宝支付: " + order.getId());
}
}
1
2
3
4
5
6
public class WechatPayStrategy implements PayStrategy {
@Override
public void pay(Order order) {
System.out.println("微信支付: " + order.getId());
}
}

上下文调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PayService {
private final Map<String, PayStrategy> strategyMap;

public PayService(Map<String, PayStrategy> strategyMap) {
this.strategyMap = strategyMap;
}

public void pay(String type, Order order) {
PayStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付方式");
}
strategy.pay(order);
}
}

适用场景

  • 多支付渠道
  • 多登录方式
  • 多消息发送方式
  • 多优惠计算规则

坑点

  • 分支很少时,别过度设计
  • 不是所有 if-else 都要上策略模式

16. 建议学习顺序

第一阶段:立刻能上手

  1. record
  2. var
  3. Optional
  4. Stream
  5. 方法引用 ::
  6. switch 表达式
  7. Text Block

第二阶段:项目提效最明显

  1. Lombok
  2. Bean Validation
  3. MapStruct
  4. MyBatis-Plus 链式查询
  5. 统一返回体 + 泛型

第三阶段:更有设计感

  1. Builder
  2. 枚举承载行为
  3. 策略模式

17. 高频组合拳

组合 1:Controller 入参

1
2
3
4
5
6
public record RegisterRequest(
@NotBlank String username,
@NotBlank String password,
@Email String email
) {
}
  • record
  • Bean Validation

组合 2:查数据 + 转 VO + 兜底

1
2
3
UserVO vo = Optional.ofNullable(userService.getById(id))
.map(userMapper::toVO)
.orElseThrow(() -> new RuntimeException("用户不存在"));
  • Optional
  • MapStruct

组合 3:集合筛选与转换

1
2
3
4
List<Long> ids = users.stream()
.filter(User::isEnabled)
.map(User::getId)
.toList();
  • Stream
  • 方法引用

组合 4:状态映射

1
2
3
4
5
String desc = switch (status) {
case 1 -> "正常";
case 0 -> "禁用";
default -> "未知";
};
  • switch 表达式

组合 5:快速单表业务

1
2
3
4
5
List<User> list = lambdaQuery()
.eq(User::getStatus, 1)
.like(User::getName, keyword)
.orderByDesc(User::getCreateTime)
.list();
  • MyBatis-Plus lambda 链式查询

最后一句总结

Java 里这些“高阶用法”的共同目标,不是为了让代码看起来 fancy,而是为了做到:

把样板代码最小化,把业务表达最大化。

你以后看到一段代码时,可以先问自己三句:

  1. 这段是不是在重复搬运数据?
  2. 这段是不是在手写集合处理流程?
  3. 这段是不是在做声明式工具已经能做的事情?

如果答案是 yes,大概率就有更 modern 的写法可以替代。


一页速记表

目标 推荐武器
干掉 DTO 样板 record
降低局部变量噪音 var
干掉 null if-else Optional
干掉手写遍历筛选 Stream API
干掉短 lambda 噪音 方法引用 ::
干掉老式 switch switch 表达式
干掉多行字符串拼接 Text Block
干掉 getter / setter / builder Lombok
干掉手写参数校验 Bean Validation
干掉对象搬运 MapStruct
干掉简单 CRUD 样板 MyBatis-Plus
干掉状态分支散落 枚举承载行为
干掉长构造器 Builder
干掉重复响应体 泛型统一返回体
干掉分支型业务 if-else 策略模式