开发团队在开发过程中,由于每个人的开发习惯,以及对于技术的理解深浅程度不一,往往一个项目在开发过程中,代码的质量,代码的风格都不尽相似,所以有一份适合团队的代码规范是非常有必要的,而一个团队的代码规范,包含了开发常见的风格习惯以及一些常见代码细节的写法规范等,本篇就来浅谈一些代码规范涉及的技术细节和对这些部分的思考。
命名规范
相信经历过项目开发的人都知道,在开发过程中会涉及无数次的申明操作,这个过程中最让人头疼的就是给申明的文件(实例)起个名字了。名字需要准确的表达出背后代表的含义,并且还要通俗易懂,使得代码干净漂亮,否则不好的命名反而成为开发的阻碍,干扰维护者和开发者的思路。那么一个好的命名会给我们开发带来什么好处呢?
为标识符提供附加的信息,赋予标识符现实意义。帮助我们理顺编码的逻辑,减少阅读和理解代码的工作量;
使代码审核变得更有效率,专注于更重要的问题,而不是争论语法和命名规范这类小细节,提高开发效率;
提高代码的清晰度、可读性以及美观程度;
避免不同产品之间的命名冲突。
那么常见的命名方式有哪些呢?根据各大规范和Java框架主流的方案来说,一般分为四种:
驼峰命名法
驼峰命名法基本上是各大企业使用最多,也是各大规范首推的命名方式。其使用大小写混合的格式,单词之间不使用空格隔开或者连接字符连接的命名方式,因此发展处两种格式:大驼峰命名法(UpperCamelCase)和小驼峰命名法(lowerCamelCase)
这两种命名方式的区别主要在第一个单词的首字母上,大驼峰命名法则是首字母大写命名,而小驼峰则是首个单词的首字母小写,比如:firstName, toString等。在jdk中参照了谷歌制定的驼峰命名转换规则,用来细分不同情况下的驼峰转换:
1.从正常的表达形式开始,把短语转换成 ASCII 码,并且移除单引号,如“Müller’s algorithm”转换为“Muellers algorithm”
2.如果存在连接符号,就将连接符开始分割为两个单词,如果某个分割前某个单词已经是驼峰命名,也拆分为小写的两个单词,如:AdWords会转换为ad words,而non-current则转换为non current 等
3.将所有的字母转为小写字母,每个单词的首字母大写,就转换为了大驼峰命名/首字母小写则转为小驼峰
4.将所有的单词连接在一起,即为标示符命名
例如下面的转换案例:
蛇形命名法
蛇形命名法在Java中极少见到,一般为每个单词之间都通过‘’进行连接,例如‘outof’
串式命名法
串式命名法和蛇形规则一样,唯一区别是,每个单词之间通过'-'连接,例如'out-of'
匈牙利命名法
匈牙利命名法在Java早期的框架中开始出现,由一个或者多个小写字母开始,使用这些字母作为标示符,用来标记当前命名的变量的用途,例如:usName(表示是用户的名称),lAccountNum(表示是Long类型的长整数)等
而在jdk中,针对每一种类型的命名有特定的规范,针对每一种编码规范来组合使用在不同场景的命名中,如下:
总结下来,jdk命名遵循了三点:
1.命名有准确的意义,绝不使用单词缩写或者单词的部分,例如GoodsItem,绝不会命名为GdItem
2.严格遵守命名规范,决不允许一个规则内出现多个规范混用的情况,例如在一个命名中同时出现驼峰命名与蛇形命名等
3.尽量将可读性的命名放在前面,开发者的习惯一般都是从左到右开始阅读和编码,所以将能体现出想要的信息的内容优先放在前面,例如BeijingTime和TimeBeijing的区别
变量申明的时机
前面我们说过命名的规范,那么申明变量是否也需要规范呢?其实也需要,例如现在申明一个类型的变量的时候,往往有人喜欢一个类型的变量在一行内申明完毕,例如:
int size, length;
甚至于出现了一行申明了七八个属性的情况,或者是在一行内申明了好几个类型的变量,例如:
int size,entity[];//一行申明多个不同类型变量
看起来代码似乎节省了,但是对于开发和维护来说,其实反而更容易忽略错误,更重要的是申明类型是数组的时候不要把基本类型和[]分开,因为int[] 才是代表了一个类型的整体,分开申明容易被忽略,或者埋下隐患的错误,所以往往建议每一行仅申明一个变量,如下:
int size;
int[] entity;
在开发中往往还存在另外一个情况,就是方法内申明局部变量的时候,往往喜欢在方法开始的时候就创建或者申明该变量,但是使用的时机往往在n行代码以后,甚至于到后面这个申明的变量并没有使用到,由于间隔太远,也没有关注,后面就成了一个死变量,这种情况是很多见的,而反观jdk的规范中,可以看到都是在需要使用变量的时候创建,或者在需要使用的前几行代码申明再去创建,例如:
public void test(String userName){
Account userAccount;
String groceryStoreName;
//中间一堆业务代码和操作
/*****
****
***/
//通过用户名获取userAccount
userAccount = AccountManager.getUserAccount(userName);
if(userAccount == null){
//为null的操作,抛异常
}
//再去获取名称
groceryStoreName = userAccount.getGroceryStoreName();
if(groceryStoreName == null){
//为null,抛异常
}
//后续一堆业务代码
}
但是我们看下规范后的写法:
public void test(String userName){
//中间一堆业务代码和操作
/*****
****
***/
//通过用户名获取userAccount
Account userAccount = AccountManager.getUserAccount(userName);
if(userAccount == null){
//为null的操作,抛异常
}
//再去获取名称
String groceryStoreName = userAccount.getGroceryStoreName();
if(groceryStoreName == null){
//为null,抛异常
}
//后续一堆业务代码
}
很明显的可以看出来,代码更清晰明了,也更有逻辑性。另外在申明类属性变量的时候,我们建议将变量申明在一起,分块存放,不建议在类中变量和方法混合在一起使用,例如:
另外在申明类变量的时候,切记不要忘记类变量如果是基础类型,会有默认值,如非必要,在类属性创建中建议使用包装类型,防止因默认值带来的数据不一致等问题,而在方法内创建局部变量的时候,由于基本类型变量没有默认值,需要手动申明值,反而建议使用基本类型,而不是使用包装类,这样同样也可以尽量避免无意的拆箱、装箱行为,在数十万次百万次的情况下,对于程序也会造成一定的影响。
if与大括号
if语句是我们开发中最常见的逻辑分支语句之一,同样的在java中if也会有一些简洁写法,例如逻辑业务仅有一行代码的时候,我们可以省去大括号,直接在if下一行编写业务代码,如下:
if(flag)
count ++;
//if以外的逻辑
user.setAge(10);
......
但是熟悉规范的都知道,无论是阿里规范还是jdk的规范,都不推荐使用简化代码,这是为什么呢?这让我想起了2014年苹果的ios系统爆出来的一个严重安全漏洞(“GoTo Fail 漏洞”),而这个漏洞就和大括号有关系,而对应漏洞的代码大概可以理解为这样:
if ((error = doSomething()) != 0)
goto fail;
//无论如何都是走到这里,下面再也触发不了了
goto fail;
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
是不是看出来什么了?没错,如果前面的条件生效,就会跳转到fail的操作,返回error,但是如果不满足也会跳转到fail,那么也就是说后续的业务代码无论如何也触发不了了,其实了解这个问题的人其实大概可以猜出来,这里就是多写了一个goto fail;导致编译器认为了别的业务代码,但是假设我们加了大括号,这个问题就会迎刃而解,例如:
if ((error = doSomething()) != 0)
{
goto fail;
//无论如何都是走到这里,下面再也触发不了了
goto fail;
}
if ((error= doMore()) != 0)
goto fail;
fail:
return error;
其实这个时候就会发现即使是多写了一行代码,也不会影响整个业务的逻辑,减少了bug产生。看到这里我们似乎明白了,为什么各大规范都建议不省略大括号的写法了。
包装类与基本类型
做Java开发的都知道,Java中默认有八种基本类型,但是同样的也有八种对应的包装类型,很多时候企业开发和使用的时候对于包装类型和基本类型的使用并不规范,往往会导致一部分小的隐患的发生。前面我们有介绍建议在类属性申明的时候使用包装类型,而在方法内建议使用基本类型,这里我们可以再去思考两个开发的时候常用的使用场景:
1.判断两个数值类型的值是否相等
2.创建数值类型
看过阿里手册和JDK规范的应该知道,里面都有一条规范,明确指出基本数值类型的包装类型在比较的时候不允许使用==的方式,而是使用equals,这是为什么呢?我们来看看一个例子:
Integer a = 100, b = 100, c = 150, d = 150;
System.out.println(a == b);//true
System.out.println(c == d);//false
可以看到两个Integer类型的变量,值一样的情况下,==比较的结果居然是false?我们通过断点的方式知道 Integer var = ? 形式声明变量,会通过 java.lang.Integer#valueOf(int) 来构造 Inte ger 对象,我们来看看valueOf方法的源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到,会去判断value的值是否在IntegerCache的范围内,如果在,就会使用IntegerCache中缓存的实例,不存在才会创建新的Integer实例,这个缓存的值,默认是-128到127之间,并且是可以通过配置环境变量的方式动态改变的,这点可以从IntegerCache源码中看到:
privatestaticclassIntegerCache{
staticfinalint low = -128;
staticfinalint high;
staticfinalInteger cache[];
static{
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
// 省略其它代码
}
// 省略其它代码
}
从这我们也可以看出问题2的答案,为什么很多规范都推荐构建实例的时候是Integer a = 5;的形式,而不是new Integer(5);的原因,可以减少实例的创建,复用缓存对象。接着我们再来看第一个问题,==比较和equals比较的区别在哪?我们知道==比较的是两个实例对象的内存地址,而equals则是比较的具体的实现,而基本类型的包装类实现实例如果不在缓存范围内,肯定不是同一个对象,逻辑上内存地址肯定是不一样的,所以==在超过缓存范围后,比较的结果并不准确,那么我们该如何比较呢?事实上,基本类型的包装类中都有获取具体value的方法,例如Integer中就有intValue的方法,获取具体的值,类型为基本类型,这样我们再去==比较就可以了,那么equlas方法为什么可以比较呢?我们就拿Long类型的equals方法的源码来看一下具体实现:
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
可以看到这类包装类型的比较其实也就是我们上述说的获取具体value值以后再去==比较的操作。
空指针
空指针基本是每个Java开发人员最恶心的异常也是见过最多的异常之一,可能出现在各种业务代码和场景中,在阿里规范手册中,有很多针对空指针的规范和处理,如下:
【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。反例:public int f () { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
数据库的查询结果可能为 null。
集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
级联调用 obj.getA ().getB ().getC (); 一连串调用,易产生 NPE。
可见空指针出现的场景可能会有很多,而在开发中一些必要的检查,减少空指针是每个程序员都应该有的素质,但是有些不规范的操作或者疏忽可能会导致空指针的诞生,例如:
1.服务交互信息不规范的坑:一个经典的接口服务交互的场景下,往往有时候会将服务中的异常进行try-catch处理,返回的是一个固定的result封装实例,这种情况下,如果内部属性设置不规范很容易在调用方使用返回实例进行操作的时候因为疏忽导致空指针异常。
2.返回实例的坑:还有一些服务的代码编写过程中,部分开发人员有自己的个性写法,例如数据库查询某个数据的时候,如果查询不出结果集,并不是返回null,而是创建一个空的实例,进行返回,这一下可好,调用方无论怎么校验空指针都会在使用getxxx方法获取到的属性进行操作的时候报空指针异常,除非调用方将内部所有的get返回的结果都去进行一次空指针判断,或者根据某几个唯一属性确认实例是否为空等,但无论如何操作,都无法避免可能存在的大量的空指针。
3.自动拆箱装箱的坑:在企业开发的过程中,往往存在大量的实例转换操作,这个时候我们往往是通过工具类进行转换,但是有时候我们的实例是存在于两个工程内的,往往有时候因为是两个人定义的,同样名称的类变量,但是类型一个是基础类型,一个是包装类型,这个时候往往我们下意识会觉得java会自动拆箱装箱,所以没关系的,肯定会转换过去的,再或者基本类型有默认值的,肯定不会出现空指针,想法很美好,但是事实真的如此吗?我们看一个例子:
@Data
public class GoodCreateDTO {
private String title;
private Long price;
private Long count;
}
@Data
public class GoodCreateParam implements Serializable {
private static final long serialVersionUID = -560222124628416274L;
private String title;
private long price;
private long count;
}
这个时候我们潜意识中会认为外部接口的变量都是包装类型或者引用类型,所以我们在实现了类似如下的转换代码的时候就容易出现空指针操作:
public class GoodCreateConverter {
public static GoodCreateParam convertToParam(GoodCreateDTO goodCreateDTO) {
if (goodCreateDTO == null) {
return null;
}
GoodCreateParam goodCreateParam = new GoodCreateParam();
//赋值操作
goodCreateParam.setTitle(goodCreateDTO.getTitle());
goodCreateParam.setPrice(goodCreateDTO.getPrice());
goodCreateParam.setCount(goodCreateDTO.getCount());
return goodCreateParam;
}
}
但是如果在传递来的实例中,count不是必传参数,可能存在null的时候,这个时候我们使用getCount操作,由于获取的类型是包装类型,而我们需要赋值的是基本类型,这个时候就会触发自动拆箱装箱,null的拆箱就会报空指针异常!
往期精选
CHOICENESS
是兄弟,就来“kan”我