Day636.思考题解答② -Java业务开发常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day636.思考题解答② -Java业务开发常见错误相关的知识,希望对你有一定的参考价值。

思考题解答②

Hi,我是阿昌,这是针对前面学习文章记录结尾总和在一起的思考题解答。


一、数据库索引

在介绍二级索引代价时,我们通过 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
  
]

二、索引失效

索引除了可以用于加速搜索外,还可以在排序时发挥作用,你能通过 EXPLAIN 来证明吗?你知道,针对排序在什么情况下,索引会失效吗?

答:排序使用到索引,在执行计划中的体现就是 key 这一列。如果没有用到索引,会在 Extra 中看到 Using filesort,代表使用了内存或磁盘进行排序。而具体走内存还是磁盘,是由 sort_buffer_size 和排序数据大小决定的。


排序无法使用到索引的情况有

  • 对于使用联合索引进行排序的场景,多个字段排序 ASC 和 DESC 混用;
  • a+b 作为联合索引,按照 a 范围查询后按照 b 排序;
  • 排序列涉及到的多个字段不属于同一个联合索引;
  • 排序列使用了表达式。

三、判等问题

在实现 equals 时,我是先通过 getClass 方法判断两个对象的类型,你可能会想到还可以使用 instanceof 来判断。你能说说这两种实现方式的区别吗?

答:事实上,使用 getClass 和 instanceof 这两种方案都是可以判断对象类型的。它们的区别就是

  • getClass 限制了这两个对象只能属于同一个类,
  • instanceof 却允许两个对象是同一个类或其子类。

正是因为这种区别,不同的人对这两种方案有不同的喜好,争论也很多。Lombok 使用的是 instanceof 的方案。


在“hashCode 和 equals 要配对实现”这一节的例子中,我演示了可以通过 HashSet 的 contains 方法判断元素是否在 HashSet 中。那同样是 Set 的 TreeSet,其 contains 方法和 HashSet 的 contains 方法有什么区别吗?

答:

  • HashSet 基于 HashMap,数据结构是哈希表。所以,HashSet 的 contains 方法,其实就是根据 hashcode 和 equals 去判断相等的。
  • TreeSet 基于 TreeMap,数据结构是红黑树。所以,TreeSet 的 contains 方法,其实就是根据 compareTo 去判断相等的。

四、数值计算

BigDecimal提供了 8 种舍入模式,你能通过一些例子说说它们的区别吗?

答:

  • 第一种,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 -> 6,2.5 -> 2。
  • 第八种,ROUND_UNNECESSARY,假设请求的操作具有精确的结果,也就是不需要进行舍入。如果计算结果产生不精确的结果,则抛出 ArithmeticException。

数据库(比如 mysql)中的浮点数和整型数字,你知道应该怎样定义吗?又如何实现浮点数的准确计算呢?

答:MySQL 中的整数根据能表示的范围有 TINYINT、SMALLINT、MEDIUMINT、INTEGER、BIGINT 等类型,浮点数包括单精度浮点数 FLOAT 和双精度浮点数 DOUBLE 和 Java 中的 float/double 一样,同样有精度问题。

要解决精度问题,主要有两个办法:

  • 第一,使用 DECIMAL 类型(和那些 INT 类型一样,都属于严格数值数据类型),比如 DECIMAL(13, 2) 或 DECIMAL(13, 4)。
  • 第二,使用整数保存到分,这种方式容易出错,万一读的时候忘记 /100 或者是存的时候忘记 *100,可能会引起重大问题。当然了,我们也可以考虑将整数和小数分开保存到两个整数字段。

五、List集合

调用类型是 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]

循环遍历 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);
    

六、空值处理

ConcurrentHashMap 的 Key 和 Value 都不能为 null,而 HashMap 却可以,你知道这么设计的原因是什么吗?TreeMap、Hashtable 等 Map 的 Key 和 Value 是否支持 null 呢?

答:原因正如 ConcurrentHashMap 的作者所说:

The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t 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。

对于 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;

七、异常处理

关于在 finally 代码块中抛出异常的坑,如果在 finally 代码块中返回值,你觉得程序会以 try 或 catch 中的返回值为准,还是以 finally 中的返回值为准呢?

答:以 finally 中的返回值为准。从语义上来说,finally 是做方法收尾资源释放处理的,我们不建议在 finally 中有 return,这样逻辑会很混乱。这是因为,实现上 finally 中的代码块会被复制多份,分别放到 try 和 catch 调用 return 和 throw 异常之前,所以 finally 中如果有返回值,会覆盖 try 中的返回值。


对于手动抛出的异常,不建议直接使用 Exception 或 RuntimeException,通常建议复用 JDK 中的一些标准异常,比如IllegalArgumentExceptionIllegalStateExceptionUnsupportedOperationException。你能说说它们的适用场景,并列出更多常见的可重用标准异常吗?

答:我们先分别看看 IllegalArgumentException、IllegalStateException、UnsupportedOperationException 这三种异常的适用场景。

  • IllegalArgumentException:参数不合法异常,适用于传入的参数不符合方法要求的场景。
  • IllegalStateException:状态不合法异常,适用于状态机的状态的无效转换,当前逻辑的执行状态不适合进行相应操作等场景。
  • UnsupportedOperationException:操作不支持异常,适用于某个操作在实现或环境下不支持的场景。

还可以重用的异常有 IndexOutOfBoundsException、NullPointerException、ConcurrentModificationException 等。


以上是关于Day636.思考题解答② -Java业务开发常见错误的主要内容,如果未能解决你的问题,请参考以下文章

Day507.图灵学院之面试题② -面经

HQ-day15 HTML基础②

北京集训②DAY2 Mroning -未完

团队作业8——第二次项目冲刺(Beta阶段)Day2--5.19

Day486.尚硅谷之高频重点面试题② -面经

Day745.如何应对变慢的Redis② -Redis 核心技术与实战