阿里手册自测要点

Posted by Haiming on June 2, 2025

(四) OOP 规约

基本类型和包装类型的使用

  1. 关于基本数据类型与包装数据类型的使用标准,手册强制规定在哪些场景下必须使用包装数据类型?推荐在哪些场景下使用基本数据类型?手册对此给出了怎样的个人理解?使用基本类型接收数据库查询结果(可能为 null)时存在什么风险?[8, 9, 13-15]

他们的差别实际上就是有没有null, 以及需要考虑自动拆装箱

  1. 所有的 POJO 和 RPC 调用, 都必须使用包装类型, 因为可以用 null 表示服务不正常或者值缺失. 如果用基本类型, 无法表达这一语义.

  2. 函数的局部变量使用基本数据类型,:

    1. 避免自动拆装箱的性能损失

    2. 局部变量的使用范围很小, 开发者可控.

一定不能用基本类型接收数据库的值

数据库查询结果可能是null, 那么在转换成基本类型的时候, 需要拆箱, 这就会导致直接报出 NPE.

布尔类型的默认getter 规则

  1. 对于 基本数据类型 boolean 的属性,始终使用 isXxx() 作为 getter 方法

  2. 对于 包装数据类型 Boolean 的属性,使用 getXxx() 作为 getter 方法。同时,要避免该 Boolean 属性对应的 isXxx() 方法与之共存。

String.split() 方法的注意点

  1. 使用索引访问 String 的 split 方法得到的数组时,手册推荐注意什么?为什么不传入 limit 参数时,split() 方法会从尾部处理连续的空字符串?
  1. 使用索引访问 split() 的数组时候, 需要检查最后一个分隔符后面是否有内容, 避免下标越界

  2. 不传入 limit 参数, 等于传入 limit = 0, split() 会从后向前检查:

    1. 连续

    2. 空字符串, 注意空格不算

    且剔除. 可以当成自动做了 trim()

(六) 集合处理

hashCode 和 equals() 的使用方式

  1. 关于 hashCodeequals 的处理,手册强制规定了哪些规则?针对 Set 或 Map 的 key 的对象类型,有何特殊要求?[19, 22, 23]

首先分析一下 hashCode 和 equals 的作用.

hashCode 是快速定位对象可能存在的桶 Bucket, equals() 方法是在同一个桶之中确定对象是否相等.

下面是准则:

  1. 重写 equals() 必须重写 hashCode

  2. Set 或者 map 的 equals() 和 hashCode 都必须重写.

这是因为 Java 官方对于 Object 类中 equals 和 hashCode 方法定义了一套约定(Contract)。虽然手册没有直接列出这套约定,但这是一个基础的 Java 编程原则,手册的强制规定是基于此。这套约定的核心是:

  • 如果两个对象通过 equals() 方法比较是相等的(即 a.equals(b) 返回 true),那么这两个对象调用 hashCode() 方法必须产生相同的整数结果

  • 反之则不一定成立:如果两个对象通过 hashCode() 方法产生的整数结果相同,它们通过 equals() 方法比较不一定是相等的(这称为哈希冲突)

我们用一个极端例子来举例:

某个程序员希望这一个类之中所有对象都相等, 所以重写了 equals() 方法, 直接返回 true.

如果没有重写 hashCode, 那么在判断是否相等的时候, hashCode 不相同, 直接就不相等了. 这个 equals() 白写

而 map 或者 Set 之中, 我们保存的不重复的语义是开发自定义的, 所以必须覆写 equals() 和 hashCode, 才能保证业务逻辑上应该相等的对象会被当做相等对待.

Collectors.toMap() 的 value 不能为 null

  1. 使用 Collectors.toMap() 方法时,为什么要注意当 value 为 null 时会抛出 NPE 异常?手册从底层实现(HashMap#merge)角度是如何解释的?

因为 Collectors.toMap() 方法的底层调用的是 HashMap#merge, 然而在 HashMap#merge 之中, 会在入口处进行如下判断:

if (value == null || remappingFunction == null) throw new NullPointerException();

所以会直接抛出异常

那么为什么 HashMap#merge 这个方法不能传入 null value 呢?

merge 方法是为了 合并 值, 那么 null 值如何参与合并呢?

  1. 有null 值会让合并更加复杂: remappingFunction 的签名通常是 BinaryOperator<V>,它接收两个 V 类型的参数(旧 value 和 新 value),并返回一个 V 类型的结果。如果允许 null 值作为输入,那么 remappingFunction 的实现就需要显式地处理 null 输入的情况。

  2. null 是有歧义的: 是没有这个key, 还是这个key 的value 被显式的设置成了 null?

所以在合并这个方法中, 强制两个对象都有值, 并且显式的做校验, 确实是更好的方式

集合转数组的注意事项

必须使用 toArray(T[] array), 其中 array 是类型一致, 长度为 0 的空数组

  1. 不能使用无参的 toArray(): 其返回的是 Object[], 会产生 ClassCastException

  2. 长度为0, toArray(T[] array) 会动态创建一个和实际集合大小完全一致的数组, 避免额外的内存分配和数据拷贝

Arrays.asList() 返回对象不能用修改集合的方法 (add/remove/clear)

  1. 使用工具类 Arrays.asList() 把数组转换成集合时,为什么不能使用其修改集合相关的方法(add/remove/clear)?这样做会抛出什么异常?手册解释 Arrays.asList() 返回的是什么类型的对象,并将其体现为哪种设计模式?

其体现的是适配器模式, 所以只能对元素层面做修改, 如果使用了 add/remove 这些会干扰原始 list 长度的方法, 就会违背底层的 Array 的长度限制, 不是一个纯粹的适配器(只为了适配不同查询接口, 底层数据只有一份)

泛型通配符 <? extends T><? super T> 的接收/写入数据规定

  1. 泛型通配符 <? extends T><? super T> 在接收/读取数据和写入数据(add 方法)方面各有何强制规定?手册引用了什么原则来解释?请简述这两种通配符的使用场景和原因(个人理解部分)
  1. <? extends T> 只能读取, 不能写入, 因为读取的时候对象在编译时会被当做 T 类型(哪怕实际是 T 类型的一个子类), 可以安全的赋值给一个 T 类型的变量 不可以写入: 为了保证类型安全.

例如,如果你有一个 List<? extends Number>,它可能实际是一个 List 或 List。如果你被允许添加一个 Number 对象(比如 new BigDecimal(10)),那么如果这个列表实际是 List,就会出现类型不匹配的问题。编译器在编译时无法确定 ? extends T 具体代表 T 的哪个子类,为了防止将一个不兼容的子类或 T 本身添加到实际的集合类型中,它禁止了 add 操作

  1. <? super T> 只能写入, 不能读取. 写入的时候可以添加类型为 T 的对象, 以及任何类型是 T 的子类的对象.

    读取的时候只能认为其都是 Object 这个最上层的父类.

PECS 原则 (Producer Extends Consumer Super)

PECS 原则解释:

  • Producer Extends (生产者使用 Extends): 当您需要从泛型集合中读取数据(即集合作为数据的生产者)时,使用 <? extends T>。例如,List<? extends Number> 可以从中读取 Number 或其子类(如 Integer, Double),将它们作为 Number 类型来使用。但不能往里面添加元素。

  • Consumer Super (消费者使用 Super): 当您需要向泛型集合中写入数据(即集合作为数据的消费者)时,使用 <? super T>。例如,List<? super Integer> 可以向其中添加 Integer 或其子类(如 int 字面量),因为它们都能安全地被存储在 Integer 的任何父类类型中。但从里面读取元素时,只能当作 Object