集合、空值处理、异常处理、日志、文件IO常见开发问题

 极客时间《Java业务开发常见错误100例》学习笔记

List列表操作常见坑

  • Arrays.asList 得到的是 Arrays 的内部类 ArrayList,List.subList 得到的是 ArrayList 的内部类 SubList,不能把这两个内部类转换为 ArrayList 使用。
  • Arrays.asList 直接使用了原始数组,可以认为是共享“存储”,而且不支持增删元素;
  • List.subList 直接引用了原始的 List,也可以认为是共享“存储”,而且对原始 List 直接进行结构性修改会导致 SubList 出现异常。如下代码抛出UnsupportedOperationException异常
  • 
    String[] arr = {"1", "2", "3"};
    List list = Arrays.asList(arr);
    arr[1] = "4";
    try {
        list.add("5");
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    log.info("arr:{} list:{}", Arrays.toString(arr), list);

     

  • 对 Arrays.asList 和 List.subList 容易忽略的是,新的 List 持有了原始数据的引用,可能会导致原始数据也无法 GC 的问题,最终导致 OOM。
  • Arrays.asList 不一定可以把所有数组转换为正确的 List。当传入基本类型数组的时候,List 的元素是数组本身,而不是数组中的元素。
  • 内存中任何集合的搜索都是很快的,结果在搜索超大 ArrayList 的时候遇到性能问题。我们考虑利用 HashMap 哈希表随机查找的时间复杂度为 O(1) 这个特性来优化性能,不过也要考虑 HashMap 存储空间上的代价,要平衡时间和空间。
  • 链表适合元素增删的场景,选用 LinkedList 作为数据结构。在真实场景中读写增删一般是平衡的,而且增删不可能只是对头尾对象进行操作,可能在 90% 的情况下都得不到性能增益,建议使用之前通过性能测试评估一下。

null和空指针处理

  • 针对判空,通过 Optional 配合 Stream 可以避免大多数冗长的 if-else 判空逻辑,实现一行代码优雅判空。另外,要定位和修复空指针异常,除了可以通过增加日志进行排查外,在生产上使用 Arthas 来查看方法的调用栈和入参会更快捷。
  • 业务系统最基本的标准是不能出现未处理的空指针异常,因为它往往代表了业务逻辑的中断,所以我建议每天查询一次生产日志来排查空指针异常,有条件的话建议订阅空指针异常报警,以便及时发现及时处理。
  • POJO 中字段的 null 定位,从服务端的角度往往很难分清楚,到底是客户端希望忽略这个字段还是有意传了 null,因此我们尝试用 Optional类来区分 null 的定位。同时,为避免把空值更新到数据库中,可以实现动态 SQL,只更新必要的字段。
  • 数据库字段使用 NULL 可能会带来的三个坑(包括 sum 函数、count 函数,以及 NULL 值条件)
  • null 的正确处理以及避免空指针异常,绝不是判空这么简单,还要根据业务属性从前到后仔细考虑,客户端传入的 null 代表了什么,出现了 null 是否允许使用默认值替代,入库的时候应该传入 null 还是空值,并确保整个逻辑处理的一致性,才能尽量避免 Bug。
  • 为处理好 null,作为客户端的开发者,需要和服务端对齐字段 null 的含义以及降级逻辑;而作为服务端的开发者,需要对入参进行前置判断,提前挡掉服务端不可接受的空值,同时在整个业务逻辑过程中进行完善的空值处理。

异常处理

  • 注意捕获和处理异常的最佳实践。首先,不应该用 AOP 对所有方法进行统一异常处理,异常要么不捕获不处理,要么根据不同的业务逻辑、不同的异常类型进行精细化、针对性处理;其次,处理异常应该杜绝生吞,并确保异常栈信息得到保留;最后,如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息。
  • 务必小心 finally 代码块中资源回收逻辑,确保 finally 代码块不出现异常,内部把异常处理完毕,避免 finally 中的异常覆盖 try 中的异常;或者考虑使用 addSuppressed 方法把 finally 中的异常附加到 try 中的异常上,确保主异常信息不丢失。此外,使用实现了 AutoCloseable 接口的资源,务必使用 try-with-resources 模式来使用资源,确保资源可以正确释放,也同时确保异常可以正确处理。
  • 
    @GetMapping("useresourcewrong")
    public void useresourcewrong() throws Exception {
        TestResource testResource = new TestResource();
        try {
            testResource.read();
        } finally {
            testResource.close();
        }
    }

     

  • 虽然在统一的地方定义收口所有的业务异常是一个不错的实践,但务必确保异常是每次 new 出来的,而不能使用一个预先定义的 static 字段存放异常,否则可能会引起栈信息的错乱。
  • 确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。

记录日志可能会踩的坑

  • Java 的日志框架众多,SLF4J 实现了这些框架记录日志的统一。在使用 SLF4J 时,我们需要理清楚其桥接 API 和绑定这两个模块。如果程序启动时出现 SLF4J 的错误提示,那很可能是配置出现了问题,可以使用 Maven 的 dependency:tree 命令梳理依赖关系。
  • Logback 是 Java 最常用的日志框架,其配置比较复杂,你可以参考官方文档中关于 Appender、Layout、Filter 的配置,切记不要随意从其他地方复制别人的配置,避免出现错误或与当前需求不符。
  • 使用异步日志解决性能问题,是用空间换时间。但空间毕竟有限,当空间满了之后,我们要考虑是阻塞等待,还是丢弃日志。如果更希望不丢弃重要日志,那么选择阻塞等待;如果更希望程序不要因为日志记录而阻塞,那么就需要丢弃日志
  • 日志框架提供的参数化日志记录方式不能完全取代日志级别的判断。如果你的日志量很大,获取日志参数代价也很大,就要进行相应日志级别的判断,避免不记录日志也要花费时间获取日志参数的问题。

文件IO操作

  • 如果需要读写字符流,那么需要确保文件中字符的字符集和字符流的字符集是一致的,否则可能产生乱码。
  • 使用 Files 类的一些流式处理操作,注意使用 try-with-resources 包装 Stream,确保底层文件资源可以释放,避免产生 too many open files 的问题。
  • 进行文件字节流操作的时候,一般情况下不考虑进行逐字节操作,使用缓冲区进行批量读写减少 IO 次数,性能会好很多。一般可以考虑直接使用缓冲输入输出流 BufferedXXXStream,追求极限性能的话可以考虑使用 FileChannel 进行流转发
  • 文件操作因为涉及操作系统和文件系统的实现,JDK 并不能确保所有 IO API 在所有平台的逻辑一致性,代码迁移到新的操作系统或文件系统时,要重新进行功能测试和性能测试。

你可能感兴趣的:(集合、空值处理、异常处理、日志、文件IO常见开发问题)