作者 | Henley\
起源 | https://henleylee.github.io/p…
前言
代码优化,一个很重要的课题。可能有些人感觉没用,一些细小的中央有什么好批改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么思考的,就像大海外面的鲸鱼一样,它吃一条小虾米有用吗?没用,然而,吃的小虾米一多之后,鲸鱼就被喂饱了。
代码优化也是一样,如果我的项目着眼于尽快无 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.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 为例:
- StringBuilder() // 默认调配 16 个字符的空间
- StringBuilder(int size) // 默认调配 size 个字符的空间
- StringBuilder(String str) // 默认调配 16 个字符 +str.length() 个字符空间
能够通过类(这里指的不仅仅是下面的 StringBuilder)的来设定它的初始化容量,这样能够显著地晋升性能。比方 StringBuilder 吧,length 示意以后的 StringBuilder 能放弃的字符数量。因为当 StringBuilder 达到最大容量的时候,它会将本身容量减少到以后的 2 倍再加 2,无论何时只有 StringBuilder 达到它的最大容量,它就不得不创立一个新的字符数组而后将旧的字符数组内容拷贝到新字符数组中—- 这是非常消耗性能的一个操作。试想,如果能预估到字符数组中大略要寄存 5000 个字符而不指定长度,最靠近 5000 的 2 次幂是 4096,每次扩容加的 2 不论,那么:
- 在 4096 的根底上,再申请 8194 个大小的字符数组,加起来相当于一次申请了 12290 个大小的字符数组,如果一开始能指定 5000 个大小的字符数组,就节俭了一倍以上的空间;
- 把原来的 4096 个字符拷贝到新的的字符数组中去。
这样,既节约内存空间又升高代码运行效率。所以,给底层以数组实现的汇合、工具类设置一个正当的初始化容量是错不了的,这会带来空谷传声的成果。然而,留神,像 HashMap 这种是以数组 + 链表实现的汇合,别把初始大小和你预计的大小设置得一样,因为一个 table 上只连贯一个对象的可能性简直为 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 对象援用存在,count 很大的话,就消耗内存了,倡议为改为:
Object obj = null;
for (int i = 0; i <= count; i++) {obj = new Object();
}
这样的话,内存中只有一份 Object 对象援用,每次 new Object() 的时候,Object 对象援用指向不同的 Object 罢了,然而内存中只有一份,这样就大大节俭了内存空间了。
13、基于效率和类型查看的思考,应该尽可能应用 array,无奈确定数组大小时才应用 ArrayList
14、尽量应用 HashMap、ArrayList、StringBuilder,除非线程平安须要,否则不举荐应用 Hashtable、Vector、StringBuffer,后三者因为应用同步机制而导致了性能开销
15、不要将数组申明为 public static final
因为这毫无意义,这样只是定义了援用为 static final,数组的内容还是能够随便扭转的,将数组申明为 public 更是一个安全漏洞,这意味着这个数组能够被外部类所扭转。
16、尽量在适合的场合应用单例
应用单例能够加重加载的累赘、缩短加载的工夫、进步加载的效率,但并不是所有中央都实用于单例,简略来说,单例次要实用于以下三个方面:
- 管制资源的应用,通过线程同步来管制资源的并发拜访
- 管制实例的产生,以达到节约资源的目标
- 控制数据的共享,在不建设间接关联的条件下,让多个不相干的过程或线程之间实现通信
17、尽量避免随便应用动态变量
要晓得,当某个对象被定义为 static 的变量所援用,那么 GC 通常是不会回收这个对象所占有的堆内存的,如:
public class A {private static B b = new B();
}
此时动态变量 b 的生命周期与 A 类雷同,如果 A 类不被卸载,那么援用 B 指向的 B 对象会常驻内存,直到程序终止。
18、及时革除不再须要的会话
为了革除不再流动的会话,许多应用服务器都有默认的会话超时工夫,个别为 30 分钟。当应用服务器须要保留更多的会话时,如果内存不足,那么操作系统会把局部数据转移到磁盘,应用服务器也可能依据 MRU(最近最频繁应用)算法把局部不沉闷的会话转储到磁盘,甚至可能抛出内存不足的异样。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很低廉的。因而,当会话不再须要时,该当及时调用 HttpSession 的 invalidate() 办法革除会话。
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();
}
}
循环的底层实现原理就是迭代器 Iterator,参见 Java 语法糖 1:可变长度参数以及 foreach 循环原理。所以后半句“反过来,如果是程序拜访的,则应用 Iterator 会效率更高”的意思就是程序拜访的那些类实例,应用 foreach 循环去遍历。
20、应用同步代码块代替同步办法
这点在多线程模块中的 synchronized 锁办法块一文中曾经讲得很分明了,除非能确定一整个办法都是须要进行同步的,否则尽量应用同步代码块,防止对那些不须要进行同步的代码也进行了同步,影响了代码执行效率。
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 效率。
26、程序插入和随机拜访比拟多的场景应用 ArrayList,元素删除和两头插入比拟多的场景应用 LinkedList 这个,了解 ArrayList 和 LinkedList 的原理就晓得了
27、不要让 public 办法中有太多的形参
public 办法即对外提供的办法,如果给这些办法太多形参的话次要有两点害处:
- 违反了面向对象的编程思维,Java 讲求一切都是对象,太多的形参,和面向对象的编程思维并不符合
- 参数太多势必导致办法调用的出错概率减少
至于这个“太多”指的是多少个,3、4 个吧。比方咱们用 JDBC 写一个 insertStudentInfo() 办法,有 10 个学生信息字段要插入 Student 表中,能够把这 10 个参数封装在一个实体类中,作为 insert() 办法的形参。
28、字符串变量和字符串常量 equals 的时候将字符串常量写在后面
这是一个比拟常见的小技巧了,如果有以下代码:
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 示意 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) 在语义上没有任何区别,然而从浏览习惯上讲,倡议应用前者会更好些。
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 掉它们,那么将会造成这个专用汇合一直增大,使得零碎有内存泄露的隐患。
33、把一个根本数据类型转为字符串,“根本数据类型.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.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() 办法。至于为什么,很简略:
- String.valueOf() 办法底层调用了 Integer.toString() 办法,然而会在调用前做空判断
-
Integer.toString() 办法就不说了,间接调用了
i + “” 底层应用了 StringBuilder 实现,先用 append() 办法拼接,再用 toString() 办法获取字符串
三者比照下来,显著是 2 最快、1 次之、3 最慢
34、应用最有效率的形式去遍历 Map
遍历 Map 的形式有很多,通常场景下咱们须要的是遍历 Map 中的 Key 和 Value,那么举荐应用的、效率最高的形式是:
public static void main(String[] args) {HashMap<String, String> map = new HashMap<String, String>(); map.put("111", "222"); Set<Map.Entry<String, String>> entrySet = map.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 的 key 值,那用 Set keySet = map.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 都会被 close 掉。
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿 (2021 最新版)
2. 别在再满屏的 if/ else 了,试试策略模式,真香!!
3. 卧槽!Java 中的 xx ≠ null 是什么新语法?
4.Spring Boot 2.6 正式公布,一大波新个性。。
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!