雷军有一句名言广为流传:你写程序有写诗一样的感觉吗?
我回答:很有写诗的感觉了,反正都是回车键嘛。
写诗固然要放飞想象,但也要注意格律,才会有美感。
写代码亦是如此,犹如戴着镣铐起舞,谨守章法,否则不成了社会摇了么。
比如前些年大火的“梨花体”、“乌青体”、“废话体”,更多的是戏谑的意味在里面吧,要说艺术成就,仿佛是在开一个很认真的玩笑。
假如用以上三种风格来写代码,几乎可以对程序员生涯宣判死刑了,可见诗歌圈还是很宽容的。
有人说,我就乐意用自己的风格来写,反正也能运行,效果也不错。这确实很有可能,毕竟游击队有时候也能打得过正规军呢,但是想当将军,首先就需要对编制标准了然于心。除非你是李云龙嘛。
刚进入公司写代码,我泥腿子一个,写得真是惨不忍睹。老大很耐心,建议我首先养成规范的习惯,这对于代码可读性和维护以及日后的问题定位都是大有裨益的,并给我推荐了一份Google Java Style的文档。
由于是全英文,我读得效率很低(还是要提高姿势水平),于是我找来了阿里的Java规范文档来读,阿里作为大中华Java界最坚实的堡垒,其规范很有标杆性。文档不长,40页,比较完备,在风格约定的同时,简述了原因,其实背后还是有很多Java原理中的细节体现,我读了受益匪浅,将其中一些适合初级程序员的内容筛选总结出来,分享给大家,也约束自己渐渐养成良好的编码习惯。
像写诗一样去写代码,需要很深的功力和理解,首先能做的,是把自己的代码写得像那么回事。
编程规约
命名风格
前提
- 禁止使用拼音和英文单词混搭
- 尽量避免使用拼音
- 切忌盲目缩写。
类
- 类名使用
UpperCamelCase
方式。 - 抽象类命名使用
Abstract
开头,测试类使用Test
结尾 。 - 枚举类名后面带上
Enum
,成员全部大写。 - 假如类、接口、方法使用了某设计模式,需要在名称中体现出来。
变量
- 布尔类型的变量,不要加
is
前缀。否则可能会导致一些框架的解析错误,因为有的框架就是通过变量名来解析的。 - 变量、方法名使用
lowerCamelCase
方式。
常量定义
-
不允许任何未经定义的字符串直接出现在代码中。可读性差,如果是一些固定值的字符串,可以将其设为常量,并规范命名。
-
将常量通过其功能分类,分开维护。
-
如果常量在一个固定范围内变化,建议使用枚举型。
代码风格
空格
- 左右小括号不允许与括号内相邻字符间出现空格.比如,禁止:
if (`空格`a == b`空格`)
复制代码
if
/for
/while
/switch
/do
等保留字与小括号之间必须空格。二目,三目运算符的左右两边必须加空格。比如,正确:
if (a == b && a == c);
a == b ? a = c : a = d;
复制代码
- 注释的双斜线必须与内容之间有且仅有一个空格。比如,正确:
// 这是一条注释
a = b;
复制代码
- 多个参数情况下,每个参数的逗号后加一个空格。比如:
void swap(int a, int b)
复制代码
缩进
- 采用四个空格空格进行缩进,禁止使用tab,如果采用tab,必须设置为1个tab为4个空格。(使用tab还是空格键是世纪论题)
- 单行字符数不超过120个,超出需要换行,换行时:第二行相对第一行缩进4个空格,方法调用的点参与换行;但是如果是多个方法参数需要换行时,参数间的逗号不参与。比如:
List<VideoData> data = entityIds.stream()
.map(i -> {
VideoData o = videoDataRepository
.findByPartnerIdAndEntityId(partnerId, i);
o = o == null ? new VideoData() : o;
o.setEntityId(i);
o.setPartnerId(partnerId);
return o;
})
.collect(Collectors.toList());
复制代码
public ResponseEntity<?> addxxx(@PathVariable("id") Long id,
@RequestBody List<Long> ids,
@RequestHeader(value="Authorization") String auth)
复制代码
空行
- 不同逻辑、语义、业务的代码之间可以插入一个空行,但不必插入多个空行区分。
OOP规约
Object方法
- Object的equals方法容易抛空指针异常,所以应该使用常量或确定有值的对象来调用此方法。比如:
"test".equals(object);
复制代码
避免:
object.equals("test");
复制代码
- 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。因为比如
Integer
,String
在某种情况下,会引用缓存池中的对象,而另外的情况会在堆上生成对象,而==
比较的是引用的地址。(具体什么时候指向缓存,什么时候指向堆中新建的对象可以查资料) - 谨慎使用
Object
的clone
方法来拷贝对象,因为这是浅拷贝,想要深拷贝可以对clone
方法进行重写。
POJO类
- POJO类的属性必须使用包装类型,提醒使用者来进行初始化赋值。而基本数据类型会产生默认初始值,这虽然使程序正常运行,但是是不正确的。
- 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。
- 构造器中禁止任何业务逻辑,如果有必要的初始化逻辑,可以放在
init()
中。 - POJO类必须写
toString
方法。主要是为了在发生异常时,可以直接调用toString来打印属性值,便于排查问题。
其他
- 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。(因为公有方法是调用者或维护者最关心的,这条规则我之前一直做反了)
- 循环中的字符串连接方式,使用
StringBuilder
的append
方法,不要直接使用字符串相加,因为这会在堆中循环创建新的字符串对象,造成内存浪费(具体参考StringBuilder
,StringBuffer
,String
,这三者的区别以及String
的不可变性也是面试的一个初级高频问题) - 方法权限从严控制,严禁宽泛访问权限。这有利于模块解耦,也有利于维护时更加清晰。
集合处理
-
Set
之所以可以储存不可重复对象,是因为根据对象的equals
和hashcode
方法来判断的。所以这两个方法有一定的关联性,重写equals
就务必要重写hashcode
。我们平时常采用的String
类型作为Map
的键来用,因为String
中已经帮我们把这两个方法重写过了。如果要用其他类型的对象作为Map
的键,务必重写这两个方法。而关于HashSet
,可以点开HashSet
的源码,发现其中主要是一个HashMap
。 -
在使用泛型通配符时,有这两种形式:
<? extends T>
和<? super T>
前者的容器无法接收返回的数据,而后者只能接收T
类型及其子类的对象,其原因都是编译器提供了一种类型安全的保护。 -
不要在
foreach
循环里进行元素的remove/add
操作。remove
元素请使用Iterator
方式。具体为什么这么做,可以看一下手册中的举例以及这个博客的解答foreach循环中为什么不要进行remove/add操作 -
集合初始化时,尽量根据实际情况指定集合的大小,因为集合动态扩容是很消耗性能的,具体可以查看源码或者相关博客,也是初级高频面试问题。
-
使用
entrySet
遍历Map
类集合KV
,而不是keySet
方式进行遍历。 -
利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains
方法进行遍历、对比、去重操作。
并发处理
- 创建线程或线程池时,指定有意义的名字,方便维护和检查。
- 线程资源必须通过线程池分配,不能显示创建。这样是为了节省线程之间切换导致的过度开销。
- 线程池不允许使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险(这一点感觉有点苛刻,个人认为Executors
提供的四种线程池能较好地解决问题时,不需要自己去构建一个ThreadPoolExecutor
对象):
1. FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2. CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
复制代码
- 在高并发时,需要考虑锁的性能损耗,不要多余加锁,并且将锁的范围控制在最小化。
- HashMap是线程不安全的,在并发情况下容易在扩容时出现死链,导致CPU飙升。可以使用其他数据结构比如
ConcurrentHashMap
来替代,或者加锁。
控制语句
- 在一个
switch
块中,必须包含一个default
语句,即使为空。 - 在判断或循环中,必须使用大括号。即使只有一行代码,避免采用一行流。
- 在高并发场景下,尽量避免使用“=”来作为中断或退出的条件,可以使用“<”、“>”作为区间来判断。假设一个电商场景,商品数量为0时关闭购买入口,但是假如并发出现了错误,数量可能瞬间变为负值,那么程序会一直继续下去。
- 在使用判断时,若存在复杂的逻辑语句,可以使用一个命名规范的布尔值来代替这个冗长的语句,以提高代码的可读性。比如:
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
复制代码
异常日志
异常处理
- 区分稳定代码与不稳定代码,避免粗暴地对大段代码进行
try-catch
,这不利于维护和阅读。同时,尽量区分可能抛出的异常类型,再进行相应的处理。 - 捕获异常是为了处理它,如果当前处理不了或者不想处理,可以抛给它的调用者。
finally
中必须对资源对象、流对象进行关闭。并且不能在内使用return
,因为这会导致方法结束,而try
中的return
得不到执行。- 可能出现的空指针异常:
- 包装类型自动拆箱成基本类型
- 数据库查询结果为null
- 集合中即使有元素,取出的元素也可能为null(比如
HashMap
允许存入null) - 远程调用返回对象时,一律需要判断是否为空。
- 对于session中获取的值,建议进行空指针检查。
- 级联调用容易产生空指针异常,比如
obj.getA().getB().getC()
日志规约
- 对于
trace
/debug
/info
级别的日志输出,必须使用条件或占位符的方式,否则可能会出现执行了操作,浪费了资源,但是日志没有打印的情况。建议占位符方式:
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol);
复制代码
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过 关键字 throws 往上抛出。,比如:
logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
复制代码
- 注意日志输出量的问题,并记得及时删除观察日志。
- 注意日志输出的级别。
MYSQL数据库
建表规约
数据类型
- 表达布尔概念的字段,必须使用
is_xxx
来命名,由于mysql中不存在布尔类型的数据类型,使用unsigned tinyint
(0和1来表示) - 小数类型为
decimal
,禁止使用float
和double
。因为后两者存在精度丢失的问题,导致在值比较时出现差错。 - 使用
char
定长字符串类型(会预先分配空间)来储存字符串长度几乎相等的数据,比如手机号。varchar
是可变长字符串(不会预先分配空间),但是长度不要超过5000。
命名方式
- 任何库名、表名、字段名都只能使用小写字母、数字和下划线,并且数字不能放在开头和两个下划线之间
- 任何库名、表名、字段名都禁用保留字(这算一个坑,踩过,具体有哪些保留字可以去查文档)表名不能使用复数形式。
- 表必备三字段:id, gmt_create, gmt_modified。
- 表的命名最好是加上“业务名称_表的作用”。
- 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
分库分表
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
索引规约
- 具有唯一特性的字段,必须建成唯一索引。唯一索引对insert操作的损耗是可以忽略不计的,而对于提升查找速度的效果非常明显。
工程结构
- 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。
- Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
- Service 层:相对具体的业务逻辑服务层。
- Manager 层:通用业务处理层,它有如下特征:
- 对第三方平台封装的层,预处理返回结果及转化异常信息;
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
- 与 DAO 层交互,对多个 DAO 的组合复用。
- DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
- 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。