Java学习系列(二十六)Java代码优化讲解

转载请注明出处:http://blog.csdn.net/lhy_ycu/article/details/45506549

 

在开篇之前,先补充一下《Java学习系列》里面的instanceof关键字的使用及其陷阱。简要说明:instanceof是一个简单的二元操作符,它是用来判断一个对象是否为一个类的实例。只要instanceof左右操作数有继承或实现的关系,程序都是可以编译通过的。下面通过一个简单实例来说明一下instanceof关键字的使用及其陷阱:

 

class A {
	public boolean isDateInstance(T t) {
		return t instanceof Date;
	}
}

public class InstanceofTest {

	public static void main(String[] args) {
		// true。一个String对象是Object实例(java中Object是所有类的父类)
		System.out.println("zhangsan" instanceof Object);
		// false。Object是父类,它的对象明显不是String类的实例
		System.out.println(new Object() instanceof String);
		// true。一个String对象是String的实例
		System.out.println(new String() instanceof String);
		// 编译不能通过。'a' 为一个char类型,即基本类型
		System.out.println('a' instanceof Character);
		// false。只要左操作数为null(本质是无类型),那么结果就直接返回false
		System.out.println(null instanceof String);
		// false。即使将null强转也还是个null
		System.out.println((String) null instanceof String);
		// 编译不能通过。因为Date和String并没有继承或实现关系
		System.out.println(new Date() instanceof String);
		// false。在编译成字节码时,T已经是Object类型了,由于传递了一个"lisi"实参字符串,所以T实际是String类型了。
		System.out.println(new A().isDateInstance("lisi"));
		List list = new ArrayList();
		// 编译不能通过。instanceof不允许存在泛型参数。
		System.out.println(list instanceof List);
	}
}

 

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

 

下面开始正式进入主题,先从一个自增的陷阱开始吧。

1)自增的陷阱

 

int num = 0;
for (int i = 0; i < 100; i++) {
	num = num++;
}
System.out.println("num = " + num);

打印结果是什么呢?答案是0,为什么呢?先看看执行步骤吧,程序第一次循环时的详细步骤如下:JVM把num值(0)拷贝到临时变量区,然后num值加1,这是num的值为1,接着返回临时变量区的值,注意这个值是1没修改过,最后将返回值赋给num,此时num的值被重置为了0。简单说来就是int temp = num; num += 1; return temp;这3步。所以打印结果还是0,num始终保持着原来的状态。

 

优化:将num=num++; 修改为num++即可。

 

2)常量竟成变量?

大家想想,常量有可能成为变量吗?答案是有可能,只不过这种做法是不被认同的。

 

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

public static void main(String[] args) {
	// 通过打印几次,可以看到结果变了,也就是说常量在定义的时候就没有保证它的值运行期保持不变
	System.out.println("常量变了吗?" + RAND_CONST);
}

优化建议:务必常量的值在运行期保持不变,所以可以让RAND_CONST在定义时直接赋值写死。

 

 

3)“l” 你能看出这个字母是i的大写、数字1还是字母l的小写?

 

public static  long l = 11;


优化:字母后缀l尽量大写L

 

 

4)三目运算符的类型不一致?

 

int i = 70;
System.out.println(i < 100 ? 80 : 90.0);

打印结果出人意料,结果竟然为80.0,这是为什么呢?i<100确实为true,但由于最后一个操作数为90.0,是一个浮点数,这时编译器会将第二个操作数80转为80.0浮点数,统一结果类型,所以打印结果为80.0。
优化:90.0改为90

 

5)不要重载含有变长参数的方法

 

简要说明:变长参数必须是方法的最后一个参数,且一个方法不能定义多个变长参数。

 

public class Test01 {
	public static void fruitPrice(int price, int discount) {
		float realPrice = price * discount / 100.0F;
		System.out.println("非变长参数得出的结果:realPrice = " + realPrice);
	}

	public static void fruitPrice(int price, int... discounts) {
		float realPrice = price;
		for (int discount : discounts) {
			realPrice = price * discount / 100.0F;
		}
		System.out.println("变长参数得出的结果:realPrice = " + realPrice);
	}

	public static void main(String[] args) {
		fruitPrice(48888, 85);
	}
}

打印结果是什么呢?答案是:非变长参数得出的结果:realPrice = 41554.8,也就是程序执行的是第一个方法,而没有执行变长参数方法,这是为什么呢?因为Java在编译时,首先会根据实参的数量和类型(这里是2个都是int类型的实参,注意没有转成int数组)来进行处理,也就是找到fruitPrice(int price, int discount)方法,而且确认它符合方法签名条件,由于编译器也爱“偷懒”,所以程序会执行第一个方法。再看一个:

 

public class Test02 {
	public void method1(String str, Integer... integers) {
		System.out.println("变长参数类型为Integer的方法被调用...");
	}

	public void method1(String str, String... strs) {
		System.out.println("变长参数类型为String的方法被调用...");
	}

	public static void main(String[] args) {
		Test02 t = new Test02();
		// 编译不通过。虽然两个方法都符合要求,但编译器并不知道调用哪一个,于是就报错了。
		t.method1("test02");
		// 编译不通过。因为[直接量null是没有类型的],理由同上。
		t.method1("test02", null);
	}

}

 

对于t.method("test02",null);如果我们提前声明String[] strs = null或者Integer[] ints = null;也就是让编译器知道这个null是String或者Integer类型的,那么就可以通过编译了。

6)慎用静态导入

这点比较容易理解,因为静态导入的作用是将某个类的类成员(静态变量、静态方法)引入到本类中,而如果此时刚好本类中也有同名的类成员,那么这样便可能产生混淆,后面维护起来也比较麻烦。

优化:类型.类成员

7) 不要让类型默默转换

 

public class Test03 {
	// 光速为30万公公里
	public static final int LIGHT_SPEED = 30 * 10000 * 1000;

	public static void main(String[] args) {
		long distance = 8 * 60 * LIGHT_SPEED;
		// 打印结果(为负数):地球与太阳的距离为:-2028888064
		System.out.println("地球与太阳的距离为:" + distance);
	}
}

为什么是负数呢?这是因为Java是先运算再进行类型转换的。distance的3个运算参数都是int类型,三者结果相等虽然也是int类型,但已经超过了int取值的最大范围,所以为负数,这样再转为long型,结果仍是负数。解决方案:long distance = 1L * 8 * 60 * LIGHT_SPEED;1L是个长整型,右边等式类型自动升级,计算出来的结果也是长整型。

 

优化:基本类型转换时,最好使用主动声明的方式参与运算。

8)包装类性值为null?

 

 

public static void main(String[] args) {
	List list = new ArrayList();
	// 自动装箱(基本类型转为包装类型)。装箱过程是调用valueOf方法实现的。
	list.add(1);
	list.add(2);
	list.add(null);
	// 自动拆箱(包装类型转为基本类型)。拆箱过程默认调用包装对象的intValue方法实现的。
	int count = 0;
	for (int item : list) {
		count += item;
	}
	System.out.println("count = " + count);
}

运行结果报异常java.lang.NullPointerException。原因很简单:拆箱过程默认调用包装对象的intValue方法实现的,由于包装类是null值,所以就报空指针异常了。解决方案:

 

 

for (Integer item : list) {
	count += (item == null) ? 0 : item;
}

 

优化:包装类型参与运算时,要做null校验。

9)让工具类不可实例化

工具类的方法和属性都是静态的,不需要生成实例即可访问,而且其类成员在内存中只有一份拷贝,jdk也做了很好的处理。由于不希望被初始化,于是就设置其构造函数为私有(private)访问权限。

 

public class UtilClass {
	// 构造器私有化
	private UtilClass() {

	}
}

但这样有个问题,就是在工具类里面可能方法很多,无意间new了一个新的对象,一时间也没有发现。这样就没有达到真正不需要生成实例的目的。

 

优化:使用工具类时,要保证所有的访问都是通过类名进行的。

public class UtilClass {
	// 构造器私有化
	private UtilClass() {
		throw new Error("please don't instantial this util class...");
	}
}

 

10)不要在循环条件中带有计算

如果在循环(for、while等)条件中计算,则每次循环都得计算一遍,这样就会降低,例如:

 

while (n < count * 2) {
	//...
}

优化:将while里面的运算提取即可

int total = count * 2;
while (n < total) {
	//...
}

11)不要主动进行垃圾回收

 

尽量不要调用System.gc();来主动对垃圾进行回收。因为System.gc它会停止所有响应,才能检查内存中是否有可回收的对象。把所有对象都检查一遍,然后处理掉那些垃圾对象。这对一个应用系统来说风险极大,如果是一个web项目,调用System.gc它会让所有的请求都暂停,等待垃圾回收器执行完毕(可能会严重影响正常业务运行),如果web项目里面对象很多,那么System.gc执行的时间会非常耗时,所以最好不要主动进行垃圾回收。

 

12)静态变量一定要先声明后赋值(或使用)

 

 

public class Test01 {
	static {
		num = 20;
	}
	public static int num = 2;

	public static void main(String[] args) {
		System.out.println(num);
	}
}

大家想想,结果是多少呢?打印结果是:2。为什么呢?这是因为静态变量(类变量)是类加载时被分配到数据区,它在内存中只有一份拷贝,详细说来就是:静态变量是在类初始化时首先被加载的,而JVM会去查找类中所有的静态声明,然后分配地址空间(此时还没有赋值),之后JVM会根据类中静态赋值(包括静态类赋值和静态代码块赋值)的先后顺序来执行

 

优化:静态变量先声明后使用。

 

补充1——字符串常量池

 

大家都知道,Java中的对象是保存在堆内存中的,但是字符串(常量)池非常特殊,它在编译期就已经决定了其存在JVM的常量池中,垃圾回收器是不会对它进行回收的。它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字符序列相等的字符串,如果有则不再创建,直接返回池中该对象的引用;若没有则创建之,然后放入池中并返回创建对象的引用。下面看一个实例:

 

public class Test {
	public static void main(String[] args) {
		String str1 = "java代码优化";
		String str2 = "java代码优化";
		String str3 = new String("java代码优化");
		String str4 = str3.intern();

		System.out.println(str1 == str2);
		System.out.println(str1 == str3);
		System.out.println(str1 == str4);
	}
}

结果是什么呢?答案是true、false、true。解析:创建第一个字符串"java代码优化"时,首先检查字符串池中是否有该对象,发现没有,于是就创建第一个"java代码优化"这个字符串并放入池中,待再创建str2字符串时,由于池中已经有了该字符串,于是就直接返回了该对象的引用,此时str1与str2指向的是同一个地址,所有str1==str2返回true。而new String("java代码优化")声明的是一个String对象,是不检查字符串池,也不会把对象放入池中,那当然返回false了。而使用intern方法为什么会返回true呢?因为intern会检查当前对象在池中是否有字符序列相等的引用对象,如果有则返回true,如果没有则返回false。

 

优化建议:若没有特殊要求,推荐使用String直接量赋值

 

补充2——String、StringBuffer(线程安全)、StringBuilder(线程不安全)的使用场景

 

①String的使用场景:在字符串不经常变化的场景中使用String类,例如常量的声明、少量的变量运算等。

②StringBuffer的使用场景:在频繁进行字符串运算(如:字符串拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用StringBuffer,例如:XML解析、HTTP参数解析和封装等。

③StringBuilder的使用场景:在频繁进行字符串运算(如:字符串拼接、替换、删除等),并且运行在单线程环境中,则可以考虑使用StringBuilder,例如:SQL语句的封装、JSON封装等。

 

参考文献:《编写高质量代码》

 

 

 

你可能感兴趣的:(Java学习系列)