1:非空判斷
錯誤例子:
if(user.getUserName().equals("hollis")){ }
這段代碼極有可能在實際運行的時候跑出NullPointerException。無論是user本身為空,還是user.getUserName()為空,都會拋出異常。 所以,在調用一個參數時要確保他是非空的。
上面的代碼可以改為:
if(user!=null&&"hollis".equals(user.getUserName())){ }
2:用StringBuffer代替String
在循環中構建一個String對象時從性能上講使用StringBuffer來代替String對象 例如:
// This is bad String s = ""; for (int i = 0; i < field.length; ++i) { s = s + field[i]; }
應該改為StringBuffer,使用append方法:
StringBuffer buf = new StringBuffer(); for (int i = 0; i < field.length; ++i) { buf.append(field[i]); } String s = buf.toString();
3:盡量減少對變量的重復計算
明確一個概念,對方法的調用,即使方法中只有一句語句,也是有消耗的,包括創建棧幀、調用方法時保護現場、調用方法完畢時恢復現場等。所以例如下面的操作:
for (int i = 0; i < list.size(); i++){
...
}
建議替換為:
for (int i = 0, length = list.size(); i < length; i++){ ... }
這樣,在list.size()很大的時候,就減少了很多的消耗
4:盡量采用懶加載的策略,即在需要的時候才創建
例如:
String str = "aaa"; if (i == 1) { list.add(str); }
建議替換為:
if (i == 1) { String str = "aaa"; list.add(str); }
5:慎用異常
異常對性能不利。拋出異常首先要創建一個新的對象,Throwable接口的構造函數調用名為 fillInStackTrace()的本地同步方法,fillInStackTrace()方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,Java虛擬機就必須調整調用堆棧,因為在處理過程中創建了一個新的對象。異常只能用於錯誤處理,不應該用來控制程序流程。
6:不要在循環中使用try…catch…,應該把其放在最外層
7:如果能估計到待添加的內容長度,為底層以數組方式實現的集合、工具類指定初始長度
比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder為例:
StringBuilder() // 默認分配16個字符的空間
StringBuilder(int size) // 默認分配size個字符的空間
StringBuilder(String str) // 默認分配16個字符+str.length()個字符空間
8:當復制大量數據時,使用 System.arraycopy()命令
9:乘法和除法使用移位操作
例如:
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; }
移位操作雖然快,但是可能會使代碼不太好理解,因此最好加上相應的注釋。
10:循環內不要不斷創建對象引用
例如:
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罷了,但是內存中只有一份,這樣就大大節省了內存空間了。
11:不要將數組聲明為public static final
因為這毫無意義,這樣只是定義了引用為static final,數組的內容還是可以隨意改變的,將數組聲明為public更是一個安全漏洞,這意味着這個數組可以被外部類所改變
12:盡量在合適的場合使用單例
使用單例可以減輕加載的負擔、縮短加載的時間、提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:
控制資源的使用,通過線程同步來控制資源的並發訪問
控制實例的產生,以達到節約資源的目的
控制數據的共享,在不建立直接關聯的條件下,讓多個不相關的進程或線程之間實現通信
13:盡量避免隨意使用靜態變量
要知道,當某個對象被定義為static的變量所引用,那么gc通常是不會回收這個對象所占有的堆內存的,如:
public class A { private static B b = new B(); }
此時靜態變量b的生命周期與A類相同,如果A類不被卸載,那么引用B指向的B對象會常駐內存,直到程序終止
14:使用同步代碼塊替代同步方法
分析:
//下列兩個方法有什么區別 public synchronized void method1(){} public void method2(){ synchronized (obj){} }
synchronized用於解決同步問題,當有多條線程同時訪問共享數據時,如果不進行同步,就會發生錯誤,java提供的解決方案是:只要將操作共享數據的語句在某一時段讓一個線程執行完,在執行過程中,其他線程不能進來執行可以。解決這個問題。這里在用synchronized時會有兩種方式,一種是上面的同步方法,即用synchronized來修飾方法,另一種是提供的同步代碼塊。
這里總感覺怪怪的,這兩種方法有什么區別呢?讓我們看下代碼:
public class SynObj { public synchronized void methodA() { System.out.println("methodA....."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } public void methodB() { synchronized(this) { System.out.pritntln("methodB....."); } } public void methodC() { String str = "sss"; synchronized (str) { System.out.println("methodC....."); } } } public class TestSyn { public static void main(String[] args) { final SynObj obj = new SynObj(); Thread t1 = new Thread(new Runnable() { @Override public void run() { obj.methodA(); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { obj.methodB(); } }); t2.start(); Thread t3 = new Thread(new Runnable() { @Override public void run() { obj.methodC(); } }); t3.start(); } }
這段小代碼片段打印結果如下:
methodA.....
methodC.....
//methodB會隔一段時間才會打印出來
methodB.....
這段代碼的打印結果是,methodA…..methodC…..會很快打印出來,methodB…..會隔一段時間才打印出來,那么methodB為什么不能像methodC那樣很快被調用呢?
在啟動線程1調用方法A后,接着會讓線程1休眠5秒鍾,這時會調用方法C,注意到方法C這里用synchronized進行加鎖,這里鎖的對象是str這個字符串對象。但是方法B則不同,是用當前對象this進行加鎖,注意到方法A直接在方法上加synchronized,這個加鎖的對象是什么呢?顯然,這兩個方法用的是一把鎖。
*由這樣的結果,我們就知道這樣同步方法是用什么加鎖的了,由於線程1在休眠,這時鎖還沒釋放,導致線程2只有在5秒之后才能調用方法B,由此,可知兩種加鎖機制用的是同一個鎖對象,即當前對象。
另外,同步方法直接在方法上加synchronized實現加鎖,同步代碼塊則在方法內部加鎖,很明顯,同步方法鎖的范圍比較大,而同步代碼塊范圍要小點,一般同步的范圍越大,性能就越差,一般需要加鎖進行同步的時候,肯定是范圍越小越好,這樣性能更好*。
同步代碼塊可以用更細粒度的控制鎖,比如:
public class Test{ private String name = "xiaoming"; private String id = "0753"; public void setName(String name) { synchornized(name) { this.name = name; } } public void setId(String id) { synchornized(id) { this.id = id; } } }
如果你有一個Test對象 你想在多線程下同時修改Name和id, 如果你兩個set方法都聲明為同步方法,那么在同一時間只能修改name或者id. 但是這兩個是可以同時修改的,所以你需要同步代碼塊,將信號量分別設置成name和id.
15:將常量聲明為static final,並以大寫命名
這樣在編譯期間就可以把這些內容放入常量池中,避免運行期間計算生成常量的值。另外,將常量的名字以大寫命名也可以方便區分出常量與變量
16:不要創建一些不使用的對象,不要導入一些不使用的類
這毫無意義,如果代碼中出現”The value of the local variable i is not used”、”The import java.util is never used”,那么請刪除這些無用的內容
17:程序運行過程中避免使用反射
反射是Java提供給用戶一個很強大的功能,功能強大往往意味着效率不高。不建議在程序運行過程中使用尤其是頻繁使用反射機制,特別是Method的invoke方法,如果確實有必要,一種建議性的做法是將那些需要通過反射加載的類在項目啟動的時候通過反射實例化出一個對象並放入內存—-用戶只關心和對端交互的時候獲取最快的響應速度,並不關心對端的項目啟動花多久時間。
18:使用帶緩沖的輸入輸出流進行IO操作
帶緩沖的輸入輸出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,這可以極大地提升IO效率。
19:不要讓public方法中有太多的形參
public方法即對外提供的方法,如果給這些方法太多形參的話主要有兩點壞處:
違反了面向對象的編程思想,Java講求一切都是對象,太多的形參,和面向對象的編程思想並不契合
參數太多勢必導致方法調用的出錯概率增加
至於這個”太多”指的是多少個,3、4個吧。比如我們用JDBC寫一個insertStudentInfo方法,有10個學生信息字段要插如Student表中,可以把這10個參數封裝在一個實體類中,作為insert方法的形參。
20:公用的集合類中不使用的數據一定要及時remove掉
如果一個集合類是公用的(也就是說不是方法里面的屬性),那么這個集合里面的元素是不會自動釋放的,因為始終有引用指向它們。所以,如果公用集合里面的某些數據不使用而不去remove掉它們,那么將會造成這個公用集合不斷增大,使得系統有內存泄露的隱患。
21:把一個基本數據類型轉為字符串
基本數據類型.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最慢。
22:使用最有效率的方式去遍歷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() + " " + entry.getValue()); } }
如果你只是想遍歷一下這個Map的key值,那用”Set<String> keySet = hm.keySet();”會比較合適一些。
23:對資源的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掉。
24: 切記以常量定義的方式替代魔鬼數字,魔鬼數字的存在將極大地降低代碼可讀性,字符串常量是否使用常量定義可以視情況而定
25: long或者Long初始賦值時,使用大寫的L而不是小寫的l,因為字母l極易與數字1混淆,這個點非常細節,值得注意
26:靜態類、單例類、工廠類將它們的構造函數置為private
這是因為靜態類、單例類、工廠類這種類本來我們就不需要外部將它們new出來,將構造函數置為private之后,保證了這些類不會產生實例對象。
27: 避免Random實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一seed 導致的性能下降,JDK7之后,可以使用ThreadLocalRandom來獲取隨機數
解釋一下競爭同一個seed導致性能下降的原因,比如,看一下Random類的nextInt()方法實現:
public int nextInt() { return next( 32 ); } //調用了next(int bits)方法,這是一個受保護的方法: protected int next( int bits) { long oldseed, nextseed; AtomicLong seed = this .seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return ( int )(nextseed >>> ( 48 - bits)); } //而這邊的seed是一個全局變量: /** * The internal state associated with this pseudorandom number generator. * (The specs for the methods in this class describe the ongoing * computation of this value.) */ private final AtomicLong seed;
多個線程同時獲取隨機數的時候,會競爭同一個seed,導致了效率的降低。:
28:jvm對常量為-128~127的整數進行緩存。

打印結果為false,而下面結果為true為啥

解答:Integer類型當正整數小於128時是在內存棧中創建值的,並將對象指向這個值,這樣當比較兩個棧引用時因為是同一地址引用兩者則相等。當大於127時將會調用new Integer(),兩個整數對象地址引用不相等了。這就是為什么當值為128時不相等,當值為100時相等了(java自己有個常量緩沖池 存着 -128~127的整數 )。
29:金額運算
@Test public void test4() { double num1 = 0.02d; double num2 = 0.03d; double num3 = num2 - num1; System.out.println(num3); }
console結果: 0.009999999999999998
為什么會這樣呢? 因為float和double都是浮點數, 都有取值范圍, 都有精度范圍. 浮點數與通常使用的小數不同, 使用中, 往往難以確定. 常見的問題是定義了一個浮點數, 經過一系列的計算, 它本來應該等於某個確定值, 但實際上並不是! 金額必須是完全精確的計算, 故不能使用double或者float, 而應該采用java.math.BigDecimal.
使用BigDecimal的add, substract, multiply和divide做加減乘除, 用compareTo方法比較大小
@Test public void test4() { BigDecimal num1 = new BigDecimal("0.02"); BigDecimal num2 = new BigDecimal("0.03"); //加 System.out.println(num1.add(num2)); //減 System.out.println(num2.subtract(num1)); //乘 System.out.println(num1.multiply(num2)); //除 System.out.println(num1.divide(num2, RoundingMode.HALF_UP)); BigDecimal num3 = new BigDecimal("0.03"); if(num3.compareTo(BigDecimal.ZERO) == -1) { System.out.println("num3 小於0"); }else if(num3.compareTo(BigDecimal.ZERO) == 1) { System.out.println("num3大於0"); }else if(num3.compareTo(BigDecimal.ZERO) == 1) { System.out.println("num3等於0"); } BigDecimal num4 = new BigDecimal("0.1234567"); //其中setScale的第一個參數是小數位數, 這個示例是保留2位小數, 后面是四舍五入規則. System.out.println("num4:" + num4.setScale(2, BigDecimal.ROUND_UP)); }
console結果:
0.05
0.01
0.0006
0.67
num3大於0
num4:0.13
30:oracle獲取時間的坑
執行:select to_char(sysdate, 'yyyymmddhhmmss'),to_char(sysdate, 'yyyyMMddHH24mmss'),to_char(sysdate, 'yyyyMMddHH24MISS') from dual;
輸出:

oracle中的日期格式為:
yyyy-MM-dd HH24:mi:ss和 yyyy-MM-dd HH:mi:ss,分別代表oracle中的24小時制和12小時制
java中的的日期格式為:
yyyy-MM-dd HH:mm:ss:代表將時間轉換為24小時制,例: 2018-06-27 15:24:21
yyyy-MM-dd hh:mm:ss:代表將時間轉換為12小時制,例: 2018-06-27 03:24:21
之所以 oracle和java不同,是因為我們知道oracle是不區分大小寫的,所以java中根據大小寫來代表24小時和12小時的表達式在oracle中就會出問題,oracle中將24小時的小時和分鍾做了特殊處理.如上所示,在hh后面加上了24,將mm改為了mi,而一旦不注意取到的時間就會出問題!
oracle中yyyyMMddHH24mmss其中yyyyMMddHH24mmss中的mm會返回月份,不能喝java的混淆
結論:oralce取當前時間用 to_char(sysdate, 'yyyyMMddHH24MISS')
31: 獲取時間差
阿里巴巴手冊建議:

計算兩段代碼時間差,很多同學公司的代碼是采用以下這種方式。
long startTime = System.currentTimeMillis(); // 執行代碼 long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime);
這種方式並不是不行。按照“能跑就行”的原則,這段代碼,肯定是能用的!但是這並不是最佳實踐,為何? 我們先來看一下JDK中的注釋

我們來看另外一種方式。
long startTime = System.nanoTime(); // 執行代碼 long endTime = System.nanoTime(); System.out.println(endTime - startTime);
我們再來看看注釋:

32: 需要 Map 的主鍵和取值時,應該迭代 entrySet()
當循環中只需要 Map 的主鍵時,迭代 keySet() 是正確的。但是,當需要主鍵和取值時,迭代 entrySet() 才是更高效的做法,比先迭代 keySet() 后再去 get 取值性能更佳。
反例:
Map<String, String> map = ...; for (String key : map.keySet()) { String value = map.get(key); ... }
正例:
Map<String, String> map = ...; for (Map.Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); ... }
33: 應該使用Collection.isEmpty()檢測空
使用 Collection.size() 來檢測空邏輯上沒有問題,但是使用 Collection.isEmpty()使得代碼更易讀,並且可以獲得更好的性能。任何 Collection.isEmpty() 實現的時間復雜度都是 O(1) ,但是某些 Collection.size() 實現的時間復雜度可能是 O(n) 。
反例:
if (collection.size() == 0) { ... }
正例:
if (collection.isEmpty()) { ... }
如果需要還需要檢測 null ,可采用 CollectionUtils.isEmpty(collection) 和 CollectionUtils.isNotEmpty(collection)。
34: List 的隨機訪問
大家都知道數組和鏈表的區別:數組的隨機訪問效率更高。當調用方法獲取到 List 后,如果想隨機訪問其中的數據,並不知道該數組內部實現是鏈表還是數組,怎么辦呢?可以判斷它是否實現* RandomAccess *接口。
正例:
// 調用別人的服務獲取到list List<Integer> list = otherService.getList(); if (list instanceof RandomAccess) { // 內部數組實現,可以隨機訪問 System.out.println(list.get(list.size() - 1)); } else { // 內部可能是鏈表實現,隨機訪問效率低 }
35: 頻繁調用 Collection.contains 方法請使用 Set
在 java 集合類庫中,List 的 contains 方法普遍時間復雜度是 O(n) ,如果在代碼中需要頻繁調用 contains 方法查找數據,可以先將 list 轉換成 HashSet 實現,將 O(n) 的時間復雜度降為 O(1) 。
反例:
ArrayList<Integer> list = otherService.getList(); for (int i = 0; i <= Integer.MAX_VALUE; i++) { // 時間復雜度O(n) list.contains(i); }
正例:
ArrayList<Integer> list = otherService.getList(); Set<Integer> set = new HashSet(list); for (int i = 0; i <= Integer.MAX_VALUE; i++) { // 時間復雜度O(1) set.contains(i); }
36: 不要使用魔法值
當你編寫一段代碼時,使用魔法值可能看起來很明確,但在調試時它們卻不顯得那么明確了。這就是為什么需要把魔法值定義為可讀取常量的原因。但是,-1、0 和 1不被視為魔法值。
反例:
for (int i = 0; i < 100; i++){ ... } if (a == 100) { ... }
正例:
private static final int MAX_COUNT = 100; for (int i = 0; i < MAX_COUNT; i++){ ... } if (count == MAX_COUNT) { ... }
37: 不要使用集合實現來賦值靜態成員變量
對於集合類型的靜態成員變量,不要使用集合實現來賦值,應該使用靜態代碼塊賦值。
反例:
private static Map<String, Integer> map = new HashMap<String, Integer>() { { put("a", 1); put("b", 2); } }; private static List<String> list = new ArrayList<String>() { { add("a"); add("b"); } };
正例:
private static Map<String, Integer> map = new HashMap<>(); static { map.put("a", 1); map.put("b", 2); }; private static List<String> list = new ArrayList<>(); static { list.add("a"); list.add("b"); };
38: 工具類應該屏蔽構造函數
工具類是一堆靜態字段和函數的集合,不應該被實例化。但是,Java 為每個沒有明確定義構造函數的類添加了一個隱式公有構造函數。所以,為了避免 java "小白"使用有誤,應該顯式定義私有構造函數來屏蔽這個隱式公有構造函數。
反例:
public class MathUtils { public static final double PI = 3.1415926D; public static int sum(int a, int b) { return a + b; } }
正例:
public class MathUtils { public static final double PI = 3.1415926D; private MathUtils() {} public static int sum(int a, int b) { return a + b; } }
39: 使用String.valueOf(value)代替""+value
當要把其它對象或類型轉化為字符串時,使用 String.valueOf(value) 比""+value 的效率更高。
反例:
int i = 1; String s = "" + i;
正例:
int i = 1; String s = String.valueOf(i);
40: 枚舉的屬性字段必須是私有不可變
枚舉通常被當做常量使用,如果枚舉中存在公共屬性字段或設置字段方法,那么這些枚舉常量的屬性很容易被修改。理想情況下,枚舉中的屬性字段是私有的,並在私有構造函數中賦值,沒有對應的 Setter 方法,最好加上 final 修飾符。
反例:
public enum UserStatus { DISABLED(0, "禁用"), ENABLED(1, "啟用"); public int value; private String description; private UserStatus(int value, String description) { this.value = value; this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
正例:
public enum UserStatus { DISABLED(0, "禁用"), ENABLED(1, "啟用"); private final int value; private final String description; private UserStatus(int value, String description) { this.value = value; this.description = description; } public int getValue() { return value; } public String getDescription() { return description; } }
41: 小心String.split(String regex)
字符串 String 的 split 方法,傳入的分隔字符串是正則表達式!部分關鍵字(比如.[]()\| 等)需要轉義
反例:
"a.ab.abc".split("."); // 結果為[]
"a|ab|abc".split("|"); // 結果為["a", "|", "a", "b", "|", "a", "b", "c"]
正例:
"a.ab.abc".split("\\."); // 結果為["a", "ab", "abc"]
"a|ab|abc".split("\\|"); // 結果為["a", "ab", "abc"]
42: 優先使用常量或確定值來調用 equals 方法
對象的 equals 方法容易拋空指針異常,應使用常量或確定有值的對象來調用 equals 方法。當然,使用 java.util.Objects.equals() 方法是最佳實踐。
反例:
public void isFinished(OrderStatus status) { return status.equals(OrderStatus.FINISHED); // 可能拋空指針異常 }
正例:
public void isFinished(OrderStatus status) { return OrderStatus.FINISHED.equals(status); } public void isFinished(OrderStatus status) { return Objects.equals(status, OrderStatus.FINISHED); }
