十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java——开发中通用的方法和准则(一)

Java——开发中通用的方法和准则(一) 开发中通用的方法和准则1、不要在常量和变量中出现易混淆的字母2、莫让常量蜕变成变量3、三元操作符的类型务必一致4、避免带有变长参数的方法重载5、别让null值和空值威胁到变长方法6、覆写变长方法也循规蹈矩7、警惕自增的陷阱8、不要让旧语法困扰你9、少用静态导入10、不要在本类中覆盖静态导入的变量和方法1、不要在常量和变量中出现易混淆的字母包名全小写类名首字母全大写常量全部大写并用下划线分隔变量采用驼峰命名法Camel Case命名等这些都是最基本的Java编码规范是每个Javaer都应熟知的规则但是在变量的声明中要注意不要引入容易混淆的字母。尝试阅读如下代码思考一下打印出的i等于多少publicclassClient{publicstaticvoidmain(String[]args){longi1l;System.out.println(i的两倍是(ii));}}//2肯定有人会说这么简单的例子还能出错运行结果肯定是22实践是检验真理的唯一标准将其拷贝到Eclipse中然后Run一下看看或许你会很奇怪结果是2而不是22难道是Eclipse的显示有问题少了个“2”​因为赋给变量i的数字就是“1”​只是后面加了长整型变量的标示字母“l”而已。别说是我挖坑让你跳如果有类似程序出现在项目中当你试图通过阅读代码来理解作者的思想时此情此景就有可能会出现。所以为了让您的程序更容易理解字母“l”​还包括大写字母“O”​尽量不要和数字混用以免使阅读者的理解与程序意图产生偏差。如果字母和数字必须混合使用字母“l”务必大写字母“O”则增加注释。注意字母“l”作为长整型标志时务必大写。2、莫让常量蜕变成变量常量蜕变成变量你胡扯吧加了final和static的常量怎么可能会变呢不可能二次赋值的呀。真的不可能吗看我们神奇的魔术代码如下publicclassClient{publicstaticvoidmain(String[]args){System.out.println(常量会变哦Const.RAND_CONST);}}/*接口常量*/interfaceConst{//这还是常量吗publicstaticfinalintRAND_CONSTnewRandom().nextInt();}RAND_CONST是常量吗它的值会变吗绝对会变这种常量的定义方式是极不可取的常量就是常量在编译期就必须确定其值不应该在运行期更改否则程序的可读性会非常差甚至连作者自己都不能确定在运行期发生了何种神奇的事情。RAND_CONST是常量吗它的值会变吗绝对会变这种常量的定义方式是极不可取的常量就是常量在编译期就必须确定其值不应该在运行期更改否则程序的可读性会非常差甚至连作者自己都不能确定在运行期发生了何种神奇的事情。注意务必让常量的值在运行期保持不变。3、三元操作符的类型务必一致三元操作符是if-else的简化写法在项目中使用它的地方很多也非常好用但是好用又简单的东西并不表示就可以随便用我们来看看下面这段代码publicclassClient{publicstaticvoidmain(String[]args){inti80;StringsString.valueOf(i100?90:100);//90Strings1String.valueOf(i100?90:100.0);//90.0System.out.println(两者是否相等:s.equals(s1));}}//false分析一下这段程序i是80那它当然小于100两者的返回值肯定都是90再转成String类型其值也绝对相等毋庸置疑的。恩分析得有点道理但是变量s中三元操作符的第二个操作数是100而s1的第二个操作数是100.0难道没有影响吗不可能有影响吧三元操作符的条件都为真了只返回第一个值嘛与第二个值有一毛钱的关系吗貌似有道理。果真如此吗我们通过结果来验证一下运行结果是​“两者是否相等false”​什么不相等Why问题就出在了100和100.0这两个数字上在变量s中三元操作符中的第一个操作数90和第二个操作数100都是int类型类型相同返回的结果也就是int类型的90而变量s1的情况就有点不同了第一个操作数是90int类型​第二个操作数却是100.0而这是个浮点数也就是说两个操作数的类型不一致可三元操作符必须要返回一个数据而且类型要确定不可能条件为真时返回int类型条件为假时返回float类型编译器是不允许如此的所以它就会进行类型转换了int型转换为浮点数90.0也就是说三元操作符的返回值是浮点数90.0那这当然与整型的90不相等了。这里可能有读者疑惑了为什么是整型转为浮点而不是浮点转为整型呢这就涉及三元操作符类型的转换规则若两个操作数不可转换则不做转换返回值为Object类型。若两个操作数是明确类型的表达式比如变量​则按照正常的二进制数字来转换int类型转换为long类型long类型转换为float类型等。若两个操作数中有一个是数字S另外一个是表达式且其类型标示为T那么若数字S在T的范围内则转换为T类型若S超出了T类型的范围则T转换为S类型可以参考“建议22”​会对该问题进行展开描述​。若两个操作数都是直接量数字Literal[插图]​则返回值类型为范围较大者。知道是什么原因了相应的解决办法也就有了保证三元操作符中的两个操作数类型一致即可减少可能错误的发生。4、避免带有变长参数的方法重载在项目和系统的开发中为了提高方法的灵活度和可复用性我们经常要传递不确定数量的参数到方法中在Java 5之前常用的设计技巧就是把形参定义成Collection类型或其子类类型或者是数组类型这种方法的缺点就是需要对空参数进行判断和筛选比如实参为null值和长度为0的Collection或数组。而 Java 5引入变长参数varags就是为了更好地提高方法的复用性让方法的调用者可以“随心所欲”地传递实参数量当然变长参数也是要遵循一定规则的比如变长参数必须是方法中的最后一个参数一个方法不能定义多个变长参数等这些基本规则需要牢记但是即使记住了这些规则仍然有可能出现错误我们来看如下代码publicclassClient{//简单折扣计算publicvoidcalPrice(intprice,intdiscount){floatknockdownPriceprice discount/100.0F;System.out.println(简单折扣后的价格是formateCurrency(knockdownPrice));}//复杂多折扣计算publicvoidcalPrice(intprice,int...discounts){floatknockdownPriceprice;for(intdiscount:discounts){knockdownPriceknockdownPrice discount/100;}System.out.println(复杂折扣后的价格是formateCurrency(knockdownPrice));}//格式化成本的货币形式privateStringformateCurrency(floatprice){returnNumberFormat.getCurrencyInstance().format(price/100);}publicstaticvoidmain(String[]args){ClientclientnewClient();//499元的货物打75折client.calPrice(49900,75);}}//简单折扣后的价格是374.25这是一个计算商品价格折扣的模拟类带有两个参数的calPrice方法该方法的业务逻辑是提供商品的原价和折扣率即可获得商品的折扣价是一个简单的折扣计算方法该方法在实际项目中经常会用到这是单一的打折方法。而带有变长参数的calPrice方法则是较复杂的折扣计算方式多种折扣的叠加运算模拟类是一种比较简单的实现在实际生活中也是经常见到的比如在大甩卖期间对VIP会员再度进行打折或者当天是你的生日再给你打个9折也就是俗话说的“折上折”​。业务逻辑清楚了我们来仔细看看这两个方法它们是重载吗当然是了重载的定义是“方法名相同参数类型或数量不同”​很明显这两个方法是重载。但是再仔细瞧瞧这个重载有点特殊calPriceint price,int…discounts的参数范畴覆盖了calPriceint price,intdiscount的参数范畴。那问题就出来了对于calPrice49900,75这样的计算到底该调用哪个方法来处理呢我们知道Java编译器是很聪明的它在编译时会根据方法签名Method Signature来确定调用哪个方法比如calPrice499900,75,95这个调用很明显75和95会被转成一个包含两个元素的数组并传递到calPriceintprice,in… discounts中因为只有这一个方法签名符合该实参类型这很容易理解。但是我们现在面对的是calPrice49900,75调用这个“75”既可以被编译成int类型的“75”​也可以被编译成int数组“{75}”​即只包含一个元素的数组。那到底该调用哪一个方法呢我们先运行一下看看结果运行结果是简单折扣后的价格是374.25。看来是调用了第一个方法为什么会调用第一个方法而不是第二个变长参数方法呢因为Java在编译时首先会根据实参的数量和类型这里是2个实参都为int类型注意没有转成int数组来进行处理也就是查找到calPrice(int price,int discount)方法而且确认它是否符合方法签名条件。现在的问题是编译器为什么会首先根据2个int类型的实参而不是1个int类型、1个int数组类型的实参来查找方法呢这是个好问题也非常好回答因为int是一个原生数据类型而数组本身是一个对象编译器想要“偷懒”​于是它会从最简单的开始“猜想”​只要符合编译条件的即可通过于是就出现了此问题。问题是阐述清楚了为了让我们的程序能被“人类”看懂还是慎重考虑变长参数的方法重载吧否则让人伤脑筋不说说不定哪天就陷入这类小陷阱里了。5、别让null值和空值威胁到变长方法上一建议讲解了变长参数的重载问题本建议还会继续讨论变长参数的重载问题。上一建议的例子是变长参数的范围覆盖了非变长参数的范围这次我们从两个都是变长参数的方法说起代码如下publicclassClient{publicvoidmethodA(Stringstr,Integer...is){}publicvoidmethodA(Stringstr,String...strs){}publicstaticvoidmain(String[]args){ClientclientnewClient();client.methodA(China,0);client.methodA(China,People);client.methodA(China);client.methodA(China,null);}}两个methodA都进行了重载现在的问题是上面的代码编译通不过问题出在什么地方看似很简单哦。有两处编译通不过client.methodA“China”和client.methodA“China”,null​估计你已经猜到了两处的提示是相同的方法模糊不清编译器不知道调用哪一个方法但这两处代码反映的代码味道可是不同的。对于methodA“China”方法根据实参“China”​String类型​两个方法都符合形参格式编译器不知道该调用哪个方法于是报错。我们来思考这个问题Client类是一个复杂的商业逻辑提供了两个重载方法从其他模块调用系统内本地调用或系统外远程调用时调用者根据变长参数的规范调用传入变长参数的实参数量可以是N个N0​那当然可以写成client.methodA“china”方法啊完全符合规范但是这却让编译器和调用者都很郁闷程序符合规则却不能运行如此问题谁之责任呢是Client类的设计者他违反了KISS原则Keep It Simple, Stupid即懒人原则​按照此规则设计的方法应该很容易调用可是现在在遵循规范的情况下程序竟然出错了这对设计者和开发者而言都是应该严禁出现的。对于client.methodA“china”,null方法直接量null是没有类型的虽然两个methodA方法都符合调用请求但不知道调用哪一个于是报错了。我们来体会一下它的坏味道除了不符合上面的懒人原则外这里还有一个非常不好的编码习惯即调用者隐藏了实参类型这是非常危险的不仅仅调用者需要“猜测”该调用哪个方法而且被调用者也可能产生内部逻辑混乱的情况。对于本例来说应该做如下修改publicstaticvoidmain(String[]args){ClientclientnewClient();String[]strsnull;client.methodA(China,strs);}也就是说让编译器知道这个null值是String类型的编译即可顺利通过也就减少了错误的发生。6、覆写变长方法也循规蹈矩在Java中子类覆写父类中的方法很常见这样做既可以修正Bug也可以提供扩展的业务功能支持同时还符合开闭原则Open-Closed Principle​我们来看一下覆写必须满足的条件重写方法不能缩小访问权限。参数列表必须与被重写方法相同。返回类型必须与被重写方法的相同或是其子类。重写方法不能抛出新的异常或者超出父类范围的异常但是可以抛出更少、更有限的异常或者不抛出异常。估计你已经猜测出下面要讲的内容了为什么“参数列表必须与被重写方法的相同”采用不同的字体这其中是不是有什么玄机是的还真有那么一点点小玄机。参数列表相同包括三层意思参数数量相同、类型相同、顺序相同看上去好像没什么问题那我们来看一个例子业务场景与上一个建议相同商品打折代码如下publicclassClient{publicstaticvoidmain(String[]args){//向上转型BasebasenewSub();base.fun(100,50);//不转型SubsubnewSub();sub.fun(100,50);}}//基类classBase{voidfun(intprice,int...discounts){System.out.println(Base......fun);}}//子类覆写父类方法classSubextendsBase{Overridevoidfun(intprice,int[]discounts){System.out.println(Sub......fun);}}请问该程序有问题吗—编译通不过。那问题出在什么地方呢Override注解吗非也覆写是正确的因为父类的calPrice编译成字节码后的形参是一个int类型的形参加上一个int数组类型的形参子类的参数列表也与此相同那覆写是理所当然的了所以加上Override注解没有问题只是Eclipse会提示这不是一种很好的编码风格。难道是“sub.fun100, 50​”这条语句正解确实是这条语句报错提示找不到fun int,int方法。这太奇怪了子类继承了父类的所有属性和方法甭管是私有的还是公开的访问权限同样的参数、同样的方法名通过父类调用没有任何问题通过子类调用却编译通不过为啥难道是没继承下来或者子类缩小了父类方法的前置条件那如果是这样就不应该覆写Override就应该报错真是奇妙的事情事实上base对象是把子类对象Sub做了向上转型形参列表是由父类决定的由于是变长参数在编译时​“base.fun(100, 50)”中的“50”这个实参会被编译器“猜测”而编译成“{50}”数组再由子类Sub执行。我们再来看看直接调用子类的情况这时编译器并不会把“50”做类型转换因为数组本身也是一个对象编译器还没有聪明到要在两个没有继承关系的类之间做转换要知道Java是要求严格的类型匹配的类型不匹配编译器自然就会拒绝执行并给予错误提示。这是个特例覆写的方法参数列表竟然与父类不相同这违背了覆写的定义并且会引发莫名其妙的错误。所以读者在对变长参数进行覆写时如果要使用此类似的方法请找个小黑屋仔细想想是不是一定要如此。注意覆写的方法参数与父类相同不仅仅是类型、数量还包括显示形式。7、警惕自增的陷阱记得大学刚开始学C语言时老师就说自增有两种形式分别是i和ii表示的是先赋值后加1i是先加1后赋值这样理解了很多年也没出现问题直到遇到如下代码我才怀疑我的理解是不是错了publicclassClient{publicstaticvoidmain(String[]args){intcount0;for(inti0;i10;i){countcount;}System.out.println(countcount);//0}}这个程序输出的count等于几是count自加10次吗答案等于10可以非常肯定地告诉你答案错误运行结果是count等于0。为什么呢count是一个表达式是有返回值的它的返回值就是count自加前的值Java对自加是这样处理的首先把count的值注意是值不是引用拷贝到一个临时变量区然后对count变量加1最后返回临时变量区的值。程序第一次循环时的详细处理步骤如下步骤1 JVM把count值其值是0拷贝到临时变量区。步骤2 count值加1这时候count的值是1。步骤3 返回临时变量区的值注意这个值是0没修改过。步骤4 返回值赋值给count此时count值被重置成0。“countcount”这条语句可以按照如下代码来理解publicstaticintmockAdd(intcount){//先保存初始值inttempcount;//做自增操作countcount1;//返回原始值returntemp;}于是第一次循环后count的值还是0其他9次的循环也是一样的最终你会发现count的值始终没有改变仍然保持着最初的状态。此例中代码作者的本意是希望count自增所以想当然地认为赋值给自身就成了不曾想掉到Java自增的陷阱中了。解决方法很简单只要把“countcount”修改为“count”即可。该问题在不同的语言环境有不同的实现C中“countcount”与“count”是等效的而在PHP中则保持着与Java相同的处理方式。每种语言对自增的实现方式各不同读者有兴趣可以多找几种语言测试一下思考一下原理。下次如果看到某人T恤上印着“ii”​千万不要鄙视他记住能够以不同的语言解释清楚这句话的人绝对不简单应该表现出“如滔滔江水”般的敬仰心理默念着“高人绝世高人哪”​。8、不要让旧语法困扰你N多年前接手了一个除了源码以外什么都没有的项目没需求、没文档、没设计原创者也已鸟兽散了我们只能通过阅读源码来进行维护。期间同事看到一段很“奇妙”的代码让大家帮忙分析代码片段如下publicclassClient{publicstaticvoidmain(String[]args){//数据定义及初始化intfee200;//其他业务处理saveDefault:save(fee);//其他业务处理}staticvoidsaveDefault(){}staticvoidsave(intfee){}}该代码的业务含义是计算交易的手续费最低手续费是2元其业务逻辑大致看懂了但是此代码非常神奇​“saveDefault:savefee​”这句代码在此处出现后后续就再也没有与此有关的代码了这做何解释呢更神奇的是编译竟然还没有错运行也很正常。Java中竟然有冒号操作符一般情况下它除了在唯一一个三元操作符中存在外就没有其他地方可用了呀。当时连项目组里的高手也是一愣一愣的翻语法书也没有介绍冒号操作符的内容而且也不可能出现连括号都可以省掉的方法调用、方法级联啊这也太牛了吧隔壁做C项目的同事过来串门看我们在讨论这个问题很惊奇地说“耶Java中还有标号呀我以为Java这么高级的语言已经抛弃goto语句了……”​一语点醒梦中人项目的原创者是C语言转过来的开发人员所以他把C语言的goto习惯也带到项目中了后来由于经过N手交接重构了多次到我们这里goto语句已经被重构掉了但是跳转标号还保留着估计上一届的重构者也是稀里糊涂的不敢贸然修改所以把这个重任留给了我们。goto语句中有着“double face”作用的关键字它可以让程序从多层的循环中跳出不用一层一层地退出类似高楼着火了来不及一楼一楼的下goto语句就可以让你“biu”的一声从十层楼跳到地面上。这点确实很好但同时也带来了代码结构混乱的问题而且程序跳来跳去让人看着就头晕还怎么调试这样做甚至会隐祸连连比如标号前后对象构造或变量初始化一旦跳到这个标号程序就不可想象了所以Java中抛弃了goto语法但还是保留了该关键字只是不进行语义处理而已与此类似的还有const关键字。Java中虽然没有了goto关键字但是扩展了break和continue关键字它们的后面都可以加上标号做跳转完全实现了goto功能同时也把goto的诟病带了进来所以我们在阅读大牛的开源程序时根本就看不到break或continue后跟标号的情况甚至是break和continue都很少看到这是提高代码可读性的一剂良药旧语法就让它随风而去吧9、少用静态导入从Java 5开始引入了静态导入语法import static​其目是为了减少字符输入量提高代码的可阅读性以便更好地理解程序。我们先来看一个不使用静态导入的例子也就是一般导入publicclassMathUtils{//计算圆面积publicstaticdoublecalCircleArea(doubler){returnMath.PI r r;}//计算球面积publicstaticdoublecalBallArea(doubler){return4Math.PI r r;}}这是很简单的数学工具类我们在这两个计算面积的方法中都引入了java.lang.Math类该类是默认导入的中的PI圆周率常量而Math这个类写在这里有点多余特别是如果MathUtils中的方法比较多时如果每次都要敲入Math这个类繁琐且多余静态导入可解决此类问题使用静态导入后的程序如下importstaticjava.lang.Math.PI;publicclassMathUtils{//计算圆面积publicstaticdoublecalCircleArea(doubler){returnPI r r;}//计算球面积publicstaticdoublecalBallArea(doubler){return4PI r r;}}静态导入的作用是把Math类中的PI常量引入到本类中这会使程序更简单更容易阅读只要看到PI就知道这是圆周率不用每次都要把类名写全了。但是滥用静态导入会使程序更难阅读更难维护。静态导入后代码中就不用再写类名了但是我们知道类是“一类事物的描述”​缺少了类名的修饰静态属性和静态方法的表象意义可以被无限放大这会让阅读者很难弄清楚其属性或方法代表何意甚至是哪一个类的属性方法都要思考一番当然IDE友好提示功能是另说​特别是在一个类中有多个静态导入语句时若还使用了*星号通配符把一个类的所有静态元素都导入进来了那简直就是恶梦。我们来看一段例子importstaticjava.lang.Double.;importstaticjava.lang.Math.;importstaticjava.lang.Integer.;importstaticjava.text.NumberFormat.;publicclassClient{//输入半径和精度要求计算面积publicstaticvoidmain(String[]args){doublesPIparseDouble(args[0]);NumberFormatnfgetInstance();nf.setMaximumFractionDigits(parseInt(args[1]));formatMessage(nf.format(s));}//格式化消息输出publicstaticvoidformatMessage(Strings){System.out.println(圆面积是s);}}就这么一段程序看着就让人火大常量PI这知道是圆周率parseDouble方法可能是Double类的一个转换方法这看名称也能猜测到。那紧接着的getInstance方法是哪个类的是Client本地类不对呀没有这个方法哦原来是NumberFormate类的方法这和formateMessage本地方法没有任何区别了—这代码也太难阅读了非机器不可阅读。所以对于静态导入一定要遵循两个规则不使用*星号通配符除非是导入静态常量类只包含常量的类或接口​。方法名是具有明确、清晰表象意义的工具类。何为具有明确、清晰表象意义的工具类我们来看看JUnit 4中使用的静态导入的例子代码如下importstaticorg.junit.Assert.;publicclassDaoTest{TestpublicvoidtestInsert(){//断言assertEquals(foo,foo);assertFalse(Boolean.FALSE);}}我们从程序中很容易判断出assertEquals方法是用来断言两个值是否相等的assertFalse方法则是断言表达式为假如此确实减少了代码量而且代码的可读性也提高了这也是静态导入用到正确地方所带来的好处。10、不要在本类中覆盖静态导入的变量和方法如果一个类中的方法及属性与静态导入的方法及属性重名会出现什么问题呢我们先来看一个正常的静态导入代码如下importstaticjava.lang.Math.PI;importstaticjava.lang.Math.abs;publicclassClient{publicstaticvoidmain(String[]args){System.out.println(PIPI);System.out.println(abs(100)abs(-100));}}很简单的例子打印出静态常量PI值计算-100的绝对值。现在的问题是如果我们在Client类中也定义了PI常量和abs方法会出现什么问题代码如下importstaticjava.lang.Math.PI;importstaticjava.lang.Math.abs;publicclassClient{//常量名与静态导入的PI相同publicfinalstaticStringPI祖冲之;//方法名与静态导入的相同publicstaticintabs(intabs){return0;}publicstaticvoidmain(String[]args){System.out.println(PIPI);System.out.println(abs(100)abs(-100));}}以上代码中定义了一个PI字符串类型的常量又定义了一个abs方法与静态导入的相同。首先说好消息编译器没有报错接下来是不好的消息了我们不知道哪个属性和哪个方法被调用了因为常量名和方法名相同到底调用了哪一个方法呢我们运行一下看看结果PI祖冲之abs(100)0很明显是本地的属性和方法被引用了为什么不是Math类中的属性和方法呢那是因为编译器有一个“最短路径”原则如果能够在本类中查找到的变量、常量、方法就不会到其他包或父类、接口中查找以确保本类中的属性、方法优先。因此如果要变更一个被静态导入的方法最好的办法是在原始类中重构而不是在本类中覆盖。
返回列表