Java学习系列(二十六)Java代码优化讲解
转载请注明出处: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封装等。
参考文献:《编写高质量代码》
上一篇: Asp.Net函数总结