代码优化的重要性:细节决定效率,小虾米也能喂饱鲸鱼

[id_[id_19[id_327855600]1[id_[id_359256092]5099113[id_1340806966]]15905]526334448]

代码优化的重要性:细节决定效率,小虾米也能喂饱鲸鱼

代码优化是一个很重要的课题。有些人或许认为它没用,觉得一些细小的地方无需修改,也不清楚改与不改对代码运行效率会有怎样的影响。我是这样考虑这个问题的:就如同大海中的鲸鱼,吃一条小虾米能有什么用呢?确实没什么用。然而,当鲸鱼吃的小虾米数量增多后,它就被喂饱了。

代码优化的情况类似。若项目重点在于尽快无 BUG 上线,那么便可抓主要的方面而忽略小的细节,无需对代码的细节精雕细琢;然而,倘若有足够的时间来开发和维护代码,此时就必须考量每一个能够优化的细节,将一个个细小的优化点累积起来,这对代码的运行效率肯定是有提升作用的。

代码优化的目标是

减小代码的体积

提高代码运行的效率

代码优化细节

1、尽量指定类、方法的final修饰符

类被 final 修饰符修饰后是不可派生的。在 Java 核心 API 中,存在很多应用 final 的实例,像 java.lang.String ,它整个类都是 final 的。给类加上 final 修饰符,类就不可以被继承;给方法加上 final 修饰符,方法就不可以被重写。如果一个类被指定为 final ,那么这个类的所有方法都是 final 的。Java 编译器会去寻找机会内联所有的 final 方法,因为内联对提升 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 的大小很大时,就减少了许多的消耗。

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 有一个构造函数 StringBuilder(int size),此构造函数默认会分配 size 个字符的空间。

StringBuilder 构造函数接收一个字符串参数 str,默认会分配 16 个字符的空间,再加上 str 的长度个字符空间。

可以利用类(并非仅指上述的 StringBuilder)来设定其初始化容量,如此便能显著提升性能。例如 StringBuilder ,其 length 表示当前 StringBuilder 所能容纳的字符数量。当 StringBuilder 达到最大容量时,它会将自身容量提升至当前的 2 倍再加 2。无论在何时,只要 StringBuilder 达到其最大容量,它就必须创建一个新的字符数组,并把旧的字符数组内容复制到新字符数组中。这是一个非常耗费性能的操作。试想,如果可以预估到字符数组大概要存放 5000 个字符且不指定其长度。最接近 5000 的 2 的幂是 4096,暂且不考虑每次扩容加的 2 。那么:

在 4096 的基础上,接着申请 8194 个大小的字符数组,将两者相加,就相当于一次申请了 12290 个大小的字符数组。如果一开始就能够指定 5000 个大小的字符数组,那么就能够节省一倍以上的空间。

(2)把原来的4096个字符拷贝到新的的字符数组中去。

这样会浪费内存空间并且降低代码运行效率。因此,给底层以数组实现的集合和工具类设置一个合理的初始化容量是可行的,这样能带来显著的效果。不过要注意,像 HashMap 这种以数组 + 链表实现的集合,不要将初始大小设置为你估计的大小,因为一个 table 上只连接一个对象的情况几乎不存在。初始大小建议设置为 2 的 N 次幂。如果能够估计到有 2000 个元素,那么设置成 new HashMap(128)或者 new HashMap(256)都是可以的。

当需要复制大量数据的时候,会使用 System.arraycopy()这个命令。乘法和除法这两种运算会使用移位操作。

例如:

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 static final。

这毫无意义。这样做只是将引用定义为 static final ,但数组的内容依然能够随意改变。把数组声明为 public 更是存在安全漏洞,这表明这个数组能够被外部类所更改。

16、尽量在合适的场合使用单例

使用单例能够减轻加载所带来的负担,能够缩短加载所花费的时间,能够提高加载所具备的效率。然而,并非在所有的地方都适合使用单例。简单来讲,单例主要适用于以下这三个方面:

(1)控制资源的使用,通过线程同步来控制资源的并发访问

(2)控制实例的产生,以达到节约资源的目的

控制数据共享,在不构建直接关联的情况下,使多个不相关的进程能够进行通信,同时也能让多个不相关的线程实现通信。

17、尽量避免随意使用静态变量

要知道,某个对象被 static 的变量所引用时,gc 通常不会回收这个对象所占的堆内存。例如:

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如果迭代器有下一个元素,就获取下一个元素。即当迭代器的 hasNext() 方法返回 true 时,执行迭代器的 next() 方法。
}

foreach 循环的底层实现原理为迭代器 Iterator,这在 Java 语法糖 1:可变长度参数以及 foreach 循环原理中有提及。因此,“反过来,如果是顺序访问的,则使用 Iterator 会效率更高”这句话的含义是,对于那些需要顺序访问的类实例,使用 foreach 循环来进行遍历。

20、使用同步代码块替代同步方法

在多线程模块的 synchronized 锁方法块一文中,这点已经阐述得很清晰了。如果不能确定整个方法都需要进行同步,那么就应尽量使用同步代码块,这样可以避免对那些不需要同步的代码也进行同步,从而不会影响代码的执行效率。

21、将常量声明为static final,并以大写命名

在编译期间,能够将这些内容放置到常量池中,从而避免在运行期间计算并生成常量的值。同时,把常量的名字用大写来命名,这样就可以很方便地将常量与变量区分开来。

22、不要创建一些不使用的对象,不要导入一些不使用的类

如果代码中出现“局部变量 i 的值未被使用”以及“从未使用 import java.util”,那么就请删除这些无用的内容,这是毫无意义的。

23、程序运行过程中避免使用反射

关于,请参考反射。反射是 Java 为用户提供的一个功能,这个功能很强大,而功能强大通常意味着效率不高。不建议在程序运行期间使用,尤其不能频繁使用反射机制,尤其是 Method 的 invoke 方法。倘若确实有此需要,一种较为建议的做法是:在项目启动之时,将那些需通过反射加载的类,利用反射实例化出一个对象,并将其放入内存。因为用户只在意与对端交互时获取最快的响应速度,而并不关心对端的项目启动花费多长时间。

24、使用数据库连接池和线程池

这两个池的用途都是重用对象。其中一个池能够避免频繁地进行打开和关闭连接的操作;另一个池则可以避免频繁地进行创建和销毁线程的行为。

25、使用带缓冲的输入输出流进行IO操作

带缓冲的输入流有 BufferedReader 和 BufferedInputStream,带缓冲的输出流有 BufferedWriter 和 BufferedOutputStream,这样可以极大地提升 IO 效率。

顺序插入和随机访问较多的场景会使用 ArrayList。元素删除和中间插入较多的场景会使用 LinkedList。只要理解了 ArrayList 和 LinkedList 的原理,就会明白这一点。不要让 public 方法中有过多的形参。

公共方法就是向外部提供的方法。如果给这些方法赋予过多的形参,主要会有两个方面的坏处:一是可能会使方法的调用变得复杂,增加调用者的理解难度;二是可能会降低方法的灵活性和可维护性,在后续对方法进行修改或扩展时,可能会因为形参过多而带来更多的麻烦。

Java讲求一切都是对象,而过多的形参会违反面向对象的编程思想,二者并不契合。

2、参数太多势必导致方法调用的出错概率增加

至于“太多”具体指多少个呢,大概 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 (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 中,“if (i = 1)”这种语法不会出现。因为若写了这样的语法,Java 就会进行编译并报错,报错信息为“Type mismatch: cannot convert from int to boolean”。但是,Java 的“if (i == 1)”与“if (1 == i)”在语义方面不存在任何差别。然而,从阅读习惯的角度来说,建议采用前者会更为适宜。

30、不要对数组使用toString()方法

看一下对数组使用toString()打印出来的是什么:

public static void main(String[] args)
{ int[] is = new int[]{1, 2, 3};

输出 is 对象的字符串表示形式。具体来说,就是打印出 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; longSystem.currentTimeMillis() 的值被赋值给了 startTime 。for (int j = 0; j < loopTime; j++)
{
定义一个字符串变量 str,将整数 i 转换为字符串后赋值给 str 。
}
System.out.println("String.valueOf():"系统当前时间(System.currentTimeMillis())减去开始时间(startTime)的结果再加上。"ms");
System.currentTimeMillis() 的值被赋值给了 startTime 。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()方法。至于为什么,很简单:

1String.valueOf()方法在底层调用了 Integer.toString()方法,并且在调用之前会进行空判断。

2直接调用了 Integer.toString()方法,关于它就不再提及了。

3底层使用 StringBuilder 来实现。首先使用 append 方法进行拼接,然后通过 toString()方法获取字符串。

三者对比下来,明显是2最快、1次之、3最慢

34、使用最有效率的方式去遍历Map

遍历 Map 有多种方式,一般情况下我们的需求是遍历 Map 中的 Key 和 Value,在此情形下,推荐且效率最高的方式是:

public static void main(String[] args)
{

HashMap hm = new HashMap();
hm.put("111", "222");Set> entrySet = hm.entrySet();
Iteratoriter 等于 entrySet 的迭代器;while (iter.hasNext())
{
Map.Entry entry = iter.next();
System.out.println 输出 entry 的键,也就是 entry.getKey(),然后接着输出一个空格和一个加号以及一个空格,为后续可能的输出做准备。" " + 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() 抛出异常,就会进入 cath 块,此时 YYY.close() 不会执行,YYY 的资源就无法回收,会一直被占用。这样的代码多了,是有可能导致资源句柄泄露的。改为上面的写法后,就能够确保无论怎样,XXX 和 YYY 都会被关闭掉。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

25阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

27阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

23阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读