代码优化的重要性:从小虾米到鲸鱼,细小的优化如何提升运行效率
[id_[id_110[id_1[id_973794312]09276234]196323]020674584]代码优化的情况类似。若项目注重尽快无 Bug 上线,那么便可抓主要的方面而忽略小的细节,代码的细节无需过于精细打磨;然而,倘若有足够的时间来开发和维护代码,此时就必须考量每一个能够优化的细节,一个个细小的优化点累积起来,对于代码的运行效率肯定是有提升的。

代码优化的目标是:
1、减小代码的体积
2、提高代码运行的效率
(1)尽量指定类、方法的final修饰符
类被 final 修饰符修饰后是不可派生的。在 Java 核心 API 中,存在很多应用 final 的实例,像 java.lang.String ,它整个类都是 final 的。给类加上 final 修饰符,就能使该类不可以被继承;给方法加上 final 修饰符,就能使该方法不可以被重写。如果一个类被指定为 final ,那么这个类的所有方法都是 final 的。Java 编译器会去寻找可以内联所有 final 方法的机会,内联对于提升 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, length = list.size(); i < length; i++){...}
这样,当 list 的大小(list.size())很大时,就减少了大量的消耗。
(6)尽量采用懒加载的策略,即在需要的时候才创建
例如:
String str = "aaa";if (i == 1){ list.add(str);}
建议替换为:
if (i == 1){ String str = "aaa"; list.add(str);}
(7)慎用异常
异常会对性能产生不利影响。当抛出异常时,首先需要创建一个新的对象。Throwable 接口的构造函数会调用名为 fillInStackTrace()的本地同步方法,该方法会检查堆栈并收集调用跟踪信息。只要有异常被抛出,Java 虚拟机就必须对调用堆栈进行调整,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
不要在循环的内部使用 try...catch...。应当将 try...catch...放置在最外层。
根据网友们提出的意见,这一点我认为值得商榷。
如果能够估计到待添加的内容的长度,就为底层是以数组方式来实现的集合以及工具类指定初始的长度。
ArrayList、LinkedList、StringBuilder、StringBuffer、HashMap、HashSet 等。以 StringBuilder 为例:
StringBuilder() // 默认分配16个字符的空间
StringBuilder(int size) // 默认分配size个字符的空间
StringBuilder(String str) 默认会分配 16 个字符的空间,并且还会分配与 str 的长度相等的字符空间。
可以通过类的构造函数来设定其初始化容量,从而提升性能。例如StringBuilder,其length表示当前StringBuilder能保持的字符数量。当 StringBuilder 达到最大容量时,它会把自身容量提升至当前的 2 倍再加 2。无论在何时,只要 StringBuilder 达到其最大容量,它就必须创建一个新的字符数组,接着把旧的字符数组内容复制到新字符数组中。这是一个非常耗费性能的操作。试想,若能预估到字符数组大概要存放 5000 个字符且不指定长度,最接近 5000 的 2 的幂是 4096,暂不考虑每次扩容加的 2,那么:
这样会浪费内存空间并且降低代码运行效率。因此,为底层以数组实现的集合和工具类设置一个合理的初始化容量是可行的,这能带来显著的效果。然而,需要注意的是,像 HashMap 这种以数组 + 链表实现的集合,不要将初始大小设置为你估计的大小,因为一个 table 上只连接一个对象的情况几乎不存在。初始大小建议设置为 2 的 N 次幂。若能估计到有 2000 个元素,设置成 new HashMap(128)是可以的,设置成 new HashMap(256)也是可以的。
在复制大量数据的时候,会运用 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 对象的引用。如果 count 很大,就会耗费内存。建议进行更改。
Object obj = null;for (int i = 0; i <= count; i++){
obj = new Object();}
这样的话,内存中仅有一份 Object 对象的引用。每次进行 new Object()操作时,Object 对象的引用会指向不同的 Object 而已。然而,内存中始终只有一份,如此一来就极大地节省了内存空间。
从效率和类型检查的角度考虑,应优先使用 array。只有在无法确定数组大小时,才去使用 ArrayList。
尽量使用 HashMap 和 ArrayList 以及 StringBuilder 。如果没有线程安全方面的需求,就不建议使用 Hashtable、Vector 和 StringBuffer 。因为后三者使用了同步机制,从而导致了性能方面的开销
不要把数组声明为是 public 的、静态的并且是 final 的。
这毫无意义。这样做只是将引用定义为 static final 而已,但数组的内容仍能随意更改。把数组声明为 public 更是存在安全漏洞,因为这意味着这个数组能够被外部类所改变。
(16)尽量在合适的场合使用单例
单例能够减轻加载的负担,能够缩短加载的时间,能够提高加载的效率。然而,并非所有地方都适合使用单例。简单来讲,单例主要适用于以下三个方面:
(17)尽量避免随意使用静态变量
要知道,某个对象被 static 的变量所引用时,gc 通常不会回收这个对象所占的堆内存。例如:
Java学习交流的 QQ 群是 589809992,我们一同来学习 Java 吧。public class A{
private static B b = new B(); }
此时静态变量 b 的生命周期与 A 类的生命周期相同。如果 A 类没有被卸载,那么引用 B 所指向的 B 对象将会常驻内存。并且,这种常驻内存的状态会一直持续到程序终止。
(18)及时清除不再需要的会话
许多应用服务器都有默认的会话超时时间,目的是清除不再活动的会话,通常这个时间为 30 分钟。当应用服务器要保存更多会话且内存不足时,操作系统会把部分数据转移到磁盘,应用服务器也可能依据 MRU(最近最频繁使用)算法把部分不活跃会话转储到磁盘,甚至有可能抛出内存不足的异常。如果会话要被转存到磁盘,那么就必须先进行序列化。在大规模集群中,对对象进行序列化的代价是很高的。所以,当会话不再被需要时,应当及时调用 HttpSession 的 invalidate()方法来清除会话。
实现 RandomAccess 接口的集合,像 ArrayList 这类,在遍历时应当使用最普通的 for 循环,而不要使用 foreach 循环。
JDK 向用户进行了推荐。JDK API 对 RandomAccess 接口作出解释:实现该接口是为了表明其能够支持快速随机访问。此接口的主要目的在于允许一般的算法改变自身行为,以便在应用于随机或连续访问列表时能展现出良好的性能。实际经验显示,对于实现了 RandomAccess 接口的类实例,如果是随机访问的情况,那么使用普通 for 循环的效率要比使用 foreach 循环高;而如果是顺序访问的情况,使用 Iterator 的效率则会更高。可以通过类似如下的代码来进行判断:
if (list instanceof RandomAccess){
for (int i = 0; i < list.size(); i++){}}else{
Iterator> iterator = list.iterable();
while如果迭代器有下一个元素,那么就执行迭代器的相关操作。具体来说,就是在满足迭代器有下一个元素这个条件的情况下,对迭代器进行相应的操作。next()}}
foreach 循环的底层实现原理为迭代器 Iterator。所以,后半句“反过来,如果是顺序访问的,则使用 Iterator 会效率更高”意味着对于顺序访问的那些类实例,可使用 foreach 循环来进行遍历。
(20)使用同步代码块替代同步方法
如果不能确定一整个方法都需要进行同步,那么就尽量使用同步代码块,这样可以避免对那些不需要进行同步的代码也进行同步,从而不会影响代码的执行效率。
(21)将常量声明为static final,并以大写命名
在编译期间能够将这些内容放置到常量池中,从而避免在运行期间计算来生成常量的值。并且,把常量的名字用大写来命名,就可以便于区分常量与变量。
(22)不要创建一些不使用的对象,不要导入一些不使用的类
如果代码中出现“The value of the local variable i is not used”,那么请删除这些无用的内容。如果代码中出现“The import java.util is never used”,那么请删除这些无用的内容。这毫无意义。
(23)程序运行过程中避免使用反射
Java 为用户提供了一个很强大的功能,那就是反射。功能强大通常意味着效率不会很高。不建议在程序运行时使用,尤其不要频繁使用反射机制,特别是 Method 的 invoke 方法。如果确实有必要,一种建议做法是:在项目启动时,通过反射将那些需要加载的类实例化出一个对象并放入内存。用户只关心在与对端交互时获取最快的响应速度,而不关心对端的项目启动花费多长时间。
(24)使用数据库连接池和线程池
这两个池的作用都是用于重用对象。其中一个池能够避免频繁地进行打开和关闭连接的操作;另一个池则可以避免频繁地进行创建和销毁线程的行为。
(25)使用带缓冲的输入输出流进行IO操作
带缓冲的输入流有 BufferedReader,带缓冲的输出流有 BufferedWriter,带缓冲的输入字节流有 BufferedInputStream,带缓冲的输出字节流有 BufferedOutputStream,这些带缓冲的流可以极大地提升 IO 效率。
在顺序插入和随机访问比较多的场景会使用 ArrayList ;在元素删除和中间插入比较多的场景会使用 LinkedList 。
了解 ArrayList 和 LinkedList 的原理就能明白这个。
(27)不要让public方法中有太多的形参
公共方法就是向外部提供的方法。如果给这些方法赋予过多的形参,主要存在以下两点坏处:一是可能导致方法的调用变得复杂,增加了调用者的理解和使用难度;二是可能使方法的灵活性降低,在不同的调用场景下可能需要修改方法的形参定义,影响代码的可维护性。
这个“太多”具体指多少个呢?大概 3 个或者 4 个吧。例如,当我们使用 JDBC 编写一个 insertStudentInfo 方法时,如果有 10 个学生信息字段要插入到 Student 表中,那么可以将这 10 个参数封装在一个实体类中,以此作为 insert 方法的形参。
字符串变量与字符串常量进行 equals 操作时,应将字符串常量写在前面。
这是一个比较常见的小技巧了,如果有以下代码:
String str = "123";if (str.equals("123")){
...}
建议修改为:
String str = "123";if ("123".equals(str)){
...}
这么做主要是可以避免空指针异常。
在 java 中,要知道 if (i == 1) 与 if (1 == i) 是不存在区别的。然而从阅读习惯方面来讲,是建议使用前者的。
平时有人问,“if (i == 1)”与“if (1 == i)”是否有区别呢?这需要从 C/C++开始讲起。
在 C/C++里,“if (i == 1)”这种判断条件成立与否是以 0 和非 0 作为基准的。其中 0 意味着 false,非 0 则意味着 true。倘若有这样一段代码:
int i = 2;if (i == 1){
...}else{
...}
C/C++判断“i 等于 1”不成立,于是用 0 来表示,也就是 false。然而如果:
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 就会编译报错“Type mismatch: cannot convert from int to boolean”。但是,Java 的“if (i == 1)”与“if (1 == i)”在语义方面不存在任何差别。从阅读习惯的角度来看,建议使用“if (i == 1)”这种形式会更好一些。
(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,而这就是我们上面的控制台上所输出的内容。从这个例子当中,还能够顺便得出两个结论:
整型默认的数据类型为 int。long l = 12345678901234L,此数字超出了 int 的范围。因此最后有一个 L,这表示它是一个 long 型数。顺便一提,浮点型的默认类型是 double。所以在定义 float 时,要写成“float f = 3.5f”。
接下来写一句“int ii = l + i;”会出现报错情况。原因在于 long 与 int 相加的结果是一个 long 类型的值,而不能将其直接赋值给 int 类型的变量。
(32)公用的集合类中不使用的数据一定要及时remove掉
如果一个集合类是公用的,即不是在方法里面的属性。那么这个集合里的元素不会自动释放,因为始终有引用指向它们。所以,如果公用集合里的某些数据不使用却不去将其 remove 掉,就会导致这个公用集合不断增大,从而使系统存在内存泄露的隐患。
把一个基本数据类型转换为字符串,其中基本数据类型使用 toString()方法是最快的方式;String.valueOf(数据)这种方式次之;而数据加上空字符串""这种方式是最慢的。
将一个基本数据类型转换通常有三种方式。我拥有一个 Integer 型数据 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从当前时间(.currentTime())中减去开始时间(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()方法。原因很简单:
三者对比下来,明显是2最快、1次之、3最慢
(34)使用最有效率的方式去遍历Map
遍历 Map 有多种方式。通常在场景中,我们需要遍历 Map 中的 Key 和 Value。那么,推荐且效率最高的方式是:
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 的迭代器;
while (iter.hasNext())
{
Map.Entry<String, String> entry = iter.next();
System.out.println(entry.getKey() + "\t" + entry.getValue());
}}
如果你只是想要遍历这个 Map 的 key 值,那么使用“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() 抛出异常,就会进入 catch 块,此时 YYY.close() 不会执行,YYY 所占用的资源就无法回收,一直被占用着。这样的代码一多,就有可能引发资源句柄泄露。改为下面的写法后,能确保无论怎样 XXX 和 YYY 都将被关闭。