(四) 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
泛型限制集合/无泛型限制集合的使用注意事项
- 在无泛型限制的集合赋值给泛型集合时,手册强制规定在使用集合元素时需要注意什么?为什么?手册个人认为这种情况主要出现在哪里的代码中?泛型的引入(JDK5 后)是为了解决什么问题?为了保持兼容性,JDK5 引入泛型后采取了哪些措施(类型擦除、原始类型、桥接方法)?请简要解释类型擦除和桥接方法的作用。
-
在没有泛型限制的集合赋值给泛型集合的时候, 规定在使用集合元素的时候必须要用上 instanceof 来检查对象的类型
-
这种情况主要出现在还没有泛型的 jdk5 之前的老代码中
-
泛型的引入, 是为了解决一个集合之中可能存入多种类型的对象, 从而导致使用起来不方便且容易出错的情况
-
为了保持兼容性, JDK5 引入泛型之后采取了下面的措施:
-
类型擦除: 编译之后变成该类extend 的类, 或者 Object类, 让字节码可以和旧版JVM兼容. 编译器在编译的时候进行类型检查, 且自动插入类型转换
-
原始集合类型: 仍然允许使用原始不带泛型的集合类型, 代价是牺牲了编译时类型安全
-
桥接方法: 一个类继承泛型类或者实现泛型接口, 类型擦除会让方法签名在字节码上变化. 为了保持多态性, 编译器在这个类之中会生成一个有原始类型签名的桥接方法, 内部实际调用自己编写的具有泛型签名的方法 下面是例子:
class Node<T> { public T data; public void setData(T data) { this.data = data; } } // 自己实现的方法 class MyNode extends Node<String> { @Override public void setData(String data) { // 覆盖了泛型方法 System.out.println("MyNode setData: " + data); super.setData(data); } }
在编译
MyNode
时,编译器会发现Node<T>
的setData(T data)
在擦除后变成setData(Object data)
。而MyNode
中定义的是setData(String data)
。为了让通过Node
引用调用setData
时能够正确调用到MyNode
的覆盖版本,编译器会为MyNode
生成一个桥接方法:// 由编译器在 MyNode.class 中自动生成的桥接方法(示意) public void setData(Object data) { setData((String) data); // 调用实际编写的 setData(String) 方法 }
-
ConcurrentHashMap/HashMap 对于 key 和 value 为 null 的处理
-
ConcurrentHashMap 的 K 和 V 都不允许为 null. 因为 map 之中 get(K) 结果为 null 会有两种可能: key 本身不存在或者K的值为null. 这样给并发下面的内部状态判断带来了复杂的逻辑, 所以干脆不支持
-
多线程下面使用 hashMap 的 containsKey, 可能出现竞态问题, 因为一个线程检查 hashMap 是否含有某个 key 的时候, 另一个线程可能对 Map 已经进行了修改, 导致无法准确判断.
SimpleDateFormat 线程不安全
- SimpleDateFormat 为什么是线程不安全的?手册强制规定不能将其定义为 static 变量,如果定义为 static,必须怎么处理?手册推荐使用 JDK8 中的哪个类来代替 SimpleDateFormat?这个类有何特性?
-
SimpleDateFormat 线程不安全是因为其内部的 Calendar 类线程不安全, 所以当多个线程竞争的时候, 对日期格式化或者日期生成的操作可能因为线程竞争导致数据混乱
-
因为 SimpleDateFormat 线程不安全, 所以不能定义为 Static 变量(static变量本身就是类级别的变量, 很容易被多线程共享, 会出问题). 如果必须定义为 static 变量, 必须给对象显性加锁, 但是这样性能会有很大的问题. 或者使用 threadLocal, 对每一个 thread 搞一个自己的 SimpleDateFormat 类
-
手册推荐使用 JDK8 的 java.time.format.DateTimeFormatter 来代替 SimpleDateFormat. 因为 DateTimeFormatter 内部是线程安全的.
ThreadLocal 的使用
- 关于 ThreadLocal 的补充说明中,个人认为为什么 ThreadLocal 对象必须使用 static final 修饰?这样做有什么好处?手册如何解释不同线程对同一个 ThreadLocal 对象可以取出不同的值?ThreadLocal 能解决共享对象的更新问题吗?
ThreadLocal 本质上就是一个 key ! 按照这个思路去理解, key 一定要全局唯一, 那么 static 能保证一个类之中只有一个 threadLocal, 不会随着对象的生成而增加; final 保证了这个 threadLocal 在生成之后就不可变.
-
threadLocal 必须用 static final 修饰, 原因:
-
static 意味着threadLocal 变量是类级别的对象, 且只会被初始化一次, 这样就代表了每个线程对这个类拿到的都是一样的对象.
-
final: 意味着生成之后就不可重新赋值.
-
-
每个线程是有自己的 threadLocalMap, 那么对于同样的key 取出不同的值就很正常的
-
threadLocal 不能解决共享对象的更新问题, 因为其实现方式是每一个线程对同样的一个值有自己的副本, 要解决还是得加锁
Lock 上锁规范
- 在使用阻塞等待获取锁的方式(如
lock.lock()
)时,手册强制规定加锁方法必须放在代码块的什么位置?为什么加锁方法和 try 代码块之间不能有任何可能抛出异常的方法调用?如果在 try 块之内调用lock.lock()
会有什么后果?
-
阻塞等待获取锁, 加锁方法必须放在 try 代码块之外
-
如果放在try 代码块之内, 有两种情况会造成 IllegalMonitorStateException:
-
try 代码块之中其他方法抛出异常, 导致 lock.lock() 还未执行就到了 finally, finally 内部对一个还没上锁的 lock 进行解锁, 导致 IllegalMonitorStateException
-
上锁过程 lock.lock() 本身就抛出异常, 那么直接去 finally 里面解锁, 也会产生 IllegalMonitorStateException
-
-
加锁方法和 try 代码块之中不能有任何可能抛出异常的调用, 按我看来, 最好是不要有任何调用. 避免上锁之后还未进入 try 代码块就直接抛出异常, 锁永远解不开, 造成死锁
多线程定时任务 ScheduledExecutorService
- 多线程并行处理定时任务时,手册推荐使用 ScheduledExecutorService 而非 java.util.Timer,为什么?Timer 有什么问题
-
ScheduledExecutorService 是一个线程池实现, 所以更好的管理了其内部线程的生命周期
-
不能用 java.util.Timer, 是因为 Timer 的内部是一个单线程执行所有任务, 那么如果一个TimerTask 没有捕获到异常, Timer 的线程会被终止, 从而影响到其他所有任务的运行!
敏感数据悲观锁的使用方针
- 资金相关的金融敏感信息,手册推荐使用哪种锁策略?为什么?悲观锁遵循什么原则?
-
金融敏感信息使用悲观锁
-
金融敏感信息处理之中, 强一致性和可靠性是首要目标. 乐观锁可能造成问题:
-
乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞
-
乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常
乐观锁核心思想是先获取版本号, 只要能获取, 就将版本号作为where条件一部分来更新数据.
程序在执行更新操作之前, 会先读取当前数据, 然后基于这个旧数据进行业务逻辑的计算和校验, 最后才尝试通过带版本号的 UPDATE 语句更新. 而乐观锁只能保证最后一步 update 的时候不出问题, 但是这个时候业务逻辑已经完成了计算和校验, 那么需要业务显性手动的写冲突解决的补偿代码, 在高并发下面很可能造成问题.
-
举例:
假设有一个订单系统,用户 A 购买商品,需要扣减库存。商品当前库存为 100,版本号为
V1
。
- 用户 A 的操作:
- 应用程序读取商品信息:
库存 = 100
,版本 = V1
。- 业务校验: 应用程序判断
100 - 1 > 0
,认为可以扣减库存。- 问题出现: 就在用户 A 的应用程序进行业务校验和准备更新的这个瞬间,另一个用户 B 也同时购买了该商品,并且用户 B 的更新操作成功了。 此时,数据库中的商品库存变为 99,版本号变为
V2
。- 用户 A 的更新尝试: 应用程序尝试执行
UPDATE product SET stock = 99, version = V2 WHERE id = X AND version = V1;
。- 结果: 由于数据库中的版本已是
V2
而非V1
,用户 A 的这条UPDATE
语句不会生效。从数据库层面看,数据是安全的,没有被错误更新。但是,从应用程序的业务逻辑层面来看,用户 A 的扣减库存操作在校验时是基于“库存 100”这个旧数据判断的,它“认为”这个操作是合法的。如果在应用程序中,没有妥善处理这种更新失败后的逻辑(例如,没有重新读取最新数据并重新校验),那么就可能出现业务逻辑上的“漏洞”:应用程序可能在某个时间点短暂地“认为”某个操作是有效的,即使它最终因版本冲突而未能提交。在金融敏感信息场景下,任何基于过时数据的“认为”都可能带来严重的后果,例如,用户可能被短暂地告知转账成功,但实际上操作失败,这会极大地影响用户体验和数据可信度。
2. “乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常。”
解释: 乐观锁在发生冲突时(即版本不匹配导致更新失败),不会自动重试。它要求应用程序自行处理冲突。这意味着,当
UPDATE
语句返回受影响的行数为 0(表示更新未成功,因为版本不匹配)时,应用程序需要:
- 重新读取最新的数据(包括最新的版本号)。
- 重新执行业务逻辑和校验。
- 重新尝试更新操作。
这个重试机制是乐观锁解决冲突的核心策略。来源中强调了“处理不当容易造成系统压力或数据异常”。
举例:
假设在高并发秒杀场景下,有 1000 个用户同时抢购仅剩 1 个库存的商品。
- 冲突频率高: 绝大多数用户的第一次尝试都会读取到相同的版本号(例如
V1
)和库存数量(例如 1)。在他们尝试更新时,只有第一个提交成功的用户能将库存变为 0,版本变为V2
。其余 999 个用户的更新都会失败,因为他们的UPDATE
语句中的WHERE version = V1
不再匹配。- 系统压力:
- 这 999 个失败的用户应用程序会立即进入重试流程。这意味着它们需要再次读取数据库以获取最新数据(库存 0,版本
V2
)。- 这些大量的重试(包括多次读和写操作,即使写入失败)会持续地对数据库造成查询和更新的压力。在极高并发和高冲突率的情况下,数据库可能不堪重负,表现为 CPU 飙升、连接耗尽、响应时间变长,从而导致“系统压力”增大。
- 如果重试次数没有限制,或者重试逻辑设计不当,可能导致“活锁”:所有线程都在忙着重试,但没有一个能真正完成操作,系统吞吐量急剧下降。
- 数据异常(广义):
- 如果重试策略没有设计好,例如重试次数过少,或者没有引入适当的退避(backoff)机制,那么即使是本应成功的操作(如在低冲突时间段的合法购买),也可能因为重试失败而最终导致业务操作失败。这虽然不是数据库层面的数据不一致,但从业务角度看,就是一种“数据异常”——用户未能完成本应成功的交易。
- 此外,在一些更复杂的业务场景中,乐观锁的校验和更新操作如果跨越多个字段,或者涉及到复杂的业务状态机,那么重试逻辑的编写和维护会变得异常复杂,一旦有缺陷,也容易引入数据上的逻辑错误。例如,如果更新 A 字段失败了,但 B 字段在重试前被更新了,导致数据不一致的风险。
因此,在资金相关的金融敏感信息处理中,强一致性和高可靠性是首要目标。乐观锁虽然在并发低时性能较好,但其固有的冲突解决机制在高冲突场景下容易带来额外的系统负担,且对应用程序的重试和异常处理逻辑要求极高,稍有不慎就可能引发业务问题或系统稳定性问题。这也是手册推荐使用悲观锁的原因,因为它通过独占性保证了在操作期间的绝对一致性,虽然牺牲了一部分并发性能,但极大地简化了业务处理的复杂性和风险。
CountDownLatch 用法
- 使用 CountDownLatch 进行异步转同步操作时,每个线程退出前必须调用什么方法?为了确保这个方法被执行到,代码结构需要怎么安排?主线程的 await() 方法最好使用什么版本?手册提示子线程抛出的异常堆栈在主线程能否 catch 到?
-
每个线程退出前必须调用 countDown()
-
确保其被执行到, 需要在 try…catch…finally 里面的 finally 之中调用 countDown()
-
主线程的 await() 要使用带有超时的版本, 避免子线程未正常调用导致卡死
-
子线程和主线程的 try…catch 是两个东西, 所以要分别 try…catch
Random 不适合高并发的原因
- Random 实例(包括
java.util.Random
和Math.random()
)为什么不推荐被多线程使用?虽然它是线程安全的,但会因什么导致性能下降?手册推荐在多线程中使用什么类来代替?
因为 Random 类每次生成一个新的伪随机数时,这个 seed 值都会根据一个特定的数学算法进行计算和更新,以产生下一个随机数。不论是用 synchronized 来修饰还是用乐观锁 CAS 重试, 其都会在高并发下面产生线程堵塞.
多线程之中使用 ThreadLocalRandom.
双重校验锁之中 volatile 的作用
- 通过双重检查锁实现延迟初始化时,为什么推荐将目标属性声明为
volatile
型?volatile 在这里解决了什么问题?手册个人理解 volatile 防止指令重排如何确保对象完全初始化后才可见?
volatile 解决了两个问题:
-
内存可见性问题: 一个线程对共享变量的修改, 不会立刻被其他线程看到, 因为每个线程有自己的CPU缓存, 修改先是放到 CPU 缓存, 然后才是内存. volatile 保证对其修饰的变量的修改会立刻同步到主内存. 且每次读取 volatile 变量的时候, 都会强制从主内存获取最新的值.
-
指令重排序问题: 避免在生成新对象的时候指令重排序导致的还未完全生成的对象就被读取到的问题
volatile 通过内存屏障来防止指令重排. 就是编译阶段在访问 volatile 指令的前后自动插入不同类型的屏障指令, 借此约束编译器和处理器不得随意调整指令次序.
这个内存屏障是什么? 是一种特殊的CPU指令:
-
处理器必须先完成屏障前的所有内存读写, 且写回缓存, 再执行屏障后的读写
-
禁止两侧指令交叉重排
为什么 switch 必须先判空
- 当 switch 括号内变量为 String 且是外部参数时,为什么强制规定必须先进行 null 判断?(请解释其可能触发 NPE 的底层原因)其他的包装类型是否也有类似问题?
因为 switch 借鉴了哈希表的思想, 先通过计算字符串的 hash 来快速定位可能的 case, 再精确进行 equals() 来比较 hash 冲突.
所以如果外部参数 string 为 null, 那么调用 hashCode() 一定会抛出 NPE, 对于其他的包装类型也是如此
三目运算符的自动拆装箱
- 三目运算符
condition ? 表达式 1 : 表达式 2
中,为什么强制规定需要对表达式 1 和 2 进行判空?这样做是避免什么异常?什么场景会触发类型对齐的拆箱操作?手册举了什么反例?
三目运算符一定只能返回一种类型的对象, 但是表达式1和表达式2可能返回类型不一致, 那么怎么办! 肯定要转换, 转换就会涉及到自动拆箱, 自动拆箱就得判空, 不然 NPE
-
避免自动拆箱的 NPE
-
自动拆箱:
-
表达式1 或者 表达式2 之中一个是primary type
-
表达式1 和 表达式2 的值的类型不一致, 先强制拆箱, 再升级成表示范围更大的类型
-
内部重定向和外部重定向
- 服务器内部重定向必须使用什么?外部重定向地址必须使用什么生成?否则可能导致什么问题?手册对此给出了怎样的个人理解?
现代互联网服务, 尤其是微服务体系,很难在一个请求之中完成用户的所有需要. 那么多个服务的配合或者多个浏览器请求, 就需要重定向.
重定向分为内部重定向和外部重定向, 内部重定向指的是对外界不可见, 外部客户端始终认为只访问了一个服务器. 服务器内部进行不同服务之间的重定向. 外部重定向指的是浏览器收到302 或者类似的 http status, 进行二次请求跳转.
内部重定向使用 forward 头, 外部重定向是 http status 来决定的
内部重定向更好一些:
-
外部只有一次请求, 因为外网请求内部服务的延时一定比内网的服务器跳转要长, 耗时更短
-
内部重定向使用的是 forward 头, 并且其请求可以在后端各个微服务之间共享.各个微服务可以互相传递临时对象
异常处理
- Java 类库中定义的可以通过预检查方式规避的 RuntimeException(如 NullPointerException, IndexOutOfBoundsException)为什么不应该通过 catch 的方式来处理?手册给出了什么正例和反例?
本题指的是不要使用异常处理的方式来控制状态流转. 这些可以通过预检查规避的, 比如 NPE, 应当通过 if 判空来确定其是否要进行状态转换.
什么情况下会抛出 NoSuchMethodError
- 在调用 RPC、二方包、或动态生成类的相关方法时,为什么推荐捕捉异常使用 Throwable 类进行拦截?手册解释了什么情况下会抛出 NoSuchMethodError?
-
因为不确定这些外部服务的返回是什么, 那么要用最底层的类进行拦截
-
NoSuchMethodError 的可能情况:
-
二方包类冲突的时候, 仲裁机制引入的版本非预期, 导致方法签名不匹配. 这意味着,尽管你的代码在编译时是正确的(因为你依赖的 API 接口是存在的),但在运行时,由于实际加载的二方包版本发生了意想不到的变化,导致方法签名不匹配,进而抛出 NoSuchMethodError
-
字节码修改框架, 比如 ASM 在动态创建或者修改类的时候, 也修改了相应的方法签名. 那么代码在编译时候正确, 但是运行的时候字节码层面的方法的实际签名改变, 也会给出 NoSuchMethodError
-
禁止使用 Json 工具类将对象转换为 String
- 为什么手册强制禁止直接用 JSON 工具将对象转换成 String 来打印日志?
核心原因为如果对象中存在被override 的 get 防范, 且这些 get 方法内部可能抛出异常, 那么打印日志(通过JSON 序列化对象)的时候, 这些异常就会被触发.
这种在日志打印过程之中触发的异常, 可能影响到正常的业务流程的执行. 从而导致系统不稳定或者出现意外行为
varchar 和 text 类型的注意点
- varchar 类型的字段长度有什么限制(数值)?如果存储长度大于此值,定义字段类型为什么?为什么需要独立一张表且用主键对应?这样做是为了避免影响什么?varchar(n) 的实现方式是什么?text 的实现方式呢?MySQL 对 TEXT 列索引有什么默认行为?
-
text 的字段在 MySQL 中存储的只是一个指针, 指针指向外部一个存储其真实内容的部分
-
mysql 底层仍然是数据页, 在 buffer pool 之中加载的也是数据页. 一个数据页包含若干行. 如果一行很大, 那么每一页能容纳的条数就很少, 查询一样的行数就需要载入更多的页, 给系统更大的负担.
基于上面这两点, 我们逐一分析:
-
varchar 类型的字段长度有什么限制? 一般不要超过 5000, 实践中一般是 500. 再长的字段定义成 text
-
text 独立一张表这个我不能认同, 实际上text 本身已经只存储一个索引了, 除非说有很多的查询 text 字段的需求, 不然也不必担心对text字段的额外查询. 如果有查询text 字段的需求, 实际上也是需要每次返回都吐出来, 一样的低效率. 但是确实要避免每一行之中有大数据, 导致整个页存不了几条数据, 让查询效率变得很差
-
varchar(n) 的实现方式是有多两个字节作为长度存储.
-
text 的字段在 MySQL 中存储的只是一个指针, 指针指向外部一个存储其真实内容的部分
-
mysql 对 text 索引列, 只会包含 text 的前 255 个字符
禁止用 datetime
因为 datetime 不会随着时区的变化而变化
数据库 db 冗余字段要求
- 数据库中的冗余字段原则手册推荐遵循什么(哪些字段适合冗余,哪些不适合)?冗余字段除了提高查询性能(以空间换时间)外,还有什么作用?
适合冗余的字段要不频繁修改,且数据量不大(不能是text 或者 varchar).
冗余字段除了提高查询性能之外, 还能保存数据当时的状态, 比如订单快照
join 的要求
- 超过多少个表禁止 join?需要 join 的字段,有什么要求?多表关联查询时,被关联的字段有什么要求?
-
超过3个表禁止join
-
需要 join 的字段, 要确保
-
数据类型完全一致
-
有索引
-
varchar 类型的索引要求
- 对 varchar 类型建立索引时,强制规定必须指定什么?为什么没必要对全字段建立索引?如何根据实际文本区分度决定索引长度?手册提到一般对字符串类型数据,长度为 20 的索引区分度能达多少?如何计算区分度?前缀索引有什么局限性?
-
varchar 类型建立索引的时候, 强制规定要指定索引长度
-
全字段建立索引只会浪费空间
-
区分度的计算公式:
区分度 = COUNT(DISTINCT column_name) / COUNT(*)
前缀索引区分度的计算公式:
区分度 = COUNT(DISTINCT LEFT(column_name, prefix_length)) / COUNT(*)
所以可以看出, 数据越不同, 区分度越大
- 前缀索引的问题在于其索引之中的数据不完整, 如果要拿到数据本身的值, 需要回表
使用延迟关联或子查询 优化 LIMIT M OFFSET N 超大查询场景
- 使用延迟关联或者子查询优化超多分页场景,手册推荐这样做的原因(MySQL 对 LIMIT M OFFSET N 的处理方式)是什么?优化核心思想是什么?延迟关联的具体 SQL 示例是怎样的?为什么这种方式可行(与直接读取所有数据再筛选相比)?
首先我们要确定, limit m offset n 肯定是先取出 M+N 条数据, 然后舍弃掉前面 M 条, 这个是不可避免的.
但是取出什么数据是我们可以决定的, 所以核心思想就是: 尽量晚的拿取真正需要拿取的全量数据
基于这个思想, 我们要做的就是让 limit M offset N 发生在索引列上面, 拿取到最终需要访问的 N 条数据的 id, 然后再用这些 id 去完成回表查询