learn-tech/专栏/Java业务开发常见错误100例/答疑篇:代码篇思考题集锦(二).md
2024-10-16 00:20:59 +08:00

419 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
答疑篇:代码篇思考题集锦(二)
今天,我们继续一起分析这门课第 7~12 讲的课后思考题。这些题目涉及了数据库索引、判等问题、数值计算、集合类、空值处理和异常处理的 12 道问题。
接下来,我们就一一具体分析吧。
07 | 数据库索引:索引并不是万能药
问题 1在介绍二级索引代价时我们通过 EXPLAIN 命令看到了索引覆盖和回表的两种情况。你能用 optimizer trace 来分析一下这两种情况的成本差异吗?
答:如下代码所示,打开 optimizer_trace 后,再执行 SQL 就可以查询 information_schema.OPTIMIZER_TRACE 表查看执行计划了,最后可以关闭 optimizer_trace 功能:
SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
假设我们为表 person 的 NAME 和 SCORE 列建了联合索引,那么下面第二条语句应该可以走索引覆盖,而第一条语句需要回表:
explain select * from person where NAME='name1';
explain select NAME,SCORE from person where NAME='name1';
通过观察 OPTIMIZER_TRACE 的输出可以看到索引覆盖index_only=true的成本是 1.21 而回表查询index_only=false的是 2.21,也就是索引覆盖节省了回表的成本 1。
索引覆盖:
analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "name_score",
"ranges": [
"name1 <= name <= name1"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": true,
"rows": 1,
"cost": 1.21,
"chosen": true
}
]
回表:
"range_scan_alternatives": [
{
"index": "name_score",
"ranges": [
"name1 <= name <= name1"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 1,
"cost": 2.21,
"chosen": true
}
]
问题 2索引除了可以用于加速搜索外还可以在排序时发挥作用你能通过 EXPLAIN 来证明吗?你知道,针对排序在什么情况下,索引会失效吗?
答:排序使用到索引,在执行计划中的体现就是 key 这一列。如果没有用到索引,会在 Extra 中看到 Using filesort代表使用了内存或磁盘进行排序。而具体走内存还是磁盘是由 sort_buffer_size 和排序数据大小决定的。
排序无法使用到索引的情况有:
对于使用联合索引进行排序的场景,多个字段排序 ASC 和 DESC 混用;
a+b 作为联合索引,按照 a 范围查询后按照 b 排序;
排序列涉及到的多个字段不属于同一个联合索引;
排序列使用了表达式。
其实,这些原因都和索引的结构有关。你可以再有针对性地复习下第 07 讲的聚簇索引和二级索引部分。
08 | 判等问题:程序里如何确定你就是你?
问题 1在实现 equals 时,我是先通过 getClass 方法判断两个对象的类型,你可能会想到还可以使用 instanceof 来判断。你能说说这两种实现方式的区别吗?
答:事实上,使用 getClass 和 instanceof 这两种方案都是可以判断对象类型的。它们的区别就是getClass 限制了这两个对象只能属于同一个类,而 instanceof 却允许两个对象是同一个类或其子类。
正是因为这种区别不同的人对这两种方案有不同的喜好争论也很多。在我看来你只需要根据自己的要求去选择。补充说明一下Lombok 使用的是 instanceof 的方案。
问题 2在“hashCode 和 equals 要配对实现”这一节的例子中,我演示了可以通过 HashSet 的 contains 方法判断元素是否在 HashSet 中。那同样是 Set 的 TreeSet其 contains 方法和 HashSet 的 contains 方法有什么区别吗?
HashSet 基于 HashMap数据结构是哈希表。所以HashSet 的 contains 方法,其实就是根据 hashcode 和 equals 去判断相等的。
TreeSet 基于 TreeMap数据结构是红黑树。所以TreeSet 的 contains 方法,其实就是根据 compareTo 去判断相等的。
09 | 数值计算:注意精度、舍入和溢出问题
问题 1BigDecimal提供了 8 种舍入模式,你能通过一些例子说说它们的区别吗?
答:@Darren 同学的留言非常全面,梳理得也非常清楚了。这里,我对他的留言稍加修改,就是这个问题的答案了。
第一种ROUND_UP舍入远离零的舍入模式在丢弃非零部分之前始终增加数字始终对非零舍弃部分前面的数字加 1。 需要注意的是,此舍入模式始终不会减少原始值。
第二种ROUND_DOWN接近零的舍入模式在丢弃某部分之前始终不增加数字从不对舍弃部分前面的数字加 1即截断。 需要注意的是,此舍入模式始终不会增加原始值。
第三种ROUND_CEILING接近正无穷大的舍入模式。 如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同; 如果为负,则舍入行为与 ROUND_DOWN 相同。 需要注意的是,此舍入模式始终不会减少原始值。
第四种ROUND_FLOOR接近负无穷大的舍入模式。 如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同; 如果为负,则舍入行为与 ROUND_UP 相同。 需要注意的是,此舍入模式始终不会增加原始值。
第五种ROUND_HALF_UP向“最接近的”数字舍入。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则,舍入行为与 ROUND_DOWN 相同。 需要注意的是,这是我们大多数人在小学时就学过的舍入模式(四舍五入)。
第六种ROUND_HALF_DOWN向“最接近的”数字舍入。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则,舍入行为与 ROUND_DOWN 相同(五舍六入)。
第七种ROUND_HALF_EVEN向“最接近的”数字舍入。这种算法叫做银行家算法具体规则是四舍六入五则看前一位如果是偶数舍入如果是奇数进位比如 5.5 -> 62.5 -> 2。
第八种ROUND_UNNECESSARY假设请求的操作具有精确的结果也就是不需要进行舍入。如果计算结果产生不精确的结果则抛出 ArithmeticException。
问题 2数据库比如 MySQL中的浮点数和整型数字你知道应该怎样定义吗又如何实现浮点数的准确计算呢
MySQL 中的整数根据能表示的范围有 TINYINT、SMALLINT、MEDIUMINT、INTEGER、BIGINT 等类型,浮点数包括单精度浮点数 FLOAT 和双精度浮点数 DOUBLE 和 Java 中的 float/double 一样,同样有精度问题。
要解决精度问题,主要有两个办法:
第一,使用 DECIMAL 类型(和那些 INT 类型一样,都属于严格数值数据类型),比如 DECIMAL(13, 2) 或 DECIMAL(13, 4)。
第二,使用整数保存到分,这种方式容易出错,万一读的时候忘记 /100 或者是存的时候忘记 *100可能会引起重大问题。当然了我们也可以考虑将整数和小数分开保存到两个整数字段。
10 | 集合类:坑满地的 List 列表操作
问题 1调用类型是 Integer 的 ArrayList 的 remove 方法删除元素,传入一个 Integer 包装类的数字和传入一个 int 基本类型的数字,结果一样吗?
答:传 int 基本类型的 remove 方法是按索引值移除,返回移除的值;传 Integer 包装类的 remove 方法是按值移除,返回列表移除项目之前是否包含这个值(是否移除成功)。
为了验证两个 remove 方法重载的区别,我们写一段测试代码比较一下:
private static void removeByIndex(int index) {
List<Integer> list =
IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toCollection(ArrayList::new));
System.out.println(list.remove(index));
System.out.println(list);
}
private static void removeByValue(Integer index) {
List<Integer> list =
IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toCollection(ArrayList::new));
System.out.println(list.remove(index));
System.out.println(list);
}
测试一下 removeByIndex(4),通过输出可以看到第五项被移除了,返回 5
5
[1, 2, 3, 4, 6, 7, 8, 9, 10]
而调用 removeByValue(Integer.valueOf(4)),通过输出可以看到值 4 被移除了,返回 true
true
[1, 2, 3, 5, 6, 7, 8, 9, 10]
问题 2循环遍历 List调用 remove 方法删除元素,往往会遇到 ConcurrentModificationException原因是什么修复方式又是什么呢
原因是remove 的时候会改变 modCount通过迭代器遍历就会触发 ConcurrentModificationException。我们看下 ArrayList 类内部迭代器的相关源码:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
要修复这个问题,有以下两种解决方案。
第一种,通过 ArrayList 的迭代器 remove。迭代器的 remove 方法会维护一个 expectedModCount使其与 ArrayList 的 modCount 保持一致:
List<String> list =
IntStream.rangeClosed(1, 10).mapToObj(String::valueOf).collect(Collectors.toCollection(ArrayList::new));
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
String next = iterator.next();
if ("2".equals(next)) {
iterator.remove();
}
}
System.out.println(list);
第二种,直接使用 removeIf 方法,其内部使用了迭代器的 remove 方法:
List<String> list =
IntStream.rangeClosed(1, 10).mapToObj(String::valueOf).collect(Collectors.toCollection(ArrayList::new));
list.removeIf(item -> item.equals("2"));
System.out.println(list);
11 | 空值处理:分不清楚的 null 和恼人的空指针
问题 1ConcurrentHashMap 的 Key 和 Value 都不能为 null而 HashMap 却可以你知道这么设计的原因是什么吗TreeMap、Hashtable 等 Map 的 Key 和 Value 是否支持 null 呢?
答:原因正如 ConcurrentHashMap 的作者所说:
The main reason that nulls arent allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps cant be accommodated. The main one is that if map.get(key) returns null, you cant detect whether the key explicitly maps to null vs the key isnt mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.
如果 Value 为 null 会增加二义性,也就是说多线程情况下 map.get(key) 返回 null我们无法区分 Value 原本就是 null 还是 Key 没有映射Key 也是类似的原因。此外,我也更同意他的观点,就是普通的 Map 允许 null 是否是一个正确的做法,也值得商榷,因为这会增加犯错的可能性。
Hashtable 也是线程安全的,所以 Key 和 Value 不可以是 null。
TreeMap 是线程不安全的,但是因为需要排序,需要进行 key 的 compareTo 方法,所以 Key 不能是 null而 Value 可以是 null。
问题 2对于 Hibernate 框架,我们可以使用 @DynamicUpdate 注解实现字段的动态更新。那么,对于 MyBatis 框架来说,要如何实现类似的动态 SQL 功能,实现插入和修改 SQL 只包含 POJO 中的非空字段呢?
MyBatis 可以通过动态 SQL 实现:
<select id="findUser" resultType="User">
SELECT * FROM USER
WHERE 1=1
<if test="name != null">
AND name like #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</select>
如果使用 MyBatisPlus 的话,实现类似的动态 SQL 功能会更方便。我们可以直接在字段上加 @TableField 注解来实现,可以设置 insertStrategy、updateStrategy、whereStrategy 属性。关于这三个属性的使用方式,你可以参考如下源码,或是这里的官方文档:
/**
* 字段验证策略之 insert: 当insert操作时该字段拼接insert语句时的策略
* IGNORED: 直接拼接 insert into table_a(column) values (#{columnProperty});
* NOT_NULL: insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
* NOT_EMPTY: insert into table_a(<if test="columnProperty != null and columnProperty!=''">column</if>) values (<if test="columnProperty != null and columnProperty!=''">#{columnProperty}</if>)
*
* @since 3.1.2
*/
FieldStrategy insertStrategy() default FieldStrategy.DEFAULT;
/**
* 字段验证策略之 update: 当更新操作时该字段拼接set语句时的策略
* IGNORED: 直接拼接 update table_a set column=#{columnProperty}, 属性为null/空string都会被set进去
* NOT_NULL: update table_a set <if test="columnProperty != null">column=#{columnProperty}</if>
* NOT_EMPTY: update table_a set <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
*
* @since 3.1.2
*/
FieldStrategy updateStrategy() default FieldStrategy.DEFAULT;
/**
* 字段验证策略之 where: 表示该字段在拼接where条件时的策略
* IGNORED: 直接拼接 column=#{columnProperty}
* NOT_NULL: <if test="columnProperty != null">column=#{columnProperty}</if>
* NOT_EMPTY: <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
*
* @since 3.1.2
*/
FieldStrategy whereStrategy() default FieldStrategy.DEFAULT;
12 | 异常处理:别让自己在出问题的时候变为瞎子
问题 1关于在 finally 代码块中抛出异常的坑,如果在 finally 代码块中返回值,你觉得程序会以 try 或 catch 中的返回值为准,还是以 finally 中的返回值为准呢?
答:以 finally 中的返回值为准。
从语义上来说finally 是做方法收尾资源释放处理的,我们不建议在 finally 中有 return这样逻辑会很混乱。这是因为实现上 finally 中的代码块会被复制多份,分别放到 try 和 catch 调用 return 和 throw 异常之前,所以 finally 中如果有返回值,会覆盖 try 中的返回值。
问题 2对于手动抛出的异常不建议直接使用 Exception 或 RuntimeException通常建议复用 JDK 中的一些标准异常比如IllegalArgumentException、IllegalStateException、UnsupportedOperationException。你能说说它们的适用场景并列出更多常见的可重用标准异常吗
答:我们先分别看看 IllegalArgumentException、IllegalStateException、UnsupportedOperationException 这三种异常的适用场景。
IllegalArgumentException参数不合法异常适用于传入的参数不符合方法要求的场景。
IllegalStateException状态不合法异常适用于状态机的状态的无效转换当前逻辑的执行状态不适合进行相应操作等场景。
UnsupportedOperationException操作不支持异常适用于某个操作在实现或环境下不支持的场景。
还可以重用的异常有 IndexOutOfBoundsException、NullPointerException、ConcurrentModificationException 等。
以上,就是咱们这门课第 7~12 讲的思考题答案了。
关于这些题目,以及背后涉及的知识点,如果你还有哪里感觉不清楚的,欢迎在评论区与我留言,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。