养成一个合格的编码风格,有益于自己和阅读你代码的人理解:
**** 命名风格 ****
-
不允许任何类名,包名,方法名,变量名以下划线_或者美元符号$开头或者结尾
反例:name, name, name, name, $name, name $ -
代码中的命名不允许出现拼音,拼音与英语混合或者中文命名(专有名词除外:alibaba,taobao,suzhou等)
反例:dazhe, guanggao, 明天,getJihuoByDate() -
类名使用大驼峰(UpperCamelCase)风格命名,但是PO/VO/DTO等例外
举例:XmlUtil, QrCode, TcpIpDeal
反例:XMLUtil, HTTPUtil, TCPIPDeal, QRCode -
方法名,参数,成员变量,局部变量使用小驼峰(lowerCamelCase)风格命名
举例:localValue, getUsernameById(Long id) -
常量命名全部要大写,单词之间以单个下划线_连接,尽量见名知意,不要嫌弃名字太长
举例:MAX_SERVER_COUNT, CACHE_EXPIRED_TIME
反例:MAX_COUNT, EXPIRED_TIME -
抽象类命名必须以Abstract或Base开头,异常类必须以Exception结尾,测试类必须以被测试类名为开头,以Test(s)结尾
-
数组命名:类型与方括号[]之间无空格相连定义
举例:String[] args
反例:String args[] -
需要序列化的类中boolean类型的变量名请勿使用is为前缀,容易导致部分框架的序列化问题以及代码getter/setter逻辑问题
-
包名统一使用小写,点分隔符之间有且只有一个英语单词;包名使用单数形式,但是类名有复数含义,则类名可以使用复数
-
子类和父类中间的成员变量名不能使用相同的变量名,避免造成误读,降低代码可读性
-
杜绝使用不规范的单词缩写,避免单词歧义或者词不达意
-
接口实现类,一定要用接口类名+Impl来定义
**** 常量 ****
- 关于常量,需要去判断是否真正为常量,并不仅仅是用static final定义来判断
static final int NUMBER = 5;
static final ImmutableList NAMES = ImmutableList.of(“Ed”, “Ann”);
static final Joiner COMMA_JOINER = Joiner.on(’,’); // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};
enum SomeEnum { ENUM_CONSTANT }
反例
//缺少final
static String nonFinal = “non-final”;
//缺少static
final String nonStatic = “non-static”;
//set是可变的,即使是用static final修饰也不会是常量
static final Set mutableCollection = new HashSet();
//与上一个类似
static final ImmutableSet mutableElements = ImmutableSet.of(mutable);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {“these”, “can”, “change”}; - 常量
每个常量都是一个静态final字段,但不是所有静态final字段都是常量。在决定一个字段是否是一个常量时, 考虑它是否真的感觉像是一个常量。例如,如果任何一个该实例的观测状态是可变的,则它几乎肯定不会是一个常量。 只是永远不打算改变对象一般是不够的,它要真的一直不变才能将它示为常量
/**
1.不允许任何魔法值(即未预先定义的常量)直接出现在代码中
2.long和Long初始赋值时,数值后应该使用大写L,避免小写l与数字1混淆,造成误解
3.不要使用一个常量类维护所有常量,最好按照常量功能进行分类,分开维护
**/
// Constants
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};
enum SomeEnum { ENUM_CONSTANT }
// Not constants
static String nonFinal = "non-final";
final String nonStatic = "non-static";
static final Set<String> mutableCollection = new HashSet<String>();
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {"these", "can", "change"};
3. 代码格式
- 大括号{}
如果大括号内容为空则简洁地写成{}即可,不需要换行;例外:
左大括号不另起一行
右大括号另起一行
右大括号后有else/catch/finally等代码块时不换行,否则必须换行
// 多代码块的情况下,即使内容为空,也应该换行
if (a == b) {
// do something
} else if (a == c) {
} else {
}
try {
// do something
} catch(Exception e) {
} finally {
}
-
小括号():左小括号与字符之间没有空格,同理右小括号与字符之间也没有空格
反例:if ( a == b ) -
if/else/for/while/do/swtich等保留字与括号之间必需有一个空格
反例:for(int i = 0; i <= 10; i++){}, if(a == b){} -
运算符:任何双目、三目运算符的左右两边都必须有一个空格(赋值运算符,算数运算符,逻辑运算符,按位运算符)
举例:
int a = 1;
int b += a;
int c = a - b;
int d = (c > 0) ? 1 : 0;
if ((d == a) && (a + c == 0)) {
System.out.println(true);
} else {
}
-
缩进:缩进必须使用4个空格,严禁使用Tab控制符,更不要混用
-
单行注释:双斜线//与注释内容之间有且仅有一个空格;单行注释不要写在代码尾部
-
单行代码字符不超过120(100)个,超出则必须在合适的位置换行,并且遵循下列规则:
第二行相对第一行缩进8(4)个空格,第三方及以后与第二行保持垂直对齐
运算符与下文一起换行(即运算符要在行首,不要放在行尾)
方法调用的点符号.与下文一起换行
方法调用中的多个参数需要换行时,在逗号后面换行(即不要把逗号放在行首)
括号前不要换行
// 反例
StringBuilder sb = new StringBuilder();
sb.append('a').append('b')...append
("no line break here");
- 方法参数在定义与传入时,多个参数逗号后必须有一个空格
void excute(String arg1, String arg2, String arg3);
method(1, 2, 3);
- 单个方法的实现行数不要超过80行(注释,方法签名,左右大括号,方法内空行,回车及任何不可见字符除外)
4. OOP规约
-
避免使用类的实例访问类的静态变量或者今天方法(无谓增加编译器的解析成本,使用类名访问即可)
-
所有的重写方法都必须添加@Override注解
原因:
1)避免某些字符相似导致的没有重写的错误;
2)添加了注解之后,如果父类/接口签名修改,实现类马上报编译错误 -
可变参数:相同的参数类型,相同的业务含义,才能使用Java的可变参数;请不要使用Object类型的可变参数
// 举例
public List<user> listUsers(Long...ids) {...}
-
外部正在使用的或者二方库依赖的接口,都不允许修改方法签名,以避免调用方产生影响;若方法已过时,则在方法上添加@Deprecated注解,并清晰地说明采用的新接口或者新的服务是什么
-
不允许使用过时的类或者方法
-
Object的equals方法易抛出空指针,使用时应该使用常量或者确定不为空的对象调用equals(JDK7以上推荐使用:java.util.Objects#equals工具类来比较两个对象)
举例:"test".equals(obj)
-
所有相同类型的封装类对象之间的值比较时,全部使用equals方法(最好使用工具类)
封装对象使用判断时,比较的是对象地址
延伸:对Integer对象在-128~127范围内的赋值时,对象在IntegerCache.cache中产生,会服用已有对象,可以直接使用判断;但是超出区间外的对象在堆中生成,不会复用 -
封装类型和基本数据类型:
所有POJO的成员变量必须使用封装类型
原因:POJO的属性没有初始值,是要提醒使用者使用时需要显式赋值;POJO对应的数据库数据表中的字段有可能为空,使用基本数据类型自动拆箱,会有NPE(NullPointerException)的风险
RPC方法的返回值和参数列表必须使用封装类型
原因:远程服务调用如果返回基本数据类型,在某个服务失败时会返回默认值(0/false)而不是null,会导致调用方逻辑判断有误,如果返回为null,则调用方可以显示错误或者抛出异常
局部变量推荐使用基本数据类型 -
POJO中的属性不要赋初始值
-
所有实现了java.io.Serializable接口的类,都必须添加private static final long serialVersionUID属性(请不要使用默认值-1也不要复制其他类的值,让IDE自动生成,保证值唯一;请不要使用@SuppressWarnings忽略警告)
-
当序列化类中添加新属性时,请不要修改serialVersionUID的值,防止反序列化失败;如果要做不兼容升级,请修改serialVersionUID的值,防止反序列化混乱
-
构造方法中禁止添加业务逻辑代码,初始化的逻辑代码请放在init方法中
-
POJO必须重写toString方法,在Debug或者排查错误是,调用toString输出POJO的属性,便于排查问题
-
在POJO类中,禁止同时存在同一个属性的getXxx()和isXxx()方法
-
当一个类中存在多个构造方法,或者存在多个同名方法时,请按照一定顺序把所有相同的方法放置在一起
-
尽量不要在getter/setter方法中添加业务逻辑
5. 集合
-
hashCode和equals
只要重写equals,则必须重写hashCode
使用自定义对象作为Map的键时,必须重写equals和hashCode -
Map的keySet(), values(), entrySet()方法返回的集合,不允许添加元素,否则会报UnsupportedOperationException
-
Collections类emptyList(), singletonList()等方法返回的集合都是Immutable的,不可用添加或者删除元素
-
ArrayList的subList()方法返回的对象是SubList,不可强转为其他类型
-
在subList的场景中,对原集合元素个数的修改会导致对子集合的所有操作报ConcurrentModificationException
-
在把集合转换成数组时,请使用 T[] toArray(T[] a)方法,不要使用无参的重载,实现如下:
List<String> list = new ArrayList<>(2);
list.add("1");
list.add("2");
String[] array = new String[list.size()];
array = list.toArray(array);
- 在把数组转成集合时,注意如下:
String[] array = new String[] { "1", "2" };
List<String> list = Arrays.asList(array);
// List<String> list = Arrays.asList("1", "2");
// 这行代码会抛出UnsupportedOperationException
list.add("3");
// 这行代码执行后,list.get(0)的值也会随之修改
str[0] = "0";
//原因:asList()方法的返回值是java.util.Arrays.ArrayList,并没有重写AbstractList类实现的add方法;这个方法体现的是适配器模式,只作为转换接口使用,数据结构上仍是数组,
//避免以上问题的写法:new java.util.ArrayList<>(Arrays.asList("1", "2"));
-
boolean addAll(Collection<? extends E> c)方法的调用,在调用Collection接口任何实现类的addAll方法时,传入的参数都必须做NPE检查
-
使用泛型通配符<? extends T>的集合,不能调用add方法,而<? super T>的集合不能调用get方法;根据PECS(Producer Extends, Consumer Super)原则:频繁往外读取数据适合使用<? extends T>,频繁往里插入内容适合使用<? super T>
-
把无泛型限制的集合赋值给有泛型的集合时,需要进行instanceof判断,避免抛出ClassCastException
-
不要在foreach中添加或者删除元素(不要在对集合遍历时调用此集合的add/remove方法)
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
// 错误写法
for (String str : list) {
if (Objects.equals("1", str)) {
list.add("4");
continue;
}
if (Objects.equals("2", str)) {
list.remove(str)
break;
}
}
// 正确写法
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
iterator.remove();
}
-
JDK7以上,使用泛型时,使用diamond语法(菱形语法):List list = new ArrayList<>();
-
对Map进行遍历时使用entrySet而不是keySet,JDK8以上则使用Map.foreach
注意:keySet方式遍历Map其实是遍历了两次,一次是keySet生成Iterator,一次是从HashMap中取出key对应的value
6. 多线程、并发
- 创建单例对象是要保证线程安全:(推荐一下两种方式,其他方式各有优缺点自行了解)
枚举:最简单,最优秀的创建单例的方式
public class SingletonEnum {
private SingletonEnum() {
}
private enum Singleton {
INSTANCE;
private final SingletonEnum instance;
Singleton() {
instance = new SingletonEnum();
}
public SingletonEnum getInstance() {
return instance;
}
}
public static SingletonEnum getInstance() {
return Singleton.INSTANCE.getInstance();
}
}
内部类方式:既能保证线程安全,又不会大量JVM资源
public class SingletonClass {
private SingletonClass() {
}
// 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE
// 调用的时候只创建一个 所以单例
private static class InstanceHolder {
private final static SingletonClass instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return InstanceHolder.instance;
}
}
-
创建线程时,需要指定有意义的线程名称,方便问题回溯
-
线程创建必须通过线程池提供,不允许显式地自行创建线程
线程池的好处就是减少线程创建和销毁的开销,极大地优化系统资源不足的问题 -
不允许使用Executors创建线程池,需要使用ThreadPoolExecutor的方式创建,这种方式可以更加明确线程池的允许规则
-
JDK8使用Instant代替Date,使用LocalDateTime代替Calendar,使用DateTimeFormatter代替SimpleDateFormat时间操作更方便,线程安全
-
必须回收自定义的ThreadLocal变量,有可能导致内存泄漏OOM
-
高并发场景下,认真考量线程锁,能不用锁尽量不用,能不用带锁的数据结构就尽量不用,加锁的代码块尽量小;避免在锁中调用RPC服务
-
在对多个资源,数据表,对象加锁时,保持一致的加锁顺序,避免死锁
-
与资金相关的金融敏感信息使用悲观锁
7. 控制语句
-
switch
每个case语句要么通过break/return来终止,要么添加注释说明要执行到哪个语句结束
必须包含一个default语句,即使什么逻辑都没有 -
所有的代码块都必须包含大括号,即使代码块只有一行代码(if/else/for/while/do)
禁止单行编码方式:if (true) doSomething(); -
禁止出现if-else if…else的代码
一定需要这种逻辑判断的时候,代码不能超过三层(即只允许出现一次else if),正确写法(卫语句,策略模式,状态模式实现):
// 卫语句
public void doSomething() {
if (isBusy()) {
// change time
return;
}
if (isFree()) {
// do something
return;
}
// study
return;
}
- 不要在判断条件中执行复杂逻辑,除了一般常用的getXx()/isXx()/hasXx()/existXx()方法判断外,判断条件一般使用单个boolean值
final boolean flag = (getXx() || hasXx()) && (a >= b || c != 0);
if (flag) {
// do something
}
-
在高并发的场景下,不要使用==作为程序中断或者退出的判断条件,因为在并发处理错误的时候,会导致程序永远都不会退出
-
循环体中的语句要考量性能。以下操作尽量提取到循环体外:
定义对象、变量
获取数据库连接
不必要的try-catch -
循环体/递归尽量不要超过两次,禁止超过三层
-
避免不必要的取反逻辑
8. 注释
-
类,类属性,类方法的注释都必须符合Javadoc规范,使用/注释内容/格式,禁止使用单行注释方式
-
所有的抽象方法(包括接口方法),都必须要用Javadoc注释,清楚地说明参数,返回值,异常,以及该方法做什么事情,实现什么功能,对子类的实现有什么要求,调用时需要注意什么等
-
所有类必须添加创建者及创建时间
-
方法内的单行注释,在被注释代码上方另起一行(禁止行尾注释),使用//注释;方法内的多行注释,使用/* */注释,注意代码对齐
-
所有的枚举类型字段必须要有注释,说明每个数据的含义及用途
-
注释可以使用中文,专有名词和关键字注意保持与原文一致;注释要语句通顺,意思表达完整
-
修改代码时,注意修改原注释
-
注释代码时,请在被注释掉的代码上方,清楚地说明原因(使用///三斜线);如果代码无用,请直接删除
-
特殊注释: TODO, FIXME
待办事项TODO: 在添加时,请写清楚:标注人(执行人),标注时间,预计处理的时间,未实现的功能
错误异常FIXME: 在添加时,请写清楚:标注人(执行人),标注时间,预计处理的时间,使用fixme标记某代码是错误的,不能工作,需要及时修改
异常日志
-
关于RuntimeException
类似NullPointException,IndexOutOfBoundsException等不应该使用catch来处理异常,而应该在代码中通过判断来规避这类异常
// JDK8
public final class Optional
类似NumberFormatException可以使用catch捕获并处理捕获异常 -
捕获的异常不允许不做任何处理就丢弃,如果不想处理则抛出给调用者
最外层调用者一定要把异常处理成用户可以理解的内容 -
在事务中,需要仔细判断是否需要事务回滚,调用rollback
不要在finally代码块中使用return语句
捕获的异常必须与抛出的异常类型完全匹配,或者捕获抛出异常的父类型 -
大段的try-catch代码需要对不同异常作出不同的应激反应,不允许对大量不同类型的异常统一捕获Exception对象,不利于对不同问题作出对应的处理
对应不同异常类型,但是处理方式一致的情况,使用multi-catch方式编码 -
资源释放
必须在finally代码块中释放掉所有的资源对象,流对象,关闭对象时的异常需要进行try-catch操作,禁止直接抛出
//JDK7及以上建议使用try-with-resource方式编码
try (InputStream is = new FileInputStream("text.txt")) {
// do something
} catch (IOException e) {
// handle io exception
}
// 此处不需要显式的通过finally块关闭流,流会在代码执行完毕后自动释放
- 空值null
允许方法返回null,但是最好尽量避免;在返回null的方法要注释充分说明,什么情况下才会返回null,调用方需要添加NPE判断
自动拆箱时,注意空指针
数据库查询到的数据属性可能是null
集合即使isNotEmpty,取出的数据依然可能是null
远程调用返回对象时,一定要做NPE检查
级联调用时注意空指针:obj.getA().getB().getC(),建议写法:
Optional.ofNullable(obj)
.map(Obj::getA)
.map(A::getB)
.map(B::getC)
.get()
-
自定义异常
不要在代码中显式的抛出new RuntimeException(),更不允许抛出Exception或者Throwable
根据自己的业务定义自定义异常(ServerException, UsernameExistException)
定义ErrorCode -
所有对外的接口,都必须有统一的数据结构和返回规则;统一的错误编码可以更好帮助调用者定位问题或者处理逻辑
-
重复代码
不允许大段复制代码(Don’t repeat yourself)即DRY原则
把需要重复使用的代码提取成共性代码,公共方法等 -
日志
输出日志建议使用SLF4J,或者框架推荐日志实现方式
日志文件至少保存15天以上
输出日志时不要使用字符串拼接方式,建议使用日志占位符
// 占位符{}
logger.info(“id-{}, message-{}”, id, message);
对于debug/info/trace级别的日志,必须使用条件输出或者占位符的方式输出
// 条件输出
if (logger.isDubugEnable()) {
logger.debug(“id-” + id + “, message-” + message);
}
禁止在生产环境中使用System.out, System.err输出日志;禁止使用e.printStackTrace()输出异常堆栈
开发时可以用来打桩调试,提交代码时务必删除
输出异常信息时,需要输出异常信息和堆栈;如果不做处理,请把异常往上一层抛出
logger.error("异常信息toString: " + e.getMessage, e);
生产环境禁止输出debug级别的日志,info级别的日志请有选择的输出
避免大量的无效日志,大量的无效日志及影响系统的运行,又不能快速的定位问题;输出日志时请认真思考这些日志能用来做什么,这些日志能帮助排查问题吗
刚上线的项目允许通过warn级别输出业务行为信息,但是要注意输出量,避免把服务器磁盘搞爆炸,并及时删除日志文件
适当的使用warn级别日志,记录用户输入的错误参数情况,可以在用户投诉时提供帮助
推荐使用英文输出日志信息,当用英文表达不清楚时,可以使用中文(谨慎使用);对应国外服务器或者国际服务器或者中外同服的情况下,全部使用英文输出日志,避免字符集的问题 -
安全规约
任何属于用户个人的页面或者功能点都必须添加权限控制校验
用户的敏感信息禁止直接展示,必须对展示的信息进行脱敏处理(部分/全部数据替换成***)
注意:对外接口、服务返回的数据里不要包含敏感数据
密码、手机号、身份证号、真实姓名等必要的数据 -
SQL:用户输出的SQL参数必须进行严格的参数绑定和验证,防止SQL注入;禁止使用字符串拼接SQL访问数据库
-
所有用户输入的数据都必须进行参数绑定和有效性验证
-
在使用平台资源(如:短信,邮件,电话,支付等)时,必须正确实现防重放机制(如:数量限制,疲劳值限制,验证码校验),避免被滥刷或者资产损失
MYSQL
-
建表
表达是否概念的字段一律使用is_xxx,数据类型为unsigned tinyint(1表示是,0表示否)
is_deleted:是否删除
表明或者字段名必须使用小写字母或者数字,单词之间用下划线连接,禁止以数字打头,禁止两个下划线之间只包含数字
MYSQL在Windows下不区分大小写,但是在Linux上默认区分大小写,所有禁止在数据库中使用大写字母,避免不必要的麻烦
表名不允许使用复数名称命名
禁止使用数据库保留字(附:MYSQL8.0保留字库)
经常被使用的保留字:name(s), key(s), desc等,需要特别注意
小数类型应该为:decimal,禁止使用float/double
如果存储的字符串长度基本相等,应使用定长的char类型代替varchar。例如:MD5加密的密码,以一定规则生成的KEY,UUID等
varchar是可变长度字符串类型,不预先分配存储空间,最大长度不要超过5000。如果存储长度超过5000,需要使用text类型,并且建议建立关联表以主键关联,避免影响其他字段的索引效率
允许数据表数据冗余,提高查询效率,但是必须考虑数据的一致性
此冗余字段不能频繁修改
此字段不能是超长的varchar或者text -
单表数据超过500W行或者单表容量超过2GB时才推荐使用分表分库
-
对字段设置合适的存储长度,可以节约数据表空间和索引存储,更重要的是可以提升检索速度
索引
-
业务上具有唯一特效的字段必须建立唯一索引
虽然唯一索引会影响插入速度,这个速度损耗可以忽略,但是会明显的提升查询速度
根据墨菲定律,即使在应用层做了严格的参数验证,只要没有唯一索引,就一定会有脏数据产生 -
绝对禁止三个表以上的join操作
需要join的字段,数据类型一定要一致
当多表关联查询时,保证关联字段要有索引
即使双表的join操作,也要注意索引和SQL性能
在varchar字段上建立索引时,需要指定建立索引的长度
模糊搜素时,禁止使用左模糊或者全模糊,如果需要请搜索索引文件,否则会导致索引失效
SQL
-
不要使用count(列名)或者count(常量)代替count()
count()是SQL92的标准统计行数的语法(跟数据库无关,与NULL值也无关),并且MYSQL对其进行了深度优化
count(列名)的方式会忽略掉此列为NULL的数据行 -
使用SUM(列名)时,需要注意NPE问题
使用IS NULL来判断NULL。因为NULL与其他任意值得比较结果均为NULL,而不是TRUE/FALSE
禁止在数据库中使用外键或者级联,一起外键的概念必须在应用层解决
禁止使用存储过程
难以调试,难以扩展,没有移植性 -
数据删除或者修改时,必须先做查询操作,确认无误后,在查询到的数据基础上再做更新或者删除
-
规避在SQL中使用in操作,如果无法避免,则要仔细评估in后面的集合元素数量,控制在1000个以内