0
点赞
收藏
分享

微信扫一扫

[代码规范]分享20条实用的代码规范,一起来诊断你的代码

墨春 2021-12-20 阅读 50
日记本

代码规范的意义

我们编写的代码,通过编译打包,最终都会运行在网络上面。在网络世界中,进程与进程之间通信,就得通过通信协议,这是为了约定一个规范,一个行业标准,使得数据之间通信变得标准化,这也往往可以增加代码运行的可靠性。
制定软件的代码规范,对于一个完整的产品团队,尤为重要。因为一个完整的产品,往往意味着长期的版本迭代,在这个过程中,由于需求、人力资源、架构变迁等因素,往往会出现截然不同的代码风格。因此遵守统一的代码规范,不仅使得代码更加优雅,同时也能增加项目的稳定性、容错率。

参考资料

  • 《Java开发手册》- 嵩山版
  • 《码出高效:Java开发手册》
  • 《代码整洁之道》

20条代码规范的建议

1. 代码中命名应该有意义

命名这件事,我们可能都在做,定义一个工程、包、类、方法、参数等。然而,在许多的代码里面,却充斥着许多意义不明的代码。
在一些团队的代码审查环节中,遇到一个奇怪的命名,许多人可能第一时间想到: 我加点注释,通过这种方式修正这个问题,往往是掩耳盗铃的。
因为在Java中,注释会被编译器所忽略,所以做好命名这件事情,尤为重要,如果你不想被你的同事吐槽的话。

  • 不友好的命名示例
/**
 * 这段代码充满了坏味道:  <br>
 * 1. p代表什么意义?     <br>
 * 2. selectList()到底返回了什么? list能准确表达语意吗? <br>
 * 3. 遍历中的s类型是Student,但是如果你不看Student,你无法知道它真正代表什么 <br>
 * 4. 0到底代表什么?   <br>
 * 5. 方法名:do_something代表什么意思?这样命名是规范的吗? <br>
 */
public Date do_something(String p) {
    List<Student> list = selectList(p);
    for (Student s : list) {
        if (s.getGender().equals(0)) {
            return s.getBirthDay();
        }
    }
    return null;
}
  • 给代码起一个好名字
/**
 * 男人
 */
private static final int MAN = 0;

/**
 * 获取当前男学生姓名的生日
 * @param studentName 学生姓名
 * @return Date
 */
public Date getBirthDayOfMan(String studentName) {
    List<Student> students = selectList(studentName);
    if (CollectionUtils.isEmpty(students)) {
        return null;
    }
    for (Student student : students) {
        if (isMan(student.getGender())) {
            return student.getBirthDay();
        }
    }
    return null;
}
private boolean isMan(Integer gender) {
    return Objects.equals(gender, MAN);
}
命名的一些好方法
  1. 使用标准的英文命名,切勿使用中文命名,除了常量,切勿使用_.
  2. 包名小写,类名遵循UpperCamelCase风格,方法名、变量名遵循lowCamelCase风格.
  3. 常量需要大写,单词间以_进行区分,例如USER_NAME.
  4. 表示复数的时候,用s比较好,使用userList这种命名,如果调用的方法在后面的版本返回了Map类型,就会产生歧义.
  5. 遵循"望文知义"的原则,在方法和参数命名上体现功能和参数的意义.
类的功能类型 命名规范
抽象类 Abstract或者Base开头
异常类 以Exception结尾,例如:BaseBusinessException.
单元测试类 以Test结尾,例如:FastJsonTest.
枚举类 以Enum结尾,例如:GenderEnum.

2. 使用枚举类或者常量来代替魔法值

  • 魔法值可读性较低
/**
 * 获取当前季节对应的英文名称
 * @param season 季节值
 * @return
 */
public String convertSeasonName(Integer season) {
    if (season.equals(1)) {
        return "Spring";
    } else if (season.equals(2)) {
        return "Summer";
    } else if (season.equals(3)) {
        return "Autumn";
    } else if (season.equals(4)) {
        return "Winter";
    }
    return null;
}
  • 使用枚举进行转换
/**
 * 季节枚举类
 */
public enum SeasonEnum {
    /**
     * 春天
     */
    SPRING(1, "Spring"),
    /**
     * 夏天
     */
    SUMMER(2, "Summer"),
    /**
     * 秋天
     */
    AUTUMN(3, "Autumn"),
    /**
     * 冬天
     */
    WINTER(4, "Winter");
    /**
     * 季节值
     */
    private Integer season;
    /**
     * 季节名称
     */
    private String name;

    SeasonEnum(int season, String name) {
        this.season = season;
        this.name = name;
    }

    /**
     * 转换季节名称
     *
     * @param season 季节值
     * @return seasonName
     */
    public static String convertSeasonName(Integer season) {
        for (SeasonEnum seasonEnum : SeasonEnum.values()) {
            if (seasonEnum.season.equals(season)) {
                return seasonEnum.name;
            }
        }
        return null;
    }
}

3. 千万不要粘贴复制代码,使用封装来提高复用性

无论你的任务多么繁重,切记粘贴复制代码,当你发现代码存在大量相同的代码块,使用封装来解决这种问题.

  • 大量重复的setter
/**
 * 每次新增数据,都要设置这几个必要的属性值.造成大量的setter泛滥.
 * @param user 用户
 */
public void register(User user) {
    String userId = "textUserId";
    Date now = new Date();
    user.setCreateUser(userId);
    user.setUpdateUser(userId);
    user.setCreateTime(now);
    user.setUpdateTime(now);
    user.setHasDelete(DeleteEnum.NO_DELETE.getIsDelete());
    save(user);
}
  • 使用继承对代码进行复用
package com.tea.modules.model;

import com.tea.modules.enums.DeleteEnum;
import lombok.Data;

import java.util.Date;

/**
 * com.tea.modules.model
 *
 * @author jaymin
 * @since 2021/5/15
 */
@Data
public class BaseModel {
    private Date createTime;
    private Date updateTime;
    private String createUser;
    private String updateUser;
    private Integer hasDelete;

    public void initialize(String userId) {
        Date now = new Date();
        this.createUser = userId;
        this.updateUser = updateUser;
        this.createTime = now;
        this.updateTime = now;
        this.hasDelete = DeleteEnum.DELETE.getIsDelete();
    }
}

/**
 * 使用封装让代码复用性更加强.
 * @param user
 */
public void registerWithReuse(User user){
    String userId = "textUserId";
    user.initialize(userId);
    save(user);
}

4. 注释:将你的想法告诉他人,而不是解释坏味道的代码

我们来用注释对之前的案例进行一番"解释":

  • 在坏味道的代码中添加注释试图挽救
/**
 * 找到男性中名字等于p的生日日期    
 */
public Date do_something(String p) {
    // 学生列表
    List<Student> list = selectList(p);
    // 遍历学生列表
    for (Student s : list) {
        // 如果是男生,0-男生
        if (s.getGender().equals(0)) {
            return s.getBirthDay();
        }
    }
    return null;
}
  • Spring中的注释是怎样写的
/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>This method allows a Spring BeanFactory to be used as a replacement for the
 * Singleton or Prototype design pattern. Callers may retain references to
 * returned objects in the case of Singleton beans.
 * <p>Translates aliases back to the corresponding canonical bean name.
 * Will ask the parent factory if the bean cannot be found in this factory instance.
 * @param name the name of the bean to retrieve
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no bean with the specified name
 * @throws BeansException if the bean could not be obtained
 */
Object getBean(String name) throws BeansException;
关于注释的几个建议
  • 接口使用JavaDoc注释标注输入参数、返回参数、可能抛出的异常、实现了什么功能.
  • 类注明职责,写上创建者和创建时间.可以在IDEA定制template.
  • 修改代码,也应该同步修改注释.
  • 如果存在优化空间,最好加上TODO.
  • 好的代码应该从代码层面进行整洁,而不是为每一行代码加上注释,那无疑是掩耳盗铃.

5. 避免出现大量的嵌套控制语句

控制语句在代码中很常见,但有些时候,我们可以避免一些控制语句的泛滥.
来看一个简单的例子

  • 方法中对参数进行校验
public void activateAccount(String id) {
    if (StringUtils.isNotEmpty(id)) {
        if (isValid(id)) {
            this.activate(id);
        }
    }
}
  • 使用卫语句进行优化
public void activateUserAccount(String id) {
    if (StringUtils.isBlank(id)) {
        return;
    }
    if (isValid(id)) {
        this.activate(id);
    }
}

6. for-each优于传统的for循环

  • index-i值没有意义
List<Student> students = new ArrayList<>();
students.add(new Student());
students.add(new Student());
for (int i = 0; i < students.size(); i++) {
    Student student = students.get(i);
    System.out.println(student.getName());
}
  • 使用for-each
for (Student student : students) {
    System.out.println(student.getName());
}

7. 涉及金融的计算,请使用BigDecimal

  • float带来的精度问题
// 输出 0.6000000000000001
System.out.println(1.02 - 0.42);
  • 使用BigDecimal进行精度计算
BigDecimal balance = BigDecimal.valueOf(1.02);
BigDecimal price = BigDecimal.valueOf(0.42);
// 输出0.60
System.out.println(balance.subtract(price));

8. 使用try-with-resources来关闭资源

  • 使用IO流时,关闭资源特别地啰嗦
File noExitsFile = new File("D:\\logs\\notExistFile");
FileOutputStream fileOutputStream = null;
try {
    fileOutputStream = new FileOutputStream(noExitsFile);
} catch (FileNotFoundException e) {
    // 实际工作中,应使用自定义异常将此错误抛出
    log.error("An exception occurred when opening a file", e);
} finally {
    if (Objects.nonNull(fileOutputStream)) {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            log.error("An exception occurred when closing the stream");
        }
    }
}
  • 使用try-with-resources关闭资源
/**
 * 使用try-with-resources,事半功倍
 */
public void closeByTryWithResources() {
    try (FileOutputStream fileOutputStream = new FileOutputStream("D:\\logs\\notExistFile")) {
        fileOutputStream.write("something".getBytes());
    } catch (IOException exception) {
        log.error("An exception occurred when closing the stream");
    }
}

9. 方法的入参不宜过多

  • 方法中声明过长的参数列表
public List<Student> getStudentsInfo(String studentName,
                                    String school,
                                    Date createTime,
                                    Long page,
                                    Long pageSize,
                                    Integer age) {
    List<Student> students = selectList(studentName, school, createTime, page, pageSize, age);
    return students;
}
  • 使用对象对参数列表进行封装
@Data
public class StudentQueryVO {
    private String studentName;
    private String school;
    private Date createTime;
    private Long page;
    private Long pageSize;
    private Integer age;
}
public List<Student> getStudentsInfo(StudentQueryVO queryVO) {
    List<Student> students = selectList(queryVO);
    return students;
}

10. 涉及查询列表的接口,返回空集合比null更好

  • 直接返回null
public List<Student> getStudentsInfo(String studentName) {
    List<Student> students = list(studentName);
    if (Objects.isNull(students)) {
        return null;
    }
    return students;
}
  • 返回空集合
public List<Student> queryStudentsInfo(String studentName) {
    List<Student> students = list(studentName);
    if (Objects.isNull(students)) {
        return Collections.emptyList();
    }
    return students;
}

11. 打印日志,请使用门面模式的SLF4J日志框架

日志框架可能会被替换,当你从logback转换到log4j2时,之前使用的logback打印日志会出现问题,使用SLF4J来规避这种升级带来的麻烦.

12. 使用日志而非System.out或者System.err打印异常堆栈.

13. 如果你的方法超过80行,应考虑重构.

太长的方法一屏放不下,在维护的过程中,我们也更希望看到短小的代码块而非冗长的“面条”。我们不仅倡导类遵循单一职责原则,同时,也应该让方法本身执行一件事情。

  • 写得比较长的方法
package com.tea.methodtoolong;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.tea.modules.exception.RestfulException;
import com.tea.modules.model.ExchangeInfoRequest;
import com.tea.modules.model.ExchangeResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * com.tea.methodtoolong<br>
 * 方法写太长,可读性较低
 *
 * @author jaymin
 * @since 2021/5/15
 */
@Component
@Slf4j
public class MethodTooLongDemonstration {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private RedisTemplate redisTemplate;
    private final String URL = "http://demo/v6/lasted";
    private static final List<String> exchangeCodeSortList = new ArrayList<>();

    static {
        exchangeCodeSortList.add("USD");
        exchangeCodeSortList.add("CHY");
        exchangeCodeSortList.add("HK");
    }

    public ExchangeResponse getExchangeInfo(String exchangeCode) {
        if (StringUtils.isEmpty(exchangeCode)) {
            return new ExchangeResponse();
        }
        Object cacheResult = redisTemplate.opsForValue().get(exchangeCode);
        if (Objects.nonNull(cacheResult)) {
            String jsonString = JSON.toJSONString(cacheResult);
            ExchangeResponse exchangeResponse = JSONObject.parseObject(jsonString, ExchangeResponse.class);
            return exchangeResponse;
        }
        ExchangeInfoRequest exchangeInfoRequest = new ExchangeInfoRequest();
        exchangeInfoRequest.setExchangeCode(exchangeCode);
        exchangeInfoRequest.setUpdateTime(new Date());
        ExchangeResponse exchangeResponse = null;
        long startTime = System.currentTimeMillis();
        log.info("request api :[{}],params:{}", URL, JSON.toJSONString(exchangeInfoRequest));
        try {
            exchangeResponse = restTemplate.getForObject(URL, ExchangeResponse.class, exchangeInfoRequest);
            long endTime = System.currentTimeMillis();
            log.info("request api :[{}] uses {} ms", URL, endTime - startTime);
        } catch (RestClientException e) {
            throw new RestfulException("调用:[" + URL + "]api出错,错误原因:" + e);
        }
        if (Objects.isNull(exchangeResponse)) {
            return new ExchangeResponse();
        }
        Map<String, Object> resultMap = new TreeMap();
        for (Map.Entry<String, Object> conversionRate : exchangeResponse.getConversionRates().entrySet()) {
            String currentExchangeCode = conversionRate.getKey();
            Object currentExchange = conversionRate.getValue();
            int index = exchangeCodeSortList.indexOf(currentExchangeCode);
            resultMap.put(Integer.valueOf(index).toString(), currentExchange);
        }
        exchangeResponse.setConversionRates(resultMap);
        redisTemplate.opsForValue().set("exchange:cache", JSON.toJSONString(exchangeResponse), 30, TimeUnit.MINUTES);
        return exchangeResponse;
    }
}

  • 改进
  • 将setter进行封装
package com.tea.modules.model;


import lombok.Data;

import java.util.Date;

/**
 * com.tea.modules.model
 *
 * @author jaymin
 * @since 2021/5/15
 */
@Data
public class ExchangeInfoRequest {
    private String exchangeCode;
    private Date updateTime;

    public static ExchangeInfoRequest init(String exchangeCode){
        ExchangeInfoRequest exchangeInfoRequest = new ExchangeInfoRequest();
        exchangeInfoRequest.setExchangeCode(exchangeCode);
        exchangeInfoRequest.setUpdateTime(new Date());
        return exchangeInfoRequest;
    }
}
  • 将http请求进行封装
private ExchangeResponse requestExchangeInfo(String exchangeCode) {
    ExchangeInfoRequest exchangeInfoRequest = ExchangeInfoRequest.init(exchangeCode);
    ExchangeResponse exchangeResponse = null;
    long startTime = System.currentTimeMillis();
    log.info("request api :[{}],params:{}", URL, JSON.toJSONString(exchangeInfoRequest));
    try {
        exchangeResponse = restTemplate.getForObject(URL, ExchangeResponse.class, exchangeInfoRequest);
        long endTime = System.currentTimeMillis();
        log.info("request api :[{}] uses {} ms", URL, endTime - startTime);
    } catch (RestClientException e) {
        throw new RestfulException("调用:[" + URL + "]api出错,错误原因:" + e);
    }
    if (Objects.isNull(exchangeResponse)) {
        return new ExchangeResponse();
    }
    sortExchangeCode(exchangeResponse);
    return exchangeResponse;
}
  • 将排序方法进行封装
private void sortExchangeCode(ExchangeResponse exchangeResponse) {
    TreeMap<String, Object> resultMap = exchangeResponse.getConversionRates().entrySet().stream()
            .sorted(Comparator.comparingInt(exchangeCodeSortList::indexOf))
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    Map.Entry::getValue,
                    (oldValue, newValue) -> oldValue,
                    TreeMap::new));
    exchangeResponse.setConversionRates(resultMap);
}
  • 使用@Cacheable进行缓存
@Cacheable(key = "#exchangeCode", value = "exchange:cache", unless = "#result == null")
public ExchangeResponse queryExchangeInfo(String exchangeCode) {
    ExchangeResponse exchangeResponse = requestExchangeInfo(exchangeCode);
    return exchangeResponse;
}

14. 暴露的接口始终对参数进行校验

服务端的接口都是对外暴露的,恶意攻击者可以直接绕过前端向服务端发起http请求,所以不要相信请求的数据,始终对参数进行校验.
参数校验需要关注的点:

  1. page size过大.
  2. 恶意order by导致数据库慢查询.
  3. SSRF.
  4. 缓存击穿.
  5. SQL注入,反序列化攻击
  6. 拒绝服务攻击.

15. 对象属性始终遵循驼峰命名规则

  • 错误的对象定义
package com.tea.modules.model;

import lombok.Data;

import java.math.BigDecimal;

/**
 * com.tea.modules.model
 *
 * @author jaymin
 * @since 2021/5/15
 */
@Data
public class HttpResponse {
    private String user_name;
    private String _id;
    private BigDecimal book_price;
}
  • demo
String responseJson = "{\n" +
        "    \"_id\": 1,\n" +
        "    \"user_name\": \"jaymin\",\n" +
        "    \"book_price\": 20\n" +
        "}";
HttpResponse httpResponse = JSONObject.parseObject(responseJson, HttpResponse.class);
System.out.println(httpResponse.toString());
  • 使用序列化框架的注解映射驼峰属性
@Data
public class HttpResponse {
    private String userName;
    @JSONField(name = "_id")
    private String id;
    private BigDecimal bookPrice;
}

16. 不要在循环中使用+拼接字符串

  • 错误示范:循环中拼接字符串
String result = "";
for (int i = 0; i < 1000; i++) {
    result = result + i;
}
System.out.println(result);

  • 正确示范:使用StringBuilder
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    result.append(i);
}
System.out.println(result.toString());

17. 不要直接吞掉异常

try {
    FileInputStream fileInputStream = new FileInputStream("/notexist");
} catch (FileNotFoundException e) {
}

18. 写完代码格式化一下

IDEA有一个format code的功能,写完代码不妨格式化一下.
但请注意,别对全局代码进行这样的操作,很有可能你的前辈跟你用的不是同一种格式化风格,在merge的时候会出现大量的冲突.

19. 重写equals的同时,必须重写hashCode

Object规范:

  1. 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对于同一个对象的多次调用,hashCode都必须始终返回同一个值.
  2. 如果两个对象根据equals方法比较是相同的,那么调用两个对象的hashCode方法都必须产生同样的整数结果.
  3. 如果两个对象根据equals方法比较是不相同的,那么调用两个对象的hashCode方法,则不要求hashCode方法必须产生不同的结果.

20. 避免返回Map和JSONObject这样的结果集,使用对象

如果接口中返回Map和JSONObject这样的通用结果集,维护人员很难知道返回了什么东西.

  • 充斥着拼装返回结果体的代码
    public JSONObject queryStudentInfo() {
        List<Student> students = null;
        try {
            students = selectList();
        } catch (Exception e) {
            JSONObject result = new JSONObject();
            result.put("status", -1);
            result.put("data", Collections.emptyMap());
        }
        JSONObject result = new JSONObject();
        result.put("status", 200);
        result.put("data", students);
        return result;
    }
  • 直接返回对象,使用统一的拦截器进行参数拼装
public List<Student> queryStudentsInfo(){
    return selectList();
}
举报

相关推荐

0 条评论