java编译器的懒人原则_01-第一章 Java开发中通用的方法和准则

建议1:不用在常量和变量中出现易混淆的字母

包括名全小写,类名首字母全大写,常量全部大写并用下划线分割,变量采用驼峰命名法(Camel Case)命名等。

例如:

package com.company;

/**

* 数字后跟小写字母l的问题

*/

public class Client {

public static void main(String[] args) {

long i = 1l;

System.out.println("i的两倍是:" + (i+i));

}

}

句中定义一个长整型变量1,但后面的字母‘l’标识符在很多字体中都非常类似数字‘1’,所以很容易误以为变量i的值为十一。

因此,如果字母和数字必须混合使用,字母‘l’务必大写,字母‘o’则增加注释。

建议2:莫让常量蜕变成变量

package com.company;

import java.util.Random;

/**

* 莫让常量变成变量

*/

public class Client {

public static void main(String[] args) {

System.out.println("常量会变哦:" + Const.RAND_CONST);

}

}

/*接口常量*/

interface Const{

//这还是常量吗?

public static final int RAND_CONST = new Random().nextInt();

}

语句中虽然想要定义一个常量,但却赋值了一个不确定的值,这样使得程序可读性非常差。

常量就是常量,在编译期必须确定。

建议3:三元操作符的类型务必一致

package com.company;

/**

* 三元操作符两个操作数的类型必须一致

*/

public class Client {

public static void main(String[] args) {

int i = 80;

String s = String.valueOf(i<100?90:100);

String s1 = String.valueOf(i<100?90:100.0);

System.out.println("两者是否相等:"+s.equals(s1));

}

}

运行结果:两者是否相等:false

分析:

三元操作符必须要返回一个数据,而且类型确定,不可能条件为真时返回int类型,条件为假时返回float类型,编译器是不允许如此的,所以它会进行类型转换。

三元操作符类型转换规则:

a、如果两个操作数不可转换,则不做转换,返回值为Object类型。

b、若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。

c、若两个操作数中有一个数字S,另一个是表达式,且其类型标识为T,那么,若数字S在T的范围内,则转换为T类型;若S超出T类型的范围,则T转换为S类型。

d、若两个操作数都是直接量数字(Literal),则返回值类型为范围较大者。

建议4:避免带有变长参数的方法重载

为了提高方法的灵活度和可复用性,我们经常要传递不确定数量的参数到方法中,在Java5之前常用的设计技巧就是把形参定义成Collection类型或其子类类型,或者是数组类型,这种方法的缺点就是需要对空参数进行判断和筛选,比如引入实参为null值和长度为0的Collection或数组。而Java5引入变长参数(varags)就是为了更好地提高方法复用性,让方法调用者可以“随心所欲”地传递是参数量,当然变长参数也是要遵循一定规则的,比如变长参数必须是方法中的最后一个参数;一个方法不能定义多个变长参数等,这些规则要牢记,但是即使记住规则,往往还是会犯错。

package com.company;

import java.text.NumberFormat;

/**

* 建议4:避免带变长参数的方法的重载

*/

public class Client {

//简单折扣计算

public void calPrice(int price,int discount){

float knockdownPrice =price * discount / 100.0F;

System.out.println("简单折扣后的价格是:"+formateCurrency(knockdownPrice));

}

//复杂多折扣计算

public void calPrice(int price,int... discounts){

float knockdownPrice = price;

for(int discount:discounts){

knockdownPrice = knockdownPrice * discount / 100;

}

System.out.println("复杂折扣后的价格是:" +formateCurrency(knockdownPrice));

}

//格式化成本地货币形式

private String formateCurrency(float price){

return NumberFormat.getCurrencyInstance().format(price/100);

}

public static void main(String[] args) {

Client client = new Client();

//499元的货物,打75折

client.calPrice(49900, 75);

}

}

上面程序中存在两个重载的方法,程序执行时选择了第一个。

编译器在选择方法的时候会根据方法签名(Method Signature)来确定调用哪个方法。然后根据实参的数量和类型确定调用哪个方法。编译器之所以选择两个int型的实参而不是一个int型一个int数组的方法,是因为int是一个原生数据类型,而且数组本身是一个对象,编译器想要偷懒,所以会选择简单的,只要符合编译条件就通过。

变长参数的方法可以使用,但要尽量避免重载,否则也会使程序的可读性降低。

建议5:别让null值和空值威胁到变长方法

package com.company.section1;

/**

* 带有变长参数的方法重载,在调用时失败。

*

*/

public class Client {

public void methodA(String str,Integer... is){

System.out.println("Integer");

}

public void methodA(String str,String... strs){

System.out.println("String");

}

public static void main(String[] args) {

Client client = new Client();

client.methodA("China", 0);

client.methodA("China", "People");

client.methodA("China");

client.methodA("China",null);

}

}

程序中client.methodA("China");和client.methodA("China",null);两处编译不通过,提示相同:方法模糊不清,编译器不知道调用哪一个方法。

该Client类违反了KISS原则(Keep it Simple, Stupid, 即懒人原则),按照此规则设计的方法应该很容易调用。

对于client.methodA("China",null);方法,直接量null是没有类型的,虽然两个方法都符合调用请求,但不知道调用哪一个,于是报错了。另外调用者最好不该隐藏实参类型,这样的话不仅仅需要调用者猜测该调用哪个方法,而且被调用者也产生内部逻辑混乱。应该修改如下:

package com.company.section2;

/**

* 带有变长参数的方法重载,在调用时失败。

*

*/

public class Client {

public void methodA(String str,Integer... is){

System.out.println("Integer");

}

public void methodA(String str,String... strs){

System.out.println("String");

}

public static void main(String[] args) {

Client client = new Client();

String[] strs = null;

client.methodA("China",strs);

}

}

建议6:重写变长方法也循规蹈矩

重写必须满足的条件:

1、重写方法不能缩小访问权限。

2、参数列表必须与被重写方法相同。

3、返回类型必须与被重写方法的相同或是其子类。

4、重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常。

参数列表相同指:参数数量相同、类型相同、顺序相同

package com.company;

/**

* 覆写变长方法也循规蹈矩

*/

public class Client {

public static void main(String[] args) {

//向上转型

Base  base = new Sub();

base.fun(100, 50);

//不转型

Sub sub = new Sub();

//sub.fun(100, 50);

}

}

//基类

class Base{

void fun(int price,int... discounts){

System.out.println("Base……fun");

}

}

//子类,覆写父类方法

class Sub extends Base{

@Override

void fun(int price,int[] discounts){

System.out.println("Sub……fun");

}

}

程序中子类调用方法的地方会编译错误,因为int类型数组也是一种对象,编译器并不会把int类型转换为int类型数组。由于父类的方法是变长参数,所以会自动转换为int类型数组。

建议7:警惕自增的陷阱

package com.company;

/**

* 警惕自增的陷阱

*

*/

public class Client {

public static void main(String[] args) {

int count =0;

for(int i=0;i<10;i++){

count=count++;

}

System.out.println("count="+count);

}

}

class Mock{

public static void main(String[] args) {

int count =0;

for(int i=0;i<10;i++){

count=mockAdd(count);

}

System.out.println("count="+count);

}

public static int mockAdd(int count){

//先保存初始值

int temp =count;

//做自增操作

count = count+1;

//返回原始值

return temp;

}

}

Client的main函数中count的值依然是0。

count++是一个表达式,返回值是count自加前的值。即count=count++;就相当于count=mockAdd(count);

若要修改这种问题只需把count=count++改为count++

这种情况PHP和Java的处理方式相同,但是C++中count=count++和count++是相同的。

建议8:不要让就语法困扰你

package com.company;

/**

* 不用让旧语法困扰你

*

*/

public class Client {

public static void main(String[] args) {

//数据定义及初始化

int fee=200;

//其他业务处理

saveDefault:save(fee);

//其他业务处理

}

static void saveDefault(){

}

static void save(int fee){

}

}

语句saveDefault:save(fee);使用的语法是C语言中用到的标号,用于goto语句。

虽然Java抛弃了goto语法,但还是保留了该关键字,只是不进行语义处理而已,与此类似的还有const关键字。

Java虽然没有goto,但是扩展了break和continue关键字,它们的后面都可以加上标号做跳转,完全实现了goto功能,但同时也把goto的诟病带了进来。在阅读大牛的开源程序时,根本就看不到break或continue后跟标号的情况,甚至break和continue都很少看到,这是提高代码可读性很好的一个方法,所以要尽量摒弃旧语法。

建议9:少用静态导入

从Java5开始引入了静态导入语法(import static),其目的是为了减少字符输入量,提高代码的可阅读性。

但是滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是"一些事物的描述",缺少了类名的修饰,静态属性和静态方法的表象意义就可以被无限放大,这会让阅读者很难弄清楚其属性或方法代表何意,甚至是哪个类的属性(方法)都有思考一番。例如:

package com.company.section3;

import java.text.NumberFormat;

import static java.lang.Double.*;

import static java.lang.Math.*;

import static java.lang.Integer.*;

import static java.text.NumberFormat.*;

public class Client {

//输入半径和精度要求,计算面积

public static void main(String[] args) {

double s = PI * parseDouble(args[0]);

NumberFormat nf = getInstance();

nf.setMaximumFractionDigits(parseInt(args[1]));

formatMessage(nf.format(s));

}

//格式化消息输出

public static void formatMessage(String s){

System.out.println("圆面积是:"+s);

}

}

程序中NumberFormat nf = getInstance();一句中的getInstance()让人摸不着头脑,不能直接鲜明的看到这个方法是哪个类的。

所以对于静态导入,一定要遵循两个原则:

》不使用*(星号通配符,除非是导入静态常量类(只包含常量的类或接口))。

》方法名是具有明确、清晰表象意义的工具类。

建议10:不要在本类中覆盖静态导入的变量和方法

如果在本类中覆盖了静态导入的变量和方法,那么在调用的时候会调用本类中的变量和方法,这符合编译器的“最短路径”原则。

“最短路径”原则:如果能够在本类中查找到变量、常量、方法,就不会到其他包或父类、接口中查找,以确保本类中的属性、方法优先。

因此,如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖。

建议11:养成良好习惯,显示声明UID

首先介绍一下序列化和反序列化:

类实现Serializable接口的目的是为了可持久化,比如网络传输和本地存储,为系统在分布和异构部署提供先决条件。

在序列化和反序列化的过程中,如果两边类版本不一致(例如增加了个属性)。反序列化时就会报一个InvalidClassException异常。

那么如何解决这种版本不一致的问题呢?

SerialVersionUID,也叫作流标识符(Stream Unique Identifier),即类的版本定义,它可以显示声明,也可以隐式声明。显示声明格式如下:

private static final long serialVersionUID = XXXXXL;

隐式声明由编译器自动通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等组多因子计算得出的。(所以属性改动了,版本就不一致了)。

但如果显示声明了serialVersionUID,JVM在反序列化时会根据serialVersionUID判断版本,如果相同,则认为类没有发生改变,可以把数据流load为实例对象,如果不同,这会抛出InvalidClassException异常。

如果显示声明了标识,但是两个类却不同(例如增加了属性),则在反序列化中不会报错,这提高了代码的健壮性,但这种情况带来的后果是反序列时无法反序列出现在的属性,从而引起两边数据不一致。

所以显示声明serialVersionUID可以避免对象不一致,但尽量不要以这种方式向JVM”撒谎“。

建议12:避免用序列化类在构造函数为不变量赋值

即final修饰的变量。

因为反序列化时构造函数不会执行,如果在在构造函数中为不变量赋值,反序列化时不会执行构造函数,因此构造函数对该变量做的操作就得不到,所以反序列化后该变量依然是老版本的值。

建议13:避免为final变量复杂赋值

建议12中说的赋值中的值是指的简单对象。简单对象包括8个基本类型,以及数组、字符串(字符串情况很复杂,不通过new关键字生成String对象的情况下,final变量的赋值与基本类型相同),但是不能方法赋值。

其中原理是这样的,序列化时保存到磁盘上(或网络传输)的对象文件包括两部分:

(1)类描述信息

包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。之所以类描述会被保存,很简单,是因为能去也能回来,这保证发序列化的健壮运行。

(2)非瞬态(transient关键字)和非静态(static关键字)的实例变量值

当值为基本类型时,就被直接保存下来,如果是复杂对象,则该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则出现序列化异常),也就是说递归后还是基本数据类的保存。

正是因为这两点,一个持久化后的对象文件会比一个class文件大很多

总结一下,反序列化时final变量在一下情况下不会被重新赋值:

》通过构造函数为final变量赋值。

》通过方法返回值为final变量赋值。

》final修饰的属性不是基本类型。

建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题

序列化过程中除了给不需要持久化的属性上加瞬态关键字(transient关键字)之外,还有另一个方法。

实现了Serializable接口的类可以实现两个私有方法:writeObject和readObject,在方法的实现中只处理需要处理的部分属性即可。

建议15:break万万不可忘

在写switch语句时,每个case后必须带有break。

为了防止这种情况,可以在IDE中设置警告级别:

Performaces->Java->Compiler->Errors/Warnings->Potential Programming probems,然后修改“switch

”case fall-through为Errors级别。

建议16:易变业务使用脚本语言编写

脚本语言的特性有灵活、便捷、简单。(如PHP、Ruby、Groovy、JavaScript等),而且是在运行期解释执行。

这正是Java所缺少的。

于是Java6开始正是支持脚本语言,但是脚本语言较多。于是JCP(Java Community Process)提出了JSR规范,只要符合该规范的语言都可以在Java平台上运行(它对JavaScript是默认支持的)。

所以也可以自己写个脚本语言,然后再实现ScriptEngine,即可在Java平台上运行。

建议17:慎用动态编译

从Java6开始支持动态编译,可以在运行期直接编译.java文件,执行.class,并且能够获得相关的输入输出,甚至还能监听相关的事件。

Java的动态编译对源提供了多个渠道。比如可以是字符串,可以是文本,也可以是编译过的字节码文件,甚至可以是存放在数据库中的明文代码或是字节码。总之,只要是符合Java规范的就都可以在运行期动态加载,其实现方式就是实现JavaFileObject接口,重写getCharContent、openInputStream、openOutputStream,或者实现JDK已经提供的两个SimpleJavaFileObject、ForwardingJavaFileObject。

因为静态编译基本已经可以满足我们绝大多是需求,所以动态编译用的很少。即使真的需要,也有很好的代替方案,比兔Ruby、Groovy等无缝的脚本语言。

使用动态编译时需要注意一下几点:

(1)在框架中谨慎使用

比如在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action,能做到,但是debug很困难;在比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。

(2)不用在要求高性能的项目中使用

动态编译必究需要一个编译的过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具写一个插件,就可以很好的使用动态编译,不用重启即可实现运行、调试功能,非常方便。

(3)动态编译要考虑安全问题

如果你在web页面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说;“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只有上传一个而已Java程序就可以让你所有的安全工作毁于一旦。

(4)记录动态编译过程

建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行时很不让人放心的,留下这些依据可以更好的优化程序。

建议18:避免instanceof非预期结果

instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的。只有操作符两边的类有继承或者实现关系就可以编译通过。

instanceof只能用于对象的判断,不能用于基本类型的判断。

若有null则返回false。

建议19:断言绝对不是鸡肋

断言在很多语言中都存在,在防御式编程中经常会用断言(Assertion)对参数和环境做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常。断言的基本语法:

assert

assert :

在布尔表达式为假时,抛出AssetionError错误,并附带了错误信息。assert的语法简单,有一些两个特性

(1)assert默认不启用(要启用就需要在编译、运行时附加上相关的关键字)

(2)assert抛出异常AssertionError是继承自Error的

断言在两种情况下不可使用:

(1)在对外公开的方法中

(2)在执行逻辑代码的情况下

一般在以下情况下使用:

(1)在私有方法中设置assert作为输入参数的校验

(2)流程控制中不可能达到的区域

(3)建立程序探针

建议20:不要只替换一个类

我们经常在系统中定义一个常量接口(或常量类),已囊括系统中所涉及的常量,从而简化代码,方便开发,在很多的开源项目中已采取了类似方式。

但在原始时代(非IDE编码)情况下,若改动了常量类中的常量值,则另一个引用该值的类若不重新编译,则还是记录的常量类中原来的常量值(因为final修饰的j常量,编译器会任务它是稳定态的,所以在编译时直接把值编译到字节码中,避免了在运行期的引用,所以若改变了常量类中的final常量值,除了重新编译该常量类之外还要重新编译引用类)。

当然IDE编码时会自动处理这种情况。

发布应用程序系统是禁止使用类文件替换方式,整体war包发布才是万全之策。

欢迎关注公众号:零点小时光

lingdianxiaoshiguang

506356.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值