java后端好习惯---新手养成记

一、前言

在当今快速发展的软件开发领域,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 多使用工具类

为什么:

  • StringUtilsCollectionsUtils、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);

插播一篇把日志打印好的文章:

Java 日志打印最佳实践:15 条建议--新手养成记-CSDN博客https://blog.csdn.net/weixin_57259781/article/details/155784258?spm=1011.2124.3001.6209

2.7 考虑异常,处理好异常

为什么:

  • 异常 = 业务分支,不是 Bug

  • 统一异常 = 统一返回 + 统一日志

好处:

  • 线上故障秒定位,减少「这是啥异常?」的吐槽

插播一篇定义全局统一异常的文章:

springboot统一异常处理_springboot中统一异常处理-CSDN博客https://blog.csdn.net/weixin_57259781/article/details/136752013?spm=1001.2014.3001.5502

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的文章:

mySQL数据库,mybatis实现大批量插入和更新的SQL语句(亲测有效)_mybatis mysql 批量插入-CSDN博客https://blog.csdn.net/weixin_57259781/article/details/144031428?spm=1001.2014.3001.5502

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 代码,提升开发效率和代码质量。

相关推荐
Acc1oFl4g2 小时前
Java安全之SpEL表达式注入入门学习
java·学习·安全
风华同学2 小时前
【系统移植篇】系统烧写
java·开发语言·前端
武哥聊编程2 小时前
【从0带做】基于Springboot3+Vue3的生态养殖管理系统
java·学习·vue·毕业设计·springboot
隔山打牛牛2 小时前
如何实现jvm中自定义加载器?
java
by__csdn2 小时前
JavaScript性能优化实战:异步与延迟加载全方位攻略
开发语言·前端·javascript·vue.js·react.js·typescript·ecmascript
阿里嘎多学长2 小时前
2025-12-11 GitHub 热点项目精选
开发语言·程序员·github·代码托管
菜鸟233号2 小时前
力扣106 从中序与后序遍历序列构造二叉树 java实现
java·算法·leetcode
YJlio2 小时前
Active Directory 工具学习笔记(10.11):AdRestore 实战脚本与命令速查——从事故回滚到合规留痕
java·笔记·学习
diudiu96282 小时前
Logback使用指南
java·开发语言·spring boot·后端·spring·logback