代码优化的重要性与细节:如何减小代码体积并提高运行效率

[id_604406[id_165[id_167[id_1930446838]634996]672846]53]

代码优化的重要性与细节:如何减小代码体积并提高运行效率

[id_462260193]

代码优化是一个很重要的课题。有些人或许认为它没用,觉得一些细小的地方无需修改,也不清楚改与不改对代码运行效率的影响。我是这样考虑这个问题的:就如同大海中的鲸鱼,吃一条小虾米能有什么用呢?确实没什么用。然而,当鲸鱼吃了很多小虾米之后,它就被喂饱了。代码优化的情况类似。若项目注重尽快无 BUG 上线,那么便可抓主要的,舍弃次要的,代码的细节无需过于精细打磨;然而,倘若有足够的时间来开发和维护代码,此时就必须考量每一个可优化的细节,将一个个细小的优化点累积起来,这对代码的运行效率肯定是有提升的。

[id_611466886]

1、减小代码的体积

2、提高代码运行的效率

代码优化细节

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 构造函数中,有一个参数为 String 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()这个命令。

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 这三个类。因为后三者使用了同步机制,从而导致了性能开销。

不要把数组声明为具有公共的、静态的且是最终的属性。不要将数组定义为具有公共的、静态的并且是不可更改的。不要把数组声明为公共的、静态的以及最终的形式。不要将数组声明为具备公共性、静态性和最终性。不要把数组声明为公共、静态且最终的状态。不要将数组声明为具有公共、静态和不可变的特性。不要把数组声明为公共、静态以及不可更改的形式。

这毫无意义。这样做只是将引用定义为 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如果迭代器有下一个元素,那么就获取下一个元素。
}

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

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

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

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

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

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

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

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

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

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

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

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

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

在顺序插入和随机访问较多的场景会使用 ArrayList,而在元素删除和中间插入较多的场景则会使用 LinkedList。

理解了 ArrayList 和 LinkedList 的原理,就能明白这个了。

27、不要让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 (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 判断里面的内容因为 i 被赋值为 1 而变为非 0 ,所以会返回 true 。但实际上 i 为 2 ,比较的值是 1 ,本应返回 false 。这种情况在 C/C++ 的开发中很可能出现,并且会引发一些难以理解的错误。因此,为避免开发者在 if 语句中进行不正确的赋值操作,建议把 if 语句写成:

int i = 2;if (1 == i) { ... }else{ ... }

这样,若开发者不慎写成了“1 = i”,C/C++编译器能够即刻检查出来。因为我们能够给一个变量赋值为 1 ,但不能给一个常量赋值为 i 。

在 Java 中,不可能出现 C/C++那样“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};
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; longSystem.currentTimeMillis() 被赋值给了 startTime 。for (int j = 0; j < loopTime; j++)
{
String str = String.valueOf(i);
}
System.out.println("String.valueOf():"系统当前时间(System.currentTimeMillis())减去开始时间(startTime)的结果加上。"ms");
系统当前时间的毫秒数被赋值给了 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()方法。原因很简单:

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

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

底层使用了 StringBuilder 来实现 i + “” 。首先使用 append 方法进行拼接,然后通过 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");SetString, String>> entrySet = hm.entrySet();
IteratorString, String迭代器 iter 被创建,用于遍历 entrySet 中的元素。while (iter.hasNext())
{
Map.Entry<String, String> entry = iter.next();
输出 entry 的键值,即 entry 的键与一个空字符串进行拼接后输出。具体来说,就是将 entry 的键与一个空字符串通过加号连接起来并输出。"\t" + entry.getValue());
}
}

如果你只是想要遍历这个 Map 的 key 值,那么使用“Set keySet = hm.keySet();”这种方式会更为恰当一些。

35、对资源的close()建议分开操作

意思是,比如我有这么一段代码:

try{
XXX.close();
YYY.close();
}catch (Exception e)
{...}

建议修改为:

tryXXX 执行关闭操作;如果出现异常 e ,则... }tryYYY 执行关闭操作;如果出现异常 e,则执行以下操作:... }

有些麻烦,但能避免资源泄露。我们思考,若没有修改过的代码,一旦 XXX.close()抛出异常,就会进入 cath 块,此时 YYY.close()不会被执行,YYY 所占用的资源就无法回收,一直被占用着。这样的代码多了,是有可能导致资源句柄泄露的。将其改为如下写法后,就能确保无论怎样,XXX 和 YYY 都会被关闭。

来自:

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

相关推荐

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

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

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

2026年09月06日

24阅读

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

2026年09月06日

28阅读

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

2026年09月06日

26阅读

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

2026年09月06日

26阅读