Java 开发优化策略和重要规范记录

11 minute

前言

这里记录了 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