gitbook/Java业务开发常见错误100例/docs/261446.md

315 lines
16 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 答疑篇:代码篇思考题集锦(二)
你好,我是朱晔。
今天我们继续一起分析这门课第7~12讲的课后思考题。这些题目涉及了数据库索引、判等问题、数值计算、集合类、空值处理和异常处理的12道问题。
接下来,我们就一一具体分析吧。
### [07 | 数据库索引:索引并不是万能药](https://time.geekbang.org/column/article/213342)
**问题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讲](https://time.geekbang.org/column/article/213342)的聚簇索引和二级索引部分。
### [08 | 判等问题:程序里如何确定你就是你?](https://time.geekbang.org/column/article/213604)
**问题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 | 数值计算:注意精度、舍入和溢出问题](https://time.geekbang.org/column/article/213796)
**问题1**[BigDecimal](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html)提供了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列表操作](https://time.geekbang.org/column/article/216778)
**问题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和恼人的空指针](https://time.geekbang.org/column/article/216830)
**问题1**ConcurrentHashMap的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属性。关于这三个属性的使用方式你可以参考如下源码或是[这里](https://mp.baomidou.com/guide/annotation.html#tablefield)的官方文档:
```
/**
* 字段验证策略之 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 | 异常处理:别让自己在出问题的时候变为瞎子](https://time.geekbang.org/column/article/220230)
**问题1**关于在finally代码块中抛出异常的坑如果在finally代码块中返回值你觉得程序会以try或catch中的返回值为准还是以finally中的返回值为准呢
以finally中的返回值为准。
从语义上来说finally是做方法收尾资源释放处理的我们不建议在finally中有return这样逻辑会很混乱。这是因为实现上finally中的代码块会被复制多份分别放到try和catch调用return和throw异常之前所以finally中如果有返回值会覆盖try中的返回值。
**问题2**对于手动抛出的异常不建议直接使用Exception或RuntimeException通常建议复用JDK中的一些标准异常比如[IllegalArgumentException](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalArgumentException.html)、[IllegalStateException](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html)、[UnsupportedOperationException](https://docs.oracle.com/javase/8/docs/api/java/lang/UnsupportedOperationException.html)。你能说说它们的适用场景,并列出更多常见的可重用标准异常吗?
我们先分别看看IllegalArgumentException、IllegalStateException、UnsupportedOperationException这三种异常的适用场景。
* IllegalArgumentException参数不合法异常适用于传入的参数不符合方法要求的场景。
* IllegalStateException状态不合法异常适用于状态机的状态的无效转换当前逻辑的执行状态不适合进行相应操作等场景。
* UnsupportedOperationException操作不支持异常适用于某个操作在实现或环境下不支持的场景。
还可以重用的异常有IndexOutOfBoundsException、NullPointerException、ConcurrentModificationException等。
以上就是咱们这门课第7~12讲的思考题答案了。
关于这些题目,以及背后涉及的知识点,如果你还有哪里感觉不清楚的,欢迎在评论区与我留言,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。