寫出優質Java代碼的4個技巧

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

  1. 只作有目的性的優化
  2. 常量儘可能使用枚舉
  3. 從新定義類裏面的equals()方法
  4. 儘可能多使用多態性

值得注意的是,本文中描述的技術並非適用於全部狀況。另外這些技術應該何時使用以及在什麼地方使用,都是須要使用者通過深思熟慮的。web

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億次邏輯迭代,但會不會對性能產生影響仍然有待討論。編程

例如,假設客戶端是在它本身的線程中執行這段代碼,而且異步等待計算完成,那麼它的執行時間有多是能夠接受的。一樣,若是系統部署在了生產環境上,可是沒有客戶端進行調用,那咱們根本不必去對這段代碼進行優化,由於壓根就不會消耗系統的總體性能。事實上,優化性能之後系統會變得更加複雜,悲劇的是系統的性能卻沒有所以而提升。緩存

最重要的是天下沒有免費的午飯,所以爲了下降代價,咱們一般會經過相似於緩存、循環展開或預計算值這類技術去實現優化,這樣反而增長了系統的複雜性,也下降了代碼的可讀性。若是這種優化能夠提升系統的性能,那麼即便變得複雜,那也是值得的,可是作決定以前,必須首先知道這兩條信息:性能優化

  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 
}

可以有這種思路就已經很是好了,但這仍是有一些缺點:

沒有對傳入的整數值進行嚴格的校驗
因爲是基本數據類型,所以不能調用狀態代碼上的方法
在第一種狀況下只是簡單的建立了一個特定的常量來表示特殊的整數值,但並無對方法或變量進行限制,所以使用的值可能會超出定義的範圍。例如:

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 
}

爲了解決這些問題,咱們須要將常量類型從基本數據類型改成自定義類型,並只容許自定義類的特定對象。這正是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;
}
@Override
public 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 {
@Override
public double getIntestRate() {
    return 0.03;
}
@Override
public boolean supportsDeposits() {
    return true;
}
}
public class SavingsAccount implements BankAccount {
@Override
public double getIntestRate() {
    return 0.04;
}
@Override
public boolean supportsDeposits() {
    return true;
}
}
public class CertificateOfDepositAccount implements BankAccount {
@Override
public double getIntestRate() {
    return 0.05;
}
@Override
public 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 factory
BankAccount conditionalCheckingAccount = ConditionalAccountFactory.createCheckingAccount();
BankAccount polymorphicCheckingAccount = PolymorphicAccountFactory.createCheckingAccount();
// In both cases, the call to obtain the interest rate is the same
System.out.println(conditionalCheckingAccount.getInterestRate()); // Output: 0.03
System.out.println(polymorphicCheckingAccount.getInterestRate()); // Output: 0.03

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

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

結束語

做爲程序員,儘管日常所使用的常規技術能夠解決大部分的問題,但有時咱們應該打破這種常規,主動需求一些創新。畢竟做爲一名開發人員,擴展本身知識面的的廣度和深度,不只能讓咱們作出更明智的決定,也能讓咱們變得愈來愈聰明。

注:文章轉載自微信公衆號:Java技術zhai

原文連接:https://mp.weixin.qq.com/s/6X...

相關文章
相關標籤/搜索