欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-01 11:03:30
...

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

 

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

 

class A<T> {
	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<String> list = new ArrayList<String>();
		// 编译不能通过。instanceof不允许存在泛型参数。
		System.out.println(list instanceof List<String>);
	}
}

 

【注意】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<Integer> list = new ArrayList<Integer>();
	// 自动装箱(基本类型转为包装类型)。装箱过程是调用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封装等。

 

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