(四) OOP 规约
基本类型和包装类型的使用
- 关于基本数据类型与包装数据类型的使用标准,手册强制规定在哪些场景下必须使用包装数据类型?推荐在哪些场景下使用基本数据类型?手册对此给出了怎样的个人理解?使用基本类型接收数据库查询结果(可能为 null)时存在什么风险?[8, 9, 13-15]
他们的差别实际上就是有没有null, 以及需要考虑自动拆装箱
-
所有的 POJO 和 RPC 调用, 都必须使用包装类型, 因为可以用 null 表示服务不正常或者值缺失. 如果用基本类型, 无法表达这一语义.
-
函数的局部变量使用基本数据类型,:
-
避免自动拆装箱的性能损失
-
局部变量的使用范围很小, 开发者可控.
-
一定不能用基本类型接收数据库的值
数据库查询结果可能是null, 那么在转换成基本类型的时候, 需要拆箱, 这就会导致直接报出 NPE.
布尔类型的默认getter 规则
-
对于 基本数据类型 boolean 的属性,始终使用 isXxx() 作为 getter 方法
-
对于 包装数据类型 Boolean 的属性,使用 getXxx() 作为 getter 方法。同时,要避免该 Boolean 属性对应的 isXxx() 方法与之共存。
String.split() 方法的注意点
- 使用索引访问 String 的
split
方法得到的数组时,手册推荐注意什么?为什么不传入 limit 参数时,split()
方法会从尾部处理连续的空字符串?
-
使用索引访问 split() 的数组时候, 需要检查最后一个分隔符后面是否有内容, 避免下标越界
-
不传入 limit 参数, 等于传入 limit = 0, split() 会从后向前检查:
-
连续
-
空字符串, 注意空格不算
且剔除. 可以当成自动做了 trim()
-
(六) 集合处理
hashCode 和 equals() 的使用方式
- 关于
hashCode
和equals
的处理,手册强制规定了哪些规则?针对 Set 或 Map 的 key 的对象类型,有何特殊要求?[19, 22, 23]
首先分析一下 hashCode 和 equals 的作用.
hashCode 是快速定位对象可能存在的桶 Bucket, equals() 方法是在同一个桶之中确定对象是否相等.
下面是准则:
-
重写 equals() 必须重写 hashCode
-
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
- 使用
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 值如何参与合并呢?
-
有null 值会让合并更加复杂: remappingFunction 的签名通常是 BinaryOperator<V>,它接收两个 V 类型的参数(旧 value 和 新 value),并返回一个 V 类型的结果。如果允许 null 值作为输入,那么 remappingFunction 的实现就需要显式地处理 null 输入的情况。
-
null 是有歧义的: 是没有这个key, 还是这个key 的value 被显式的设置成了 null?
所以在合并这个方法中, 强制两个对象都有值, 并且显式的做校验, 确实是更好的方式
集合转数组的注意事项
必须使用 toArray(T[] array), 其中 array 是类型一致, 长度为 0 的空数组
-
不能使用无参的 toArray(): 其返回的是 Object[], 会产生 ClassCastException
-
长度为0, toArray(T[] array) 会动态创建一个和实际集合大小完全一致的数组, 避免额外的内存分配和数据拷贝
Arrays.asList() 返回对象不能用修改集合的方法 (add/remove/clear)
- 使用工具类
Arrays.asList()
把数组转换成集合时,为什么不能使用其修改集合相关的方法(add/remove/clear)?这样做会抛出什么异常?手册解释Arrays.asList()
返回的是什么类型的对象,并将其体现为哪种设计模式?
其体现的是适配器模式, 所以只能对元素层面做修改, 如果使用了 add/remove 这些会干扰原始 list 长度的方法, 就会违背底层的 Array 的长度限制, 不是一个纯粹的适配器(只为了适配不同查询接口, 底层数据只有一份)
泛型通配符 <? extends T>
和 <? super T>
的接收/写入数据规定
- 泛型通配符
<? extends T>
和<? super T>
在接收/读取数据和写入数据(add 方法)方面各有何强制规定?手册引用了什么原则来解释?请简述这两种通配符的使用场景和原因(个人理解部分)
<? extends T>
只能读取, 不能写入, 因为读取的时候对象在编译时会被当做 T 类型(哪怕实际是 T 类型的一个子类), 可以安全的赋值给一个 T 类型的变量 不可以写入: 为了保证类型安全.
例如,如果你有一个 List<? extends Number>,它可能实际是一个 List
或 List 。如果你被允许添加一个 Number 对象(比如 new BigDecimal(10)),那么如果这个列表实际是 List ,就会出现类型不匹配的问题。编译器在编译时无法确定 ? extends T 具体代表 T 的哪个子类,为了防止将一个不兼容的子类或 T 本身添加到实际的集合类型中,它禁止了 add 操作
-
<? 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