前言
代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。
代码优化的目标
1、减小代码的体积
2、提高代码运行的效率
3、提高代码的可读性
代码优化细节
1、字符串变量和字符串常量equals的时候将字符串常量写在前面
这是一个比较常见的小技巧了,如果有以下代码:
1String str = "123"; 2if (str.equals("123")) { 3... 4} 5建议修改为: 6String str = "123"; 7if ("123".equals(str)) 8{ 9... 10}
这么做主要是可以避免空指针异常。
2、把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+”"最慢
把一个基本数据类型转为一般有三种方式,我有一个Integer型数据i,可以使用i.toString()、String.valueOf(i)、i+”"三种方式,三种方式的效率如何,看一个测试:
1public static void main(String[] args) 2{ 3int loopTime = 50000; 4Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) 5{ 6String str = String.valueOf(i); 7} 8System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms"); 9startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) 10{ 11String str = i.toString(); 12} 13System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms"); 14startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++) 15{ 16String str = i + ""; 17} 18System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms"); 19}
运行结果为:
String.valueOf():11ms Integer.toString():5ms i + "":25ms
所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:
a、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断
b、Integer.toString()方法就不说了,直接调用了
c、i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串
三者对比下来,明显是2最快、1次之、3最慢。
3、尽量减少对变量的重复计算
明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:
1for (int i = 0; i < list.size(); i++) 2{...} 3 4建议替换为: 5 6for (int i = 0, int length = list.size(); i < length; i++) 7{...}
这样,在list.size()很大的时候,就减少了很多的消耗。
4、设计API时永远不要返回空(null)数组或List
1public static void main(String[] args){ 2 int limit =1; 3 List<String> ls = getUsers(limit); 4 for(String str:ls){ 5 System.out.println(str); 6 } 7} 8 9private static List<String> getUsers(){ 10 //return null;直接返回null,在上面的遍历会有异常java.lang.NullPointerException 11 return Collection.EMPTY_LIST; 12}
Java 的标准库设计者已经在 Collections 类中放了一个空的 List 常量 EMPTY_LIST,除此之外,还有 EMPTY_MAP, EMPTY_SET,真是贴心。
5、将字符串数组转换成逗号分隔字符串
通常会这么写:
1public static void main(String[] args) { 2String strs = ""; 3String[] arr = new String[]{"aa", "cc", "bb"}; // 转换前的字符串数组 4StringBuilder sb = new StringBuilder(); 5for (String ele : arr) { 6if (sb.length() > 0) { 7sb.append(","); 8} 9sb.append(ele); 10} 11strs = sb.toString(); // 转换后的逗号分隔字符串 12System.out.println(strs);//aa,cc,bb 13}
更简单的写法:
1public static void main(String[] args) { 2String[] arr = new String[]{"aa", "cc", "bb"}; // 转换前的字符串数组 3String strs = String.join(",", arr); // 转换后的逗号分隔字符串 4System.out.println(strs);//aa,cc,bb 5}
6、三元表达式代替if else
1if(user!=null) 2{ 3 return true; 4}else 5{ 6 return false; 7} 8 9建议替换为: 10 11return user==null?false:true;
7、不可随意使用静态变量
当某个对象被定义为 static 变量,那么 GC 通常是不会回收这个对象所占有的内存。
示例如下:
1public class A { 2 private static B b = new B(); 3}
此时静态变量 b 的生命周期与 A 类同步,即如果 A 类不卸载,b 对象会常驻内存,直到程序终止。
8、尽量采用懒加载的策略,即在需要的时候才创建
例如:
1String str = "aaa";if (i == 1) 2{ 3list.add(str); 4} 5建议替换为: 6 7if (i == 1) 8{ 9String str = "aaa"; 10list.add(str); 11}
9、慎用异常
在Java开发中,经常使用try-catch进行错误捕获,但是try-catch语句对系统性能而言是非常糟糕的。虽然一次try-catch中,无法察觉到她对性能带来的损失,但是一旦try-catch语句被应用于循环或是遍历体内,就会给系统性能带来极大的伤害。 以下是一段将try-catch应用于循环体内的示例代码:
1@Test 2 public void test11() { 3 4 long start = System.currentTimeMillis(); 5 int a = 0; 6 for(int i=0;i<1000000000;i++){ 7 try { 8 a++; 9 }catch (Exception e){ 10 e.printStackTrace(); 11 } 12 } 13 long useTime = System.currentTimeMillis()-start; 14 System.out.println("useTime:"+useTime); 15 16 }
上面这段代码运行结果是:useTime:10
下面是一段将try-catch移到循环体外的代码,那么性能就提升了将近一半。如下:
1@Test 2 public void test(){ 3 long start = System.currentTimeMillis(); 4 int a = 0; 5 try { 6 for (int i=0;i<1000000000;i++){ 7 a++; 8 } 9 }catch (Exception e){ 10 e.printStackTrace(); 11 } 12 long useTime = System.currentTimeMillis()-start; 13 System.out.println(useTime); 14 }
useTime:6
10、使用HashMap的时候,可以指定集合的初始化大小。
例如说,HashMap里面需要存放30个元素,但是由于没有进行初始化大小操作,所以在添加元素的时候,当0.75 * size(默认16) < 30.hashmap的内部会一直在进行扩容操作,影响性能。
那么为了减少扩容操作,可以在初始化的时候将hashmap的大小设置为:已知需要存储的大小/负载因子(0.75)+1
Map<String, Object> params = new HashMap<>(41);
PS:如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度:ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等
11、MyBatis中Integer型的字段 0=='' 为true 的情况确实存在,所以对于Integer型的数据应该避免 !='' 的判断。
1<if test="cardType != null and cardType !='' "> 2 and card_type = #{cardType,jdbcType=INTEGER} 3</if> 4 5改成 6 7<if test="cardType != null"> 8 and card_type = #{cardType,jdbcType=INTEGER} 9</if>
因为当cardType=0时,cardType !=''为false,所以INGEGER类型要去掉对空串的判断。
12、StringUtils工具类中:isNotEmpty与isNotBlank
11,isNotEmpty(str)等价于 str != null && str.length > 0。 2 32,isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length > 0。 4 5同理: 6 71,isEmpty 等价于 str == null || str.length == 0。 8 92,isBlank 等价于 str == null || str.length == 0 || str.trim().length == 0。 10 113,str.length > 0 && str.trim().length > 0 ---> str.length > 0。
13、及时关闭流
1FileInputStream fileInputStream = null; 2try { 3 File file = cn.hutool.core.io.FileUtil.file(filePath); 4 fileInputStream = new FileInputStream(file); 5 ..... 6} catch (FileNotFoundException e) { 7 e.printStackTrace(); 8} catch (IOException e) { 9 e.printStackTrace(); 10} finally { 11 try { 12 if (fileInputStream != null) { 13 fileInputStream.close(); 14 } 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } 18}
Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。