JavaRush /Java Blog /Random-TW /喝咖啡休息#56。Java 最佳實務快速指南

喝咖啡休息#56。Java 最佳實務快速指南

在 Random-TW 群組發布
來源:DZone 本指南包含最佳 Java 實務和參考,以提高程式碼的可讀性和可靠性。開發人員每天都有做出正確決策的重大責任,而能夠幫助他們做出正確決策的最好的東西就是經驗。儘管並非所有人都擁有豐富的軟體開發經驗,但每個人都可以藉鏡他人的經驗。我根據自己的 Java 經驗為您準備了一些建議。我希望它們能幫助您提高 Java 程式碼的可讀性和可靠性。喝咖啡休息#56。 Java 最佳實務快速指南 - 1

程式設計原理

不要寫只管用的程式碼。努力編寫可維護的程式碼—不僅可以由您維護,還可以由將來可能最終使用該軟體的任何其他人維護。開發人員 80% 的時間用於閱讀程式碼,20% 的時間用於編寫和測試程式碼。因此,專注於編寫可讀的程式碼。您的程式碼不需要任何人都可以理解它的作用的註解。要寫好的程式碼,我們可以使用許多程式設計原則作為指導。下面我將列出最重要的一些。
  • • KISS – 代表「保持簡單,愚蠢」。您可能會注意到,開發人員在他們的旅程開始時嘗試實現複雜、模糊的設計。
  • • DRY - “不要重複自己。” 盡量避免任何重複,而是將它們放入系統或方法的單個部分中。
  • YAGNI - “你不需要它。” 如果您突然開始問自己,“添加更多內容(功能、程式碼等)怎麼樣?”,那麼您可能需要考慮是否真的值得添加它們。
  • 乾淨的程式碼而不是智慧程式碼-簡單地說,把你的自我放在門口,忘記寫智慧程式碼。您需要乾淨的程式碼,而不是智慧代碼。
  • 避免過早優化-過早優化的問題是,在瓶頸出現之前,您永遠不知道程式中的瓶頸在哪裡。
  • 單一職責- 程式中的每個類別或模組應該只關心提供特定功能的一位。
  • 組合而不是實作繼承-具有複雜行為的物件應該包含具有單獨行為的物件實例,而不是繼承類別並新增行為。
  • 物體體操是一種程式設計練習,設計有9 個規則
  • 快速失敗,快速停止- 該原則意味著當發生任何意外錯誤時停止當前操作。遵循這項原則可以使運作更加穩定。

套餐

  1. 按主題領域而不是技術水平優先構建包。
  2. 支援促進封裝和資訊隱藏的佈局,以防止誤用,而不是出於技術原因組織類別。
  3. 將套件視為具有不可變的 API - 不要公開僅用於內部處理的內部機制(類別)。
  4. 不要公開僅在包內使用的類別。

課程

靜止的

  1. 不允許建立靜態類別。始終建立私有構造函數。
  2. 靜態類別應該保持不可變,不允許子類化或多執行緒類別。
  3. 應保護靜態類別免受方向變化的影響,並應作為列表過濾等實用程式提供。

遺產

  1. 選擇組合而不是繼承。
  2. 不要設定受保護的欄位。相反,請指定安全存取方法。
  3. 如果類別變數可以標記為Final,請這樣做。
  4. 如果不需要繼承,請將類別設為Final
  5. 如果不希望子類別重寫某個方法,則將其標記為Final 。
  6. 如果不需要建構函數,請勿建立沒有實作邏輯的預設建構函數。如果沒有指定,Java 會自動提供一個預設建構函式。

介面

  1. 不要使用常量介面模式,因為它允許類別實作並污染 API。請改用靜態類別。這樣做的另一個好處是允許您在靜態區塊中進行更複雜的物件初始化(例如填充集合)。
  2. 避免過度使用介面
  3. 擁有一個且只有一個實作介面的類別可能會導致介面的過度使用,並且弊大於利。
  4. 「為介面編程,而不是實現」並不意味著您應該將每個域類別與或多或少相同的介面捆綁在一起,這樣做會破壞YAGNI
  5. 始終保持介面小而具體,以便客戶只知道他們感興趣的方法。從 SOLID 檢查 ISP。

終結器

  1. #finalize()物件應謹慎使用,並且僅作為清理資源(例如關閉檔案)時防止失敗的一種手段。始終提供明確的清理方法(例如close())。
  2. 在繼承層次結構中,始終在try區塊中呼叫父級的Finalize()。類別清理應該在finally區塊中。
  3. 如果未呼叫明確清理方法且終結器關閉了資源,請記錄此錯誤。
  4. 如果記錄器不可用,請使用執行緒的異常處理程序(最終會傳遞在日誌​​中捕獲的標準錯誤)。

一般規則

聲明

斷言通常採用前提條件檢查的形式,強制執行「快速失敗,快速停止」契約。它們應該被廣泛使用來識別盡可能接近原因的程式錯誤。對象條件:
  • • 永遠不應建立物件或將其置於無效狀態。
  • • 在建構子和方法中,總是使用測試來描述和執行契約。
  • • 應避免使用Java 關鍵字assert,因為它可能被禁用並且通常是一個脆弱的結構。
  • • 使用Assertions實用程式類別可以避免前置條件檢查中出現冗長的if-else條件。

泛型

Java泛型常見問題解答中提供了完整、極為詳細的解釋。以下是開發人員應注意的常見場景。
  1. 只要有可能,最好使用類型推斷而不是返回基底類別/介面:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. 如果無法自動確定類型,請將其內聯。

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. 通配符:

    當您僅從結構中取得值時,請使用擴充通配符;當您僅將值放入結構中時,請使用超級通配符;當您同時執行這兩種操作時,請不要使用通配符。

    1. 每個人都喜歡PECS!(生產者擴展,消費者超級
    2. 使用Foo作為生產者 T。
    3. 使用 Foo作為消費者 T。

單例

單例永遠不應該用經典的設計模式風格編寫,這在 C++ 中很好,但在 Java 中不合適。即使它是正確的線程安全的,也永遠不要實現以下內容(這將是效能瓶頸!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
如果確實需要延遲初始化,那麼這兩種方法的組合將會起作用。
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring:預設情況下,bean 註冊為單例範圍,這表示容器只會建立一個實例並連接到所有消費者。這提供了與常規單例相同的語義,沒有任何效能或綁定限制。

例外情況

  1. 對可修正的條件使用檢查異常,對程式錯誤使用運行時異常。範例:從字串中取得整數。

    不好:NumberFormatException 擴展了 RuntimeException,因此它旨在指示程式設計錯誤。

  2. 請勿執行以下操作:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    正確使用:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. 您必須在正確的位置、網域層級的正確位置處理例外狀況。

    錯誤的方式 - 當資料庫異常發生時,資料物件層不知道該怎麼做。

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    推薦方式- 資料層應該簡單地重新拋出異常,並將處理或不處理異常的責任傳遞給正確的層。

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. 異常通常不應在發出時記錄,而應在實際處理時記錄。當拋出或重新拋出異常時,日誌記錄往往會在日誌檔案中充滿噪音。另請注意,異常堆疊追蹤仍然記錄拋出異常的位置。

  5. 支援使用標準異常。

  6. 使用異常而不是回傳代碼。

等於和哈希碼

編寫正確的物件和雜湊碼等效方法時需要考慮許多問題。為了使其更易於使用,請使用 java.util.Objects 的equalshash
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

資源管理

安全釋放資源的方法:try-with-resources語句確保每個資源在語句結束時關閉。任何實作 java.lang.AutoCloseable 的物件(包括實作java.io.Closeable 的所有物件)都可以用作資源。
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

使用關閉掛鉤

使用JVM 正常關閉時呼叫的關閉掛鉤。(但它無法處理突然中斷,例如由於斷電)這是建議的替代方案,而不是聲明僅在 System.runFinalizersOnExit() 為trueFinalize()才會運行的
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
透過在伺服器之間分配資源,使資源變得完整(以及可更新)。(這將允許從突然中斷(例如停電)中恢復。)請參閱上面使用 ExpiringGeneralLock(所有系統通用的鎖)的範例程式碼。

約會時間

Java 8 在 java.time 套件中引入了新的日期時間 API。Java 8 引進了新的 Date-Time API,以解決舊 Date-Time API 的以下缺點:非執行緒、設計不良、複雜的時區處理等。

平行性

一般規則

  1. 請注意以下庫,它們不是線程安全的。如果物件被多個執行緒使用,則始終與物件同步。
  2. 日期(不是不可變的)- 使用新的日期時間 API,它是線程安全的。
  3. SimpleDateFormat - 使用新的日期時間 API,它是線程安全的。
  4. 偏好使用java.util.concurrent.atomic類別而不是使變數成為volatile
  5. 原子類的行為對於一般開發人員來說更為明顯,而volatile則需要了解 Java 記憶體模型。
  6. 原子類將易失性變數包裝到更方便的介面。
  7. 了解volatility適合的用例。(見文章
  8. 使用可調用 當需要檢查異常但沒有傳回類型時。由於 Void 無法實例化,因此它傳達意圖並可以安全地傳回 null

  1. java.lang.Thread應該被棄用。儘管官方情況並非如此,但在幾乎所有情況下,java.util.concurrent套件都提供了更清晰的問題解決方案。
  2. 擴展 java.lang.Thread被認為是不好的做法 -改為實作 Runnable並在建構函式中使用實例來建立一個新執行緒(組合規則優於繼承)。
  3. 當需要並行處理時,優先選擇執行器和執行緒。
  4. 始終建議指定您自己的自訂線程工廠來管理創建的線程的配置(更多詳細資訊請參見此處)。
  5. 對於非關鍵線程,在 Executors 中使用 DaemonThreadFactory,以便在伺服器關閉時可以立即關閉線程池(更多詳細資訊請參閱此處)。
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Java 同步不再那麼慢(55-110 ns)。不要使用雙重檢查鎖定等技巧來避免它。
  2. 喜歡與內部物件而不是類別同步,因為使用者可以與您的類別/實例同步。
  3. 始終以相同的順序同步多個物件以避免死鎖。
  4. 與類別同步本質上不會阻止對其內部物件的存取。存取資源時始終使用相同的鎖。
  5. 請記住,synchronized 關鍵字不被視為方法簽章的一部分,因此不會被繼承。
  6. 避免過度同步,這可能導致效能不佳和死鎖。對於需要同步的程式碼部分,嚴格使用synchronized關鍵字。

收藏

  1. 盡可能在多執行緒程式碼中使用 Java-5 並行集合。它們安全且具有優良的特性。
  2. 如有必要,請使用 CopyOnWriteArrayList 而不是synchronizedList。
  3. 使用 Collections.unmodifying list(...) 或在接收集合時將其複製到new ArrayList(list)。避免從類別外部修改本地集合。
  4. 始終傳回集合的副本,避免使用new ArrayList (list)從外部修改清單。
  5. 每個集合必須包裝在一個單獨的類別中,因此現在與集合關聯的行為有了一個家(例如過濾方法,對每個元素應用規則)。

各種各樣的

  1. 選擇 lambda 而不是匿名類別。
  2. 選擇方法引用而不是 lambda。
  3. 使用枚舉代替 int 常數。
  4. 如果需要精確的答案,請避免使用 float 和 double,而是使用 BigDecimal,例如 Money。
  5. 選擇原始類型而不是裝箱原始類型。
  6. 您應該避免在程式碼中使用幻數。使用常數。
  7. 不要返回 Null。使用“Optional”與您的方法用戶端進行通訊。對於集合也是如此 - 傳回空數組或集合,而不是空值。
  8. 避免創建不必要的對象,重複使用對象,避免不必要的GC清理。

延遲初始化

延遲初始化是一種效能優化。當數據因某種原因被認為“昂貴”時使用它。在 Java 8 中,我們必須為此使用函數提供者介面。
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
這就是現在的全部內容,我希望這對您有所幫助!
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION