代码规范的意义
我们编写的代码,通过编译打包,最终都会运行在网络上面。在网络世界中,进程与进程之间通信,就得通过通信协议,这是为了约定一个规范,一个行业标准,使得数据之间通信变得标准化,这也往往可以增加代码运行的可靠性。
制定软件的代码规范,对于一个完整的产品团队,尤为重要。因为一个完整的产品,往往意味着长期的版本迭代,在这个过程中,由于需求、人力资源、架构变迁等因素,往往会出现截然不同的代码风格。因此遵守统一的代码规范,不仅使得代码更加优雅,同时也能增加项目的稳定性、容错率。
参考资料
- 《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);
}
思考一下: 这是最好的写法嘛?希望可以发散思考,永远别放弃对clean code的执着.
命名的一些好方法
- 使用标准的英文命名,切勿使用中文命名,除了常量,切勿使用
_
. - 包名小写,类名遵循UpperCamelCase风格,方法名、变量名遵循lowCamelCase风格.
- 常量需要大写,单词间以
_
进行区分,例如USER_NAME
. - 表示复数的时候,用
s
比较好,使用userList
这种命名,如果调用的方法在后面的版本返回了Map
类型,就会产生歧义. - 遵循"望文知义"的原则,在方法和参数命名上体现功能和参数的意义.
类的功能类型 | 命名规范 |
---|---|
抽象类 | 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;
返回一个实例,该实例对于指定bean来说可以是共享或独立的。
此方法允许使用Spring BeanFactory替代Singleton或Prototype设计模式。
对于Singleton bean,调用者可以保留对返回对象的引用。
将别名转换回相应的规范bean名称。
将询问父工厂是否在该工厂实例中找不到该bean。
<p style="color:red">tips:使用JavaDoc可以让注释形成文档,在IDEA中显示也更加友好.</p>
关于注释的几个建议
- 接口使用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);
}
}
如果是不同的判断分支,在使用if-else if时可以使用策略模式进行重构.
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);
为什么会产生这种现象,原因是浮点数无法精确地表示0.1这样的数字,数字在计算机底层,都是二进制数.
- 使用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;
}
使用对象封装不仅仅是简洁,也可以适应更多的变化,试想一下,如果
getStudentsInfo
是一个Controller中的方法,此时产品经理要求增加一个birthDay
字段的查询条件,过长的参数列表改动起来是怎样的?
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;
}
为什么要返回空的集合而不是null,因为null在程序中经常会报NPE,对于前端来说,也需要增加很多的null和undefined判断.
11. 打印日志,请使用门面模式的SLF4J日志框架
日志框架可能会被替换,当你从logback转换到log4j2时,之前使用的logback打印日志会出现问题,使用SLF4J来规避这种升级带来的麻烦.
12. 使用日志而非System.out或者System.err打印异常堆栈.
对于异常,也应该使用
log.error
而非exception.printStackTrace();
,否则,打印的异常堆栈无法推送到日志采集框架中,难以定位问题.
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;
}
}
这段代码在做的事情:
- 从缓存中获取当前汇率对应的汇率表
- 如果缓存中没有,调用api获取汇率表
- 按照产品经理设定的顺序对汇率表进行排序
- 返回汇率表
- 改进
- 将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请求,所以不要相信请求的数据,始终对参数进行校验.
参数校验需要关注的点:
- page size过大.
- 恶意order by导致数据库慢查询.
- SSRF.
- 缓存击穿.
- SQL注入,反序列化攻击
- 拒绝服务攻击.
15. 对象属性始终遵循驼峰命名规则
在对接一些中间件或者第三方接口时,对方可能并不是使用java语言开发,在返回的结果集中,可能会返回各种格式的数据,但无论如何,请尽量遵守驼峰命名规则
- 错误的对象定义
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());
- 使用序列化框架的注解映射驼峰属性
fastJson默认的映射策略会帮你把下划线转成驼峰.
@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) {
}
catch住异常,但是没有打印任何日志信息,也没有尝试再抛出异常,如果这段代码出错,那么排查起来将会是噩梦.
18. 写完代码格式化一下
IDEA有一个format code
的功能,写完代码不妨格式化一下.
但请注意,别对全局代码进行这样的操作,很有可能你的前辈跟你用的不是同一种格式化风格,在merge的时候会出现大量的冲突.
19. 重写equals的同时,必须重写hashCode
Object规范:
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对于同一个对象的多次调用,hashCode都必须始终返回同一个值.
- 如果两个对象根据equals方法比较是相同的,那么调用两个对象的hashCode方法都必须产生同样的整数结果.
- 如果两个对象根据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();
}
怎么实现统一拦截器去拼装参数?继续关注我,在后面的章节,我会手把手跟你剖析如何实现这件事情。