首页
Effective Coding
Coding
- 单例模式要防止被反射机制访问私有构造器,要保证创建第二个对象时能抛出异常。
推荐使用枚举方式实现严格的单例模式(需要拓展超类的情况除外)。
-
if-else不要嵌套,少于3行的分支应使用卫语句(fail-fast)。
-
组合使用if-else、switch和策略模式,超高频条件使用if判断。
-
优先选择使用复合和转发方式(装饰者模式)拓展类功能而不是继承。
- 最好不要导出具有相同参数个数的重载方法,而是通过方法名区分。
错误示例如List的remove(int)和remove(Object),正确示例如ObjectOutputStream的writeInt(int)和writeLong(Long)方法。
-
在需要使用可变参数的场景下,应选择定义多个重载方法(分别带0-n个普通参数,只有最后一个才带有可变参数)。
- 延迟初始化,实例域使用双重检查模式,静态域使用lazy initialization holder class idiom模式,如果可以接受重复初始化则使用单重检查模式。
Java
-
非静态成员类、匿名类、局部类(定义在方法内部)统称为内部类。
尽可能使用静态成员类,非静态成员类会隐式包含一个外围对象的引用,故只有需要访问外围实例时才需要定义未非静态成员类;
只有不存在预置的类型时才需要定义局部类,否则应该定义匿名类。
- 使用优先级:方法引用 > Lambda表达式 > 匿名类
Lambda表达式中this指向外围实例,而匿名类中this指向匿名类实例。
- 数组是协变的,泛型是可变的,故无法创建泛型数组,但可以定义泛型数组类型。
Sub[]是Super[]的子类型,但List<Sub>不是List<Super>的子类型。
数组的协变性表示可以将Sub1[]转换为Super[],之后设置Sub2类型对象则会导致运行时错误,即在运行时才去判断数组元素的类型约束。但是泛型是不可变的且安全的,且只在编译期间生效,如果允许创建泛型数组,且数组是协变的,那么就会违背泛型的类型安全承诺。
-
不要在线程池中使用ThreadLocal的set/remove,因为在CallerRunsPolicy拒绝策略以及展开的ForkJoinTask情况下会在任务提交线程执行任务,这种情况下会导致上下文丢失。
- 永远不要在循环之外使用wait方法,如非必要应该始终使用notifyAll方法。
为了防止被恶意唤醒,只有在检查状态正确后才允许退出循环。
- 使用序列化代理(基于writeReplace和readResolve实现)代替序列化实例,实际上参与序列化的类是代理类。
writeReplace:在writeObject之前执行,使用writeReplace的返回值作为真实被序列化的对象,用于序列化时把外围类实例转变为序列化代理;
readResolve:在readObject之后执行,使用readResolve的返回值替代readObject的返回值,用于反序列化时将序列化代理转变回外围类实例;
readObject;因为外围类也必须实现Serializable,故外围类的readObject方法需要抛出异常,保证永远不会产生外围类的序列化实例。
-
集合作为参数时,如果只会取出元素使用<? extend E>上边界限定,如果只会添加元素使用<? super E>下边界限定,如果只允许移除元素则使用<?>限定。
- 使用集合的时候如果能提前预估到元素数量,一定要在初始化时合理设置容量大小,已避免扩容带来的损耗。
ArrayList初始容量为10,每次扩容为1.5倍;而HashMap初始容量为16,每次扩容为2倍。
- toArray(T[] array)方法传入new T[0]性能最好,会动态创建正确大小的数组,集合转数组应该使用toArray(T[]::new)的方式(Collection接口的默认实现,实际上就是toArray(new T[0]))。
创建大小为集合size的数组作为参数会存在的问题是,高并发情况下集合的大小可能在数组创建完成后发生了变化,所以应该由toArray方法创建数组。
- 只有当可预见的Error产生并且确实需要对其进行捕获时才应该捕获Throwable,否则其他场景下只应该捕获Exception。
基础功能相关的场景也可以选择捕获或抛出Throwable以提供更高的拓展性。
MySQL
-
尽量不要使用被频繁更新的列创建索引。
-
尽量做到单表的冷热数据分离,分离后也建议对冷热数据采取不同的缓存策略。
-
可枚举字段更建议使用CHAR类型,虽然使用数字性能更高,但是字符串的可读性和拓展性更强。
- 被索引的字段要设置为非空,会导致优化器难以优化,增加处理的复杂性,需要额外的空间标识是否为空,且只能使用IS NULL和IS NOT NULL查询。
允许NULL的二级索引会将NULL值保存在最左侧(即视为最小值)。
- 字符集应该选择utf8mb4而不是utf8。
utf8只支持到1-3个字节,不支持emoji表情以及其他复杂字符。
- 使用bigint作为主键类型的表数据量最好不要超过2000万。
主键为8字节的bigint时,高度为3的B+树最多可以存储2000多万条数。
仅针对机械硬盘存储,固态硬盘存储时可以考虑通过分区表方式优化而不是分库分表。
- 衍生列(Generated Column)需要谨慎使用。
如果计算表达式出错,存储时并不会报错,会在查询时才计算并导致抛出异常。
- DATETIME类型可以指定精度,最多支持到6位小数。
5.6版本开始支持,默认精度0,占用5字节,最多会占用8字节。
- VARCHAR类型必须使用前缀索引,通常情况下长度为7即可以有很好的选择性。
如果要实现后缀索引,只能新增一列保存反转字符串,并在该列上建立前缀索引。
-
COUNT(*) & COUNT(1) 效率是完全一样的。
- 不建议做超大偏移量分页,id自增时可以使用主键索引进行一定优化。
- 延迟关联,先使用覆盖索引查出主键范围,然后再关联行。
SELECT <cols> FROM t INNER JOIN (SELECT id FROM t ORDER BY <cols> LIMIT 10000, 10) USING (id);
- 索引范围限定,效率最高,但是适用场景比较有限,需要能确定出索引匹配的范围,可以通过逻辑判断或从上一次查询中取值。
SELECT <cols> FROM t WHERE id > 10000 AND <conditions> LIMIT 10;
- 延迟关联,先使用覆盖索引查出主键范围,然后再关联行。
- 索引区分度很低时应忽略索引选择全表扫描,加载该索引后回表查询反而消耗更大。
如包含state字段的索引,state为0的数据占绝大多数,则如果要查询status为0的所有数据时使用ignore index强制不走索引。
使用 IN(能利用单个索引)和 UNION(能利用多个索引)替代 OR。小范围的 IN 和 NOT IN 查询都会走索引。
非唯一索引当值相同时,数据按磁盘存储顺序返回,而不是ID顺序。
技术方案
- 重试时间、延迟时间、缓存持续时间等要使用随机值,且重试时间要随重试次数逐渐增加。
防止雪崩效应。
- 对外暴露的数据模型中不要使用自定义枚举。
如果修改了枚举且引用方未更新版本,有出现转化异常的风险,正确的方式是通过@see方式提示字段对应的枚举值,由引用方自行转换。