一、编程规范
(一)命名规范:(命名要望文知意,不要嫌长)
-
包名要统一小写
-
类名、接口名遵从驼峰式命名,DO / BO /DTO / VO / AO/PO例外
例子:MarcoPolo / UserDO / XmlService / TcpUdpDeal
-
方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格
例子:localValue / getHttpMessage() / inputUserId
-
常量命名全部大写,单词间下划线隔开
例子:MAX_STOCK_COUNT
-
枚举类名要以Enum为后缀,且枚举成员名称要全大写,和常量命名一样
例子:枚举类名 ProcessStatusEnum 枚举成员名称 SUCCESS/UNKOWN_REASON
-
Service/DAO层方法命名:
-
获取单个对象的方法用get做前缀
-
获取多个对象的方法用list做前缀
-
获取统计值的方法用count做前缀
-
插入的方法用save/insert做前缀
-
删除的方法用remove/delete做前缀
-
修改的方法用update做前缀
-
领域模型命名
-
查询数据库对象:xxxPO
-
业务逻辑层对象:xxxBO
-
接收客户端参数的对像:xxxVO
-
层级之间的数据传输对象:xxxDTO
(二)常量定义
-
不允许任何魔法值(即未经定义的常量)直接出现在代码中。常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量
-
跨应用共享常量:放置在二方库中,通常是client.jar中的constant目录下
-
应用内共享常量:放置在一方库中,通常是modules中的constant目录下
-
子工程内部共享常量:即在当前子工程的constant目录下
-
包内共享常量:即在当前包下单独的constant目录下
-
类内共享常量:直接在类内部private static final定义
-
如果变量值在一个范围内变化,且带有名称之外的延展属性,定义为枚举类
例子:数字就是延伸信息,标识星期几
public Enum { MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6),SUNDAY(7);}
(三)代码格式
-
方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。
-
单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
-
第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
-
运算符与下文一起换行。
-
方法调用的点符号与下文一起换行。
-
4方法调用时,多个参数,需要换行时,在逗号后进行。
-
在括号前不要换行
-
适当加空格进行格式整理(idea使用用快捷键ctrl+alt+L快速整理)
(四)OOP规范
-
所有覆写的方法,必须加@Override注解。可以准确判断是否覆盖成功。另外,如果在抽象中对方法签名进行修改,其实现类会马上编译报错
-
外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
-
调用equals方法比较时,应使用常量或确定有值的对象来调用equals方法,并且包装类对象之间的比较,全部使用 equals 方法比较
-
关于基本数据类型和包装数据类型的使用标准:
-
所有的POJO类属性必须使用包装数据类型
-
RPC方法的返回值和参数必须使用包装数据类型
-
所有的局部变量推荐使用基本数据类型
-
构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
-
类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法。
说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载的信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体
-
循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费
-
类成员与方法访问控制从严:
-
如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
-
工具类不允许有 public 或 default 构造方法。
-
类非 static 成员变量并且与子类共享,必须是 protected。
-
类非 static 成员变量并且仅在本类使用,必须是 private。
-
类 static 成员变量如果仅在本类使用,必须是 private。
-
若是 static 成员变量,必须考虑是否为 final。
-
类成员方法只供类内部调用,必须是 private。
-
类成员方法只对继承类公开,那么限制为 protected。
说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦
(五)集合处理
-
在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生 ConcurrentModificationException 异常。并且不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
//正例:
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
//反例:
List list = new ArrayList();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
-
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。
说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配
内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为[ list.size() ]的数组
元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素
个数一致。
-
使用工具类 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>。
/**
* ? extends T 表示T或T的子类
* ? super T 表示T或T的父类
* ? 表示可以是任意类型
**/
class Gent<T> {
public void test(){
System.out.println("gent");
}
}
class SupC {
public void test(){
System.out.println("supC");
}
}
class Bc extends SupC {
//入参只能是SupC或SupC的子类
public void testExtends(gent<? extends SupC> o){
System.out.println("Bc");
}
//入参只能是Bc或Bc的父类
public void testSuper(gent<? super Bc> o){
System.out.println("Bc");
}
//入参可以是任意类型
public void testSuper(gent<?> o){
System.out.println("gent");
}
}
-
合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是order/sort。
-
利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains 方法进行遍历、对比、去重操作
//两个Set比较找出交集、差集、并集
public static void setCompare() {
Set<Integer> result = new HashSet<Integer>();
Set<Integer> set1 = new HashSet<Integer>() {{
add(1);
add(3);
add(4);
}};
System.out.println("set1 = " + set1.toString());
Set<Integer> set2 = new HashSet<Integer>() {{
add(1);
add(2);
add(3);
}};
System.out.println("set2 = " + set2.toString());
//交集:set1和set2相同的元素
result.clear();
result.addAll(set1);
result.retainAll(set2);
System.out.println("交集:" + result);
//result结果:[1, 3]
//差集:元素存在set1,但不存在set2
result.clear();
result.addAll(set1);
result.removeAll(set2);
System.out.println("差集:" + result);
//result结果:[4]
//并集:set1的set2的元素之和
result.clear();
result.addAll(set1);
result.addAll(set2);
System.out.println("并集:" + result);
//result结果:[1, 2, 3, 4]
}
(六)并发处理
-
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
-
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
-
避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。
说明:Random 实例包括 java.util.Random 的实例或者 Math.random()的方式。正例:在 JDK7 之后,可 以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个实例。
-
HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险
(七)控制语句
-
在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
-
在 if/else/for/while/do 语句中必须使用大括号。即使只有一行代码,避免采用单行的编码方式:if (condition) statements;
-
表达异常的分支时,少用 if-else 方式,这种方式可以改写成:if (condition) {...return obj;}// 接着写 else 的业务逻辑代码;
说明:如果非得使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。
正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,
卫语句:
//改造前(看着乱,不清楚每个逻辑分支的具体条件)
//当 a等于1
if (a == 1){
//当 b等于2
if (b == 2){
...
} else {
//当 b不等于1
//c等于3
if (c == 3){
...
}
}
}else{
//当 a不等于1
//当 d等于4
if(d == 4){
...
}
}
//改造后(从上到下,看着清晰明了,并清楚每个逻辑分支对应的条件)
//情况一:当 a等于1 并 b等于2
if(a == 1&&b == 2){
...
}
//情况二:当 a等于1 并 b不等于2 并 c等于3
if(a == 1&&b != 2&&c == 3){
...
}
//情况三:当 a不等于1 并 d等于4
if(a != 1&&d == 4){
...
}
策略模式:
/**
* 实现不同动物发出不同声音
* 如:狗--汪汪 猫--喵喵 牛--哞哞 羊--咩咩
* @author gz
* */
//改造前 (后期加多种动物类型,每次都需要修改共用的逻辑,易出错,且不易维护)
public class AnimalCryTest {
private static final String DOG = "狗";
private static final String CAR = "猫";
private static final String CATTLE = "牛";
private static final String SHEEP = "羊";
/**
* 动物的叫声
* @param animalName 动物名称
*/
public static void animalCry(String animalName){
if (DOG.equals(animalName)){
System.out.println(animalName+"----汪汪");
}else if (CAR.equals(animalName)){
System.out.println(animalName+"----喵喵");
}else if (CATTLE.equals(animalName)){
System.out.println(animalName+"----哞哞");
}else {
System.out.println(animalName+"----咩咩");
}
}
public static void main(String[] args) {
// 狗--汪汪
animalCry(DOG);
// 猫--喵喵
animalCry(CAR);
// 牛--哞哞
animalCry(CATTLE);
// 羊--咩咩
animalCry(SHEEP);
}
}
//改造后(后期加多种动物类型,只需要实现对应功能的接口,各个逻辑是相互独立的,不易出错,易维护)
/**
* 1、定义一个接口
**/
interface Animal {
/**
* 动物叫声
*/
void animalCry();
}
/**
* 2、创建实现该接口的实现类
**/
class Dog implements Animal {
@Override
public void animalCry() {
System.out.println("狗----汪汪");
}
}
class Car implements Animal {
@Override
public void animalCry() {
System.out.println("猫----喵喵");
}
}
class Cattle implements Animal {
@Override
public void animalCry() {
System.out.println("牛----哞哞");
}
}
class Sheep implements Animal {
@Override
public void animalCry() {
System.out.println("羊----咩咩");
}
}
/**
* 3、多态性质,根据实例化对象的不同,调用实例化对象对应的具体方法
**/
public class AnimalCryTest {
public static void main(String[] args) {
// 创建狗的实例对象,并调用其对应的animalCry方法 狗--汪汪
Animal dog = new Dog();
dog.animalCry();
// 创建猫的实例对象,并调用其对应的animalCry方法 猫--喵喵
Animal car = new Car();
car.animalCry();
// 创建牛的实例对象,并调用其对应的animalCry方法 牛--哞哞
Animal cattle = new Cattle();
cattle.animalCry();
// 创建羊的实例对象,并调用其对应的animalCry方法 羊--咩咩
Animal sheep = new Sheep();
sheep.animalCry();
}
}
状态模式:
/**
* 在"投了25分钱"的状态下"转动曲柄",会售出糖果;而在"没有25分钱"的状态下"转动曲柄"会提示请先投币。
* 四个状态:
* 1、没有硬币状态
* 2、投币状态
* 3、出售糖果状态
* 4、糖果售尽状态
* 四个动作:
* 1、投币
* 2、退币
* 3、转动出糖曲轴
* 4、发糖
* @author gz
*/
//改造前
public class NoPatternGumballMachine{
/*
* 四个状态
*/
/**没有硬币状态*/
private final static int NO_QUARTER = 0;
/**投币状态*/
private final static int HAS_QUARTER = 1;
/**出售糖果状态*/
private final static int SOLD = 2;
/**糖果售尽状态*/
private final static int SOLD_OUT = 3;
private int state = SOLD_OUT;
private int candyCount = 0;
public NoPatternGumballMachine(int count) {
this.candyCount = count;
if(candyCount > 0)
state = NO_QUARTER;
}
/*
* 四个动作
*/
/**
* 投币
*/
public void insertQuarter() {
if(NO_QUARTER == state){
System.out.println("投币");
state = HAS_QUARTER;
}
else if(HAS_QUARTER == state){
System.out.println("请不要重复投币!");
returnQuarter();
}
else if(SOLD == state){
System.out.println("已投币,请等待糖果");
returnQuarter();
}else if(SOLD_OUT == state){
System.out.println("糖果已经售尽");
returnQuarter();
}
}
/**
* 退币
*/
public void ejectQuarter() {
if(NO_QUARTER == state){
System.out.println("没有硬币,无法弹出");
}
else if(HAS_QUARTER == state){
returnQuarter();
state = NO_QUARTER;
}
else if(SOLD == state){
System.out.println("无法退币,正在发放糖果,请等待");
}else if(SOLD_OUT == state){
System.out.println("没有投币,无法退币");
}
}
/**
* 转动出糖曲轴
*/
public void turnCrank() {
if(NO_QUARTER == state){
System.out.println("请先投币");
}
else if(HAS_QUARTER == state){
System.out.println("转动曲轴,准备发糖");
state = SOLD;
}
else if(SOLD == state){
System.out.println("已按过曲轴,请等待");
}else if(SOLD_OUT == state){
System.out.println("糖果已经售尽");
}
}
/**
* 发糖
*/
public void dispense() {
if(NO_QUARTER == state){
System.out.println("没有投币,无法发放糖果");
}
else if(HAS_QUARTER == state){
System.out.println("this method don't support");
}
else if(SOLD == state){
if(candyCount > 0){
System.out.println("分发一颗糖果");
candyCount --;
state = NO_QUARTER;
}
else{
System.out.println("抱歉,糖果已售尽");
state = SOLD_OUT;
}
}else if(SOLD_OUT == state){
System.out.println("抱歉,糖果已售尽");
}
}
/**
* 退还硬币
*/
protected void returnQuarter() {
System.out.println("退币……");
}
}
//改造后
//1、定义一个接口或者抽象类,抽象出几个行为状态
public abstract class State {
/**
* 投币
*/
public abstract void insertQuarter();
/**
* 退币
*/
public abstract void ejectQuarter();
/**
* 转动出糖曲轴
*/
public abstract void turnCrank();
/**
* 发糖
*/
public abstract void dispense();
/**
* 退还硬币
*/
protected void returnQuarter() {
System.out.println("退币……");
}
}
// 2、为每个状态实现接口或基类
/**
* 没有硬币的状态
*/
public class NoQuarterState extends State{
GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("你投入了一个硬币");
//转换为有硬币状态
gumballMachine.setState(gumballMachine.hasQuarterState);
}
@Override
public void ejectQuarter() {
System.out.println("没有硬币,无法弹出");
}
@Override
public void turnCrank() {
System.out.println("请先投币");
}
@Override
public void dispense() {
System.out.println("没有投币,无法发放糖果");
}
}
/**
* 投硬币的状态
*/
public class HasQuarterState extends State{
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("请不要重复投币!");
returnQuarter();
}
@Override
public void ejectQuarter() {
returnQuarter();
gumballMachine.setState(gumballMachine.noQuarterState);
}
@Override
public void turnCrank() {
System.out.println("转动曲轴,准备发糖");
gumballMachine.setState(gumballMachine.soldState);
}
@Override
public void dispense() {
System.out.println("this method don't support");
}
}
/**
* 出售的状态
*/
public class SoldState extends State{
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("已投币,请等待糖果");
returnQuarter();
}
@Override
public void ejectQuarter() {
System.out.println("无法退币,正在发放糖果,请等待");
}
@Override
public void turnCrank() {
System.out.println("已按过曲轴,请等待");
}
@Override
public void dispense() {
int candyCount = gumballMachine.getCandyCount();
if(candyCount > 0){
System.out.println("分发一颗糖果");
candyCount--;
gumballMachine.setCandyCount(candyCount);
if(candyCount > 0){
gumballMachine.setState(gumballMachine.noQuarterState);
return;
}
}
System.out.println("抱歉,糖果已售尽");
gumballMachine.setState(gumballMachine.soldOutState);
}
}
/**
* 售尽的状态
*/
public class SoldOutState extends State{
GumballMachine gumballMachine;
public SoldOutState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("糖果已经售尽");
returnQuarter();
}
@Override
public void ejectQuarter() {
System.out.println("没有投币,无法退币");
}
@Override
public void turnCrank() {
System.out.println("糖果已经售尽");
}
@Override
public void dispense() {
System.out.println("糖果已经售尽");
}
}
//3、将糖果机的动作委托到状态类
public class GumballMachine extends State{
public State noQuarterState = new NoQuarterState(this);
public State hasQuarterState = new HasQuarterState(this);
public State soldState = new SoldState(this);
public State soldOutState = new SoldOutState(this);
private State state = soldOutState;
private int candyCount = 0;
public GumballMachine(int count) {
this.candyCount = count;
if(count > 0)
setState(noQuarterState);
}
@Override
public void insertQuarter() {
state.insertQuarter();
}
@Override
public void ejectQuarter() {
state.ejectQuarter();
}
@Override
public void turnCrank() {
state.turnCrank();
}
@Override
public void dispense() {
state.dispense();
}
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void setCandyCount(int candyCount) {
this.candyCount = candyCount;
}
public int getCandyCount() {
return candyCount;
}
}
从代码里面可以看出,糖果机根据此刻不同的状态,而使对应的动作呈现不同的结果。这份代码已经可以满足我们的基本需求,但稍微思考一下,你会觉得这种实现代码似乎,功能太复杂了,扩展性很差,没有面向对象的风格。
假设由于新需求,要增加一种状态,那每个动作方法我们都需要修改,都要重新增加一条else语句。而如果需求变更,某个状态下的动作需要修改,我们也要同时改动四个方法。这样的工作将是繁琐而头大的。
可以发现,这种模式下,糖果机根本不需要清楚状态的改变,它只用调用状态的方法就行。状态的改变是在状态内部发生的。这就是"状态模式"。
如果此时再增加一种状态,糖果机不需要做任何改变,我们只需要再增加一个状态类,然后在相关的状态类方法里面增加转换的过程即可
-
除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢?
正例:
// 伪代码如下
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
反例:
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
(八)注释规范
-
类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用// xxx 方式
-
所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
-
方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/ /注释,注意与代码对齐。
-
所有的枚举类型字段必须要有注释,说明每个数据项的用途。
-
代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。代码和注释要同步更新
-
对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。注释要尽可能精简准确、表达到位,要避免过多过滥的注释。
-
特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
-
待办事宜(TODO):( 标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)。
-
错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
(九)其他
-
获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime()的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类
-
不要在controller层加任何复杂的逻辑
-
任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
-
一个方法体不要过长,要对逻辑进行拆分,尽量不要超过80行