Java最佳實踐

信碼由韁發表於2022-11-08

img

計算機程式設計中,最佳實踐是許多開發人員遵循的一組非正式規則,以提高軟體質量、可讀性和可維護性。在應用程式長時間保持使用的情況下,最佳實踐尤其有益,這樣它最初是由一個團隊開發的,然後由不同的人組成的維護團隊進行維護。

本教程將提供Java最佳實踐的概述,以及每個條目的解釋,包括Java程式設計的頂級最佳實踐列表中的每一項。

Java程式設計最佳實踐概覽

雖然Java最佳實踐的完整列表可能很長,但對於那些正在努力提高程式碼質量的編碼人員來說,有幾個被認為是一個很好的起點,包括使用適當的命名規範、使類成員私有化、避免使用空的catch塊、避免記憶體洩漏以及正確地註釋程式碼塊:

  • 使用適當的命名規範
  • 類成員設定為私有
  • 在長數字文字中使用下劃線
  • 避免空的catch
  • 使用StringBuilderStringBuffer進行字串連線
  • 避免冗餘初始化
  • 使用增強型for迴圈代替帶計數器的for迴圈
  • 正確處理空指標異常
  • FloatDouble:哪一個是正確的選擇?
  • 使用單引號和雙引號
  • 避免記憶體洩漏
  • 返回空集合而不是返回Null元素
  • 高效使用字串
  • 避免建立不必要的物件
  • 正確註釋程式碼

Java中的類成員應該是私有的

在Java中,類的成員越不可訪問,越好!第一步是使用private訪問修飾符。目標是促進理想的封裝,這是物件導向程式設計(OOP)的基本概念之一。太多時候,新的開發人員沒有正確地為類分配訪問修飾符,或者傾向於將它們設定為public以使事情更容易。

考慮以下欄位被設定為public的類:

public class BandMember {
  public String name;
  public String instrument;
}

在這裡,類的封裝性被破壞了,因為任何人都可以直接更改這些值,如下所示:

BandMember billy = new BandMember();
billy.name = "George";
billy.instrument = "drums";

使用private訪問修飾符與類成員一起可以將欄位隱藏起來,防止使用者透過setter方法之外的方式更改資料:

public class BandMember {
  private String name;
  private String instrument;
  
  public void setName(String name) {
    this.name = name;
  }
  public void setInstrument(String instrument)
    this.instrument = instrument;
  }
}

setter方法中也是放置驗證程式碼和/或管理任務(如增加計數器)的理想位置。

在長數字文字中使用下劃線

得益於Java 7的更新,開發人員現在可以在長數字字面量中使用下劃線(_),以提高可讀性。以下是在允許下劃線之前一些長數字字面量的示例:

int minUploadSize = 05437326;
long debitBalance = 5000000000000000L;
float pi = 3.141592653589F;

我想您會同意下劃線使值更易讀:

int minUploadSize = 05_437_326;
long debitBalance = 5_000_000_000_000_000L;
float pi = 3.141_592_653_589F;

避免空的Catch塊

在Java中,把catch塊留空是非常不好的習慣,有兩個原因:一是它可能導致程式默默地失敗,二是程式可能會繼續執行而不會發生任何異常。這兩種結果都會使除錯變得非常困難

考慮以下程式,它從命令列引數中計算兩個數字的和:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;
    
    try {
      a = Integer.parseInt(args[0]);
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
    }
    
    int sum = a + b;
    
    System.out.println(a + " + " + b + " = " + sum);
  }
}

Java的parseInt()方法會丟擲NumberFormatException異常,需要開發人員在其呼叫周圍使用try/catch塊來捕獲異常。不幸的是,這位開發人員選擇忽略丟擲的異常!因此,傳入無效引數,例如“45y”,將導致關聯的變數被賦為其型別的預設值,對於int型別來說是0

img

通常,在捕獲異常時,程式設計師應採取以下三個行動中的一個或多個:

  1. 開發人員最起碼應該通知使用者異常情況,要麼讓他們重新輸入無效的值,要麼讓他們知道程式必須提前終止。
  2. 使用 JDK LoggingLog4J 記錄異常日誌。
  3. 將異常封裝並作為一個新的、更適合應用程式的異常重新丟擲。

以下是重新編寫後的Sum應用程式,用於通知使用者輸入無效並因此終止程式:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;
    
    try {
      a = Integer.parseInt(args[0]);
    } catch (NumberFormatException ex) {
      System.out.println(args[0] + " is not a number. Aborting...");
      return;
    }
    
    try {
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
      System.out.println(args[1] + " is not a number. Aborting...");
      return;
    }
    
    int sum = a + b;
    
    System.out.println(a + " + " + b + " = " + sum);
  }
}

以下是我們觀察到的結果:

img

使用StringBuilder或StringBuffer進行字串拼接

“+” 運算子是在 Java 中快速和簡便地組合字串的方法。在 Hibernate 和 JPA 時代之前,物件是手動持久化的,透過從頭開始構建 SQL INSERT 語句來實現!以下是一個儲存一些使用者資料的示例:

String sql = "Insert Into Users (name, age)";
       sql += " values ('" + user.getName();
       sql += "', '" + user.getage();
       sql += "')";

很遺憾,當像上面那樣連線多個字串時,Java編譯器必須建立多箇中間字串物件,然後將它們合併成最終連線的字串。

相反,我們應該使用StringBuilderStringBuffer類。兩者都包含函式,可以連線字串而無需建立中間String物件,從而節省處理時間和不必要的記憶體使用。

以前的程式碼可以使用 StringBuilder 重寫,如下所示:

StringBuilder sqlSb = new StringBuilder("Insert Into Users (name, age)");
sqlSb.append(" values ('").append(user.getName());
sqlSb.append("', '").append(user.getage());
sqlSb.append("')");
String sqlSb = sqlSb.toString();

這對開發者來說可能要費點事兒,但是這樣做非常值得!

StringBuffer 與 StringBuilder

雖然StringBufferStringBuilder類都比“+”運算子更可取,但它們並不相同。StringBuilderStringBuffer更快,但不是執行緒安全的。因此,在非多執行緒環境中進行字串操作時,應使用StringBuilder;否則,請使用StringBuffer類。

避免冗餘初始化

儘管某些語言如TypeScript強烈建議在宣告時初始化變數,但在Java中並非總是必要的,因為它在宣告時將預設初始化值(如0falsenull)分配給變數。

因此,Java的最佳實踐是要知道成員變數的預設初始化值,除非您想將它們設定為除預設值以外的其他值,否則不要顯式初始化變數。

以下是一個計算從11000的自然數之和的短程式。請注意,只有部分變數被初始化:

class VariableInitializationExample {
  public static void main(String[] args) {
    
    // automatically set to 0
    int sum;
    final numberOfIterations = 1000;


    // Set the loop counter to 1
    for (int i = 1; i &= numberOfIterations; ++i) {
      sum += i;
    }
       
    System.out.println("Sum = " + sum);
  }
}

使用增強型for迴圈代替需要計數器的for迴圈

儘管 for 迴圈在某些情況下很有用,但是計數器變數可能會引起錯誤。例如,計數器變數可能會在稍後的程式碼中被無意中更改。即使從 1 而不是從 0 開始索引,也可能導致意外行為。出於這些原因,for-each 迴圈(也稱為增強型 for 迴圈)可能是更好的選擇。

考慮以下程式碼:

String[] names = {"Rob", "John", "George", "Steve"};
for (int i = 0; i < names.length; i++) {
  System.out.println(names[i]);
}

在這裡,變數i既是迴圈計數器,又是陣列names的索引。儘管這個迴圈只是列印每個名稱,但如果下面有修改i的程式碼,就會變得棘手。我們可以透過使用下面所示的for-each迴圈輕鬆避開整個問題:

for (String name : names) {
  System.out.println(name);
}

使用增強的for迴圈,出錯的機會要少得多!

合理處理空指標異常

空指標異常在Java中是一個非常常見的問題,可能是由於其物件導向的設計所致。當您試圖在 Null 物件引用上呼叫方法時,就會發生 Null Pointer 異常。這通常發生在在類例項化之前呼叫例項方法的情況下,如下例所示:

Office office;

// later in the code...
Employee[] employees = office.getEmployees();

雖然你無法完全消除 Null Pointer Exceptions,但有方法可以將其最小化。一種方法是在呼叫物件的方法之前檢查物件是否為 Null。以下是使用三元運算子的示例:

Office office;

// later in the code...
Employee[] employees = office == null ? 0 : office.getEmployees();

你可能還想丟擲自己的異常:

Office office;
Employee[] employees;

// later in the code...
if (office == null) {
  throw new CustomApplicationException("Office can't be null!");
} else {
  employees = office.getEmployees();
}

Float或Double:應該使用哪個?

浮點數和雙精度數是相似的型別,因此許多開發人員不確定該選擇哪種型別。兩者都處理浮點數,但具有非常不同的特性。例如,float的大小為32位,而double分配了64位的記憶體空間,因此double可以處理比float大得多的小數。然後有一個精度問題:float只能容納7位精度。極小的指數大小意味著一些位是不可避免的丟失的。相比之下,double為指數分配了更多的位數,允許它處理高達15位精度。

因此,當速度比準確性更重要時,通常建議使用float。儘管大多數程式不涉及大量計算,但在數學密集型應用中,精度差異可能非常顯著。當需要的小數位數已知時,float也是一個不錯的選擇。當精度非常重要時,double應該是你的首選。只需記住,Java強制使用double作為處理浮點數的預設資料型別,因此您可能需要附加字母"f"來明確表示float,例如,1.2f

單引號和雙引號在字串連線中的使用

在Java中,雙引號(“)用於儲存字串,單引號用於字元(由char型別表示)。當我們嘗試使用+連線運算子連線字元時,可能會出現問題。問題在於使用+連線字元會將char的值轉換為ascii,從而產生數字輸出。以下是一些示例程式碼,以說明這一點:

char a, b, c;
a = 'a';
b = 'b';
c = 'c';

str = a + b + c; // not "abc", but 294!

就像字串連線最好使用StringBuilderStringBuffer類一樣,字元連線也是如此!在上面的程式碼中,變數abc可以透過以下方式組合成一個字串:

new StringBuilder().append(a).append(b).append(c).toString()

我們可以在下面觀察到期望的結果:

img

避免Java中的記憶體洩漏

在Java中,開發人員並沒有太多關於記憶體管理的控制權,因為Java透過垃圾回收自動地進行記憶體管理。儘管如此,有一些Java最佳實踐可以幫助開發人員避免記憶體洩漏,例如:

  • 避免建立不必要的物件。
  • 避免使用"+"運算子進行字串連線。
  • 避免在會話中儲存大量資料。
  • 在會話不再使用時,及時讓會話超時。
  • 避免使用靜態物件,因為它們在整個應用程式的生命週期記憶體在。
  • 在與資料庫互動時,不要忘記在finally塊中關閉ResultSetStatementsConnection物件。

返回空集合而不是null引用。

你知道嗎,null引用經常被稱為軟體開發中最嚴重的錯誤嗎?1965年,Tony Hoare在設計面嚮物件語言(OOP)的引用的第一個全面型別系統時發明了null引用。後來在2009年的一次會議上,Hoare為自己的發明道歉,承認他的創造“導致了無數的錯誤、漏洞和系統崩潰,在過去四十年中可能造成了數十億美元的痛苦和損失。”

在Java中,通常最好返回空值而不是null,特別是當返回集合、可列舉物件或物件時更為重要。儘管你自己的程式碼可能會處理返回的null值,但其他開發人員可能會忘記編寫空值檢查,甚至沒有意識到null是可能的返回值!

以下是一些Java程式碼,它以ArrayList的形式獲取庫存中的書籍列表。但是,如果列表為空,則返回一個空列表:

private final List<Book> booksInStock = ...

public List<Book> getInStockBooks() {
  return booksInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(booksInStock);
}

這使得方法的呼叫者可以在不必首先檢查null引用的情況下迭代列表:

(Book book: getInStockBooks()) {
 // do something with books
}

Java 中字串的高效使用

我們已經討論了使用 + 連線運算子可能產生的副作用,但還有其他一些方法可以更有效地使用字串,以避免浪費記憶體和處理器週期。例如,在例項化 String 物件時,通常最好直接建立 String 而不是使用建構函式。原因是什麼?使用直接建立 String 比使用建構函式更快(更不用說更少的程式碼!)。

這裡是在Java中建立字串的兩種等效方式:直接建立和使用建構函式:

// directly
String str = "abc";
// using a constructor
char data[] = {'a', 'b', 'c'};
String str = new String(data);

雖然兩種方法都是等效的,但直接建立字串的方式更好。

Java中不必要的物件建立

你知道嗎?在Java中,物件的建立是消耗記憶體最多的操作之一。因此,在沒有充分理由的情況下,最好避免建立物件,並僅在絕對必要時才這樣做。

那麼,如何將這個實踐起來呢?具有諷刺意味的是,像我們上面看到的直接建立字串就是避免不必要建立物件的一種方式!

以下是一個更復雜的示例:

以下是一個Person類的例子,它包括一個isBabyBoomer()方法,用於判斷此人是否屬於“嬰兒潮”年齡段,出生於1946年至1964年之間:

public class Person {
  private final Date birthDate;
  
  public boolean isBabyBoomer() {
    // Unnecessary allocation of expensive object!
    Calendar gmtCal =
        Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();
    
    return birthDate.compareTo(boomStart) >= 0 &&
           birthDate.compareTo(boomEnd)   <  0;
  }
}

isBabyBoomer()方法每次被呼叫都會建立一個新的CalendarTimeZone和兩個Date例項,這是不必要的。糾正這種低效的方法之一是使用靜態初始化器,以便只在初始化時建立CalendarTimeZoneDate物件,而不是每次呼叫isBabyBoomer()方法。

class Person {
  private final Date birthDate;
  
  // The starting and ending dates of the baby boom.
  private static final Date BOOM_START;
  private static final Date BOOM_END;
  
  static {
    Calendar gmtCal =
      Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_START = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_END = gmtCal.getTime();
  }
  
  public boolean isBabyBoomer() {
    return birthDate.compareTo(BOOM_START) >= 0 &&
       birthDate.compareTo(BOOM_END)       <  0;
  }
}

Java中適當的註釋

清晰簡潔的註釋在閱讀其他開發人員的程式碼時非常有用。以下是寫出高質量註釋的幾個指南:

  1. 註釋不應該重複程式碼。
  2. 好的註釋不能彌補程式碼不清晰的問題。
  3. 如果您無法編寫清晰的註釋,則程式碼可能存在問題。
  4. 在註釋中解釋不符合慣用方式的程式碼。
  5. 在最有用的地方包含指向外部參考文獻的連結。
  6. 在修復錯誤時新增註釋。
  7. 使用註釋標記未完成的實現,通常使用標記“TODO:”開頭。

總結

在本文中,我們瞭解了15個Java最佳實踐,並探討了類成員封裝、在冗長的數字字面值中使用下劃線、避免空catch塊、正確完成字串連線、如何避免冗餘初始化以及使用增強的for迴圈。


【注】本文譯自: Java Best Practices | Developer.com

相關文章