1. 程式人生 > 實用技巧 >java編寫規範及注意事項

java編寫規範及注意事項

java編寫規範及注意事項

1.註釋

常見註釋有三種 //  /**/  /****/ 

如何才能寫出漂亮的註釋呢,註釋的目的就是為了使你的程式碼讓人更容易理解和維護,寫一手好的註釋是一個優秀碼農的基本體現

註釋規範

  • 註釋應該增加程式碼的清晰度,能一針見血

  • 保持註釋的簡潔,多餘的不要寫

  • 寫程式碼之前應該先註釋好,列好各個步驟,然後根據步驟編寫

  • 註釋你寫的內容所要實現的目標,方便目標明確

註釋場景

  • 類,類目的,類功能,變數

  • 介面,介面目的,介面功能,使用場景

  • 引數,引數含義,約束,使用條件

  • 欄位,欄位描述

  • 方法使用步驟

介面型別的程式碼可以用 //空格(介紹)來進行介紹,佔用的空間小,容易解釋功能

/*和/**的區別
/*多行註釋
/**Javadoc註釋並且可以設定顯示標明,建立者,建立時間,版本,以及該類的作用

Idea設定該模板:File->Settings->Editor->File and Code Templates->Includes->File Header

Eclipse設定該模板:window->perference->Java->Code Style->Code Template->Comments->Overriding methods

若想進一步瞭解,請參考javadoc使用詳解

2.類

單一職責原則

對於一個類,只要承擔對應的功能即可,如果一個類承擔的功能太多了,那麼它的交融、耦合性就會越高,被複用的可能性就會越低

,所以這樣寫一個類,那就像埋了一個雷,以後無論修改,或者是其他的問題,都會很麻煩

類方法,變數,命名規範

現在程式碼開發中最常見的命名規範就是駝峰命名法

類名,介面名:第一個字母通常大寫

方法名,變數名:第一個字母小寫

常量名:全大寫

私有化

我們儘量不要直接操作屬性,而且有些屬性我們也不喜歡被改動,這樣就把屬性設定為private

一般用private修飾的屬性,我們都會為其新增get和set方法,來進行封裝

私有化,是為了安全,私有變數和私有類,就是隻能在本類裡面可以訪問,而其他地方訪問不到

命名規範

  • 儘量使用完整的英文描述

  • 使用駝峰命名法提高名字的可讀性,能一眼看出該名稱代表什麼

  • 避免使用太長的名字

  • 能不用縮寫,儘量不要用縮寫,很多時候容易忘記縮寫意義,如果使用要在工程中統一

  • 避免使用下劃線,靜態常量可以使用

  • 有相關術語儘量用相關術語

### 3.異常

什麼是異常?程式執行的過程中未按照預想流程執行的狀態就是異常,通常分為兩種

系統異常: 這類異常通常是大致地提示使用方系統不可用,同時需要相關人員更進處理的 非系統異常:這類異常通常是一些義務邏輯而已,比如檢查到使用者引數有誤後丟擲的異常

異常規範:

  • 一次對大量try-catch,這種就是不負責任的表現。這樣雖然使用起來簡單,但是會提升程式的複雜度,從而導致分析異常原因的難度也大大增加

  • catch時分清穩定程式碼和非穩定程式碼,穩定程式碼:無論如何都不會出錯的程式碼,非穩定程式碼:隨時有可能出錯

  • 異常不要用來做流程控制,條件控制,因為異常的處理效率比條件分支低

  • 所有捕獲的異常都要進行處理,可以進行資料庫日誌記錄,丟擲給其呼叫者並用列舉反饋為使用者能理解的內容,能修復的儘量修復

  • 如果try-catch捕獲到異常,需要回滾事務,一定要手動回滾

  • 不能在finally中使用return,因為finally語句塊都會被執行到,這樣try程式塊中執行正常也會在finally中退出,不會再回到try程式塊中

  • catch能精細化最好精細化,分門分類處理,範圍越小的exception要放在越前面,越大的放越後面,這樣能更高效的確定哪裡出問題了,儘量避免直接使用Exception

  • 不要過度使用異常這樣會導致執行效率變得緩慢

  • finally層用來關閉和釋放資源

  • 遇到迴圈,不要在迴圈中使用try-catch,應該放在最外層

捕獲異常最優實現

  • 使程式得程式碼複雜度減小

  • 捕獲並且記錄異常資訊

  • 不同的異常通知合適的人員

  • 採用合理的方式去結束異常活動

4.編寫

巢狀

for,while,if,switch等

所有這些層級最好不要超過三層,有條件不成立直接return,不再向下執行

非空判斷

錯誤例子:

if(user.getUserName().equals("hollis")){
}

  這段程式碼極有可能在實際執行的時候跑出NullPointerException。無論是user本身為空,還是user.getUserName()為空,都會丟擲異常。 所以,在呼叫一個引數時要確保他是非空的。

如果有更好的選擇,應該使用Collection.isEmpty()檢測空值

使用 Collection.size() 來檢測空邏輯上沒有問題,但是使用 Collection.isEmpty()使得程式碼更易讀,並且可以獲得更好的效能。任何 Collection.isEmpty() 實現的時間複雜度都是 O(1) ,但是某些 Collection.size() 實現的時間複雜度可能是 O(n) 。

上面的程式碼可以改為:

if(user!=null&&"hollis".equals(user.getUserName())){
}

拼接

在迴圈中拼接一個String物件,從效能來說StringBuffer比String效率要高

// 普遍使用方法
String str = "";
for (int i = 0; i < list.length; ++i) {
str = str + list[i]
}
// 優化後
StringBuffer buf = new StringBuffer();
for (int i = 0; i < list.length; ++i) {
buf.append(list[i]);
}
String str = buf.toString();

字串轉化

字串轉換是編寫中經常要遇到的,一般有如下三種轉換形式

String str=str + "";
String str=String.valueOf(str);
String str=str.toString();

其中toString轉換效率是最高的,也是最方便的

減少對變數的重複計算

明確一個概念,對方法的呼叫,即使方法中只有一句語句,也是有消耗的,包括建立棧幀、呼叫方法時保護現場、呼叫方法完畢時恢復現場等

for (int i = 0; i < list.size(); i++){
...
}

優化後

for (int i = 0, length = list.size(); i < length; i++){
  ...
}

這樣在list.size()很大的時候,就減少了很多的消耗

懶載入策略

如果有進行判斷,在判斷直接就建立內容和判斷之後建立內容是不一樣的,這樣可以避免不必要的資源消耗

例如:

String str = "aaa";
if (i == 1)
{
  list.add(str);
}

建議替換為:

if (i == 1)
{
  String str = "aaa";
  list.add(str);
}

位移操作符

乘除法可以使用位移運算子,就是直接對整數在記憶體中的二進位制位進行操作

例如:

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;
}

移位操作雖然快,但是可能會使程式碼不太好理解,因此最好加上相應的註釋。

其中<<3,指的是2的3次方
>>1,指的是2的1次方
都是2的倍數

物件引用

在迴圈中不要不斷的建立物件引用

例如:

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罷了,但是記憶體中只有一份,這樣就大大節省了記憶體空間了。

單例

使用單例可以減輕載入的負擔縮短載入的時間提高載入的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:

  • 控制資源的使用,通過執行緒同步來控制資源的併發訪問

  • 控制例項的產生,以達到節約資源的目的

  • 控制資料的共享,在不建立直接關聯的條件下,讓多個不相關的程序或執行緒之間實現通訊

靜態變數

要知道,當某個物件被定義為static的變數所引用,那麼gc通常是不會回收這個物件所佔有的堆記憶體的,如:

public class A
{
private static B b = new B();
}

此時靜態變數b的生命週期與A類相同,如果A類不被解除安裝,那麼引用B指向的B物件會常駐記憶體,直到程式終止

同步

由於同一程序的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可,存在以下問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起

  • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題

  • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致優先順序倒置,引起效能問題。

  • 由於我們可以用過private關鍵字來保證資料物件只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊

    同步方法:public synchronized void method(int args){}
  • synchronized方法控制物件的訪問,每個物件對應一把鎖,每個synchronized方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才釋放鎖,後面被阻塞的執行緒才能獲得這個鎖,繼續執行

    缺陷:==若將一個大的方法申明為synchronized將會影響效率==

    鎖的物件就是變化的量,需要增刪改的物件

同步方法弊端

  • 方法裡面需要修改的內容才需要鎖,鎖的太多,浪費資源

同步程式碼塊可以用更細粒度的控制鎖,比如:

public class Test{
private String name = "li";
private String id = "007";
public void setName(String name) {
synchornized(name) {
this.name = name;
}
}
public void setId(String id) {
synchornized(id) {
this.id = id;
}
}
}

程式執行過程中避免使用反射

  反射是Java提供給使用者一個很強大的功能,功能強大往往意味著效率不高。不建議在程式執行過程中使用尤其是頻繁使用反射機制,特別是Method的invoke方法,如果確實有必要,一種建議性的做法是將那些需要通過反射載入的類在專案啟動的時候通過反射例項化出一個物件並放入記憶體—-使用者只關心和對端互動的時候獲取最快的響應速度,並不關心對端的專案啟動花多久時間。

使用帶緩衝的輸入輸出流進行IO操作

帶緩衝的輸入輸出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,這可以極大地提升IO效率

公用的集合類中不使用的資料一定要及時remove掉

如果一個集合類是公用的(也就是說不是方法裡面的屬性),那麼這個集合裡面的元素是不會自動釋放的,因為始終有引用指向它們。所以,如果公用集合裡面的某些資料不使用而不去remove掉它們,那麼將會造成這個公用集合不斷增大,使得系統有記憶體洩露的隱患。

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();”會比較合適一些。

Map<String, String> map = ...;
for (String key : map.keySet()) {
...
}

如果需要獲取主鍵和取值,迭代entrySet()才是更加高效的做法

Map<String, String> map = ...;
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
...
}

Random

避免Random例項被多執行緒使用,雖然該執行緒是安全的,但是會因為競爭同一個seed而導致效能下降,JDK7之後,可以使用ThreadLocalRandom來獲取隨機數

金額運算

@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

獲取時間差

  阿里巴巴手冊建議:

計算兩段程式碼時間差,很多同學公司的程式碼是採用以下這種方式。

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);

我們再來看看註釋:

list隨機訪問

大家都知道陣列和連結串列的區別:

陣列的隨機訪問效率更高。當呼叫方法獲取到 List 後,如果想隨機訪問其中的資料,並不知道該陣列內部實現是連結串列還是陣列,怎麼辦呢?可以判斷它是否實現* RandomAccess *介面。

// 呼叫別人的服務獲取到list
List<Integer> list = otherService.getList();
if (list instanceof RandomAccess) {
// 內部陣列實現,可以隨機訪問
System.out.println(list.get(list.size() - 1));
} else {
// 內部可能是連結串列實現,隨機訪問效率低
}

魔法值

當你編寫一段程式碼時,使用魔法值可能看起來很明確,但在除錯時它們卻不顯得那麼明確了。這就是為什麼需要把魔法值定義為可讀取常量的原因。但是,-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) {
...
}

列舉

列舉通常被當做常量使用,如果列舉中存在公共屬性欄位或設定欄位方法,那麼這些列舉常量的屬性很容易被修改。

private String description;

理想情況下,列舉中的屬性欄位是私有的,並在私有建構函式中賦值,沒有對應的 Setter 方法,最好加上 final 修飾符。

private final int value;

split擷取

字串 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"]

5.總結

  • 專案中已經改過的錯誤註釋,方法,類,用不到的及時刪掉,不要當寶貝一樣,佔用記憶體,而且容易誤導,使程式碼的複雜度直線上升

  • 如果同一段邏輯出現了兩次及以上,你就要考慮把該段抽取出來,可以減少重複編寫

  • 如果遇到解決不了的問題,加一層呼叫試試?

  • 遇到比較深度的樹節點,用遞迴省事而且效率高

如果有一定的基礎,可以去使用一些框架,這樣可以極大的提升自己的開發效率,畢竟是站在前人的肩膀上,但是一定要知道框架的一些基本實現形式,任何新技術,絕不可能只是覆蓋了原來的技術,都有自己的優勢和補充

參考:

java程式設計規範

java開發程式碼規範之異常日誌