16.45个代码优化的小技巧
1、规范命名
命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:
见名知意
比如需要定义一个变量需要来计数
int i = 0;
名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称
int count = 0;
能够读的出来
如下代码:
private String sfzh; private String dhhm;
这些变量的名称,根本读不出来,更别说实际意义了。
所以我们可以使用正确的可以读出来的英文来命名
private String idCardNo; private String phone;
2、规范代码格式
好的代码格式能够让人感觉看起来代码更加舒适。
好的代码格式应当遵守以下几点:
- 合适的空格
- 代码对齐,比如大括号要对齐
- 及时换行,一行不要写太多代码
好在现在开发工具支持一键格式化,可以帮助美化代码格式。
3、写好代码注释
在《代码简洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。
作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。(作者很懂啊)
但是这不意味着可以不写注释,当通过代码如果无法表达意思的时候,就需要注释,比如如下代码
for (Integer id : ids) { if (id == 0) { continue; } //做其他事 }
为什么 id == 0 需要跳过,代码是无法看出来了,就需要注释了。
好的注释应当满足一下几点:
- 解释代码的意图,说明为什么这么写,用来做什么
- 对参数和返回值注释,入参代表什么,出参代表什么
- 有警示作用,比如说入参不能为空,或者代码是不是有坑
- 当代码还未完成时可以使用 todo 注释来注释
4、try catch 内部代码抽成一个方法
try catch 代码有时会干扰我们阅读核心的代码逻辑,这时就可以把 try catch 内部主逻辑抽离成一个单独的方法
如下图是 Eureka 服务端源码中服务下线的实现中的一段代码

整个方法非常长,try 中代码是真正的服务下线的代码实现,finally 可以保证读锁最终一定可以释放。
所以这段代码其实就可以对核心的逻辑进行抽取。
protected boolean internalCancel(String appName, String id, boolean isReplication) { try { read.lock(); doInternalCancel(appName, id, isReplication); } finally { read.unlock(); } // 剩余代码 } private boolean doInternalCancel(String appName, String id, boolean isReplication) { //真正处理下线的逻辑 }
5、方法别太长
方法别太长就是字面的意思。一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去;同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。
我曾经就遇到过一个方法写了 2000+行,各种 if else 判断,我光理清代码思路就用了很久,最终理清之后,就用策略模式给重构了。
所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。
6、抽取重复代码
当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。
所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。
7、多用 return
在有时我们平时写代码的情况可能会出现 if 条件套 if 的情况,当 if 条件过多的时候可能会出现如下情况:
if (条件1) { if (条件2) { if (条件3) { if (条件4) { if (条件5) { System.out.println("三友的java日记"); } } } } }
面对这种情况,可以换种思路,使用 return 来优化
if (!条件1) { return; } if (!条件2) { return; } if (!条件3) { return; } if (!条件4) { return; } if (!条件5) { return; } System.out.println("三友的java日记");
这样优化就感觉看起来更加直观
8、if 条件表达式不要太复杂
比如在如下代码:
if (((StringUtils.isBlank(person.getName()) || "三友的java日记".equals(person.getName())) && (person.getAge() != null && person.getAge() > 10)) && "汉".equals(person.getNational())) { // 处理逻辑 }
这段逻辑,这种条件表达式乍一看不知道是什么,仔细一看还是不知道是什么,这时就可以这么优化
boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "三友的java日记".equals(person.getName()); boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10; boolean isHanNational = "汉".equals(person.getNational()); if (sanyouOrBlank && ageGreaterThanTen && isHanNational) { // 处理逻辑 }
此时就很容易看懂 if 的逻辑了
9、优雅地参数校验
当前端传递给后端参数的时候,通常需要对参数进场检验,一般可能会这么写
@PostMapping public void addPerson(@RequestBody AddPersonRequest addPersonRequest) { if (StringUtils.isBlank(addPersonRequest.getName())) { throw new BizException("人员姓名不能为空"); } if (StringUtils.isBlank(addPersonRequest.getIdCardNo())) { throw new BizException("身份证号不能为空"); } // 处理新增逻辑 }
这种写虽然可以,但是当字段的多的时候,光校验就占据了很长的代码,不够优雅。
针对参数校验这个问题,有第三方库已经封装好了,比如 hibernate-validator 框架,只需要拿来用即可。
所以就在实体类上加 @NotBlank
、@NotNull
注解来进行校验
@Data @ToString private class AddPersonRequest { @NotBlank(message = "人员姓名不能为空") private String name; @NotBlank(message = "身份证号不能为空") private String idCardNo; //忽略 }
此时 Controller 接口就需要方法上就需要加上 @Valid
注解
@PostMapping public void addPerson(@RequestBody @Valid AddPersonRequest addPersonRequest) { // 处理新增逻辑 }
10、统一返回值
后端在设计接口的时候,需要统一返回值
{ "code":0, "message":"成功", "data":"返回数据" }
不仅是给前端参数,也包括提供给第三方的接口等,这样接口调用方法可以按照固定的格式解析代码,不用进行判断。如果不一样,相信我,前端半夜都一定会来找你。
Spring 中很多方法可以做到统一返回值,而不用每个方法都返回,比如基于 AOP,或者可以自定义 HandlerMethodReturnValueHandler 来实现统一返回值。
11、统一异常处理
当你没有统一异常处理的时候,那么所有的接口避免不了 try catch 操作。
@GetMapping("/{id}") public Result<T> selectPerson(@PathVariable("id") Long personId) { try { PersonVO vo = personService.selectById(personId); return Result.success(vo); } catch (Exception e) { //打印日志 return Result.error("系统异常"); } }
每个接口都得这么玩,那不得满屏的 try catch。
所以可以基于 Spring 提供的统一异常处理机制来完成。
12、尽量不传递 null 值
这个很好理解,不传 null 值可以避免方法不支持为 null 入参时产生的空指针问题。
当然为了更好的表明该方法是不是可以传 null 值,可以通过@NonNull 和@Nullable 注解来标记。@NonNull 就表示不能传 null 值,@Nullable 就是可以传 null 值。
//示例1 public void updatePerson(@Nullable Person person) { if (person == null) { return; } personService.updateById(person); } //示例2 public void updatePerson(@NonNull Person person) { personService.updateById(person); }
13、尽量不返回 null 值
尽量不返回 null 值是为了减少调用者对返回值的为 null 判断,如果无法避免返回 null 值,可以通过返回 Optional 来代替 null 值。
public Optional<Person> getPersonById(Long personId) { return Optional.ofNullable(personService.selectById(personId)); }
如果不想这么写,也可以通过@NonNull 和@Nullable 表示方法会不会返回 null 值。
14、日志打印规范
好的日志打印能帮助我们快速定位问题
好的日志应该遵循以下几点:
- 可搜索性,要有明确的关键字信息
- 异常日志需要打印出堆栈信息
- 合适的日志级别,比如异常使用 error,正常使用 info
- 日志内容太大不打印,比如有时需要将图片转成 Base64,那么这个 Base64 就可以不用打印
15、统一类库
在一个项目中,可能会由于引入的依赖不同导致引入了很多相似功能的类库,比如常见的 json 类库,又或者是一些常用的工具类,当遇到这种情况下,应当规范在项目中到底应该使用什么类库,而不是一会用 Fastjson,一会使用 Gson。
16、尽量使用工具类
比如在对集合判空的时候,可以这么写
1 public void updatePersons(List<Person> persons) { 2 if (persons != null && persons.size() > 0) { 3 4 } 5 }
但是一般不推荐这么写,可以通过一些判断的工具类来写
public void updatePersons(List<Person> persons) { if (!CollectionUtils.isEmpty(persons)) { } }
不仅集合,比如字符串的判断等等,就使用工具类,不要手动判断。
17、尽量不要重复造轮子
就拿格式化日期来来说,我们一般封装成一个工具类来调用,比如如下代码
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDateTime(Date date) { return DATE_TIME_FORMAT.format(date); }
这段代码看似没啥问题,但是却忽略了 SimpleDateFormat 是个线程不安全的类,所以这就会引起坑。
一般对于这种已经有开源的项目并且已经做得很好的时候,比如 Hutool,就可以把轮子直接拿过来用了。
18、类和方法单一职责
单一职责原则是设计模式的七大设计原则之一,它的核心意思就是字面的意思,一个类或者一个方法只做单一的功能。
就拿 Nacos 来说,在 Nacos1.x 的版本中,有这么一个接口 HttpAgent
这个类只干了一件事,那就是封装 http 请求参数,向 Nacos 服务端发送请求,接收响应,这其实就是单一职责原则的体现。
当其它的地方需要向 Nacos 服务端发送请求时,只需要通过这个接口的实现,传入参数就可以发送请求了,而不需要关心如何携带服务端鉴权参数、http 请求参数如何组装等问题。
19、尽量使用聚合/组合代替继承
继承的弊端:
- 灵活性低。java 语言是单继承的,无法同时继承很多类,并且继承容易导致代码层次太深,不易于维护
- 耦合性高。一旦父类的代码修改,可能会影响到子类的行为
所以一般推荐使用聚合/组合代替继承。
聚合/组合的意思就是通过成员变量的方式来使用类。
比如说,OrderService 需要使用 UserService,可以注入一个 UserService 而非通过继承 UserService。
聚合和组合的区别就是,组合是当对象一创建的时候,就直接给属性赋值,而聚合的方式可以通过 set 方式来设置。
组合:
public class OrderService { private UserService userService = new UserService(); }
聚合:
public class OrderService { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } }
20、使用设计模式优化代码
在平时开发中,使用设计模式可以增加代码的扩展性。
比如说,当你需要做一个可以根据不同的平台做不同消息推送的功能时,就可以使用策略模式的方式来优化。
设计一个接口:
public interface MessageNotifier { /** * 是否支持改类型的通知的方式 * * @param type 0:短信 1:app * @return */ boolean support(int type); /** * 通知 * * @param user * @param content */ void notify(User user, String content); }
短信通知实现:
@Component public class SMSMessageNotifier implements MessageNotifier { @Override public boolean support(int type) { return type == 0; } @Override public void notify(User user, String content) { //调用短信通知的api发送短信 } }
app 通知实现:
public class AppMessageNotifier implements MessageNotifier { @Override public boolean support(int type) { return type == 1; } @Override public void notify(User user, String content) { //调用通知app通知的api } }
最后提供一个方法,当需要进行消息通知时,调用 notifyMessage,传入相应的参数就行。
@Resource private List<MessageNotifier> messageNotifiers; public void notifyMessage(User user, String content, int notifyType) { for (MessageNotifier messageNotifier : messageNotifiers) { if (messageNotifier.support(notifyType)) { messageNotifier.notify(user, content); } } }
假设此时需要支持通过邮件通知,只需要有对应实现就行。
21、不滥用设计模式
用好设计模式可以增加代码的扩展性,但是滥用设计模式确是不可取的
public void printPerson(Person person) { StringBuilder sb = new StringBuilder(); if (StringUtils.isNotBlank(person.getName())) { sb.append("姓名:").append(person.getName()); } if (StringUtils.isNotBlank(person.getIdCardNo())) { sb.append("身份证号:").append(person.getIdCardNo()); } // 省略 System.out.println(sb.toString()); }
比如上面打印 Person 信息的代码,用 if 判断就能够做到效果,你说我要不用责任链或者什么设计模式来优化一下吧,没必要。
22、面向接口编程
在一些可替换的场景中,应该引用父类或者抽象,而非实现。
举个例子,在实际项目中可能需要对一些图片进行存储,但是存储的方式很多,比如可以选择阿里云的 OSS,又或者是七牛云,存储服务器等等。所以对于存储图片这个功能来说,这些具体的实现是可以相互替换的。
所以在项目中,我们不应当在代码中耦合一个具体的实现,而是可以提供一个存储接口
public interface FileStorage { String store(String fileName, byte[] bytes); }
如果选择了阿里云 OSS 作为存储服务器,那么就可以基于 OSS 实现一个 FileStorage,在项目中哪里需要存储的时候,只要实现注入这个接口就可以了。
@Autowired private FileStorage fileStorage;
假设用了一段时间之后,发现阿里云的 OSS 比较贵,此时想换成七牛云的,那么此时只需要基于七牛云的接口实现 FileStorage 接口,然后注入到 IOC,那么原有代码用到 FileStorage 根本不需要动,实现轻松的替换。
23、经常重构旧的代码
随着时间的推移,业务的增长,有的代码可能不再适用,或者有了更好的设计方式,那么可以及时的重构业务代码。
就拿上面的消息通知为例,在业务刚开始的时候可能只支持短信通知,于是在代码中就直接耦合了短信通知的代码。但是随着业务的增长,逐渐需要支持 app、邮件之类的通知,那么此时就可以重构以前的代码,抽出一个策略接口,进行代码优化。
24、null 值判断
空指针是代码开发中的一个难题,作为程序员的基本修改,应该要防止空指针。
可能产生空指针的原因:
- 数据返回对象为 null
- 自动拆箱导致空指针
- rpc 调用返回的对象可能为空格
所以在需要这些的时候,需要强制判断是否为 null。前面也提到可以使用 Optional 来优雅地进行 null 值判断。
25、pojo 类重写 toString 方法
pojo 一般内部都有很多属性,重写 toString 方法可以方便在打印或者测试的时候查看内部的属性。
26、魔法值用常量表示
public void sayHello(String province) { if ("广东省".equals(province)) { System.out.println("靓仔~~"); } else { System.out.println("帅哥~~"); } }
代码里,广东省就是一个魔法值,那么就可以将用一个常量来保存
private static final String GUANG_DONG_PROVINCE = "广东省"; public void sayHello(String province) { if (GUANG_DONG_PROVINCE.equals(province)) { System.out.println("靓仔~~"); } else { System.out.println("帅哥~~"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!