總結Java代碼書寫的四個技巧

來源:互聯網
上載者:User
本篇文章給大家帶來的內容是關於總結Java代碼書寫的四個技巧 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

我們平時的編程任務不外乎就是將相同的技術套件應用到不同的項目中去,對於大多數情況來說,這些技術都是可以滿足目標的。然而,有的項目可能需要用到一些特別的技術,因此工程師們得深入研究,去尋找那些最簡單但最有效方法。本文我們將介紹一些有助於解決常見問題的通用設計策略和目標實現技術,即:

  1. 只做有目的性的最佳化

  2. 常量盡量使用枚舉

  3. 重新定義類裡面的equals()方法

  4. 盡量多使用多態性

值得注意的是,本文中描述的技術並不是適用於所有情況。另外這些技術應該什麼時候使用以及在什麼地方使用,都是需要使用者經過深思熟慮的。

1 .只做有目的性的最佳化

大型軟體系統肯定非常關注效能問題。雖然我們希望能夠寫出最高效的代碼,但很多時候,如果想對代碼進行最佳化,我們卻無從下手。例如,下面的這段代碼會影響到效能嗎?

public void processIntegers(List<Integer> integers) {for (Integer value: integers) {    for (int i = integers.size() - 1; i >= 0; i--) {        value += integers.get(i);    }}}

這就得視情況而定了。上面這段代碼可以看出它的處理演算法是O(n³)(使用大O符號),其中n是list集合的大小。如果n只有5,那麼就不會有問題,只會執行25次迭代。但如果n是10萬,那可能會影響效能了。請注意,即使這樣我們也不能判定肯定會有問題。儘管此方法需要執行10億次邏輯迭代,但會不會對效能產生影響仍然有待討論。

例如,假設用戶端是在它自己的線程中執行這段代碼,並且非同步等待計算完成,那麼它的執行時間有可能是可以接受的。同樣,如果系統部署在了生產環境上,但是沒有用戶端進行調用,那我們根本沒必要去對這段代碼進行最佳化,因為壓根就不會消耗系統的整體效能。事實上,最佳化效能以後系統會變得更加複雜,悲劇的是系統的效能卻沒有因此而提高。

最重要的是天下沒有免費的午餐,因此為了降低代價,我們通常會通過類似於緩衝、迴圈展開或預計算值這類技術去實現最佳化,這樣反而增加了系統的複雜性,也降低了代碼的可讀性。如果這種最佳化可以提高系統的效能,那麼即使變得複雜,那也是值得的,但是做決定之前,必須首Crowdsourced Security Testing道這兩條資訊:

  1. 效能要求是什麼

  2. 效能瓶頸在哪裡

首先我們需要清楚地知道效能要求是什麼。如果最終是在要求以內,並且終端使用者也沒有提出什麼異議,那麼就沒有必要進行效能最佳化。但是,當添加了新功能或者系統的資料量達到一定規模以後就必須進行最佳化了,否則可能會出現問題。

在這種情況下,不應該靠直覺,也不應該依靠檢查。因為即使是像Martin Fowler這樣有經驗的開發人員也容易做一些錯誤的最佳化,正如在重構(第70頁)一文中解釋的那樣:

如果分析了足夠多的程式以後,你會發現關於效能的有趣之處在於,大部分時間都浪費在了系統中的一小部分代碼中裡面。如果對所有代碼進行了同樣的最佳化,那麼最終結果就是浪費了90%的最佳化,因為最佳化過以後的代碼運行得頻率並不多。因為沒有目標而做的最佳化所耗費的時間,都是在浪費時間。

作為一名身經百戰的開發人員,我們應該認真對待這一觀點。第一次猜測不僅沒有提高系統的效能,而且90%的開發時間完全是浪費了。相反,我們應該在生產環境(或者預生產環境中)執行常見用例,並找出在執行過程中是哪部分在消耗系統資源,然後對系統進行配置。例如消耗大部分資源的代碼只佔了10%,那麼最佳化其餘90%的代碼就是浪費時間。

根據分析結果,要想使用這些知識,我們應該從最常見的情況入手。因為這將確保實際付出的努力最終是可以提高系統的效能。每次最佳化後,都應該重複分析步驟。因為這不僅可以確保系統的效能真的得到了改善,也可以看出再對系統進行最佳化後,效能瓶頸是在哪個部分(因為解決完一個瓶頸以後,其它瓶頸可能消耗系統更多的整體資源)。需要注意的是,在現有瓶頸中花費的時間百分比很可能會增加,因為剩下的瓶頸是暫時不變的,而且隨著目標瓶頸的消除,整個執行時間應該會減少。

儘管在Java系統中想要對概要檔案進行全面檢查需要很大的容量,但是還是有一些很常見的工具可以協助發現系統的效能熱點,這些工具包括JMeter、AppDynamics和YourKit。另外,還可以參見DZone的效能監測指南,擷取更多關於Java程式效能最佳化的資訊。

雖然效能是許多大型軟體系統一個非常重要的組成部分,也成為產品交付管道中自動化測試套件的一部分,但是還是不能夠盲目的且沒有目的的進行最佳化。相反,應該對已經掌握的效能瓶頸進行特定的最佳化。這不僅可以協助我們避免增加了系統的複雜性,而且還讓我們少走彎路,不去做那些浪費時間的最佳化。

2.常量盡量使用枚舉

需要使用者列出一組預定義或常量值的情境有很多,例如在web應用程式中可能遇到的HTTP響應代碼。最常見的實現技術之一是建立類,該類裡面有很多靜態final類型的值,每個值都應該有一句注釋,描述該值的含義是什麼:

public class HttpResponseCodes {public static final int OK = 200;public static final int NOT_FOUND = 404;public static final int FORBIDDEN = 403;}if (getHttpResponse().getStatusCode() == HttpResponseCodes.OK) {// Do something if the response code is OK }

能夠有這種思路就已經非常好了,但這還是有一些缺點:

沒有對傳入的整數值進行嚴格的校正
由於是基礎資料型別 (Elementary Data Type),因此不能調用狀態碼上的方法
在第一種情況下只是簡單的建立了一個特定的常量來表示特殊的整數值,但並沒有對方法或變數進行限制,因此使用的值可能會超出定義的範圍。例如:

public class HttpResponseHandler {public static void printMessage(int statusCode) {    System.out.println("Recieved status of " + statusCode); }}

HttpResponseHandler.printMessage(15000);
儘管15000並不是有效HTTP響應代碼,但是由於伺服器端也沒有限制用戶端必須提供有效整數。在第二種情況下,我們沒有辦法為狀態碼定義方法。例如,如果想要檢查給定的狀態碼是否是一個成功的代碼,那就必須定義一個單獨的函數:

public class HttpResponseCodes {public static final int OK = 200;public static final int NOT_FOUND = 404;public static final int FORBIDDEN = 403;public static boolean isSuccess(int statusCode) {    return statusCode >= 200 && statusCode < 300; }}if (HttpResponseCodes.isSuccess(getHttpResponse().getStatusCode())) {// Do something if the response code is a success code }

為瞭解決這些問題,我們需要將常量類型從基礎資料型別 (Elementary Data Type)改為自訂類型,並只允許自訂類的特定對象。這正是Java枚舉(enum)的用途。使用enum,我們可以一次性解決這兩個問題:

public enum HttpResponseCodes {OK(200),FORBIDDEN(403),NOT_FOUND(404);private final int code; HttpResponseCodes(int code) {    this.code = code;}public int getCode() {    return code;}public boolean isSuccess() {    return code >= 200 && code < 300;}}if (getHttpResponse().getStatusCode().isSuccess()) {// Do something if the response code is a success code }

同樣,現在還可以要求在調用方法的時候提供必須有效狀態碼:

public class HttpResponseHandler {public static void printMessage(HttpResponseCode statusCode) {    System.out.println("Recieved status of " + statusCode.getCode()); }}

HttpResponseHandler.printMessage(HttpResponseCode.OK);
值得注意的是,舉這個例子事項說明如果是常量,則應該盡量使用枚舉,但並不是說什麼情況下都應該使用枚舉。在某些情況下,可能希望使用一個常量來表示某個特殊值,但是也允許提供其它的值。例如,大家可能都知道圓周率,我們可以用一個常量來捕獲這個值(並重用它):

public class NumericConstants {public static final double PI = 3.14;public static final double UNIT_CIRCLE_AREA = PI * PI;}public class Rug {private final double area;public class Run(double area) {    this.area = area;}public double getCost() {    return area * 2;}}// Create a carpet that is 4 feet in diameter (radius of 2 feet)Rug fourFootRug = new Rug(2 * NumericConstants.UNIT_CIRCLE_AREA);

因此,使用枚舉的規則可以歸納為:

當所有可能的離散值都已經提前知道了,那麼就可以使用枚舉

再拿上文中所提到的HTTP響應代碼為例,我們可能知道HTTP狀態碼的所有值(可以在RFC 7231中找的到,它定義了HTTP 1.1協議)。因此使用了枚舉。在計算圓周率的情況下,我們不知道關於圓周率的所有可能值(任何可能的double都是有效),但同時又希望為圓形的rugs建立一個常量,使計算更容易(更容易閱讀);因此定義了一系列常量。

如果不能提前知道所有可能的值,但是又希望包含每個值的欄位或方法,那麼最簡單的方法就是可以建立一個類來表示資料。儘管沒有說過什麼情境應該絕對不用枚舉,但要想知道在什麼地方、什麼時間不使用枚舉的關鍵是提前意識到所有的值,並且禁止使用其他任何值。

3.重新定義類裡面的equals()方法

對象識別可能是一個很難解決的問題:如果兩個對象在記憶體中佔據相同的位置,那麼它們是相同的嗎?如果它們的id相同,它們是相同的嗎?或者如果所有的欄位都相等呢?雖然每個類都有自己的標識邏輯,但是在系統中有很多西方都需要去判斷是否相等。例如,有如下的一個類,表示訂單購買…

public class Purchase {private long id;public long getId() {    return id;}public void setId(long id) {    this.id = id;}}

……就像下面寫的這樣,代碼中肯定有很多地方都是類似於的:

Purchase originalPurchase = new Purchase();Purchase updatedPurchase = new Purchase();if (originalPurchase.getId() == updatedPurchase.getId()) {// Execute some logic for equal purchases }

這些邏輯調用的越多(反過來,違背了DRY原則),Purchase類的身份資訊也會變得越來越多。如果出於某種原因,更改了Purchase類的身份邏輯(例如,更改了標識符的類型),則需要更新標識邏輯所在的位置肯定也非常多。

我們應該在類的內部初始化這個邏輯,而不是通過系統將Purchase類的身份邏輯進行過多的傳播。乍一看,我們可以建立一個新的方法,比如isSame,這個方法的入參是一個Purchase對象,並對每個對象的id進行比較,看看它們是否相同:

public class Purchase {private long id;public boolean isSame(Purchase other) {    return getId() == other.gerId();   }}

雖然這是一個有效解決方案,但是忽略了Java的內建功能:使用equals方法。Java中的每個類都是繼承了Object類,雖然是隱式的,因此同樣也就繼承了equals方法。預設情況下,此方法將檢查對象標識(記憶體中相同的對象),如JDK中的對象類定義(version 1.8.0_131)中的以下程式碼片段所示:

public boolean equals(Object obj) {return (this == obj);}

這個equals方法充當了注入身份邏輯的自然位置(通過覆蓋預設的equals實現):

public class Purchase {private long id;public long getId() {    return id;}public void setId(long id) {    this.id = id;}@Overridepublic boolean equals(Object other) {    if (this == other) {        return true;    }    else if (!(other instanceof Purchase)) {        return false;    }    else {        return ((Purchase) other).getId() == getId();    }}}

雖然這個equals方法看起來很複雜,但由於equals方法只接受類型對象的參數,所以我們只需要考慮三個案例:

另一個對象是當前對象(即originalPurchase.equals(originalPurchase)),根據定義,它們是同一個對象,因此返回true

另一個對象不是Purchase對象,在這種情況下,我們無法比較Purchase的id,因此,這兩個對象不相等

其他對象不是同一個對象,但卻是Purchase的執行個體,因此,是否相等取決於當前Purchase的id和其他Purchase是否相等

現在可以重構我們之前的條件,如下:

Purchase originalPurchase = new Purchase();Purchase updatedPurchase = new Purchase();if (originalPurchase.equals(updatedPurchase)) {// Execute some logic for equal purchases }

除了可以在系統中減少複製,重構預設的equals方法還有一些其它的優勢。例如,如果構造一個Purchase對象列表,並檢查列表是否包含具有相同ID(記憶體中不同對象)的另一個Purchase對象,那麼我們就會得到true值,因為這兩個值被認為是相等的:

List<Purchase> purchases = new ArrayList<>();purchases.add(originalPurchase);purchases.contains(updatedPurchase); // True

通常,無論在什麼地方,如果需要判斷兩個類是否相等,則只需要使用重寫過的equals方法就可以了。如果希望使用由於繼承了Object對象而隱式具有的equals方法去判斷相等性,我們還可以使用= =操作符,如下:

if (originalPurchase == updatedPurchase) {// The two objects are the same objects in memory }

還需要注意的是,當equals方法被重寫以後,hashCode方法也應該被重寫。有關這兩種方法之間關係的更多資訊,以及如何正確定義hashCode方法,請參見此線程。

正如我們所看到的,重寫equals方法不僅可以將身份邏輯在類的內部進行初始化,並在整個系統中減少了這種邏輯的擴散,它還允許Java語言對類做出有根據的決定。

4.盡量多使用多態性

對於任何一門程式設計語言來說,條件句都是一種很常見的結構,而且它的存在也是有一定原因的。因為不同的組合可以允許使用者根據給定值或對象的瞬時狀態改變系統的行為。假設使用者需要計算各銀行賬戶的餘額,那麼就可以開發出以下的代碼:

public enum BankAccountType {CHECKING,SAVINGS,CERTIFICATE_OF_DEPOSIT;}public class BankAccount {private final BankAccountType type;public BankAccount(BankAccountType type) {    this.type = type;}public double getInterestRate() {    switch(type) {        case CHECKING:            return 0.03; // 3%        case SAVINGS:            return 0.04; // 4%        case CERTIFICATE_OF_DEPOSIT:            return 0.05; // 5%        default:            throw new UnsupportedOperationException();    }}public boolean supportsDeposits() {    switch(type) {        case CHECKING:            return true;        case SAVINGS:            return true;        case CERTIFICATE_OF_DEPOSIT:            return false;        default:            throw new UnsupportedOperationException();    }}}

雖然上面這段代碼滿足了基本的要求,但是有個很明顯的缺陷:使用者只是根據給定帳戶的類型決定系統的行為。這不僅要求使用者每次要做決定之前都需要檢查賬戶類型,還需要在做出決定時重複這個邏輯。例如,在上面的設計中,使用者必須在兩種方法都進行檢查才可以。這就可能會出現失控的情況,特別是接收到添加新帳戶類型的需求時。

我們可以使用多態來隱式地做出決策,而不是使用賬戶類型用來區分。為了做到這一點,我們將BankAccount的具體類轉換成一個介面,並將決策過程傳入一系列具體的類,這些類代表了每種類型的銀行帳戶:

public interface BankAccount {public double getInterestRate();public boolean supportsDeposits();}public class CheckingAccount implements BankAccount {@Overridepublic double getIntestRate() {    return 0.03;}@Overridepublic boolean supportsDeposits() {    return true;}}public class SavingsAccount implements BankAccount {@Overridepublic double getIntestRate() {    return 0.04;}@Overridepublic boolean supportsDeposits() {    return true;}}public class CertificateOfDepositAccount implements BankAccount {@Overridepublic double getIntestRate() {    return 0.05;}@Overridepublic boolean supportsDeposits() {    return false;}}

這不僅將每個帳戶特有的資訊封裝到了到自己的類中,而且還支援使用者可以在兩種重要的方式中對設計進行變化。首先,如果想要添加一個新的銀行帳戶類型,只需建立一個新的具體類,實現了BankAccount的介面,給出兩個方法的具體實現就可以了。在條件結構設計中,我們必須在枚舉中添加一個新值,在兩個方法中添加新的case語句,並在每個case語句下插入新帳戶的邏輯。

其次,如果我們希望在BankAccount介面中添加一個新方法,我們只需在每個具體類中添加新方法。在條件設計中,我們必須複製現有的switch語句並將其添加到我們的新方法中。此外,我們還必須在每個case語句中添加每個帳戶類型的邏輯。

在數學上,當我們建立一個新方法或添加一個新類型時,我們必須在多態和條件設計中做出相同數量的邏輯更改。例如,如果我們在多態設計中添加一個新方法,我們必須將新方法添加到所有n個銀行帳戶的具體類中,而在條件設計中,我們必須在我們的新方法中添加n個新的case語句。如果我們在多態設計中添加一個新的account類型,我們必須在BankAccount介面中實現所有的m數,而在條件設計中,我們必須向每個m現有方法添加一個新的case語句。

雖然我們必須做的改變的數量是相等的,但變化的性質卻是完全不同的。在多態設計中,如果我們添加一個新的帳戶類型並且忘記包含一個方法,編譯器會拋出一個錯誤,因為我們沒有在我們的BankAccount介面中實現所有的方法。在條件設計中,沒有這樣的檢查,以確保每個類型都有一個case語句。如果添加了新類型,我們可以簡單地忘記更新每個switch語句。這個問題越嚴重,我們就越重複我們的switch語句。我們是人類,我們傾向於犯錯誤。因此,任何時候,只要我們可以依賴編譯器來提醒我們錯誤,我們就應該這麼做。

關於這兩種設計的第二個重要注意事項是它們在外部是等同的。例如,如果我們想要檢查一個支票帳戶的利率,條件設計就會類似如下:

BankAccount checkingAccount = new BankAccount(BankAccountType.CHECKING);System.out.println(checkingAccount.getInterestRate()); // Output: 0.03

相反,多態設計將類似如下:

BankAccount checkingAccount = new CheckingAccount();System.out.println(checkingAccount.getInterestRate()); // Output: 0.03

從外部的角度來看,我們只是在BankAccount對象上調用getintereUNK()。如果我們將建立過程抽象為一個工廠類的話,這將更加明顯:

public class ConditionalAccountFactory {public static BankAccount createCheckingAccount() {     return new BankAccount(BankAccountType.CHECKING);}}public class PolymorphicAccountFactory {public static BankAccount createCheckingAccount() {     return new CheckingAccount();}}// In both cases, we create the accounts using a factoryBankAccount conditionalCheckingAccount = ConditionalAccountFactory.createCheckingAccount();BankAccount polymorphicCheckingAccount = PolymorphicAccountFactory.createCheckingAccount();// In both cases, the call to obtain the interest rate is the sameSystem.out.println(conditionalCheckingAccount.getInterestRate()); // Output: 0.03System.out.println(polymorphicCheckingAccount.getInterestRate()); // Output: 0.03

將條件邏輯替換成多態類是非常常見的,因此已經發布了將條件陳述式重構為多態類的方法。這裡就有一個簡單的例子。此外,馬丁·福勒(Martin Fowler)的《重構》(p . 255)也描述了執行這個重構的詳細過程。

就像本文中的其他技術一樣,對於何時執行從條件邏輯轉換到多態類,沒有硬性規定。事實上,如論在何種情況下我們都是不建議使用。在測試驅動的設計中:例如,Kent Beck設計了一個簡單的貨幣系統,目的是使用多態類,但發現這使設計過於複雜,於是便將他的設計重新設計成一個非多態風格。經驗和合理的判斷將決定何時是將條件代碼轉換為多態代碼的合適時間。

結束語

作為程式員,儘管平常所使用的常規技術可以解決大部分的問題,但有時我們應該打破這種常規,主動需求一些創新。畢竟作為一名開發人員,擴充自己知識面的的廣度和深度,不僅能讓我們做出更明智的決定,也能讓我們變得越來越聰明。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.