概述
建议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
最后
以上就是冷傲大炮为你收集整理的01-第一章 Java开发中通用的方法和准则的全部内容,希望文章能够帮你解决01-第一章 Java开发中通用的方法和准则所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复