摘要
这篇文章是关于Java开发中阿里巴巴编码规范的经验总结。它强调了避免使用Apache BeanUtils进行属性复制,因为它效率低下且类型转换不安全。推荐使用Spring BeanUtils、Hutool BeanUtil、MapStruct或手动赋值等替代方案。文章还指出不应在视图模板中加入复杂逻辑运算,应明确MVC架构各层的职责。此外,还涉及数据结构初始化应指定大小、正则表达式的预编译、避免通过catch处理某些RuntimeException异常、finally块中资源关闭的正确方式以及防止NPE的多种方法。
1. 【强制】避免用 ApacheBeanutils 进行属性的 copy。
不推荐使用 Apache Commons BeanUtils 工具来进行对象属性复制(如 BeanUtils.copyProperties),因为它效率低、性能差、类型转换不安全,在生产环境中容易成为性能瓶颈。
Apache BeanUtils 是通过反射 + 内省(Introspector)+ 字符串转换来做属性 copy,性能非常低,不适合在高并发或大量对象转换场景中使用。
1.1. 属性赋值推荐方案
方案 |
优势 |
场景 |
Spring BeanUtils |
性能略优于 Apache,但仍是反射 |
适合小量级对象拷贝 |
Hutool BeanUtil |
性能高,支持深拷贝、自定义字段映射 |
推荐在工具类中统一封装 |
MapStruct |
编译期生成拷贝代码(无反射,极快) |
推荐在 DDD 中的 DO <-> DTO 映射 |
手动赋值 |
最安全、最清晰 |
小对象或关键转换逻辑 |
ModelMapper / Dozer(不推荐) |
仍是反射,配置复杂,性能低 |
不推荐使用 |
2. 【强制】不要在视图模板中加入任何复杂的逻辑运算。
在 MVC 架构的具体实现中,比如 Spring Boot 项目中,我们常见的结构包括:
- Controller(控制器)
- Service(服务/业务逻辑层)
- DAO(数据访问层,也叫 Mapper、Repository)
下面是这三层的职责和理解方式,结合“不要在视图中写复杂逻辑”的那条建议,进一步深化层次的划分:
2.1. Controller:控制层
职责:
- 接收 HTTP 请求参数;
- 调用 Service 进行处理;
- 封装和返回响应数据(
Response
); - 做参数校验、权限判断、日志记录等外围操作。
不要做的事:
- 不要写业务逻辑;
- 不要操作数据库;
- 不要做复杂的流程判断或数据处理。
示例:
@PostMapping("/user/upgrade")
public Response<Void> upgradeUser(@RequestBody UserUpgradeRequest request) {
userService.upgradeUserToVip(request.getUserId());
return Response.success();
}
2.2. Service:业务逻辑层
职责:
- 实现具体业务逻辑,如“升级用户为 VIP”、“扣减库存”、“发送通知”等;
- 调用多个 DAO、封装业务判断流程;
- 做事务控制(
@Transactional
); - 组装处理结果返回 Controller。
不要做的事:
- 不要和 Web 框架(如 Servlet、HttpRequest)耦合;
- 不要拼 SQL,不直接操作数据库。
示例:
public void upgradeUserToVip(Long userId) {
UserDO user = userDao.findById(userId);
if (user == null || user.isVip()) {
throw new BizException("用户不存在或已是VIP");
}
user.setVip(true);
userDao.update(user);
notifyService.sendVipNotification(user);
}
2.3. DAO(Mapper/Repository):数据访问层
职责:
- 直接与数据库交互;
- 封装 SQL 查询(或通过 MyBatis/JPA 映射);
- 只做增删改查操作;
- 返回实体对象,不做业务判断。
不要做的事:
- 不要处理业务逻辑;
- 不要做流程判断;
- 不要拼接复杂结果(如组装响应对象)。
示例:
@Mapper
public interface UserDao {
UserDO findById(Long userId);
int update(UserDO user);
}
2.4. 总结类比:工厂分工
层级 |
类比 |
职责 |
Controller |
前台接待 |
接收客户请求,转交到内部处理 |
Service |
经理 |
安排工人干活,处理流程,判断异常 |
DAO |
工人 |
操作数据库,搬原材料,不决策 |
2.5. MVC 和模板逻辑的对应关系
- 视图(View) = 页面模板、前端:只能展示数据,不参与 Controller、Service、DAO 的职责。
- 所以复杂判断、业务数据准备都应该在 Service/Controller 中完成,模板中直接展示结果即可。
3. 【强制】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
3.1. 这条建议的核心思想是
在使用如集合(List、Map、Set 等)这类可扩展数据结构时,应尽可能“预估其大小”并“显式设置初始容量”,从而避免它们在运行中频繁扩容、内存抖动,甚至 OOM(内存溢出)的问题。提前预估数据量,合理初始化集合容量,是性能优化与内存安全的重要实践。
ArrayList
HashMap
HashSet
ConcurrentHashMap
StringBuilder
自定义缓存、队列等
这些类的背后都依赖一个数组或哈希桶来存储数据,如果你没有指定容量,它们会用默认大小初始化,然后在插入过程中自动扩容(重新开数组、拷贝数据等)。
3.2. 为什么要指定大小?
3.2.1. 不指定容量的风险:
- 频繁扩容: 每次容量不够都要重新分配数组,拷贝旧数据 ➜ 性能开销大;
- 内存浪费: 扩容步长不是线性的,可能会分配远超实际需要的空间;
- 内存溢出(OOM): 在循环里构造数据结构没有设置上限 ➜ 无限增长,吃光堆内存。
3.2.2. 指定容量的好处:
- 减少扩容次数: 提高性能;
- 控制内存: 限制最大容量,防止意外 OOM;
- 体现程序边界意识: 编码更健壮。
3.3. 数据结构设置初始值示例对比
3.3.1. 不指定大小(有性能隐患):
List<String> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add("item" + i);
}
默认容量是 10,之后 1.5 倍扩容 ➜ 至少扩容 10+ 次,代价很高
3.3.2. 指定大小(性能友好):
List<String> list = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
list.add("item" + i);
}
只创建一次内部数组,避免扩容
3.4. 延伸到场景
数据结构 |
默认容量 |
推荐用法 |
|
10 |
new ArrayList<>(预计数量) |
|
16 |
new HashMap<>(预计数量 / 负载因子 + 1) |
|
16 |
new StringBuilder(预计字符串长度) |
|
16 |
new ConcurrentHashMap<>(预计大小) |
4. 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile("规则");
正则表达式在使用时,如果每次都重新编译,会严重影响性能。应该使用预编译(Pattern.compile(...)
)方式,将正则表达式提前编译好并重复使用。
在 Java 中使用正则时,一般有两种方式:
4.1. 每次都编译(效率低)
boolean isMatch = "abc123".matches("\\w+");
内部其实相当于:
Pattern.compile("\\w+").matcher("abc123").matches();
这会每次调用都重新编译正则表达式,开销很大,尤其在循环或高并发下。
4.2. 预编译后复用(推荐)
private static final Pattern PATTERN = Pattern.compile("\\w+");
boolean isMatch = PATTERN.matcher("abc123").matches();
正则表达式只在类加载时编译一次,后续调用直接复用,提高性能。
4.3. 使用场景
场景 |
是否推荐预编译 |
单次用、不频繁 |
可以临时用 |
多次校验、循环中用 |
必须预编译 |
高并发服务接口中 |
必须预编译 |
工具类/公共方法 |
强烈建议预编译并静态缓存 |
4.4. 总结
- 编译正则是耗时操作;
- 多次使用时,一定要用
Pattern.compile(...)
并缓存起来; - 正则预编译 = 性能优化 + 好习惯。
5. 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
try-catch 是用来处理不可预知的异常情况,不是用来“代替 if 判断”的。
对于 Java 类库中常见的 RuntimeException(运行时异常),如果我们可以在代码运行前通过逻辑“预检查”避免它的发生,就不应该依赖 try-catch 来处理它。
5.1. 举几个典型例子
5.1.1. 不推荐的做法(用 catch
捕获 NPE):
try {
System.out.println(user.getName());
} catch (NullPointerException e) {
// 捕获空指针异常
System.out.println("user 为空");
}
5.1.2. 推荐的做法(用 if
判断提前规避):
if (user != null) {
System.out.println(user.getName());
} else {
System.out.println("user 为空");
}
5.2. 为什么不推荐用 catch 处理这些异常?
- 这类异常不是业务异常,而是代码逻辑错误:出现 NullPointer、数组越界等,说明你的代码逻辑写得有问题,不是正常的“可恢复”情况。
- catch 成本高,影响性能:try-catch 的异常捕获机制在 JVM 中性能是开销较大的(尤其是频繁抛异常的情况)。
- 可读性变差,调试困难:滥用 catch 会把真正的问题掩盖,调试困难,也不利于代码维护。
5.3. 适用的异常类型(不建议 catch)
异常类 |
说明 |
|
空指针异常,应通过非空判断避免 |
|
下标越界,应判断下标是否合法 |
|
类型转换错误,应先 |
|
参数非法,应通过参数校验处理 |
5.4. 异常捕获正确的原则
- 能通过逻辑避免的异常,不要 try-catch
- RuntimeException 更多是一种编码警告,不是业务流程的一部分
- 只在顶层兜底或做日志监控时统一捕获这些异常
6. 【强制】finally 块必须对资源对象、 流对象进行关闭,有异常也要做 try-catch。
无论是否发生异常,finally
块中一定要确保资源被正确关闭,且关闭操作本身也要加 try-catch
,避免二次异常导致资源未释放。
6.1. 正确的使用方式示例:
自 Java 7 起,Java 提供了 try-with-resources
语法,它能够自动关闭实现了 AutoCloseable
或 Closeable
接口的资源(如 InputStream
)。使用该语法,可以消除手动管理资源关闭的复杂性,并自动处理 close()
方法可能抛出的异常。
try (InputStream in = new FileInputStream("data.txt")) {
// 读文件逻辑
} catch (IOException e) {
e.printStackTrace(); // 异常处理
}
6.2. 错误的示例(不捕获关闭异常):
finally {
in.close(); // 如果这里抛出 IOException,整个异常流程会被覆盖
}
6.3. 适用范围:
这条规范适用于所有需要关闭或释放的资源类,例如:
- IO 流(InputStream、OutputStream、Reader、Writer 等)
- 数据库连接(Connection、Statement、ResultSet)
- 网络资源(Socket、HttpURLConnection)
- 文件句柄
- 线程池(ExecutorService 的
shutdown
) - 锁(
Lock.unlock()
)
7. 【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景
1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE,反例:public int method() { return Integer 对象; },如果为 null,自动解箱抛 NPE。
2)数据库的查询结果可能为 null。
3)集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5)对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6)级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。正例: 使用 JDK8 的 Optional 类来防止 NPE 问题。
7.1. 常见的 NPE 产生场景
7.1.1. 访问未初始化的对象
当你尝试访问一个未初始化的对象(即其值为 null
)时,通常会抛出 NPE。
String str = null;
int length = str.length(); // NPE: str 是 null,无法调用 length()
7.1.2. 调用 null
对象的实例方法
如果对象为 null
,直接调用其方法会导致空指针异常。
MyClass obj = null;
obj.someMethod(); // NPE: obj 是 null,无法调用 someMethod()
7.1.3. 尝试访问 null
数组元素
对 null
数组尝试访问元素时也会抛出 NPE。
String[] arr = null;
String element = arr[0]; // NPE: arr 是 null,无法访问元素
7.1.4. 传递 null
给不接受 null
的方法
有些方法要求传入非 null
的参数,如果传入 null
,可能会触发 NPE。
public void printLength(String str) {
System.out.println(str.length()); // 如果 str 为 null,将引发 NPE
}
7.1.5. 链式调用中的空指针
在链式调用中,如果某一环节返回了 null
,而后续还对其进行方法调用,就会导致 NPE。
Person person = getPerson();
int age = person.getAddress().getCity().getZipCode(); // 如果 person 或 address 为 null,则会 NPE
7.2. 如何防止NPE问题?
7.2.1. 避免使用 null
值
尽量避免使用 null
,特别是在可能触发 NPE 的地方。可以使用 Optional 来表示可能为空的值。
Optional<String> optionalStr = Optional.ofNullable(str);
optionalStr.ifPresent(s -> System.out.println(s.length())); // 安全访问
7.2.2. 空值检查
在调用对象的方法之前,先检查对象是否为 null
。
if (str != null) {
System.out.println(str.length()); // 只有 str 非 null 时才调用方法
} else {
System.out.println("str is null");
}
7.2.3. 使用默认值
如果方法或字段值可能为 null
,考虑使用默认值或替代值。
String str = Optional.ofNullable(inputString).orElse("default value");
7.2.4. 适用断言和工具库
通过工具库如 Apache Commons Lang 提供的 StringUtils
或 ObjectUtils
等可以避免手动编写空值检查代码,减少 NPE 风险。
StringUtils.isNotEmpty(str); // 不会抛出空指针异常
7.2.5. 使用 @NonNull
和 @Nullable
注解
通过注解可以清楚标明方法参数或返回值是否可以为空,这有助于避免因不清楚空指针约束导致的 NPE。
public void processString(@NonNull String str) {
// str 必须不为 null
}
7.2.6. 避免深层嵌套的链式调用
通过设计合理的 API 接口或引入中间变量,避免深层次的链式调用,降低因某一环节为 null
导致的 NPE 风险。
Address address = person != null ? person.getAddress() : null;
if (address != null) {
// 安全地访问 address
}