《100 Java Mistakes and How to Avoid Them》笔记 3

本书的阅读又搁置了许久,虽然感觉 Manning 出版社的这一 100 Mistakes 系列从书的质量不是那么的高,但开了头还是继续从本书 40% 的位置往下。

开始要讲述到异常了,异常还是有必要认真对待的,比如

  1. Java 中很容易被 CheckedException 弄得代码不整洁
  2. 缺少必要的参数检查,不舍得抛出异常,视异常为 Bug
  3. 不明确出现异常时后续如何处理,
  4. 或者是捕获而隐藏了异常致使定位错误变得更难。

Java 的主要异常大分类是

Throwable
├── Error
└── Exception
          └── RuntimeException

NullPointerException, 这恐怕是一个最常见的异常,Java 对一个对象是否能为 null 值没什么约束,甚至用 null 来表示业务上的空。比如说方法的参数与返回值,Java 都可以是 null 值,而在 Kotlin 中非明确可为 null 的时不能为 null

fun foo(a: String): String = ""

上面的 Kotlin 方法,不能传入 foo(null), 编译器出错,同时也不能返回 null 值,如写成 fun foo(a: String): String = null。要使它既能接收和返回 null 值的话,要写成

fun foo(a: String?): String? = ...

Java 只能用 @Nullable, @NotNull 非标准的方式让第三方的 AnnotationProcessor 介入编译期织入代码,或手功加入代码,待到运行期来检验。比如借助于 JDT 的注解设定默认参数和返回值都不应该为 null

Java 15 对 NullPointerException 的显示信息增强了,如下面的代码

在 Java 15 之前执行显示的错误是

Exception in thread "main" java.lang.NullPointerException
        at Test.main(Test.java:4)

而在 Java 15 及之后的版本显示的错误是

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because the return value of "Test.foo()" is null
        at Test.main(Test.java:4)

我们很清楚造成 NullPointerException 的原因

如果方法参数不能接受 null 值,方法开始处就必须检查,如 Objects.requireNonNull(firstname), 或各种断言的方式

Java 10 提供的 List.copyOf, Set.copyOf, Map.copyOf 方法不允许 null 值元素,所以它可以帮助我们检测传入的集合参数,如

如果方法返回的是一个集合或数组,通常返回空集合或空数组好过于返回 null 值,而且也应避免返回的包括 null 值的集合或数组。再如 Stream.empty(), Optional.empty(), 枚举类型也最好定义 NOT_FOUND, UNKNOWN 等项,而非直接用 null, 像 JDK 的 RoundingMode.UNNECESSARY

Java 标准库如 java.io.File 的 list(), listFiles() 方法会在任何 I/O 错误时返回 null,这就掩盖了错误信息; 但新的 NIO Files.list() 能抛出正确的异常信息(IOException)。比如目录 abc 不存在,new File("abc").list() 返回值,Files.list(path.of("abc")) 抛出 IOException

null 作为正常值来处理时经常意义不含糊,如 Boolean 的 null 值表示第三个选择? Integer id 的 null 表示记录不存在?一个方法在返回 null 值之前需作必要的思考。

永远不要让返回类型为 Optional 的方法返回 null 值。Optional 作为方法参数是不便利的,因为总是要拆箱,还不如直接传入 null 值。

Optional.of(value) vs Optional.ofNullable(value): 如果确定 value 不该为 null 就应该使用前者,及时抛出 NullPointerException,后者虽说总是安全,但会掩盖错误。

使用 API 时一定要清楚它在某些情况下是返回 null, 空集合,还是抛出异常,不然对原本返回的空集合进行 null 值判断是没有意义的,或者抛出的异常未捕获。

Java 虽能防止下标越界,但有时候也有必要检查下标的范围,如传入的索引为负数,索引累加后溢出等,Java 9 有两个新方法检查下标是否越界

  1. Objects.checkFromIndexSize(fromIndex, size, length) 用来检查 fromIndex ~ fromIndex + size -1 是否落在 0 ~ length-1 之间,即是否有效的下标。
  2. Objects.checkIndex(index, length) 检查 index 是否是一个长度为 length 的数组或集合的有效下标

Java 16 开始的基于模式的 instanceof 表达式还是很省事

判断类型与转型一口气完成。

Java 9 的 @Deprecated(forRemoval = true) 可直接指示将被删除

又要回味一下 Java 的泛型实现了,由于历史原因,Java 的泛型不像 C++ 的模板类那样针对具体类型实现独立的类,Java 的泛型是字节码层级上仍然是无类型的(擦除了类型,虚拟机只知道类型的上界 -- Object 或 <? extends ABC> 声明的上界),只在源代码一层实现了泛型。所以对象 List<String> 本质上它还是一个 List<Object>

所以上面方法的字节码是

它总是从 List 中获得一个 Object, 然后转型为 String。

由于 List<String> 本质上是一个 List<Object>, 所以也就有办法往其中添加非 String 对象,看下面的代码

输出及异常信息

0
1
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
    at com.example.demo.Test.printFirstItem(Test.java:18)

对照关键语句的字节码

#1

20: invokeinterface #40, 3 // InterfaceMethod java/util/List.add:(ILjava/lang/Object;)V

#2

37: aload_0
38: iconst_0
39: invokeinterface #44, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
44: astore_2

#3

48: aload_0
49: iconst_0
50: invokeinterface #44, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
55: checkcast #48 // class java/lang/String
58: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

在 Java 中,List<SubType> 与 List<SuperType> 不存在父子关系,所以强制转型

(List<SuperType>)(List<?>)(new ArrayList<SubType)

是可以通过 Java 的编译,但十分危险,List 会被添加不兼容的类型。如果有需要应声明带上界的类型,如

void processList(List<? extends CharSequence> list) { ... }

转换为 Collections.unmodifiableList() 能防止 List 被意外修改

Raw type 的 List 并不等于 List<Object>, 以下代码能通过编译

但执行时会出现 ClassCastException 异常

但如果把声明 List 声明替换为 List<Object>

#1 处便不能通过编译了,提示应该提供 List<String> 但传入的是 List<Object>

PECS(producer - extends, consumer - super): 如果泛型方法需处理更宽泛的类型,只从泛型中读取声明参数为  ? extends, 只往泛型中写入的话声明参数为 ? super

避免使用 Raw type, 编译选项 -Xlint:rawtypes 帮助我们找到 Raw type 的使用

Collections 有 checkedList(), checkedMap(), checkedQueue() 等方法,防止通过

((List<Object>) (List<?>) new ArrayList<String>()).add(1);

恶意绕过泛型约束往列表中添加不兼容的类型,如果用 checkedList()

以上代码在企图往 list 中添加 Integer 时报出异常

Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
    at java.base/java.util.Collections$CheckedCollection.typeCheck(Collections.java:3355)
    at java.base/java.util.Collections$CheckedCollection.add(Collections.java:3403)

即每次操作对象时都会检查它是否是 String 类型。 

当然实际写代码时大约不会用 checkedList(new ArrayList<(), String.class) 来声明类型的,但用来临时找出哪里违规加入了不兼容的类型是很有用的。 

如果同样的类名(类全路径都相同)由不同的类加载器加载的也会出现 ClassCastException, 这种情况较少见,而且此时错误信息里会明确告知哪个 ClassLoader 加载的类不能转型为另一个 ClassLoader 加载的类

本文链接 https://yanbin.blog/100-java-mistakes-and-how-to-avoid-them-notes-3/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments