Java 开发优化策略和重要规范记录
前言
这里记录了 Java 开发中常用的优化策略以及一些重要的规范,持续更新中。
内容除了个人笔记以外,可能来源于各种地方,包括但不限于书籍、博客、视频、面试题等等,一般会经过重新整理组织,仅以学习为目的,侵删。
在线上生产环境,JVM 的 Xms 和 Xmx 设置一样大小的内存容量
避免在 GC 后调整堆大小带来的压力。
给 JVM 环境参数设置 -XX:+HeapDumpOnOutOfMemoryError
参数
让 JVM 碰到 OOM 场景时输出 dump 信息。
说明:OOM 的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助。
单语句多执行单元最好分行编写
1#19 InfluxDBClient client = InfluxDBClientFactory.create("http://localhost:8086",
2#20 token.toCharArray());
如果 token.toCharArray()
写在 19 行中且它是报错点,那么报错时会定位到 19 行,无法直接看出是它出错了,如果写在 20 行,那么报错是会定位到 20 行。
forEach 仅应用于报告流执行的计算结果
forEach 操作是终端操作中最不强大的操作之一,也是最不友好的流操作。它是明确的迭代,因此不适合并行化。
编程流管道的本质是无副作用的函数对象。这适用于传递给流和相关对象的所有许多函数对象。终结操作 forEach 仅应用于报告流执行的计算结果,而不是用于执行计算。
为了正确使用流,必须了解收集器。 最重要的收集器工厂是 toList,toSet,toMap,groupingBy 和 join。
优先使用批量操作
在一个 for 循环中,一个个调用远程接口,或者执行数据库的update操作,是比较消耗性能的。
我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。
例如:
1for(User user : userList) {
2 userMapper.update(user);
3}
4
5// 改成:
6
7userMapper.updateForBatch(userList);
from: 作为一个老程序员,想对新人说什么?
比较时把常量写前面
1if(user.getName().equals("苏三")) {
2 System.out.println("找到:"+user.getName());
3}
在上面这段代码中,如果 user 对象,或者 user.getName() 方法返回值为 null,则都报NullPointerException 异常。
1private static final String FOUND_NAME = "苏三";
2
3...
4
5if(null == user) {
6 return;
7}
8if(FOUND_NAME.equals(user.getName())) {
9 System.out.println("找到:"+user.getName());
10}
在使用 equals 做比较时,尽量将常量写在前面,即 equals 方法的左边。
这样即使 user.getName() 返回的数据为 null,equals 方法会直接返回 false,而不再是报空指针异常。
多线程处理
很多时候,我们需要在某个接口中,调用其他服务的接口。
比如,调用用户信息查询接口可能需要调用用户查询接口、积分查询接口和成长值查询接口,然后汇总数据统一返回。
调用远程接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
可以改成并行调用以优化远程接口性能,调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)。
在 java8 之前可以通过实现 Callable 接口,获取线程返回结果。
java8 以后通过 CompleteFuture 类实现该功能。我们这里以 CompleteFuture 为例:
1public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
2 final UserInfo userInfo = new UserInfo();
3 CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
4 getRemoteUserAndFill(id, userInfo);
5 return Boolean.TRUE;
6 }, executor);
7
8 CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
9 getRemoteBonusAndFill(id, userInfo);
10 return Boolean.TRUE;
11 }, executor);
12
13 CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
14 getRemoteGrowthAndFill(id, userInfo);
15 return Boolean.TRUE;
16 }, executor);
17 CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();
18
19 userFuture.get();
20 bonusFuture.get();
21 growthFuture.get();
22
23 return userInfo;
24}
温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了 executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。
初始化集合时指定大小
我们在实际项目开发中,需要经常使用集合,比如:ArrayList、HashMap 等。
在创建集合时指定了大小,比没有指定大小,添加 10 万个元素的效率提升了一倍。
查看 ArrayList 源码,可发现它的默认大小是 10,如果添加元素超过了一定的阀值,会按 1.5 倍的大小扩容。
消除过长的 if…else
我们在写代码的时候,if…else的判断条件是必不可少的。不同的判断条件,走的代码逻辑通常会不一样。
给出一些例子,如下声明一个支付接口和一些具体实现:
1public interface IPay {
2 void pay();
3}
4
5@Service
6public class AliaPay implements IPay {
7 @Override
8 public void pay() {
9 System.out.println("===发起支付宝支付===");
10 }
11}
12
13@Service
14public class WeixinPay implements IPay {
15 // ...
16}
17
18@Service
19public class JingDongPay implements IPay {
20 // ...
21}
接下来在具体服务中进行调用:
1@Service
2public class PayService {
3 @Autowired
4 private AliaPay aliaPay;
5 @Autowired
6 private WeixinPay weixinPay;
7 @Autowired
8 private JingDongPay jingDongPay;
9
10 public void toPay(String code) {
11 if ("alia".equals(code)) {
12 aliaPay.pay();
13 } elseif ("weixin".equals(code)) {
14 weixinPay.pay();
15 } elseif ("jingdong".equals(code)) {
16 jingDongPay.pay();
17 } else {
18 System.out.println("找不到支付方式");
19 }
20 }
21}
如果支付方式越来越多,比如:又加了百度支付、美团支付、银联支付等等,就需要改 toPay 方法的代码,增加新的 else…if 判断,判断多了就会导致逻辑越来越多。
很明显,这里违法了设计模式六大原则的:开闭原则 和 单一职责原则:
开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。
单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。
那么,如何优化 if…else 判断呢?
答:使用工厂模式。
1public class PayStrategyFactory {
2
3 private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();
4
5 public static void register(String code, IPay iPay) {
6 if (null != code && !"".equals(code)) {
7 PAY_REGISTERS.put(code, iPay);
8 }
9 }
10
11 public static IPay get(String code) { // 工厂模式的体现
12 return PAY_REGISTERS.get(code);
13 }
14}
如下,将具体的支付类注册到工厂中:
1@Service
2public class AliaPay implements IPay {
3
4 @PostConstruct
5 public void init() {
6 PayStrategyFactory.register("aliaPay", this);
7 }
8
9 @Override
10 public void pay() {
11 System.out.println("===发起支付宝支付===");
12 }
13}
14
15@Service
16public class WeixinPay implements IPay {
17 // ...
18}
19
20@Service
21public class JingDongPay implements IPay {
22 // ...
23}
接下来调用逻辑就很清晰了:
1@Service
2public class PayService3 {
3
4 public void toPay(String code) {
5 PayStrategyFactory.get(code).pay();
6 }
7}
这段代码的关键是 PayStrategyFactory 类,它是一个策略工厂,里面定义了一个全局的 map,在所有 IPay 的实现类中注册当前实例到 map 中,然后在调用的地方通过 PayStrategyFactory 类根据 code 从 map 获取支付类实例即可。
如果加了一个新的支付方式,只需新加一个类实现 IPay 接口,定义 init 方法,并且重写 pay 方法即可,其他代码基本上可以不用动。
消除又臭又长的 if…else 判断,还有很多方法,比如:使用注解、动态拼接类名称、模板方法、枚举等等。
详见:9条消除if…else的锦囊妙计,助你写出更优雅的代码
参考:
- 苏三说技术
- 阿里巴巴 Java 开发手册
- Effective Java