代码优化的重要性与技巧:如何通过细节提升运行效率
来源:cnblogs.com/Qian123/p/6046096.html

前言
代码优化是一个非常重要的话题。有些人可能认为这没什么用。有哪些小事是可以修改的?修改与否会对代码的运行效率产生什么影响?我这样想这个问题,就像海里的鲸鱼,它吃一只小虾有用吗?没用,但是吃多了虾,鲸鱼就吃饱了。
代码优化也是如此。如果项目的重点是尽快无bug地上线,那么此时就可以先大后小,代码的细节不需要细化;但如果有足够的时间来开发和维护代码,这个时候就必须考虑方方面面。还有一些细节可以优化。小的优化点一点一滴的积累,一定会提高代码的运行效率。
代码优化的目标是
1. 减少代码大小
2.提高代码运行效率
代码优化细节
1、尽量指定类和方法的final修饰符。
带有final修饰符的类不会被派生。在Java核心API中,有很多final应用的例子,比如java.lang.String,整个类都是final的。为类指定final修饰符可防止该类被继承,为方法指定final修饰符可防止该方法被覆盖。如果一个类被指定为final,则该类的所有方法都是final的。 Java 编译器将寻找机会内联所有最终方法。内联对于提高Java运行效率有着显着的作用。有关详细信息,请参阅 Java 运行时优化。这可以平均提高 50% 的性能。
2. 尽可能重复使用对象
特别是对于String对象的使用,在发生字符串连接时应该使用StringBuilder/StringBuffer来代替。由于Java虚拟机不仅花时间生成对象,将来可能还需要花时间垃圾收集和处理这些对象。因此,生成过多的对象会对程序的性能产生很大的影响。
3.尽可能使用局部变量
调用方法时传递的参数以及调用过程中创建的临时变量都保存在栈中,速度更快。其他变量,如静态变量、实例变量等都是在堆中创建的,速度较慢。此外,在堆栈上创建的变量的内容随着方法结束而消失,并且不需要额外的垃圾收集。
4、及时关闭流量
在Java编程中,进行数据库连接和I/O流操作时必须小心。使用完毕后及时关闭,释放资源。因为操作这些大对象会造成很大的系统开销,稍有不慎就会导致严重的后果。
5. 尽量减少变量的双重计算
澄清一个概念,调用一个方法,即使方法中只有一条语句,其代价也是高昂的,包括创建栈帧、调用方法时保护场景、调用方法时恢复场景等。所以对于例如以下操作:
for (int i = 0; i < list.size(); i++)
{...}
建议将其替换为:
for (int i = 0, int length = list.size(); i < length; i++)
{...}
这样当list.size()很大的时候,就减少了很多消耗。
6、尽量采用懒加载策略,即只在需要的时候创建。
例如:
String str = "aaa";if (i == 1)
{
list.add(str);
}
建议将其替换为:
if (i == 1)
{
String str = "aaa";
list.add(str);
}
7. 谨慎使用异常
异常不利于性能。要抛出异常,必须首先创建一个新对象。 Throwable 接口的构造函数调用名为 fillInStackTrace() 的本地同步方法。 fillInStackTrace() 方法检查堆栈并收集调用跟踪信息。每当抛出异常时,Java虚拟机必须调整调用堆栈,因为在处理过程中会创建一个新对象。异常应该只用于错误处理,而不应该用于控制程序流程。
8.不要在循环中使用try...catch...。它应该放在最外层。
除非你必须这样做。如果你无缘无故地写出这样的东西,只要你的领导级别比较高,有强迫症,他很可能会骂你写出这样的垃圾代码。
9. 如果要添加的内容的长度可以估计,请为底层集合和以数组方式实现的工具类指定初始长度。
例如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等,以StringBuilder为例:
(1) StringBuilder() // 默认分配16个字符的空间
(2) StringBuilder(int size) // 默认分配size个字符的空间
(3) StringBuilder(String str) // 默认分配16个字符+str.length()字符空间
它的初始化能力可以通过类来设置(这里指的不仅仅是上面的StringBuilder),这样可以显着提高性能。例如StringBuilder,length表示当前StringBuilder可以容纳的字符数。因为当StringBuilder达到其最大容量时,它会将其容量增加到当前容量的2倍加上2。每当StringBuilder达到其最大容量时,它必须创建一个新的字符数组,然后用它替换旧的字符。数组的内容被复制到一个新的字符数组中 - 这是一个非常消耗性能的操作。试想一下,如果在不指定长度的情况下,可以估计字符数组中会存储大约5000个字符,最接近的2到5000的幂是4096,每次扩展都加2,那么:
(1)在4096的基础上,申请8194个字符数组,加起来一次申请12290个字符数组。如果一开始就可以指定5000个字符数组,那么将节省一倍以上的空间;
(2) 将原来的4096个字符复制到新的字符数组中。
这样既浪费了内存空间,又降低了代码运行效率。因此,为底层集合和用数组实现的工具类设置合理的初始化容量是没有错的,会带来立竿见影的效果。但请注意,对于像HashMap这样以数组+链表实现的集合,不要将初始大小设置为与你估计的大小相同,因为一张表只连接一个对象的可能性几乎为0。建议初始大小设置为2的N次方。如果可以估计有2000个元素,可以设置为new HashMap(128)或者new HashMap(256)。
10、复制大量数据时,使用System.arraycopy()命令
11.乘法和除法使用移位运算
例如:
for (val = 0; val < 100000; val += 5)
{
a = val * 8;
b = val / 2;
}
使用移位操作可以大大提高性能,因为在计算机底层,位操作是最方便、最快的,所以建议修改为:
for (val = 0; val < 100000; val += 5)
{
a = val << 3;
b = val >> 1;
}
移位操作虽然很快,但是可能会导致代码难以理解,所以最好添加相应的注释。
12.不要在循环内不断创建对象引用
例如:
for (int i = 1; i <= count; i++)
{Object obj = new Object();
}
这种方法会导致内存中存在count个Object对象引用。如果计数很大,就会消耗内存。建议改为:
Object obj = null;for (int i = 0; i <= count; i++) { obj = new Object(); }
这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。
13、基于效率和类型检查的考虑,尽可能使用数组,只有在无法确定数组大小的情况下才使用ArrayList。
14. 尝试使用HashMap、ArrayList、StringBuilder。除非线程安全需要,否则不建议使用Hashtable、Vector、StringBuffer。后三者由于使用同步机制而导致性能开销。
15. 不要将数组声明为 public static final
因为这是没有意义的,只是将引用定义为staticfinal,而数组的内容仍然可以随意改变。将数组声明为公共是一个安全漏洞,这意味着该数组可以被外部类更改。
16.在适当的情况下尝试使用单例。
使用单例可以减轻加载负担,缩短加载时间,提高加载效率,但并不是所有地方都适用单例。简单来说,单例主要适合以下三个方面:
(1)控制资源的使用,通过线程同步控制资源的并发访问
(2)控制实例的生成,达到节省资源的目的
(3) 控制多个不相关的进程或线程之间的数据共享,而不建立直接关联。
实现沟通
17.尽量避免随意使用静态变量。
要知道,当一个对象被定义为static的变量引用时,gc通常不会回收这个对象占用的堆内存,如:
public class A
{
private static B b = new B();
}
此时静态变量b的生命周期与A类相同,如果A类没有被卸载,引用B指向的B对象就会驻留在内存中,直到程序终止。
18.及时清理不再需要的会话
为了清除不再活动的会话,许多应用程序服务器都有默认的会话超时,通常为 30 分钟。当应用服务器需要保存更多会话时,如果内存不足,操作系统会将部分数据转移到磁盘上。应用服务器还可能根据MRU(最近使用的)算法将一些不活动的会话转储到磁盘。甚至可能会引发内存不足异常。如果要将会话转储到磁盘,则必须首先对其进行序列化。在大规模集群中,序列化对象的成本很高。因此,当不再需要session时,应及时调用HttpSession的invalidate()方法来清除session。
19. 实现RandomAccess接口的集合,例如ArrayList,应该使用最常见的for循环而不是foreach循环来遍历。
这是JDK向用户推荐的。 JDK API对RandomAccess接口的解释是:实现RandomAccess接口用于表明支持快速随机访问。该接口的主要目的是允许通用算法改变其行为,从而在应用于随机或连续访问列表时提供良好的性能。表现。实践经验表明,如果随机访问一个实现了RandomAccess接口的类实例,使用普通的for循环会比使用foreach循环效率更高;相反,如果顺序访问,使用Iterator会更高效。可以使用类似下面的代码来进行判断:
if (list instanceof RandomAccess)
{ for (int i = 0; i < list.size(); i++){}
}else{
Iterator> iterator = list.iterable(); while (iterator.hasNext()){iterator.next()}
}
foreach循环的底层实现原理是迭代器Iterator,参见Java语法糖1:变长参数和foreach循环原理。所以后半句“另一方面,如果是顺序访问的话,使用Iterator会更高效”的意思就是那些顺序访问的类实例是使用foreach循环来遍历的。
20.使用同步代码块而不是同步方法
这一点在多线程模块中synchronized锁方法块一文中已经讲得很清楚了。除非确定整个方法需要同步,否则尽量使用同步代码块,避免出现不需要同步的代码。还要进行同步,影响代码执行效率。
21、将常量声明为staticfinal,并以大写字母命名
这样就可以在编译时将这些内容放入常量池中,避免运行时计算常量的值。另外,用大写字母命名常量也可以更容易区分常量和变量。
22.不要创建一些未使用的对象,也不要导入一些未使用的类。
这是没有意义的。如果代码中出现“局部变量i的值没有被使用”或者“The import java.util is neverused”,请删除这些无用的内容。
23、程序运行时避免使用反射
请参阅反射。反射是Java为用户提供的一个非常强大的功能。功能强大往往意味着效率低下。程序运行过程中不建议使用反射机制,特别是频繁使用反射机制,尤其是Method的invoke方法。如果确实有必要,推荐的做法是对项目启动时需要通过反射加载的类使用反射实例。创建一个对象并将其放入内存中——用户只关心与同伴交互时获得最快的响应,而不关心同伴的项目需要多长时间才能启动。
24、使用数据库连接池和线程池
两个池都用于重用对象。前者可以避免频繁打开和关闭连接,后者可以避免频繁创建和销毁线程。
25.使用缓冲输入和输出流进行IO操作
缓冲输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,可以大大提高IO效率
26、在顺序插入和随机访问较多的场景下使用ArrayList。在有大量元素删除和中间插入的场景中使用LinkedList。只要了解ArrayList和LinkedList的原理就可以了。
27.公共方法中不要有太多形式参数
公共方法是提供给外界的方法。如果给这些方法太多的形式参数,则有两个主要缺点:
1、违背了面向对象的编程思想。 Java强调一切皆对象。形式参数过多,不符合面向对象编程思想。
2、参数过多必然导致方法调用出错概率增加。
至于“太多”指的是多少,我猜是3个或者4个。比如我们用JDBC写了一个insertStudentInfo方法。有 10 个学生信息字段需要插入到 Student 表中。这10个参数可以封装在一个实体类中作为insert方法的形参。
28、当字符串变量和字符串常量相等时,将字符串常量写在前面。
这是一个比较常见的技巧。如果您有以下代码:
String str = "123";
if (str.equals("123")) {...}
建议修改为:
String str = "123";
if ("123".equals(str))
{
...
}
这样做主要是为了避免空指针异常
29.请注意,Java中if(i==1)和if(1==i)没有区别,但是从阅读习惯来看,建议使用前者
人们常常会问,“if(i==1)”和“if(1==i)”有什么区别吗?这要从 C/C++ 开始。
在C/C++中,“if(i==1)”判断条件是根据0和非0成立的。 0 表示假,非 0 表示真。假如有这样一段代码:
int i = 2;
if (i == 1)
{
...
}else{
...
}
C/C++判断“i==1”不为真,因此表示为0,为假。但如果:
int i = 2;if (i = 1) { ... }else{ ... }
万一程序员一个不小心,把”if (i == 1)”写成”if (i = 1)”,这样就有问题了。在if之内将i赋值为1,if判断里面的内容非0,返回的就是true了,但是明明i为2,比较的值是1,应该返回的false。这种情况在C/C++的开发中是很可能发生的并且会导致一些难以理解的错误产生,所以,为了避免开发者在if语句中不正确的赋值操作,建议将if语句写为:
int i = 2;if (1 == i) { ... }else{ ... }
这样,即使开发者不小心写成了”1 = i”,C/C++编译器也可以第一时间检查出来,因为我们可以对一个变量赋值i为1,但是不能对一个常量赋值1为i。
然而在Java中,C/C++的“if(i=1)”语法是不可能的,因为一旦写出这个语法,Java就会编译并报错“类型不匹配:无法从int转换为boolean”。但是,虽然Java的“if(i==1)”和“if(1==i)”在语义上没有区别,但从阅读角度来看,还是用前者更好。
30. 不要在数组上使用 toString() 方法
看一下在数组上使用 toString() 打印的内容:
public static void main(String[] args)
{ int[] is = new int[]{1, 2, 3};
System.out.println(is.toString());
}
结果发现:
[I@18a992f
本意是想打印出数组内容,却有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections重写了Object的toString()方法。
31、不要对超出范围的基本数据类型进行向下强制转换。
这永远不会给出期望的结果:
public static void main(String[] args)
{
long l = 12345678901234L;int i = (int)l;
System.out.println(i);
}
我们可能期望得到其中一些,但结果是:
1942892530
解释。 Java中的Long是8字节64位,所以12345678901234在计算机中的表示应该是:
0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010
int类型数据有4个字节,32位。上述一串二进制数据的前32位取低位为:
0111 0011 1100 1110 0010 1111 1111 0010
该字符串的二进制表示形式为十进制 1942892530,因此这就是上面控制台上的输出内容。从这个例子可以得出两个结论:
1、整数默认数据类型为int,long l = 12345678901234L。这个数字已经超出了int的范围,所以末尾有一个L,表明这是一个long数字。顺便说一句,浮点类型的默认类型是double,所以定义float时应该写成“”float f = 3.5f”
2. 接下来,写“int ii = l + i;”会报错,因为long + int是long,不能赋值给int。
32、公共集合类中未使用的数据必须及时移除
如果一个集合类是public的(即它不是方法中的属性),那么集合中的元素将不会被自动释放,因为总有引用指向它们。因此,如果公共集合中的某些数据没有被使用并且没有被删除,那么公共集合将会不断增长,导致系统存在内存泄漏的可能性。
33. 将基本数据类型转换为字符串。基本数据类型.toString() 是最快的方法。
String.valueOf(data) 第二,data+”” 最慢
基本数据类型转换通常有三种方法。我有一个整数数据i。我可以使用 i.toString()、String.valueOf(i) 和 i+””。这三种方法的效率如何?参见测试:
public static void main(String[] args)
{
int loopTime = 50000;
Integer i = 0; long startTime = System.currentTimeMillis();for (int j = 0; j < loopTime; j++){String str = String.valueOf(i);
}
System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis();for (int j = 0; j < loopTime; j++)
{
String str = i.toString();
}
System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++)
{
String str = i + "";
}
System.out.println("i + "":" + (System.currentTimeMillis() - startTime) + "ms");
}
运行结果为:
String.valueOf():11ms Integer.toString():5ms i + "":25ms
所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:
1、String.valueOf()底层方法调用了Integer.toString()方法,但调用前会做一个简短的判断。
2、Integer.toString()方法我就不讲了,直接调用即可。
3、i+""底层使用StringBuilder实现。首先使用append方法进行拼接,然后使用toString()方法获取字符串。
比较三者,很明显2最快,1次之,3最慢。
34.用最有效的方式遍历地图
遍历地图的方法有很多种。一般场景下,我们需要的是遍历Map中的Keys和Values。推荐且最有效的方法是:
public static void main(String[] args)
{
HashMap<String, String> hm = new HashMap<String, String>();
hm.put("111", "222");Set<Map.Entry<String, String>> entrySet = hm.entrySet();
Iterator<Map.Entry<String, String>> iter = entrySet.iterator(); while (iter.hasNext())
{
Map.Entry<String, String> entry = iter.next();
System.out.println(entry.getKey() + "" + entry.getValue());
}
}
如果只是想迭代这个Map的键值,那么使用“Set keySet = hm.keySet();”会更合适。
35、建议单独操作资源的close()。
意思是,例如,我有这段代码:
try{
XXX.close();
YYY.close();
}catch (Exception e)
{...}
建议修改为:
try{ XXX.close(); }catch (Exception e) { ... }try{ YYY.close(); }catch (Exception e) { ... }
虽然麻烦一点,但是可以避免资源泄漏。我想如果没有修改代码的话,如果XXX.close()抛出异常,那么就会进入cath块,YYY.close()不会被执行,YYY资源也不会被回收。像这样占用过多的代码可能会导致资源句柄泄漏。改成上面的写法后,保证XXX和YYY无论如何都会被关闭。
长按打开更多惊喜