《编写高质量代码:改善Java程序的151个建议》读书笔记一:java开发中通用的方法和准则

一、不要在常量和变量中出现易混淆的字母

public class Client {
	public static void main(String[] args) {
		long i = 1l;
		System.out.println(i+i);
	}
}
输出结果:2

注意:字母l和o尽量不要跟数字混用,如果需要时,l写成大写L,o要做下注释

二、莫让常量蜕变成变量

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();
}
输出结果:每次的值都会随机生成

注意:常量就是常量,在编译器就要确定他的唯一值,不要让他在运行期进行改变,否则程序的可读性会非常差。

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

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(s的值是90,而s1的值是90.0,这是因为三元操作符的返回值类型要确定,而出现两个操作数的类型不一致时,会发生类型的转换,所以90就会变成90.0)

注意:

三元操作符的转换规则:

1、若两个操作符不可转换时,则不做转换,返回类型是Object

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

3、若两个操作符都是直接的数字,则小范围的转换为大范围的

4、若两个操作符一个是表达式,一个是直接的数字,则如果数字在表达式类型的范围内,则数字转换为表达式的类型,如果数字大过表达式的类型,则表达式的类型转为数字的类型

所以要尽量保证三元操作符的两个操作数的类型一致。


四、避免带变长参数的方法的重载

import java.text.NumberFormat;

public class Client {
	
	//简单折扣计算
	public void calPrice(int price,int discount){
		float knockdownPrice = (price / 100F) * (discount / 100.0F);
		System.out.println("简单折扣后的价格是:"+formateCurrency(knockdownPrice));
	}
	
	//复杂多折扣计算
	public void calPrice(int price,int... discounts){
		float knockdownPrice = (price / 100F);
		for(int discount:discounts){
			knockdownPrice = knockdownPrice * discount / 100F;
		}
		System.out.println("复杂折扣后的价格是:" +formateCurrency(knockdownPrice));
	}
	
	//格式化成本地货币形式
	private String formateCurrency(float price){
		return NumberFormat.getCurrencyInstance().format(price);
	}
	
	public static void main(String[] args) {
		Client client = new Client();
		//499元的货物,打75折
		client.calPrice(49900, 75);
	}
}

输出结果:简单折扣后的价格是:¥374.25

注意:java引入变长参数是为了更好的提供方法的复用性,可以有0个或多个参数,然而要遵守一定得规则:1、变长参数必须是方法的最后一个参数,2、一个方法中只能有一个变长参数。然而有时候也会出现一些混淆的情况,比如上面的方法,如果我们是client.calPrice(49900, 75,95);调用,因为只有第2 个方法满足,所以只会调用第2个方法,而client.calPrice(49900, 75);调用则两个方法都满足,但是编辑器比较懒,发现第一种方法的参数最简单,又 满足条件,所以就选择了第一种方法来调用,但我们看代码时有时就会困惑,所以为避免这种情况,我们应该避免带变长参数的方法的重载。

五、别让null值和空值威胁到变长方法

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);//编译不通过
	}
}
注意:对于methodA("China"),两个方法都满足,编译器根据懒人原则又判断不出哪个比较简单,所以不知该调用哪个方法,就会出错,而methodA("China",null),由于null是没有类型的,所以编译器也判断不出该调用哪个,改正方法时先给出明确的类型,比如String str = null; client.methodA("China",str);或String[] str = null;

六、覆写变长方法也循规蹈矩

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");
	}
}
结果:上面的情况编译器编译不过

注意:第一种情况是因为基类对象把子类对象做了向上转型,形参列表由父类来决定,编译时base.fun(100, 50);会被编译器当成sub.fun(100, new int[]{50});来执行,所以编译通过,所以只要把sub.fun(100, 50);改为sub.fun(100, new int[]{50});就也可以编译通过。

七、警惕自增的陷阱

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);
	}
}
结果:count=0

注意:count++是有返回值的,它的返回值就是count自加前的值,java对自加是这样处理的:首先把count的值(是值不是引用)拷贝到临时变量区,然后对count变量加1,最后返回临时变量区的值。上面程序的第一次循环时处理步骤:1、将count=0拷贝到临时变量去,然后count变量加1变为1;2、临时临时变量区的值0返回赋值给了count变量,所以count变量又变为了0;每次的循环都一样,所以最后count还是为0;上面程序改正确的方法为将count=count++;改为count++;

八、不用让旧语法困扰你

public class Client {

	public static void main(String[] args) {
		//数据定义及初始化
		int fee=200;		
		//其他业务处理
		saveDefault:save(fee);
		//其他业务处理
	}
	
	static void saveDefault(){
	}
	
	static void save(int fee){
	}
}

结果:编译不会报错

注意:上面主要是用到了:旧语法,一个标志位加:相当于是goto语句,一般是用在跳出多重循环

		ok:
		for(int i=0,j=10;i<j;i++){
			for(int m=0,n=10;m<n;m++){
				System.out.println("hzb");
				if(m==5){
					break ok;
				}
			}
		}
上面只输出了6次hzb,跳出了双重循环,但建议别这么用,应该在内部循环中加一个标志属性来控制外面的循环。

九、少用静态导入

//没有用静态导入的
public class MathUtils{
	//计算圆面积
	public static double calCircleArea(double r){
		return Math.PI * r * r;
	}
	
	//计算球面积
	public static double calBallArea(double r){
		return 4*Math.PI * r * r;
	}
}
//用了静态导入的
import static java.lang.Math.PI;;
public class MathUtils{
	//计算圆面积
	public static double calCircleArea(double r){
		return PI * r * r;
	}
	
	//计算球面积
	public static double calBallArea(double r){
		return 4 * PI * r * r;
	}
}

注意:很明显,用了静态导入减少了代码量,但会大大增加了阅读难度,如果用了*把静态导入,有时候根本就不知道哪个方法是属于哪个类的,所以静态导入一般情况下不要用,要用也不要使用*

十、不要在本类中覆盖静态导入的变量和方法

import static java.lang.Math.PI;
import static java.lang.Math.abs;
public class Client {

	//常量名与静态导入的PI相同
	public final static String PI="hzb";
	//方法名与静态导入的相同
	public static int abs(int abs){
		return 0;
	}
		
	public static void main(String[] args) {
		System.out.println("PI="+PI);
		System.out.println("abs(100)=" +abs(-100));
	}
}
输出结果为:
PI=hzb

abs(100)=0

注意:编译器有个最短原则,就是能在本类中找到的变量,常量和方法就不会去别处查找,所以他用了本地的常量和方法,静态导入的常量和方法就没用了。

十一、养成良好习惯,显式声明UID

我们编写一个实现了Serializable(序列化标志接口)的类时,myeclipse会提示要增加一个uid,类实现Serializable是为了可序列化,从而可在网络传输或本地存储,我们来看下例子:

import java.io.Serializable;

public class Person implements Serializable{

	private String name;
	private int age;

	
	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	protected void test(){
		
	}
	
}
public class Producer {
    public static void main(String[] args) throws Exception {
        Person person = new Person();
        person.setName("混世魔王"); 
        //序列化,保存到磁盘上
        SerializationUtils.writeObject(person);
    }
}
/**
 * 序列化工具
 */
public class SerializationUtils {
    private static String FILE_NAME = "c:/obj.bin";
    // 序列化
    public static void writeObject(Serializable s) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
            oos.writeObject(s);
            oos.close();
        } catch (Exception e) {
            //异常处理
        }
    }
    
    public static Object readObject(){
        Object obj=null;
        // 反序列化化
        try {
            ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME));
            obj = input.readObject();
            input.close();
        } catch (Exception e) {
            //异常处理
        }
        return obj;
    }

}
public class Consumer {

    public static void main(String[] args) throws Exception {
        // 反序列化化
        Person p = (Person) SerializationUtils.readObject();
        System.out.println("name="+p.getName());
    }
}
假设Person 、SerializationUtils 、Producer 是发送端项目,Person 、SerializationUtils 、Consumer接收端项目,如果我们没有在Person显示声明uid,假设当发送端的Person多增加了个属性,而没有将该类发给接收端更新,然后就序列化,这样当接收端去反序列化时由于jvm判断了序列化的类和自己本地的类的版本不一致(根据uid来判断的,uid又是根据字段,方法等所有东西去计算出来的唯一的字符串),所以就会报异常。所以我们可以显示声明uid,这样当反序列化时,jvm就会以为两个类的版本是一致的,所以可以正常运行原先的方法,只是发送端新增加的方法和字段没法访问。

十二、序列化类避免在构造函数中为final变量赋值

注意:反序列化时构造函数不会执行










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值