阿里Java代码规范阅读记录
这里只记录一些个人认为需要注意或经常忽略的点,并对部分点进行重点分析。
1. 编程规约
1.1 命名风格
- 代码命名的开头与结尾都不能是下划线或美元符号;
- 禁止中英文混合命名或中文命名,同时应避免中文拼音命名(除非国际公认,如
alibaba
); - 常量名全部大写,单词之间用下划线隔开,尽量语义完整(不要嫌名字长),如
MAX _ STOCK _ COUNT
而不能写成MAX _ COUNT
; - 【重要】:抽象类命名使用
Abstract
或Base
开头;异常类命名使用Exception
结尾;测试类
命名以它要测试的类的名称开始,以Test
结尾; - 【重要】:POJO(Plain Ordinary Java Objec,其实就是JavaBean)类中布尔类型的变量,都不要加
is
前缀 ,否则部分框架解析会引起序列化错误,比如定义基本数据类型Boolean isDeleted
的属性,那么该对象自动生成该属性的对应getter
方法就是isDeleted()
,RPC(Remote Procedure Call Protocol远程过程调用协议)框架框架在反向解析的时候,误以为对应的isDeleted
属性名称是deleted
,导致属性获取不到,进而抛出异常; - 【重要】:包名统一小写,并且使用
单数形式,但是类名如果有复数含义,类名可以使用复数形式,比如应用工具类包名为com.alibaba.ai.util
、类名为MessageUtils
; - 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词
组合来表达其意,比如int a
这样的形式要避免出现; - 【重要】:如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式,比如
public class OrderFactory
、public class LoginProxy
、public class ResourceObserver
等; - 【重要】:接口类中的方法和属性不要加任何修饰符号(
public
也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量; - 枚举类名建议带上
Enum
后缀,枚举成员名称需要全大写,单词间用下划线隔开; - 对于分层过程中的一些命名注意点:
- Service/DAO层方法命名规约:
- 获取单个对象的方法用
get
做前缀; - 【重要】:获取多个对象的方法用
list
做前缀,复数形式结尾如:listObjects
; - 获取统计值的方法用
count
做前缀; - 插入的方法用
save/insert
做前缀; - 删除的方法用
remove/delete
做前缀; - 修改的方法用
update
做前缀;
- 获取单个对象的方法用
- 领域模型命名规约:
- 数据对象:
xxxDO
,xxx
即为数据表名; - 数据传输对象:
xxxDTO
,xxx
为业务领域相关的名称; - 展示对象:
xxxVO
,xxx
一般为网页名称; - POJO是
DO/DTO/BO/VO
的统称,禁止命名成xxxPOJO
;
- 数据对象:
- Service/DAO层方法命名规约:
1.2 常量定义
- 【重要】:在对
long
或Long
赋值时,数值后面使用L
进行标识,不要使用l
标识,小写容易跟数字1混淆,造成误解; - 不使用仅有的一个常量类维护所有常量,要按常量功能进行归类,分开便于维护和查找,比如缓存相关常量放在类
CacheConsts
下,系统配置相关常量放在类ConfigConsts
下;
1.3 代码格式
- 【重要】:左小括号和字符(左小括号的左侧字符)间不要出现空格,而左大括号前需要空格,比如
main(String[] args) {
- 【重要】:
if
/for
/while
/switch
/do
等保留字与括号之间都必须加空格,比如if (1 == 1)
; - 【重要】:任何二目、三目运算符(包括赋值运算符
=
、逻辑运算符&&
、加减乘除符号等)的左右两边都需要加一个空格,比如if (1 == 1)
; - 采用 4 个空格缩进,禁止使用tab字符,如果使用tab缩进,必须设置1个tab为4个空格。IDEA设置tab为4个空格时,请勿勾选Use tab character;
- 注释的双斜线与注释内容之间有且仅有一个空格;
- 单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进(与第二行保持一致);
- 运算符与下文一起换行;
- 方法调用的点符号与下文一起换行;
- 方法调用中的多个参数需要换行时,在逗号后进行;
- 在括号前(可以理解为
(
前换行)不要换行;
- 【重要】:IDE的编码统一设置为UTF-8,文件的换行符使用Unix格式,不要使用Windows格式;
- 单个方法的总行数不超过80行,包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符;
- 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性,任何情形,没有必要插入多个空行进行隔开;
1.4 OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可;
- 【重要】:所有的覆写方法,必须加
@Override
注解(我经常性的不写(●ˇ∀ˇ●)); - 相同参数类型,相同业务含义,才可以使用
Java
的可变参数(即f(int... a)
这样的形式,这个方法可以传入多个int
类型的参数,不是固定死的,只能传几个参数,注意和数组的区别,这两货无法重载,奇葩啊,然后重载方法时,固定参数的方法比可变参数的优先等级高),避免使用Object
,同时提倡不使用可变参数变成; - 【重要】:外部正在调用或者二方库依赖的接口,所以不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加
@Deprecated
注解,并清晰地说明采用的新接口或者新服务是什么,(因为接口可能被其他人调用了,所以能想改就改,只能丢弃,不能修改!); - 不使用过时的类或方法;
Object
的equals
方法容易抛空指针异,应使用常量或确定有值的对象来调用equals
,推荐使用Objects.equals(str1, str2)
,这样可以避免空指针异常;- 【重要!!!】:所有的相同类型的包装类对象之间值的比较,全部使用
equals
方法比较,对于Integer var = ?
在-128至127范围内的赋值,Integer
对象是在
IntegerCache.cache
产生,会复用已有对象,这个区间内的Integer
值可以直接使用==
进行
判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,
推荐使用equals
方法进行判断。长见识了U•ェ•*U,具体测试戳; 基本数据类型和包装类的使用标准:
- 【重要】:所有的POJO类属性必须使用包装数据类型(数据库的查询结果可能是
null
,因为自动拆箱,用基本数据类型接收有NPE风险,即空指针异常,下同); - RPC方法的返回值和参数必须使用包装数据类型;
- 所有的局部变量使用基本数据类型;
- 【重要】:所有的POJO类属性必须使用包装数据类型(数据库的查询结果可能是
定义
DO
/DTO
/VO
等POJO类时,不要设定任何属性默认值,因为属性在数据提取时可能并没有置入具体值,在更新其它字段时又附带更新了此字段;- 序列化类新增属性时,请不要修改
serialVersionUID
字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么修改serialVersionUID
值,serialVersionUID
不一致会抛出序列化运行时异常; - 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,放在
init
方法中; - POJO类必须写
toString
方法,如果继承了另一个POJO
类,注意在前面加一下super.toString
,主要是为了便于排查问题; - 禁止在POJO类中,同时存在对应属性
xxx
的isXxx()
和getXxx()
方法; - 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 >
getter
/setter
方法; - 慎用
Object
的clone
方法来拷贝对象,对象的clone
方法默认是浅拷贝,若想实现深拷贝需要重写clone
方法实现域对象的深度遍历式拷贝; - 类成员与方法的访问控制:
- 如果不允许外部直接通过
new
来创建对象,那么构造方法必须是private
; - 工具类不允许有
public
或default
构造方法; - 类非
static
成员变量并且与子类共享,必须是protected
; - 类非
static
成员变量并且仅在本类使用,必须是private
; - 类
static
成员变量如果仅在本类使用,必须是private
; - 若是
static
成员变量,考虑是否为final
; - 类成员方法只供类内部调用,必须是
private
; - 类成员方法只对继承类公开,那么限制为
protected
;
- 如果不允许外部直接通过
1.5 集合处理
- 关于
hashCode
和equals
的处理:
- 【重要】:只要重写
equals
,就必须重写hashCode
; - 【重要】:
Set
存储的是不重复的对象,依据hashCode
和equals
进行判断,所以Set
存储的对象必须重写这两个方法; - 果自定义对象作为
Map
的键,那么必须重写hashCode
和equals
;
- 【重要】:只要重写
- 使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add
/remove
/clear
方法会抛出UnsupportedOperationException
异常(【重要】:asList
的返回对象是一个Arrays
内部类,并没有实现集合的修改方法。Arrays.asList
体现的是适配器模式,只是转换接口,后台的数据仍是数组); - 泛型通配符
<? extends T >
来接收返回的数据,此写法的泛型集合不能使用add
方法,而<? super T>
不能使用get
方法,作为接口调用赋值时易出错。PECS(Producer Extends Consumer Super) 原则:第一、频繁往外读取内容的,适合用<? extends T >
,第二、经常往里插入的,适合用<? super T>
; - 不要在
foreach
循环里进行元素的remove
/add
操作。remove
元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁; - 在JDK7版本及以上,
Comparator
实现类要满足如下三个条件,不然Arrays.sort
,Collections.sort
会报IllegalArgumentException
异常,三个条件:
- x , y 的比较结果和 y , x 的比较结果相反;
- x > y , y > z ,则 x > z;
- x = y ,则 x , z 比较结果和 y , z 比较结果相同;
- 集合泛型定义时,在JDK7及以上,使用diamond语法(即菱形泛型,使用
<>
来指代前边已经指定的类型)或全省略,比如:List<String> list1 = new ArrayList<>();
和List<String> list2 = new ArrayList();
; - 集合初始化时,直接指定集合初始值大小,如
Map map = new HashMap(20);
,HashMap(int initialCapacity)
初始化时,initialCapacity = (需要存储的元素个数 / 负载因子) + 1
。注意负载因子(即loaderfactor
)默认为0.75, 如果暂时无法确定初始值大小,请设置为16(即默认值),如果一个HashMap
需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量7次被迫扩大,resize
需要重建hash表,严重影响性能,具体扩容参看HashMap
的源码; - 【重要】:使用
entrySet
遍历Map
类集合KV
,而不是keySet
方式进行遍历,keySet
其实是遍历了2次,一次是转为Iterator
对象,另一次是从hashMap
中取出key
所对应的value
。而entrySet
只是遍历了一次就把key
和value
都放到了entry
中,效率更高。如果是JDK 8,使用Map.foreach
方法; - 理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响,有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如:
ArrayList
是order
/unsort
;HashMap
是unorder
/unsort
;TreeSet
是order
/sort
。 - 利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains
方法进行遍历、对比、去重操作;
1.6 并发处理
- 获取单例对象需要保证线程安全,其中的方法也要保证安全(资源驱动类、工具类、单例工厂类等);
- 【重要】:线程资源必须通过线程池提供,不允许在应用中显式创建线程(使用线程池可以减少创建和销毁线程池时资源消耗,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而消耗内存或“过度切换”的问题);
- 【重要】:线程池不允许使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样可以让写的同学更加明确线程池的运行规则,规避资源消耗的风险,说明。 SimpleDateFormat
是线程不安全的类,一般不要定义为static
变量,如果定义为static
,必须加锁,或者使用DateUtils
工具类,说明;- 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁;
- 多线程加锁的顺序必须一致,否则很容易出现死锁,比如线程1对A、B、C依次加锁,那么线程2也必须是A、B、C,否则容易出现死锁;
- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用
version
(也可以使用时间戳)作为更新依据,如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次; - 多线程并行处理定时任务时,
Timer
运行多个TimeTask
时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService
则没有这个问题; - 使用
CountDownLatch
进行异步转同步操作,每个线程退出前必须调用countDown
方法(创建的时候new CountDownLatch(int x)
,每次调用countDown
方法则x
减一,在调用countDownLatch.await()
方法时,将会等待所有调用countDown
方法的线程才会继续执行),线程执行代码注意catch
异常,确保countDown
方法被执行到,避免主线程无法执行至await
方法,直到超时才返回结果。 volatile
解决多线程内存不可见问题。对于一写多读的场景,是可以解决变量同步问题,但是如果多写的场景,同样无法解决线程安全问题,说明。HashMap
在容量不够进行resize
时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险;ThreadLocal
无法解决共享对象的更新问题,ThreadLocal
对象建议使用static
修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量;
1.7 控制语句
- 在一个
switch
块内,每个case
要么通过break
/return
等来终止,要么注释说明程序将继续执行到哪一个case
为止;在一个switch
块内,都必须包含一个default
语句并且放在最后,即使空代码; - 【重要】:在高并发场景中,避免使用“等于”判断作为中断或退出的条件,如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替;
- 表达异常的分支时,少用
if-else
方式,这种方式的可以做一些优化; - 除常用方法(如
getXxx
/isXxx
)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性,说明; - 避免采用取反逻辑运算符(取反不易逻辑的理解);
- 下面这些方法通常需要进行参数校验:
- 调用频次低的方法;
- 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失;
- 需要极高稳定性和可用性的方法;
- 对外提供的开放接口,不管是
RPC
/API
/HTTP
接口; - 敏感权限入口;
- 下面这些方法通常需要不需要进行参数校验:
- 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求;
- 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般
DAO
层与Service
层都在同一个应用中,部署在同一台服务器中,所以DAO
的参数校验,可以省略; - 被声明成
private
只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数;
1.8 注释规约
- 所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能;
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途;
1.9 其他
- 后台输送给页面的变量必须加
$!{var}
(必须加中间的感叹号,如果var
等于null
或者不存在,那么${var}
会直接显示在页面上,而加了!
就不会); - 注意
Math.random()
这个方法返回是double
类型,注意取值的范围0≤x<1(能够取到零值,注意除零异常 ),如果想获取整数类型的随机数,不要将x
放大10的若干倍然后取整,直接使用Random
对象的nextInt
或者nextLong
方法; - 获取当前毫秒数
System.currentTimeMillis();
而不是new Date().getTime();
,如果想获取更加精确的纳秒级时间值,使用System.nanoTime()
,在JDK8中,针对统计时间等场景,推荐使用Instant
类; - 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存;
- 及时清理不再使用的代码段或配置信息,对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(
///
)来说明注释掉代码的理由;
5. MySQL数据库
5.1 建表规约
- 【重要】:表达是与否概念的字段,必须使用
is_xxx
的方式命名(但POJO类中的任何布尔类型的变量,都不要加is
前缀),数据类型是unsigned tinyint
(1表示是,0表示否); - 【重要】:表名、字段名必须使用小写字母或数字(MySQL在Windows下不区分大小写,但在Linux下默认是区分大小写),禁止出现数字开头,禁止两个下划线中间只出现数字;
- 表名不使用复数名词;
- 主键索引名为
pk_ 字段名
(primary key),唯一索引名为uk _字段名
(unique key),普通索引名则为idx _字段名
; - 【重要】:小数类型为
decimal
,禁止使用float
和double
(都有精度损失的问题),若存储的数据范围超过decimal
的范围,建议将数据拆成整数和小数分开存储; - 如果存储的字符串长度几乎相等,使用
char
定长字符串类型; - 【重要】:表必备三字段:
id
、gmt_create
(主动创建的时间)和gmt_modified
(最近更新的时间),其中id
必为主键,类型为bigint unsigned
、单表时自增、步长为1,gmt_create
和gmt_modified
的类型均为datetime
类型; - 表的命名最好是加上
业务名称_表的作用
,如alipay_task
/force_project
/trade_config
; - 库名与应用名称尽量一致;
- 字段允许适当冗余,以提高查询性能说明,但必须考虑数据一致。冗余字段应遵循:
- 不是频繁修改的字段(如果是频繁更改,那每次要更改多个地方,不便维护);
- 不是
varchar
超长字段,更不能是text
字段;
- 单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表,如果预计三年后的数据量根本达不到这个级别,不要在创建表时就分库分表;
5.2 索引规约
- 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引,不要以为唯一索引影响了
insert
速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生; - 超过三个表禁止
join
,需要join
的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引; - 【重要】:页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决;
5.3 SQL语句
- 【重要】:不要使用
count(列名)
或count(常量)
来替代count(*)
,count(*)
是SQL 92定义的标准统计行数的语法,跟数据库无关,跟NULL
和非NULL
无关; - 【重要】:使用
ISNULL()
来判断是否为NULL
值,NULL
与任何值的直接比较都为NULL
; - 在代码中写分页查询逻辑时,若
count
为0应直接返回,避免执行后面的分页语句; - 【重要】:不得使用外键与级联,一切外键概念必须在应用层解决,外键与级联更新适用于单机低并发,不适合分布式、高并发集群,级联更新是强阻塞,存在数据库更新风暴的风险,外键还会影响数据库的插入速度;
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性;
- 数据订正(特别是删除、修改记录操作)时,要先
select
,避免出现误删除,确认无误才能执行更新语句; in
操作能避免则避免,若实在避免不了,需要仔细评估in
后边的集合元素数量,控制在1000个之内;
5.4 ORM映射
- 在表查询中,一律不要使用
*
作为查询的字段列表,需要哪些字段必须明确写明,MyBatis中通常使用<sql>
标记复用SQL,然后使用<include>
调用复用的SQL,而且便于维护; - POJO类的布尔属性不能加
is
,而数据库字段必须加is _
,要求在resultMap中进行字段与属性之间的映射,MyBatis Generator生成的代码中,需要进行对应的修改; - 【重要】:
sql.xml
配置参数使用:#{}
、#param#
,不要使用${}
此种方式容易出现SQL注入; - 更新数据表记录时,必须同时更新记录对应的
gmt_modified
字段值为当前时间;
【问题】
问题1:在foreach
循环里进行元素的remove
/add
操作时,给出的案例如下:
// 正例
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("1".equals(item)) {
iterator.remove();
}
}
// 反例
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
确实没有看出区别,作者说
以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的
结果吗?
问题2:在控制语句那一块,有一条规约为:
在高并发场景中,避免使用“等于”判断作为中断或退出的条件,如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替;
使用==
可能被击穿可以理解,为什么用区间就不会出现被击穿?
解答2:这里主要是考虑的一旦为0的那个时刻没有抓到,而抓到那个变量的时候早就变成了负数,那么==0
这个condition内部的逻辑完了,因为它不会再回到0的那一刻让内部逻辑执行一遍,但是如果改成<=0
的逻辑就没有这个问题,就算被击穿,至少内部的逻辑还是会执行,减少损失程度。
【分析】
A1 包装类测试
测试代码:
Integer integer1 = 128;
Integer integer2 = 128;
System.out.println(integer1==integer2);
System.out.println(integer1.equals(integer2));
结果是false
和true
,长见识了……
A2 使用Executors
返回线程池的弊端
FixedThreadPool
和SingleThreadPool
:
- 允许的请求队列长度为
Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM(内存溢出异常);
- 允许的请求队列长度为
CachedThreadPool
和ScheduledThreadPool
:
- 允许的创建线程数量为
Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM;
- 允许的创建线程数量为
A3 线程不安全的SimpleDateFormat
的处理
一般推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@ Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
JDK8中可以使用Instant
代替Date
,LocalDateTime
代替Calendar
,DateTimeFormatter
代替 SimpleDateFormat
。
A4 变量同步自加操作的处理
推荐使用原子类解决变量在多写场景下的线程安全问题,以count++
操作为例,主要实现如下:
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
JDK8,推荐使用LongAdder
对象,比AtomicLong
性能更好(减少乐观锁的重试次数)。
A5 if-else
语句的优化
正常的if-else
语句可以改写为下面的这种形式:
if(condition) {
...
return obj;
}
这样做的目的是便于维护,如果非得使用if()...else if()...else...
方式表达逻辑,避免后续代码维护困难,禁止超过3层;超过3层的if-else
的逻辑判断代码可以使用卫语句(可能是因为圈复杂度)、策略模式、状态模式等来实现,其中卫语句示例如下:
public void today() {
if (isBusy()) {
System.out.println(“change time.”);
return;
}
if (isFree()) {
System.out.println(“go to travel.”);
return;
}
System.out.println(“stay at home to learn Alibaba Java Coding Guidelines.”);
return;
}
A6 条件判断的优化
if
判断中跟太多逻辑判断,影响代码阅读,不如将复杂的条件抽离出来,给一个判断的结果传到if
语句中,比如:
// 正确的写法
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
// 错误的写法
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
A7 关于数据库冗余字段的说明
对于订单业务中,肯定存在用户和订单这两张表(实际可能是用户和商品之间存在一个关联表,这个例子不好,只用于理解),user_name
字段必定是在user
表中,对于用户的增删改查都很方便,然后订单信息表中搞了一个user_id
来关联两者之间的关系,如果想查询到一个订单和具体用户之间的关联关系,就要通过订单表中的user_id
去查询用户表中的用户名字,这样就要作join
操作,当用户数量达到一定程度后,这种查询会很有压力(在那么多用户中查找一个用户),基于这样的需求允许在订单表中加入user_name
(原本应属于用户表中)这样的冗余字段来提高查询性能,定位到订单信息就可以直接在该行记录中找到原来所需要的具体用户的名字。