서문
코드 최적화는 매우 중요한 주제입니다. 어떤 사람들은 그것이 쓸모없다고 생각할 수도 있습니다. 수정할 수 있는 작은 것들은 무엇입니까? 수정이 코드의 실행 효율성에 어떤 영향을 미치나요? 저는 이런 질문에 대해 이렇게 생각합니다. 바다의 고래처럼 작은 새우를 먹는 것이 도움이 될까요? 소용없었는데 새우를 더 먹다보니 고래가 배불러요. 코드 최적화도 마찬가지입니다. 프로젝트가 최대한 빨리 버그 없이 출시하는 데 중점을 둔다면 이때는 큰 것에 집중하고 작은 것을 버릴 수 있으며, 코드의 세부 사항을 다듬을 필요가 없습니다. ; 하지만 코드를 개발하고 유지 관리할 시간이 충분하다면 이때 모든 측면을 고려해야 합니다. 최적화할 수 있는 세부 사항이 하나씩 쌓이면 코드의 실행 효율성이 확실히 향상됩니다.
코드 최적화의 목표는 다음과 같습니다.
1. 코드 크기 줄이기
2. 코드 실행 효율성 향상
코드 최적화 세부정보
1. 클래스와 메소드의 최종 수식어를 지정해 보세요.
최종 수식어가 있는 클래스는 파생될 수 없습니다. Java 핵심 API에는 전체 클래스가 최종인 java.lang.String과 같은 최종 애플리케이션의 예가 많이 있습니다. 클래스에 final 수정자를 지정하면 클래스가 상속되지 않으며, 메서드에 final 수정자를 지정하면 메서드가 재정의되지 않습니다. 클래스가 final로 지정되면 해당 클래스의 모든 메서드가 final이 됩니다. Java 컴파일러는 모든 최종 메소드를 인라인할 수 있는 기회를 찾습니다. 인라인은 Java 실행 효율성을 향상시키는 데 중요한 역할을 합니다. 이를 통해 성능을 평균 50% 향상할 수 있습니다.
2. 객체 재사용을 시도하세요
특히 String 객체를 사용하는 경우 문자열 연결이 발생할 때 StringBuilder/StringBuffer를 대신 사용해야 합니다. JVM(Java Virtual Machine)은 객체를 생성하는 데 시간을 소비할 뿐만 아니라 향후 이러한 객체를 가비지 수집하고 처리하는 데에도 시간을 소비해야 할 수 있습니다. 따라서 너무 많은 객체를 생성하면 프로그램 성능에 큰 영향을 미칩니다.
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个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:
(1)在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间
(2)把原来的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、尽量在合适的场合使用单例
使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
(1)控制资源的使用,通过线程同步来控制资源的并发访问
(2)控制实例的产生,以达到节约资源的目的
(3)控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信
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()} }
foreach循环的底层实现原理就是迭代器Iterator,参见Java语法糖1:可变长度参数以及foreach循环原理。所以后半句”反过来,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。
20. 동기화된 메서드 대신 동기화된 코드 블록을 사용하세요
이는 전체 메서드를 동기화해야 한다고 결정되지 않는 한 다중 스레드 모듈의 동기화된 잠금 메서드 블록 문서에 명확하게 명시되어 있습니다. 그렇지 않으면 , 동기화할 필요가 없는 코드의 동기화를 피하기 위해 동기화된 코드 블록을 사용하십시오. 이는 코드 실행 효율성에 영향을 미칩니다.
21. 상수를 static final로 선언하고 이름을 대문자로 지정합니다.
이렇게 하면 런타임 중에 상수 값을 계산하지 않도록 컴파일 중에 이러한 내용을 상수 풀에 넣을 수 있습니다. 또한, 상수 이름을 대문자로 지정하면 상수와 변수를 더 쉽게 구별할 수 있습니다
22. 사용하지 않는 객체를 생성하지 말고 사용하지 않는 클래스를 가져오지 마세요
의미가 없습니다. 코드에 "i 로컬 변수의 값이 사용되지 않습니다." 또는 "import java.util이 사용되지 않습니다."라고 나오는데, 쓸모없는 내용은 삭제해주세요
23. 프로그램 실행 중에는 사용을 피하세요. 반사
반사에 대해서는 반사를 참조하세요. Reflection은 Java가 사용자에게 제공하는 매우 강력한 기능입니다. 강력한 기능은 종종 효율성이 낮다는 것을 의미합니다. 프로그램 실행 중에는 리플렉션 메커니즘을 사용하지 않는 것이 좋습니다. 특히 리플렉션 메커니즘, 특히 메서드의 호출 메서드를 자주 사용하는 경우에는 필요한 클래스에 리플렉션 인스턴스를 사용하는 것이 좋습니다. 프로젝트가 시작될 때 리플렉션을 통해 로드됩니다. 객체를 생성하여 메모리에 넣습니다. 사용자는 피어와 상호 작용할 때 가장 빠른 응답을 얻는 것에만 관심이 있고 피어의 프로젝트가 시작하는 데 걸리는 시간에는 관심이 없습니다. .
24. 데이터베이스 연결 풀과 스레드 풀 사용
이 두 풀은 개체를 재사용하는 데 사용됩니다. 전자는 연결을 자주 열고 닫는 것을 피할 수 있고, 후자는 빈번한 연결 생성을 피할 수 있습니다. 스레드
25. IO 작업에 버퍼링된 입력 및 출력 스트림 사용
버퍼링된 입력 및 출력 스트림, 즉 BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream은 IO 효율성을 크게 향상시킬 수 있습니다
26. 순차삽입과 랜덤접속이 많은 시나리오에서는 ArrayList를 사용하고, 요소삭제와 중간삽입이 많은 시나리오에서는 LinkedList를 사용하세요.
ArrayList와 LinkedList의 원리를 이해하고 있다면 됩니다.
27. 퍼블릭 메소드에 너무 많은 형식 매개변수를 사용하지 마세요
퍼블릭 메소드는 외부에 제공되는 메소드입니다. 이러한 메소드에 너무 많은 형식 매개변수를 제공하면 두 가지 주요 단점이 있습니다. :
1. 객체지향 프로그래밍의 개념에 위배됩니다. Java는 모든 것이 객체임을 강조합니다. 형식 매개변수가 너무 많으면 객체지향 프로그래밍의 개념과 일치하지 않습니다.
2. 매개변수가 너무 많으면 필연적으로 메소드 호출이 발생합니다.
"너무 많다"는 것은 3~4개 정도를 의미합니다. 예를 들어 JDBC를 사용하여 insertStudentInfo 메소드를 작성합니다. Student 테이블에 삽입할 학생 정보 필드는 10개입니다. 이 10개의 매개변수는 엔터티 클래스에 캡슐화되어 삽입 메소드의 형식 매개변수로 사용될 수 있습니다.
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
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、公用的集合类中不使用的数据一定要及时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()方法。至于为什么,很简单:
1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断
2、Integer.toString()方法就不说了,直接调用了
3、i + “”底层使用了StringBuilder实现,先用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"); 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() + "\t" + entry.getValue()); } }
如果你只是想遍历一下这个Map的key值,那用”Set
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都会被close掉。