learn.lianglianglee.com/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(二).md.html
2022-05-11 18:57:05 +08:00

1541 lines
36 KiB
HTML
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.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>答疑篇:代码篇思考题集锦(二).md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/00 开篇词 业务代码真的会有这么多坑?.md.html">00 开篇词 业务代码真的会有这么多坑?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html">01 使用了并发工具类库,线程安全就高枕无忧了吗?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/02 代码加锁:不要让“锁”事成为烦心事.md.html">02 代码加锁:不要让“锁”事成为烦心事.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/03 线程池:业务代码最常用也最容易犯错的组件.md.html">03 线程池:业务代码最常用也最容易犯错的组件.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/04 连接池:别让连接池帮了倒忙.md.html">04 连接池:别让连接池帮了倒忙.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/05 HTTP调用你考虑到超时、重试、并发了吗.md.html">05 HTTP调用你考虑到超时、重试、并发了吗.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/06 2成的业务代码的Spring声明式事务可能都没处理正确.md.html">06 2成的业务代码的Spring声明式事务可能都没处理正确.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/07 数据库索引:索引并不是万能药.md.html">07 数据库索引:索引并不是万能药.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/08 判等问题:程序里如何确定你就是你?.md.html">08 判等问题:程序里如何确定你就是你?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/09 数值计算:注意精度、舍入和溢出问题.md.html">09 数值计算:注意精度、舍入和溢出问题.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/10 集合类坑满地的List列表操作.md.html">10 集合类坑满地的List列表操作.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/11 空值处理分不清楚的null和恼人的空指针.md.html">11 空值处理分不清楚的null和恼人的空指针.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/12 异常处理:别让自己在出问题的时候变为瞎子.md.html">12 异常处理:别让自己在出问题的时候变为瞎子.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/13 日志:日志记录真没你想象的那么简单.md.html">13 日志:日志记录真没你想象的那么简单.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/14 文件IO实现高效正确的文件读写并非易事.md.html">14 文件IO实现高效正确的文件读写并非易事.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/15 序列化:一来一回你还是原来的你吗?.md.html">15 序列化:一来一回你还是原来的你吗?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/16 用好Java 8的日期时间类少踩一些“老三样”的坑.md.html">16 用好Java 8的日期时间类少踩一些“老三样”的坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/17 别以为“自动挡”就不可能出现OOM.md.html">17 别以为“自动挡”就不可能出现OOM.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/18 当反射、注解和泛型遇到OOP时会有哪些坑.md.html">18 当反射、注解和泛型遇到OOP时会有哪些坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/19 Spring框架IoC和AOP是扩展的核心.md.html">19 Spring框架IoC和AOP是扩展的核心.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/20 Spring框架框架帮我们做了很多工作也带来了复杂度.md.html">20 Spring框架框架帮我们做了很多工作也带来了复杂度.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/21 代码重复:搞定代码重复的三个绝招.md.html">21 代码重复:搞定代码重复的三个绝招.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/22 接口设计:系统间对话的语言,一定要统一.md.html">22 接口设计:系统间对话的语言,一定要统一.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/23 缓存设计:缓存可以锦上添花也可以落井下石.md.html">23 缓存设计:缓存可以锦上添花也可以落井下石.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/24 业务代码写完,就意味着生产就绪了?.md.html">24 业务代码写完,就意味着生产就绪了?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/25 异步处理好用,但非常容易用错.md.html">25 异步处理好用,但非常容易用错.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/26 数据存储NoSQL与RDBMS如何取长补短、相辅相成.md.html">26 数据存储NoSQL与RDBMS如何取长补短、相辅相成.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/27 数据源头:任何客户端的东西都不可信任.md.html">27 数据源头:任何客户端的东西都不可信任.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html">28 安全兜底:涉及钱时,必须考虑防刷、限量和防重.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/29 数据和代码:数据就是数据,代码就是代码.md.html">29 数据和代码:数据就是数据,代码就是代码.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/30 如何正确保存和传输敏感数据?.md.html">30 如何正确保存和传输敏感数据?.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/31 加餐1带你吃透课程中Java 8的那些重要知识点.md.html">31 加餐1带你吃透课程中Java 8的那些重要知识点.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/32 加餐2带你吃透课程中Java 8的那些重要知识点.md.html">32 加餐2带你吃透课程中Java 8的那些重要知识点.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/33 加餐3定位应用问题排错套路很重要.md.html">33 加餐3定位应用问题排错套路很重要.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/34 加餐4分析定位Java问题一定要用好这些工具.md.html">34 加餐4分析定位Java问题一定要用好这些工具.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/35 加餐5分析定位Java问题一定要用好这些工具.md.html">35 加餐5分析定位Java问题一定要用好这些工具.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/36 加餐6这15年来我是如何在工作中学习技术和英语的.md.html">36 加餐6这15年来我是如何在工作中学习技术和英语的.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/37 加餐7程序员成长28计.md.html">37 加餐7程序员成长28计.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/38 加餐8Java程序从虚拟机迁移到Kubernetes的一些坑.md.html">38 加餐8Java程序从虚拟机迁移到Kubernetes的一些坑.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(一).md.html">答疑篇:代码篇思考题集锦(一).md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(三).md.html">答疑篇:代码篇思考题集锦(三).md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(二).md.html">答疑篇:代码篇思考题集锦(二).md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:加餐篇思考题答案合集.md.html">答疑篇:加餐篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:安全篇思考题答案合集.md.html">答疑篇:安全篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:设计篇思考题答案合集.md.html">答疑篇:设计篇思考题答案合集.md.html</a>
</li>
<li>
<a href="/专栏/Java 业务开发常见错误 100 例/结束语 写代码时,如何才能尽量避免踩坑?.md.html">结束语 写代码时,如何才能尽量避免踩坑?.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>答疑篇:代码篇思考题集锦(二)</h1>
<p>你好,我是朱晔。</p>
<p>今天,我们继续一起分析这门课第 7~12 讲的课后思考题。这些题目涉及了数据库索引、判等问题、数值计算、集合类、空值处理和异常处理的 12 道问题。</p>
<p>接下来,我们就一一具体分析吧。</p>
<h3>07 | 数据库索引:索引并不是万能药</h3>
<p>问题 1在介绍二级索引代价时我们通过 EXPLAIN 命令看到了索引覆盖和回表的两种情况。你能用 optimizer trace 来分析一下这两种情况的成本差异吗?</p>
<p>答:如下代码所示,打开 optimizer_trace 后,再执行 SQL 就可以查询 information_schema.OPTIMIZER_TRACE 表查看执行计划了,最后可以关闭 optimizer_trace 功能:</p>
<pre><code>SET optimizer_trace=&quot;enabled=on&quot;;
SELECT * FROM person WHERE NAME &gt;'name84059' AND create_time&gt;'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace=&quot;enabled=off&quot;;
</code></pre>
<p>假设我们为表 person 的 NAME 和 SCORE 列建了联合索引,那么下面第二条语句应该可以走索引覆盖,而第一条语句需要回表:</p>
<pre><code>explain select * from person where NAME='name1';
explain select NAME,SCORE from person where NAME='name1';
</code></pre>
<p>通过观察 OPTIMIZER_TRACE 的输出可以看到索引覆盖index_only=true的成本是 1.21 而回表查询index_only=false的是 2.21,也就是索引覆盖节省了回表的成本 1。</p>
<p>索引覆盖:</p>
<pre><code>analyzing_range_alternatives&quot;: {
&quot;range_scan_alternatives&quot;: [
{
&quot;index&quot;: &quot;name_score&quot;,
&quot;ranges&quot;: [
&quot;name1 &lt;= name &lt;= name1&quot;
] /* ranges */,
&quot;index_dives_for_eq_ranges&quot;: true,
&quot;rowid_ordered&quot;: false,
&quot;using_mrr&quot;: false,
&quot;index_only&quot;: true,
&quot;rows&quot;: 1,
&quot;cost&quot;: 1.21,
&quot;chosen&quot;: true
}
]
</code></pre>
<p>回表:</p>
<pre><code>&quot;range_scan_alternatives&quot;: [
{
&quot;index&quot;: &quot;name_score&quot;,
&quot;ranges&quot;: [
&quot;name1 &lt;= name &lt;= name1&quot;
] /* ranges */,
&quot;index_dives_for_eq_ranges&quot;: true,
&quot;rowid_ordered&quot;: false,
&quot;using_mrr&quot;: false,
&quot;index_only&quot;: false,
&quot;rows&quot;: 1,
&quot;cost&quot;: 2.21,
&quot;chosen&quot;: true
}
]
</code></pre>
<p>问题 2索引除了可以用于加速搜索外还可以在排序时发挥作用你能通过 EXPLAIN 来证明吗?你知道,针对排序在什么情况下,索引会失效吗?</p>
<p>答:排序使用到索引,在执行计划中的体现就是 key 这一列。如果没有用到索引,会在 Extra 中看到 Using filesort代表使用了内存或磁盘进行排序。而具体走内存还是磁盘是由 sort_buffer_size 和排序数据大小决定的。</p>
<p>排序无法使用到索引的情况有:</p>
<p>对于使用联合索引进行排序的场景,多个字段排序 ASC 和 DESC 混用;</p>
<p>a+b 作为联合索引,按照 a 范围查询后按照 b 排序;</p>
<p>排序列涉及到的多个字段不属于同一个联合索引;</p>
<p>排序列使用了表达式。</p>
<p>其实,这些原因都和索引的结构有关。你可以再有针对性地复习下第 07 讲的聚簇索引和二级索引部分。</p>
<h3>08 | 判等问题:程序里如何确定你就是你?</h3>
<p>问题 1在实现 equals 时,我是先通过 getClass 方法判断两个对象的类型,你可能会想到还可以使用 instanceof 来判断。你能说说这两种实现方式的区别吗?</p>
<p>答:事实上,使用 getClass 和 instanceof 这两种方案都是可以判断对象类型的。它们的区别就是getClass 限制了这两个对象只能属于同一个类,而 instanceof 却允许两个对象是同一个类或其子类。</p>
<p>正是因为这种区别不同的人对这两种方案有不同的喜好争论也很多。在我看来你只需要根据自己的要求去选择。补充说明一下Lombok 使用的是 instanceof 的方案。</p>
<p>问题 2在“hashCode 和 equals 要配对实现”这一节的例子中,我演示了可以通过 HashSet 的 contains 方法判断元素是否在 HashSet 中。那同样是 Set 的 TreeSet其 contains 方法和 HashSet 的 contains 方法有什么区别吗?</p>
<p>HashSet 基于 HashMap数据结构是哈希表。所以HashSet 的 contains 方法,其实就是根据 hashcode 和 equals 去判断相等的。</p>
<p>TreeSet 基于 TreeMap数据结构是红黑树。所以TreeSet 的 contains 方法,其实就是根据 compareTo 去判断相等的。</p>
<h3>09 | 数值计算:注意精度、舍入和溢出问题</h3>
<p>问题 1BigDecimal提供了 8 种舍入模式,你能通过一些例子说说它们的区别吗?</p>
<p>答:@Darren 同学的留言非常全面,梳理得也非常清楚了。这里,我对他的留言稍加修改,就是这个问题的答案了。</p>
<p>第一种ROUND_UP舍入远离零的舍入模式在丢弃非零部分之前始终增加数字始终对非零舍弃部分前面的数字加 1。 需要注意的是,此舍入模式始终不会减少原始值。</p>
<p>第二种ROUND_DOWN接近零的舍入模式在丢弃某部分之前始终不增加数字从不对舍弃部分前面的数字加 1即截断。 需要注意的是,此舍入模式始终不会增加原始值。</p>
<p>第三种ROUND_CEILING接近正无穷大的舍入模式。 如果 BigDecimal 为正,则舍入行为与 ROUND_UP 相同; 如果为负,则舍入行为与 ROUND_DOWN 相同。 需要注意的是,此舍入模式始终不会减少原始值。</p>
<p>第四种ROUND_FLOOR接近负无穷大的舍入模式。 如果 BigDecimal 为正,则舍入行为与 ROUND_DOWN 相同; 如果为负,则舍入行为与 ROUND_UP 相同。 需要注意的是,此舍入模式始终不会增加原始值。</p>
<p>第五种ROUND_HALF_UP向“最接近的”数字舍入。如果舍弃部分 &gt;= 0.5,则舍入行为与 ROUND_UP 相同;否则,舍入行为与 ROUND_DOWN 相同。 需要注意的是,这是我们大多数人在小学时就学过的舍入模式(四舍五入)。</p>
<p>第六种ROUND_HALF_DOWN向“最接近的”数字舍入。如果舍弃部分 &gt; 0.5,则舍入行为与 ROUND_UP 相同;否则,舍入行为与 ROUND_DOWN 相同(五舍六入)。</p>
<p>第七种ROUND_HALF_EVEN向“最接近的”数字舍入。这种算法叫做银行家算法具体规则是四舍六入五则看前一位如果是偶数舍入如果是奇数进位比如 5.5 -&gt; 62.5 -&gt; 2。</p>
<p>第八种ROUND_UNNECESSARY假设请求的操作具有精确的结果也就是不需要进行舍入。如果计算结果产生不精确的结果则抛出 ArithmeticException。</p>
<p>问题 2数据库比如 MySQL中的浮点数和整型数字你知道应该怎样定义吗又如何实现浮点数的准确计算呢</p>
<p>MySQL 中的整数根据能表示的范围有 TINYINT、SMALLINT、MEDIUMINT、INTEGER、BIGINT 等类型,浮点数包括单精度浮点数 FLOAT 和双精度浮点数 DOUBLE 和 Java 中的 float/double 一样,同样有精度问题。</p>
<p>要解决精度问题,主要有两个办法:</p>
<p>第一,使用 DECIMAL 类型(和那些 INT 类型一样,都属于严格数值数据类型),比如 DECIMAL(13, 2) 或 DECIMAL(13, 4)。</p>
<p>第二,使用整数保存到分,这种方式容易出错,万一读的时候忘记 /100 或者是存的时候忘记 *100可能会引起重大问题。当然了我们也可以考虑将整数和小数分开保存到两个整数字段。</p>
<h3>10 | 集合类:坑满地的 List 列表操作</h3>
<p>问题 1调用类型是 Integer 的 ArrayList 的 remove 方法删除元素,传入一个 Integer 包装类的数字和传入一个 int 基本类型的数字,结果一样吗?</p>
<p>答:传 int 基本类型的 remove 方法是按索引值移除,返回移除的值;传 Integer 包装类的 remove 方法是按值移除,返回列表移除项目之前是否包含这个值(是否移除成功)。</p>
<p>为了验证两个 remove 方法重载的区别,我们写一段测试代码比较一下:</p>
<pre><code>private static void removeByIndex(int index) {
List&lt;Integer&gt; 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&lt;Integer&gt; list =
IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toCollection(ArrayList::new));
System.out.println(list.remove(index));
System.out.println(list);
}
</code></pre>
<p>测试一下 removeByIndex(4),通过输出可以看到第五项被移除了,返回 5</p>
<pre><code>5
[1, 2, 3, 4, 6, 7, 8, 9, 10]
</code></pre>
<p>而调用 removeByValue(Integer.valueOf(4)),通过输出可以看到值 4 被移除了,返回 true</p>
<pre><code>true
[1, 2, 3, 5, 6, 7, 8, 9, 10]
</code></pre>
<p>问题 2循环遍历 List调用 remove 方法删除元素,往往会遇到 ConcurrentModificationException原因是什么修复方式又是什么呢</p>
<p>原因是remove 的时候会改变 modCount通过迭代器遍历就会触发 ConcurrentModificationException。我们看下 ArrayList 类内部迭代器的相关源码:</p>
<pre><code>public E next() {
checkForComodification();
int i = cursor;
if (i &gt;= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i &gt;= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
</code></pre>
<p>要修复这个问题,有以下两种解决方案。</p>
<p>第一种,通过 ArrayList 的迭代器 remove。迭代器的 remove 方法会维护一个 expectedModCount使其与 ArrayList 的 modCount 保持一致:</p>
<pre><code>List&lt;String&gt; list =
IntStream.rangeClosed(1, 10).mapToObj(String::valueOf).collect(Collectors.toCollection(ArrayList::new));
for (Iterator&lt;String&gt; iterator = list.iterator(); iterator.hasNext(); ) {
String next = iterator.next();
if (&quot;2&quot;.equals(next)) {
iterator.remove();
}
}
System.out.println(list);
</code></pre>
<p>第二种,直接使用 removeIf 方法,其内部使用了迭代器的 remove 方法:</p>
<pre><code>List&lt;String&gt; list =
IntStream.rangeClosed(1, 10).mapToObj(String::valueOf).collect(Collectors.toCollection(ArrayList::new));
list.removeIf(item -&gt; item.equals(&quot;2&quot;));
System.out.println(list);
</code></pre>
<h3>11 | 空值处理:分不清楚的 null 和恼人的空指针</h3>
<p>问题 1ConcurrentHashMap 的 Key 和 Value 都不能为 null而 HashMap 却可以你知道这么设计的原因是什么吗TreeMap、Hashtable 等 Map 的 Key 和 Value 是否支持 null 呢?</p>
<p>答:原因正如 ConcurrentHashMap 的作者所说:</p>
<p>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.</p>
<p>如果 Value 为 null 会增加二义性,也就是说多线程情况下 map.get(key) 返回 null我们无法区分 Value 原本就是 null 还是 Key 没有映射Key 也是类似的原因。此外,我也更同意他的观点,就是普通的 Map 允许 null 是否是一个正确的做法,也值得商榷,因为这会增加犯错的可能性。</p>
<p>Hashtable 也是线程安全的,所以 Key 和 Value 不可以是 null。</p>
<p>TreeMap 是线程不安全的,但是因为需要排序,需要进行 key 的 compareTo 方法,所以 Key 不能是 null而 Value 可以是 null。</p>
<p>问题 2对于 Hibernate 框架,我们可以使用 @DynamicUpdate 注解实现字段的动态更新。那么,对于 MyBatis 框架来说,要如何实现类似的动态 SQL 功能,实现插入和修改 SQL 只包含 POJO 中的非空字段呢?</p>
<p>MyBatis 可以通过动态 SQL 实现:</p>
<pre><code>&lt;select id=&quot;findUser&quot; resultType=&quot;User&quot;&gt;
SELECT * FROM USER
WHERE 1=1
&lt;if test=&quot;name != null&quot;&gt;
AND name like #{name}
&lt;/if&gt;
&lt;if test=&quot;email != null&quot;&gt;
AND email = #{email}
&lt;/if&gt;
&lt;/select&gt;
</code></pre>
<p>如果使用 MyBatisPlus 的话,实现类似的动态 SQL 功能会更方便。我们可以直接在字段上加 @TableField 注解来实现,可以设置 insertStrategy、updateStrategy、whereStrategy 属性。关于这三个属性的使用方式,你可以参考如下源码,或是这里的官方文档:</p>
<pre><code>/**
\* 字段验证策略之 insert: 当insert操作时该字段拼接insert语句时的策略
\* IGNORED: 直接拼接 insert into table_a(column) values (#{columnProperty});
\* NOT_NULL: insert into table_a(&lt;if test=&quot;columnProperty != null&quot;&gt;column&lt;/if&gt;) values (&lt;if test=&quot;columnProperty != null&quot;&gt;#{columnProperty}&lt;/if&gt;)
\* NOT_EMPTY: insert into table_a(&lt;if test=&quot;columnProperty != null and columnProperty!=''&quot;&gt;column&lt;/if&gt;) values (&lt;if test=&quot;columnProperty != null and columnProperty!=''&quot;&gt;#{columnProperty}&lt;/if&gt;)
*
\* @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 &lt;if test=&quot;columnProperty != null&quot;&gt;column=#{columnProperty}&lt;/if&gt;
\* NOT_EMPTY: update table_a set &lt;if test=&quot;columnProperty != null and columnProperty!=''&quot;&gt;column=#{columnProperty}&lt;/if&gt;
*
\* @since 3.1.2
*/
FieldStrategy updateStrategy() default FieldStrategy.DEFAULT;
/**
\* 字段验证策略之 where: 表示该字段在拼接where条件时的策略
\* IGNORED: 直接拼接 column=#{columnProperty}
\* NOT_NULL: &lt;if test=&quot;columnProperty != null&quot;&gt;column=#{columnProperty}&lt;/if&gt;
\* NOT_EMPTY: &lt;if test=&quot;columnProperty != null and columnProperty!=''&quot;&gt;column=#{columnProperty}&lt;/if&gt;
*
\* @since 3.1.2
*/
FieldStrategy whereStrategy() default FieldStrategy.DEFAULT;
</code></pre>
<h3>12 | 异常处理:别让自己在出问题的时候变为瞎子</h3>
<p>问题 1关于在 finally 代码块中抛出异常的坑,如果在 finally 代码块中返回值,你觉得程序会以 try 或 catch 中的返回值为准,还是以 finally 中的返回值为准呢?</p>
<p>答:以 finally 中的返回值为准。</p>
<p>从语义上来说finally 是做方法收尾资源释放处理的,我们不建议在 finally 中有 return这样逻辑会很混乱。这是因为实现上 finally 中的代码块会被复制多份,分别放到 try 和 catch 调用 return 和 throw 异常之前,所以 finally 中如果有返回值,会覆盖 try 中的返回值。</p>
<p>问题 2对于手动抛出的异常不建议直接使用 Exception 或 RuntimeException通常建议复用 JDK 中的一些标准异常比如IllegalArgumentException、IllegalStateException、UnsupportedOperationException。你能说说它们的适用场景并列出更多常见的可重用标准异常吗</p>
<p>答:我们先分别看看 IllegalArgumentException、IllegalStateException、UnsupportedOperationException 这三种异常的适用场景。</p>
<p>IllegalArgumentException参数不合法异常适用于传入的参数不符合方法要求的场景。</p>
<p>IllegalStateException状态不合法异常适用于状态机的状态的无效转换当前逻辑的执行状态不适合进行相应操作等场景。</p>
<p>UnsupportedOperationException操作不支持异常适用于某个操作在实现或环境下不支持的场景。</p>
<p>还可以重用的异常有 IndexOutOfBoundsException、NullPointerException、ConcurrentModificationException 等。</p>
<p>以上,就是咱们这门课第 7~12 讲的思考题答案了。</p>
<p>关于这些题目,以及背后涉及的知识点,如果你还有哪里感觉不清楚的,欢迎在评论区与我留言,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:代码篇思考题集锦(三).md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Java 业务开发常见错误 100 例/答疑篇:加餐篇思考题答案合集.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709970709bdb3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>