一、前言
在当今快速发展的软件开发领域,Java 作为一门成熟且广泛应用的编程语言,承载着无数企业和开发者的重要项目。然而,随着项目规模的不断扩大和团队协作的日益频繁,仅仅掌握语法知识已经远远不够。编写高质量、可维护、健壮的 Java 代码,不仅需要扎实的技术基础,更需要养成良好的开发习惯。这些习惯涵盖了从代码规范到设计原则,从性能优化到安全防护的方方面面。本文将为您梳理和分享一些在实际工作中被广泛认可的 Java 开发最佳实践,帮助您提升代码质量,增强开发效率,并为团队协作奠定坚实的基础。
二、开发好习惯
2.1 注释尽可能全面
接口方法、类、复杂的业务逻辑,都应该添加有意义的注释
-
接口或方法 的注释中,应该包含详细的入参和结果说明,有异常抛出的情况也要详细叙述
-
类 的注释应该包含类的功能说明、作者和创建时间等等。
-
若业务逻辑很复杂的代码,也需要写清楚注释。
好处:
-
降低维护成本,减少「这是啥?」的吐槽
-
面试/代码审查时直接加分
示例:
java
//正例,✅ 看注释逻辑清晰
/**
* <p>
* 服务实现类
* </p>
*
* @author yl
* @since 2025-10-16 11:18:21
*/
public class QualityClaimServiceImpl{
/**
* 编辑保存数据
*
* @param dto 参数-要被保存的数据
*/
public boolean editQualityClaim(QualityClaimEditSubmitDTO dto) {
//1.校验身份;单据是否存在,校验状态是否为【编辑】
QualityClaim qualityClaim = this.preValidation();
//2.保存数据
this.combineEditSaveQualityClaim(qualityClaim, dto);
return true;
}
}
//反例,❌ 可读性差
public class QualityClaimServiceImpl{
public boolean editQualityClaim(QualityClaimEditSubmitDTO dto) {
QualityClaim qualityClaim = this.preValidation();
this.combineEditSaveQualityClaim(qualityClaim, dto);
return true;
}
}
2.2 形参 ≥ 3 时封装对象
为什么:
-
避免「长参数列表」可读性灾难
-
后续加字段不用改方法签名
好处:
- 可读性↑,维护性↑,可直接加字段不破坏接口
案例对比:
java
// ❌ 可读性差
public void createOrder(Long userId, Long skuId, Integer quantity, BigDecimal price, String coupon) {...}
// ✅ 可读性增强
public void createOrder(CreateOrderDTO dto) {...}
2.3 封装通用模板方法
为什么:
-
避免「复制-粘贴」式代码,减少重复 Bug
-
统一算法骨架,差异点单独实现
好处:
- 复用率↑,维护成本↓,新人一眼看懂流程
案例对比:
java
// ❌ 重复校验不易维护
public void submitOrder(SubmitOrderDTO dto) {
//1.校验订单状态,单据是否存在
Order order = this.getById(dto.getId());
if(order == null){
throw new Exception("当前订单不存在");
{
if(!"PENDING_SUBMIT".equals(order.getStatus()){
throw new Exception("当前订单状态不能提交");
}
}
public void receiveOrder(receiveOrderDTO dto) {
//1.校验订单状态,单据是否存在
Order order = this.getById(dto.getId());
if(order == null){
throw new Exception("当前订单不存在");
{
if(!"PENDING_RECEIVE".equals(order.getStatus()){
throw new Exception("当前订单状态不能收货");
}
}
// ✅ 可读性增强,易于维护
public void submitOrder(SubmitOrderDTO dto) {
//1.校验订单状态,单据是否存在
Order preValid = this.preValid(dto.getId(),"PENDING_SUBMIT","提交");
}
public void receiveOrder(receiveOrderDTO dto) {
//1.校验订单状态,单据是否存在
Order preValid = this.preValid(dto.getId(),"PENDING_RECEIVE","收货");
}
public Order preValid(String id,String status, String operate){
Order order = this.getById(id);
if(order == null){
throw new Exception("当前订单不存在");
{
if(!"status".equals(order.getStatus()){
throw new Exception("当前订单状态不能"+operate);
}
return order;
}
2.4 封装复杂逻辑判断
为什么:
-
复杂 if 嵌套 = 「可读性灾难」
-
方法名 = 自然语言注释
好处:
- 可读性↑,维护性↑,新人一眼看懂业务分支
案例:
java
// ✅ 一眼看懂
if (isVipAndHasCoupon(user, coupon)) {...}
// ✅ 方法封装
private boolean isVipAndHasCoupon(User user, Coupon coupon) {
return user.isVip() && coupon != null && coupon.isValid();
}
2.5 多使用工具类
为什么:
-
StringUtils、CollectionsUtils、Assert、DateUtils= 省时 + 少 Bug -
避免「重复造轮子」
好处:
- 代码更短、更稳、更易读
案例:
java
// ✅ 工具类
StringUtils.isBlank(name)
CollectionsUtils.isEmpty(list)
DateUtils.formatDate(date, "yyyy-MM-dd")
2.6 把日志打印好
为什么:
-
线上故障 = 无日志 = 盲人摸象
-
入口、出口、异常 = 一眼定位问题
好处:
- 线上故障秒定位,减少「这是啥异常?」的吐槽
示例:
java
log.info("【入口】method={}, userId={}", "getBook", userId);
log.info("【出口】method={}, result={}", "getBook", result);
log.error("【异常】method={}, error={}", "getBook", e.getMessage(), e);
插播一篇把日志打印好的文章:
2.7 考虑异常,处理好异常
为什么:
-
异常 = 业务分支,不是 Bug
-
统一异常 = 统一返回 + 统一日志
好处:
- 线上故障秒定位,减少「这是啥异常?」的吐槽
插播一篇定义全局统一异常的文章:
2.8 项目拆分合理目录结构
为什么:
-
目录清晰 = 维护性↑ + 新人上手快
-
避免「一锅炖」目录混乱
好处:
- 维护性↑,新人上手快,目录清晰
比如项目结构如下:
java
src/main/java/com/demo/
├── dictionary/ --数据字典模块
├── job/ ---定时任务模块
├── constance/ ---常量模块
├── util/
├── config/
├── order/---订单模块
├────── controller/
├────── service/
├────── mapper/
├────── model/
├──────────── dto/ --接口入参封装类
├──────────── entity/ --实体类
├──────────── vo/ --接口返回实体封装类
├── user/---用户模块
2.9 查看工具类的实现与结构
比如,我们来看mybatis-plus的批量插入的SQL的部分代码如下:

我们从上图可以看出,在实际实行的时候是通过遍历的方式,每执行一条SQL就只插入一条数据,这在数据量比较少的情况下没什么影响,数据量多的时候就会影响系统性能,可以自定义批量插入SQL的方式实现:
插播一篇批量插入和批量更新数据的自定义sql的文章:
2.9 浮点数等值判断
为什么:
-
0.1 + 0.2 ≠ 0.3= 浮点数精度坑 -
BigDecimal 用
compareTo,基本类型用误差
好处:
- 避免「0.1 + 0.2 ≠ 0.3」的精度坑
案例:
java
//❌ 下列通过浮点数构建BigDecimal会存在精度的丢失
BigDecimal b = new BigDecimal(0.3);
// ✅ 包装类型BigDecimal
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
if (a.compareTo(b) == 0) {...}
// ✅ 基本类型float
float diff = 1e-6F;
if (Math.abs(a - b) < diff) {...}
主包踩过的浮点数的坑:
坑!!!!
若要设置让BigDecimal的数据保留几位小数,若未填写舍入模式会报错:

解决方法:
方法一:指定setScale的第二个参数舍入模式roundingMode
java
BigDecimal decimal = BigDecimal.valueOf(3243.1423412);
decimal.setScale(4, RoundingMode.DOWN);
RoundingMode的分别有如下:

-
UP:向远离零的方向舍入,正数更大,负数更小
-
DOWN:向接近零的方向舍入,正数更小,负数更大
-
CEILING:向正无穷大的方向舍入,正数更大,负数更大
-
FLOOR:向负无穷大的方向舍入,正数更小,负数更小
-
HALF_UP:向"最接近的"整数舍入,四舍五入模式
-
HALF_DOWN:向"最接近的"整数舍入,五舍六入模式
-
HALF_EVEN:向"最接近的"整数舍入
-
若(舍入位大于5) 或者(舍入位等于5 且前一位为奇数),则对舍入部分的前一位数字加1;
-
若(舍入位小于5) 或者(舍入位等于5 且前一位为偶数 ),则直接舍弃。即为银行家舍入模式。
-
-
UNNECESSARY:断言请求的操作具有精确的结果,因此不需要舍入,如果对获得精确结果的操作指定此舍入模式,抛异常ArithmeticException。
方法二:保留小数位数>=实际小数位数
如下图所示:

代码为:
java
BigDecimal decimal = BigDecimal.valueOf(3243.1423);//实际4位
System.out.printf("保留4位:"+ decimal.setScale(4));
System.out.printf("\n保留5位:"+ decimal.setScale(5));
2.10 整型包装类用 equals
为什么:
- Integer -128~127 用
==复用对象,之外用equals
好处:
-
避免「Integer 128 == 128」的坑
-
说明:对于Integer var=?在-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。
示例:
java
Integer a = 128;
Integer b = 128;
if (a.equals(b)) {...} // ✅
2.11 日期时间相关注意
-
日期格式化时,传入pattern中表示年份统一使用小写的y(日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年)
-
程序员因使用YYYY/MM/dd进行日期格式化,2017/12/31执行结果为2018/12/31,造成线上故障。2.【强制】在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。说明:日期格式中的这两对字母表意如下:1)表示月份是大写的M2)表示分钟则是小写的m3)24小时制的是大写的H4)12小时制的则是小写的h
-
获取当前毫秒数:System.currentTimeMillis();而不是newDate().getTime()。说明:获取纳秒级时间,则使用System.nanoTime的方式。在JDK8中,针对统计时间等场景,推荐使用Instant类。
-
不允许在程序任何地方中使用:1)java.sql.Date2)java.sql.Time3)java.sql.Timestamp。不记录想要的时间但是会抛异常。
-
建议使用枚举值来指代月份。如果使用数字,注意Date,Calendar等日期相关类的月份month取值范围从0到11之间。
-
禁止在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
java
//获取今年的天数
int daysOfThisYear =
LocalDate.now().lengthOfYear();
//获取指定某年的天数
LocalDate.of(2011,1,1).lengthOfYear();
2.12 集合使用注意
-
判断所有集合(map,list)内部的元素是否为空,使用isEmpty()方法,而不是size()==0的方式。
-
在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要使用参数类型为BinaryOperator,参数名为mergeFunction的方法,否则当出现相同key时会抛出IllegalStateException异常。
java// 说明:参数 mergeFunction的作用是当出现key重复时,自定义对 value的处理策略。 List<Pair<String, Double>> pairArrayList = new ArrayList<>(3); pairArrayList.add(new Pair<>("version", 12.10)); pairArrayList.add(new Pair<>("version1", 12.19)); pairArrayList.add(new Pair<>("version", 6.28)); //生成的map集合中只有一个键值对:{version=6.28} Map<String, Double> map = pairArrayList.stream() .collect(Collectors.toMap(Pair::getKey, Pair::getValue,(v1,v2)->v2)); //输出结果为: //{version1=12.19, version=6.28} -
在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要注意当value为null时会抛NPE异常。
java// 说明:在 java.util.HashMap的merge方法里会进行如下的判断: if (value == null || remappingFunction == null) throw new NullPointerException(); -
在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删 除产生 ConcurrentModificationException 异常。

-
Collections 类返回的对象,如:emptyList() / singletonList() 等都是 immutable list,不可 对其进行添加或者删除元素的操作。 反例:如果查询无结果,返回 Collections.emptyList() 空集合对象,调用方一旦在返回的集合中进行了添加元素的操 作,就会触发 UnsupportedOperationException 异常。

2.13 避免常见运行错误
在编写代码阶段,就采取措施,避免运行时错误,如数组边界溢出,被零整除,空指针等运行时错误:
数组边界溢出案例:
java
// ❌ list可能越界,因为不一定有2个元素
String name = list.get(1).getName();
//✅ 预防一下数组边界溢出
if(CollectionsUtil.isNotEmpty(list)&& list.size()>1){
String name = list.get(1).getName();
}
被零整除案例:
java
//❌ 坏做法(无日志 + 无异常处理)
public int divide(int a, int b) {
return a / b; // 可能抛 ArithmeticException
}
✅ 好做法(有日志 + 有异常处理)
public int divide(int a, int b) {
// 1. 入口日志
log.info("【除法入口】a={}, b={}", a, b);
// 2. 参数校验
if (b == 0) {
log.error("【除法失败】b=0,不允许除零");
throw new BusinessException("除数不能为0");
}
// 3. 业务逻辑
int result = a / b;
// 4. 出口日志
log.info("【除法成功】result={}", result);
return result;
}
空指针案例:
java
//❌ 坏做法(无日志 + 无异常处理)
public String getUserName(Long userId) {
User user = userService.getById(userId); // 可能返回 null
return user.getName(); // 可能抛 NullPointerException
}
//✅ 好做法(有日志 + 有异常处理)
public String getUserName(Long userId) {
// 1. 入口日志
log.info("【查询用户入口】userId={}", userId);
// 2. 业务逻辑
User user = userService.getById(userId);
if (user == null) {
log.error("【查询用户失败】userId={} 不存在", userId);
throw new BusinessException("用户不存在");
}
// 3. 出口日志
log.info("【查询用户成功】userId={}, name={}", userId, user.getName());
return user.getName();
}
2.14 在finally中释放资源
为什么在 finally 中释放资源
- 确保资源释放:无论 try 块中的代码是否抛出异常,finally 块都会执行
- 避免资源泄漏:防止文件句柄、数据库连接、网络连接等资源未被正确关闭
- 提高程序稳定性:及时释放系统资源,避免达到资源限制
下面代码中,FileInputStream 资源在 finally 块中被释放,确保即使在文件读取过程中发生异常,文件流也能被正确关闭。
java
public void readFile(String filename) {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
// 文件读取操作
// 可能抛出 IOException
} catch (IOException e) {
// 处理异常
System.err.println("文件读取失败: " + e.getMessage());
} finally {
// 在这里释放资源
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.err.println("关闭文件流失败: " + e.getMessage());
}
}
}
}
2.15 控制方法函数的复杂度
写的每一个方法都不要写得太复杂,逻辑不要混乱,也不要太长。一个函数不能超过80行(若超过80行就采用封装方法的方式处理)。
编码其他规范,解决办法:
在IDEA安装下图两个插件:
- 第一个插件可以扫描指定目录下的所有文件 或代码块 或指定文件,并给出优化建议
- 第二个插件,可以帮助我们修复第一个插件扫描后的建议

养成良好的开发习惯需要持续的实践和积累。希望这些分享能帮助您在日常开发中写出更高质量的 Java 代码,提升开发效率和代码质量。