JavaRush /Java Blog /Random-TW /通用程式設計風格指南
pandaFromMinsk
等級 39
Минск

通用程式設計風格指南

在 Random-TW 群組發布
本文是學術課程「高階Java」的一部分。本課程旨在幫助您學習如何有效地使用Java 功能。這些材料涵蓋「進階」主題,例如物件創建、競爭、序列化、反射等。課程將教您如何有效掌握 Java 技術。詳細資訊請參閱此處
內容
1. 簡介2. 變數作用域3. 類別欄位與局部變數4. 方法參數與局部變數5. 裝箱與拆箱6.介面7.字串8. 命名約定9.標準庫10. 不變性11.測試12. 下一步。 .. 13.下載原始碼
一、簡介
在本教學的這一部分中,我們將繼續討論 Java 中良好程式設計風格和響應式設計的一般原則。我們已經在本指南的前面幾章中了解了其中一些原則,但也將提供許多實用技巧,旨在提高 Java 開發人員的技能。
2. 變數範圍
在第三部分(「如何設計類別和介面」)中,我們討論了在給定範圍限制的情況下如何將可見性和可訪問性應用於類別和介面的成員。但是,我們還沒有討論方法實作中使用的局部變數。在 Java 語言中,每個局部變數一旦聲明,就有一個作用域。從宣告該變數的位置到方法(或程式碼區塊)執行完成的位置,該變數都變得可見。一般來說,唯一要遵循的規則是將局部變數宣告為盡可能靠近將要使用它的位置。讓我來看一個典型的例子: for( final Locale locale: Locale.getAvailableLocales() ) { // блок codeа } try( final InputStream in = new FileInputStream( "file.txt" ) ) { // блока codeа } 在兩個程式碼片段中,變數的範圍都限制在宣告這些變數的執行區塊內。當區塊完成時,作用域結束且變數變得不可見。這看起來更清楚,但隨著 Java 8 的發布和 lambda 的引入,該語言中許多使用局部變數的眾所周知的習慣用法正在變得過時。讓我舉一個前面例子中使用 lambda 代替迴圈的例子: Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> { // блок codeа } ); 可以看出,局部變數已經成為函數的參數,而函數又作為參數傳遞給 forEach方法。
3. 類別字段和局部變數
Java 中的每個方法都屬於一個特定的類別(或者,在 Java8 中,屬於一個接口,該方法被宣告為預設方法)。在作為實作中使用的類別或方法的欄位的局部變數之間,存在名稱衝突的可能性。Java 編譯器知道如何從可用變數中選擇正確的變量,即使多個開發人員打算使用該變數。現代 Java IDE 在透過編譯器警告和變數突​​出顯示來告訴開發人員何時將發生此類衝突方面做得非常出色。但在編寫程式碼時考慮這些事情還是更好。我建議看一個例子: public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += value; return value; } } 這個例子看起來很簡單,但它是一個陷阱。 calculateValue方法引入了一個局部變 數值,並對其進行操作,隱藏了同名的類別欄位。該行 value += value; 應該是類別欄位和局部變數的值總和,但相反,正在執行其他操作。正確的實作如下所示(使用 this 關鍵字): public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += this.value; return value; } } 雖然此範例在某些方面很幼稚,但它確實演示了一個重要的點,即在某些情況下可能需要數小時來調試和修復。
4. 方法參數與局部變數
缺乏經驗的 Java 開發人員經常陷入的另一個陷阱是使用方法參數作為局部變數。Java允許你給非常量參數重新賦值(不過,這對原始值沒有影響): public String sanitize( String str ) { if( !str.isEmpty() ) { str = str.trim(); } str = str.toLowerCase(); return str; } 上面的程式碼片段並不優雅,但很好地揭示了問題:參數 str被賦值不同的值(基本上用作局部變量)。在所有情況下(沒有任何例外),您可以而且應該不使用此範例(例如,將參數宣告為常數)。例如: public String sanitize( final String str ) { String sanitized = str; if( !str.isEmpty() ) { sanitized = str.trim(); } sanitized = sanitized.toLowerCase(); return sanitized; } 透過遵循這個簡單的規則,即使引入局部變量,也可以更輕鬆地追蹤給定程式碼並找到問題的根源。
5. 包裝與拆箱
裝箱和拆箱是 Java 中用來將基本型別( int、long、double 等)轉換為對應型別包裝器( Integer、Long、Double等) 的技術的名稱。在如何以及何時使用泛型教程的第 4 部分中,當我談到將基本類型包裝為泛型的類型參數時,您已經看到了這一點。儘管 Java 編譯器盡力透過執行自動裝箱來隱藏此類轉換,但有時這會低於預期並產生意外結果。讓我們來看一個例子: public static void calculate( final long value ) { // блок codeа } final Long value = null; calculate( value ); 上面的程式碼片段編譯得很好。 但是,它將在Longlong 之間轉換的行上 拋出 NullPointerException。對於這種情況的建議是,建議使用原始類型(但是,我們已經知道這並不總是可能的)。 // блок
6. 介面
在本教程的第 3 部分「如何設計類別和介面」中,我們討論了介面和契約編程,強調介面應盡可能優先於特定類別。本節的目的是透過現實生活中的範例來示範這一點,鼓勵您先考慮介面。介面不依賴特定的實作(預設方法除外)。它們只是合同,例如,它們在合約的執行方式上提供了很大的自由度和靈活性。當實施涉及外部系統或服務時,這種靈活性變得更加重要。讓我們來看一個簡單介面及其可能實現的範例: public interface TimezoneService { TimeZone getTimeZone( final double lat, final double lon ) throws IOException; } public class TimezoneServiceImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { final URL url = new URL( String.format( "http://api.geonames.org/timezone?lat=%.2f&lng=%.2f&username=demo", lat, lon ) ); final HttpURLConnection connection = ( HttpURLConnection )url.openConnection(); connection.setRequestMethod( "GET" ); connection.setConnectTimeout( 1000 ); connection.setReadTimeout( 1000 ); connection.connect(); int status = connection.getResponseCode(); if (status == 200) { // Do something here } return TimeZone.getDefault(); } } 上面的程式碼片段顯示了典型的介面模式及其實作。此實作使用外部 HTTP 服務 ( http://api.geonames.org/ ) 來檢索特定位置的時區。然而,因為 契約取決於接口,很容易引入接口的另一個實現,例如使用資料庫甚至常規平面文件。有了它們,介面對於設計可測試的程式碼非常有幫助。例如,在每個測試中呼叫外部服務並不總是可行,因此實現替代的、最簡單的實作(例如存根)是有意義的: 此實作可以在需要 TimezoneService public class TimezoneServiceTestImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { return TimeZone.getDefault(); } }介面的 任何地方使用,從而隔離測試腳本免受外部組件的依賴。Java 標準函式庫中封裝了許多有效使用此類介面的優秀範例。集合、列表、集合——這些介面有多種實現方式,可以無縫換出,並且在合約利用時可以互換。例如: public static< T > void print( final Collection< T > collection ) { for( final T element: collection ) { System.out.println( element ); } } print( new HashSet< Object >( /* ... */ ) ); print( new ArrayList< Integer >( /* ... */ ) ); print( new TreeSet< String >( /* ... */ ) );
7. 弦樂
字串是 Java 和其他程式語言中最常用的類型之一。Java 語言透過直接支援連接和比較操作,簡化了許多常規字串操作。此外,標準庫包含許多使字串操作高效的類別。這正是我們本節要討論的內容。在 Java 中,字串是以 UTF-16 編碼表示的不可變物件。每次連接字串(或執行任何修改原始字串的操作)時,都會建立 String類別的新實例。因此,串聯操作可能變得非常低效,導致創建 String類別的許多中間實例(通常會產生垃圾)。但是Java標準庫包含兩個非常有用的類,其目的是使字串操作變得方便。它們是 StringBuilderStringBuffer(它們之間的唯一區別是 StringBuffer是線程安全的,而 StringBuilder則相反)。讓我們來看看使用其中一個類別的幾個範例: final StringBuilder sb = new StringBuilder(); for( int i = 1; i <= 10; ++i ) { sb.append( " " ); sb.append( i ); } sb.deleteCharAt( 0 ); sb.insert( 0, "[" ); sb.replace( sb.length() - 3, sb.length(), "]" ); 雖然使用 StringBuilder/StringBuffer是操作字串的推薦方法,但在連接兩個或三個字串的最簡單場景中,它可能看起來有些過分,因此正常的加法運算子 ( (“+”),例如:通常, String userId = "user:" + new Random().nextInt( 100 ); 簡化連接的最佳替代方法是使用字串格式化以及 Java 標準函式庫來幫助提供靜態 String.format幫助器方法。它支援一組豐富的格式說明符,包括數字、符號、日期/時間等。(有關完整詳細信息,請參閱參考文檔) String.format 方法提供了 一種 String.format( "%04d", 1 ); -> 0001 String.format( "%.2f", 12.324234d ); -> 12.32 String.format( "%tR", new Date() ); -> 21:11 String.format( "%tF", new Date() ); -> 2014-11-11 String.format( "%d%%", 12 ); -> 12% 乾淨且輕量級的方法來從各種資料類型產生字串。值得注意的是,現代 Java IDE 可以從傳遞給 String.format方法的參數中解析格式規範,並在偵測到任何不符時向開發人員發出警告。
8. 命名約定
Java 是一種不強迫開發人員嚴格遵循任何命名約定的語言,但社群制定了一組簡單的規則,使 Java 程式碼在標準函式庫和任何其他 Java 專案中看起來一致:
  • 套件名稱為小寫:org.junit、com.fasterxml.jackson、javax.json
  • 類別、枚舉、介面、註解的名稱皆使用大寫字母:StringBuilder、Runnable、@Override
  • 欄位或方法的名稱(除了static final)以駝峰表示法指定:isEmpty、format、addAll
  • static Final 欄位或枚舉常數名稱為大寫,並以底線(“_”)分隔:LOG、MIN_RADIX、INSTANCE。
  • 局部變數或方法參數以駝峰表示法鍵入:str、newLength、minimumCapacity
  • 泛型的參數類型名稱由單一大寫字母表示:T、U、E
透過遵循這些簡單的約定,您編寫的程式碼將看起來簡潔,並且在風格上與其他程式庫或框架沒有區別,並且感覺像是由同一個人編寫的(這是約定真正起作用的罕見情況之一) 。
9. 標準庫
無論您正在從事哪種類型的 Java 項目,Java 標準庫都是您最好的朋友。是的,很難否認它們有一些粗糙的邊緣和奇怪的設計決策,但是,99% 的情況下,它們都是由專家編寫的高品質程式碼。值得探索。每個 Java 版本都為現有程式庫帶來了許多新功能(舊功能可能存在一些問題),並且還添加了許多新程式庫。Java 5 帶來了一個新的 並發函式庫作為 java.util.concurrent套件的一部分。Java 6 提供了(不太為人所知的)腳本支援( javax.script套件)和Java 編譯器 API (作為 javax.tools套件的一部分)。Java 7 為 java.util.concurrent帶來了許多改進,在 java.nio.file套件中引入了新的 I/O 庫,並在 java.lang.invoke中引入了對動態語言的支援。最後,Java 8在 java.time套件中加入了期待已久的 日期/時間。Java 作為一個平台正在不斷發展,隨著上述變化的發展是非常重要的。每當您考慮在專案中包含第三方程式庫或框架時,請確保標準 Java 庫中尚未包含所需的功能(當然,有許多領先於現有技術的專門的高效能演算法實現)標準庫中的演算法,但在大多數情況下確實不需要)。
10. 不變性
整個指南和本部分的不變性仍然是一個提醒:請認真對待。如果您設計的類別或實作的方法可以提供不變性保證,那麼它可以在大多數情況下隨處使用,而不必擔心同時被修改。這將使您作為開發人員的生活(也希望您的團隊成員的生活)變得更加輕鬆。
11. 測試
測試驅動開發 (TDD) 的實踐在 Java 社群中非常流行,並提高了程式碼品質的標準。儘管 TDD 提供了所有好處,但令人遺憾的是,今天的 Java 標準庫不包含任何測試框架或支援工具。然而,測試已成為現代 Java 開發的必要組成部分,在本節中,我們將介紹使用 JUnit框架的一些基本技術。在 JUnit 中,本質上,每個測試都是一組關於物件的預期狀態或行為的語句。編寫優秀測試的秘訣是保持測試簡單、簡短,一次測試一件事。作為練習,讓我們編寫一組測試來驗證 String.format是否是字串部分中傳回所需結果的函數。 package com.javacodegeeks.advanced.generic; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.Test; public class StringFormatTestCase { @Test public void testNumberFormattingWithLeadingZeros() { final String formatted = String.format( "%04d", 1 ); assertThat( formatted, equalTo( "0001" ) ); } @Test public void testDoubleFormattingWithTwoDecimalPoints() { final String formatted = String.format( "%.2f", 12.324234d ); assertThat( formatted, equalTo( "12.32" ) ); } } 這兩個測試看起來都非常可讀,而且它們的執行都是實例。如今,平均 Java 專案包含數百個測試案例,在開發過程中為開發人員提供有關回歸或功能的快速回饋。
12. 下一步
本指南的這一部分完成了與 Java 程式設計實踐和該程式語言手冊相關的一系列討論。下次我們將回到該語言的特性,探索 Java 的世界,了解異常、異常類型、如何以及何時使用它們。
13.下載原始碼
這是高級 Java 課程中關於一般開發原則的課程。 本課程的源代碼可以在此下載。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION