Java高階系列——如何高效的編寫方法(methods)

Java高階系列——如何高效的編寫方法(methods)

本文我們將會花一些時間從不同的方面去討論一下Java中方法的設計和實現方式。在本系列前面的文章中我們已經看到,在Java中編寫一個方法非常的簡單,但是在編寫方法時掌握一些關鍵的要素可以讓方法可讀性更強而且更加高效。

方法簽名(Method signatures)

我們已經知道,Java是面嚮物件語言。因此,Java中的每個方法屬於類的例項(static方法屬於類本身),每個方法都有可見(或者可訪問)規則,方法也可能被定義成abstract或者final型別等。然而,在方法中要討論的最重要的一部分就是方法的簽名、返回型別和引數、再加上實現方法中可能丟擲的一系列的異常。首先我們從一個小的例子說起:

public static void main(String[] args) {

// Some implementation here

}

main方法接受一個字串陣列作為引數並且什麼都不返回。如果所有的方法都像main方法一樣簡單是非常nice的,但是實際上,main方法的方法簽名實際上可讀性很差。讓我們看一下下面的例子:

public void setTitleVisible(int lenght, String title, boolean visible) {

// Some implementation here

}

按照慣例,首先我們需要注意的是,Java中的方法的命名要遵循駝峰規則,比如:setTitleVisible。選擇這樣命名是非常好的一種習慣並且這樣可以透過方法名就能夠描述方法的職能以及該方法可以做什麼。

第二,每個引數的名稱都需要表明(至少能夠暗示)它的意圖。為方法引數找一個正確的、說明引數用意的名稱是非常重要的,而並不是簡單的宣告int i, String s, boolean f。

第三,我們上面所宣告的方法只有3個引數。雖然Java對於方法引數的數量有最高的限制,但是推薦的方法引數數量低於6個。超過這個點的方法簽名將會變得很難理解。

從Java 5版本開始,方法的引數如果是相同型別的變數列表(成為可變引數),則可以使用一種特殊的語法,比如:

public void find(String 。。。 elements) {

// Some implementation here

}

在內部,Java編譯器會轉換可變引數為對應型別的一個數組並且在方法實現中可訪問這些可變引數。

有趣的是,Java允許使用泛型型別引數宣告可變引數。然而,由於引數型別不被知曉,所以Java編譯器要確定泛型可變引數被正確的使用並建議將方法宣告為final方法,同時使用@SafeVarargs註解註解簽名為泛型可變引數的方法。比如:

@SafeVarargs

final publicvoid find(T 。。。 elements) {

// Some implementation here

}

另外一種方式就是使用@SuppressWarnings註解,比如:

@SuppressWarnings( “unchecked” )

publicvoid findSuppressed(T 。。。 elements) {

// Some implementation here

}

我們接下來要演示的就是方法簽名部分異常檢查的用法。最近幾年異常檢查已經被證明並沒有預期的那樣有用,因為異常檢查會導致在實際問題尚未解決的情況下寫入很多的樣板程式碼。

public void write( File file ) throws IOException {

// Some implementation here

}

通常建議將方法引數標記為final(但很少使用)。當方法引數重新分配不同的值時,它有助於擺脫不良的程式碼實踐。而且,這種方法引數可以被匿名類使用,儘管Java 8透過引入有效的final變數來緩解這個約束。

方法體(Method body)

每個方法都有他自己的實現和存在的意圖。但是,有一些通用的指南真的可以幫助你寫出簡介且可讀性很高的方法。

可能最重要的一個就是單一責任原則(single responsibility principle):盡最大的努力使用這種原則去實現方法,這樣的話每個方法只做一件事情並且可以做很好。遵循這種原則可能會增新增類的方法數量,所以最重要的是如何去找到正確的權衡方案。

編碼和設計的另一個重要的事情是保持方法實現的簡短(通常是遵循單一職責原則即可)。簡短的方法通常更容易推理,再加上簡短的方法可以在一個螢幕上顯示,這樣的話就能夠讓閱讀程式碼的人能夠更快的理解程式碼。

最後的建議與使用返回語句有關。如果一個方法返回一些值,儘量減少返回語句被呼叫的地方的數量(比較深層次的人我們推薦在所有的情況下都使用單一的返回語句)。如果方法擁有過多的返回語句,那麼遵循其邏輯流程並修改(或重構)實現就會變得越來越困難。

方法過載

方法過載技術通常用於為方法引數(簽名)為不同的引數型別或組合的方法提供專用的方法版本。雖然方法名稱相同,在方法呼叫的地方編譯器會根據實際的引數值找出正確的可選方法(Java中過載的最好例子就是構造器,方法名相同,但是引數不同),如果編譯器未找到正確的方法,則編譯器會報編譯錯誤。比如:

public String numberToString(Long number) {

return Long。toString( number );

}

public String numberToString(BigDecimal number) {

return number。toString();

}

方法過載有點類似泛型,但是它是用在泛型不適合使用的情況並且每個(或大多數)泛型型別引數都需要自己的專用實現。不過,組合泛型和過載可能會相當強大,但是由於型別擦除的原因,在Java中應該是不可能了。我們來看個例子:

publicString numberToString(T number) {

return number。toString();

}

public String numberToString(BigDecimal number) {

return number。toPlainString();

}

雖然上面的程式碼可以不使用泛型來寫,但是為了我們的演示意圖這並不是很重要。有趣的部分是numberToString方法過載了一個BigDecimal的專用實現,併為所有其他數字型別提供了一個泛型版本。

方法覆蓋(Method overriding)

我們在本系列文章的如何設計類和介面一文中我們已經談論過方法覆蓋。在本節,在我們已經瞭解關於方法覆蓋的情況下,我們將會更深入的去探討為什麼使用@Override 註解是如此的重要。我們將會透過例子的形式去演示在簡單的類層次結構中方法覆蓋和方法過載的細微的差別。

public class Parent {

public Object toObject(Number number) {

return number。toString();

}

}

類Parent有一個方法toObject。我們宣告一個子類繼承這個類並嘗試覆蓋toObject方法轉化Number為String。

public class Child extends Parent {

@Override

public String toObject(Number number) {

return number。toString();

}

}

不過,Child類中的toObject方法的簽名與父類toObject方法的簽名相同,但是返回值型別有一點不同(請參閱Covariant方法的返回型別以獲取更多詳細資訊),但它確實覆蓋了父類的方法並且Java編譯器對此可以編譯透過。現在,讓我們為Child類新增一個方法:

public class Child extends Parent {

public String toObject(Double number) {

return number。toString();

}

}

同樣,在方法簽名(Double而不是Number)方面只有細微的差別,但在這種情況下,它是方法的過載版本,它不覆蓋父方法。如果Java編譯器和@Override註解的幫助不存在,那麼我們上一個例子中使用@Override註解的方法就會報編譯錯誤。

內聯(Inlining)

內聯是由Java JIT(Just-in-time)編譯器執行的一種最佳化,目的是消除特定的方法呼叫,並直接用方法實現替換它。JIT編譯器所使用的思想就是依賴方法被呼叫的頻率和方法的大小,方法太大就不能被有效的內聯。內聯可以為您的程式碼提供顯著的效能改進並且可以讓你的方法更簡短。

遞迴(Recursion)

Java中遞迴是在執行計算時方法自我呼叫的一種技術。比如,我們來看一個數字求和的例子:

public int sum( int[] numbers ) {

if( numbers。length == 0 ) {

return 0;

}

if( numbers。length == 1 ) {

return numbers[0];

} else {

return numbers[0] + sum(Arrays。copyOfRange(numbers,1, numbers。length));

}

}

上面的這段求和程式碼是一種非常低效的實現,但是用來演示遞迴已經足夠了。對於遞迴方法有一個我們所熟知的問題,即依賴於呼叫鏈的深度可能會填滿堆疊並最終導致StackOverflowError異常。但是事情並非我們所聽到的那麼糟糕,因為有一種被稱為尾部呼叫最佳化(tail call optimization)的技術可以消除棧溢位。如果方法是尾遞迴(tail-recursive,尾遞迴方法是所有遞迴呼叫都是尾呼叫的方法)方法時這種技術可以被應用。比如,我們重寫上面的演算法為尾遞迴演算法:

public int sum( int initial, int[] numbers ) {

if(numbers。length == 0) {

return initial;

}

if(numbers。length == 1) {

return initial + numbers[0];

} else {

return sum(initial + numbers[0], Arrays。copyOfRange(numbers, 1, numbers。length));

}

}

不幸的是,目前Java編譯器(以及JVM JIT編譯器)不支援尾部呼叫最佳化,但在Java中編寫遞迴演算法時,它仍然是一個需要了解和考慮的非常有用的技術。

方法引用(Method References)

透過將Functional概念引入到Java語言中,Java 8向前邁了一大步。Functional概念的基礎是將方法作為資料對待,在之前這種概念在Java中是不被支援的(但是,從Java 7開始,JVM和Java標準庫就已經有一些特性讓這種概念變得可能)。用方法引用,這一切現在都變成了可能。

引用型別 例子

引用靜態方法 SomeClass::staticMethodName

引用指定物件的例項方法 someInstance::instanceMethodName

引用某個型別的任意物件的例項方法 SomeType::methodName

引用構造方法 SomeClass::new

讓我們透過一個例子的形式來概述方法如何作為引數來傳遞給另外的方法。

public class MethodReference {

public static void println( String s ) {

System。out。println( s );

}

public static void main( String[] args ) {

final Collection< String > strings = Arrays。asList( “s1”, “s2”, “s3” );

strings。stream()。forEach( MethodReference::println );

}

}

main方法的最後一行引用println方法在控制檯列印字串集合中的每一個元素並且方法被作為一個引數傳遞給其他(forEach)方法。

不可變性(Immutability)

不變性現在正受到很多關注,Java也不例外。眾所周知Java中實現不可變性是很困難的,但這並不意味著它應該被忽略。

然而,另一種方法是不要修改狀態,而是每次都要返回一個新狀態。聽起來並沒有那麼恐怖並且新的Java 8的Date/Time API是不可變性的一個很好的例子。讓我們看一下下面的程式碼:

final LocalDateTime now = LocalDateTime。now();

final LocalDateTime tomorrow = now。plusHours(24);

final LocalDateTime midnight = now。withHour(0)。withMinute(0)。withSecond(0)。withNano(0);

對需要修改其狀態的LocalDateTime例項的每次呼叫都將返回新的LocalDateTime例項,並保持原來的一個不變。與舊的Calendar和Date相比,這是API設計慣例中的一次重大轉變。

方法註釋(Method Documentation)

讓我們看看下面的一個例子:

/**

* The method parses the string argument as a signed decimal integer。

* The characters in the string must all be decimal digits, except

* that the first character may be a minus sign {@code ’-’} or plus

* sign {@code ’+’}。

*

*

An exception of type {@code NumberFormatException} is thrown if

* string is {@code null} or has length of zero。

*

*

Examples:

*

* parse( “0” ) returns 0

* parse( “+42”) returns 42

* parse( “-2” ) returns -2

* parse( “string” ) throws a NumberFormatException

*

*

* @param str a {@code String} containing the {@code int} representation to be parsed

* @return the integer value represented by the string

* @exception NumberFormatException if the string does not contain a valid integer value

*/

public int parse( String str ) throws NumberFormatException {

return Integer。parseInt( str );

}

對於一個簡單的parse方法來講,這個註釋相對冗長,但是它展示了Javadoc工具所提供的一些有用的能力,包括其它類的引用,簡單的程式碼段以及一些高階的格式。

使用Javadoc工具生成方法註釋,初級到中級的Java開發人員都可以理解方法的意圖以及合適的使用方法。

方法引數和返回值

方法註釋是很好的一個東西,但是不幸的是,當使用不正確的或不期望的引數值呼叫方法時,它不會阻止使用。因此,從經驗上來講,所有的公共方法都應該驗證它的引數,同時永遠不要相信所給的值都是正確的。

回到我們上一節的例子,方法parse在做邏輯操作之前都應該執行方法的引數驗證:

public int parse( String str ) throws NumberFormatException {

if( str == null ) {

throw new IllegalArgumentException(“String should not be null”);

}

return Integer。parseInt( str );

}

Java有另外一種使用斷言(assert)語句的方式執行驗證和完整性檢查。但是這種方式在執行時可能會被關閉並且不會被執行。這種方式首選用來執行一些檢查並丟擲相關異常。

即使有方法註釋和引數驗證,但是有一些和返回值相關的東西任然需要提一下。在Java 8之前,一個方法如果沒有值可返回的最簡單的方式就是返回null。這也是為什麼NullPointerException異常在Java中臭名昭著的原因,Java 8嘗試透過引入Optional類來解決這個問題。我們來看個例子:

publicOptionalfind(String id) {

// Some implementation here

}

Optional提供了很多有用的方法並且完全消除了方法返回null值和無處不在的null值檢查的必要。唯一的例外可能就是集合,當方法返回集合時,它總是返回空的集合而不是null,比如:

publicCollectionfind(String id) {

return Collections。emptyList();

}

方法作為API的關鍵點

即使您只是在您的機構或者企業中開發應用程式的開發人員,或者是流行的Java框架或庫之一的貢獻者,但是你所做的設計決策在你的程式碼如何使用上面都扮演了很重要的角色。

雖然API設計指南可以寫成很多本書,但是本系列文章的這一部分我們會接觸一些(方法作為API的關鍵點),因此快速的總結是非常有用的。

方法及方法引數要使用有意義的名稱;

儘量保持方法引數少於6個;

保持方法簡短且有很強的可讀性;

註釋公共方法,包括先決條件和樣例;

執行引數驗證和完整性檢查;

避免使用null作為返回值;

只要有意義,儘量設計不可變的方法;

使用可見性和可訪問性規則隱藏不公開的方法;

https://blog。csdn。net/zyhlwzy/article/details/79084345