如何有效地编写方法
本文是我們名為“ 高級Java ”的學(xué)院課程的一部分。
本課程旨在幫助您最有效地使用Java。 它討論了高級主題,包括對象創(chuàng)建,并發(fā),序列化,反射等。 它將指導(dǎo)您完成Java掌握的過程! 在這里查看 !
目錄
1.簡介 2.方法簽名 3.方法主體 4.方法重載 5.方法覆蓋 6.內(nèi)聯(lián) 7.遞歸 8.方法參考 9.不變性 10.方法文檔 11.方法參數(shù)和返回值 12.方法作為API入口點 13.接下來是什么 14.下載源代碼1.簡介
在本教程的這一部分中,我們將花費一些時間來討論與Java設(shè)計和實現(xiàn)方法有關(guān)的不同方面。 正如我們在本教程的前面部分所看到的那樣,用Java編寫方法非常容易,但是有很多事情可以使您的方法更具可讀性和效率。
2.方法簽名
眾所周知,Java是一種面向?qū)ο蟮恼Z言。 這樣,Java中的每個方法都屬于某個類實例(如果使用static方法,則屬于一個類本身),具有可見性(或可訪問性)規(guī)則,可以聲明為abstract或final等。 但是,可以說方法的最重要部分是它的簽名:返回類型和參數(shù),以及方法實現(xiàn)可能拋出的已檢查異常的列表(但是如今,這一部分的使用越來越少了)。 下面是一個小例子:
public static void main( String[] args ) {// Some implementation here }main方法將字符串?dāng)?shù)組作為唯一參數(shù)args接受,并且不返回任何內(nèi)容。 使所有方法保持與main一樣簡單將是非常好的。 但實際上,方法簽名可能變得不可讀。 讓我們看下面的例子:
public void setTitleVisible( int lenght, String title, boolean visible ) {// Some implementation here }首先要注意的是,按照慣例,Java中的方法名稱以駝峰形式編寫,例如: setTitleVisible 。 該名稱選擇得當(dāng),并試圖描述該方法應(yīng)該執(zhí)行的操作。
其次,每個參數(shù)的名稱說明(或至少暗示)其目的。 為方法參數(shù)找到正確的解釋性名稱非常重要,而不是int i , String s , boolean f (但是在極少數(shù)情況下還是有意義的)。
第三,該方法僅接受三個參數(shù)。 盡管Java對允許的參數(shù)數(shù)量有更高的限制,但強烈建議將此數(shù)字保持在6以下。除此之外,方法簽名變得很難理解。
從Java 5發(fā)行版開始,這些方法可以使用特殊語法具有相同類型的參數(shù)變量列表(稱為varargs),例如:
public void find( String ... elements ) {// Some implementation here }在內(nèi)部,Java編譯器將varargs轉(zhuǎn)換為相應(yīng)類型的數(shù)組,這就是方法實現(xiàn)可以訪問varargs的方式。
有趣的是,Java還允許使用泛型類型參數(shù)聲明varargs參數(shù)。 但是,由于參數(shù)的類型未知,因此Java編譯器希望確保負(fù)責(zé)任地使用varargs ,并建議該方法是final并使用@SafeVarargs批注進行批注(有關(guān)批注的更多詳細(xì)信息,請參見第5部分)。教程, 以及如何以及何時使用Enums和Annotations )。 例如:
@SafeVarargs final public< T > void find( T ... elements ) {// Some implementation here }另一種方法是使用@SuppressWarnings批注,例如:
@SuppressWarnings( "unchecked" ) public< T > void findSuppressed( T ... elements ) {// Some implementation here }下一個示例演示將檢查的異常用作方法簽名的一部分。 近年來,事實證明,已檢查的異常不如預(yù)期的那樣有用,導(dǎo)致編寫更多的樣板代碼而不是解決的問題。
public void write( File file ) throws IOException {// Some implementation here }最后但并非最不重要的一點是,通常建議(但很少使用)將方法參數(shù)標(biāo)記為final 。 當(dāng)用不同的值重新分配方法參數(shù)時,它有助于擺脫不良的代碼實踐。 同樣,匿名類可以使用這種方法參數(shù)(有關(guān)匿名類的更多詳細(xì)信息,在本教程的第3部分“ 如何設(shè)計類和接口”中進行了介紹 ),盡管Java 8通過有效地引入final變量來緩解了這一限制。
3.方法主體
每種方法都有其自己的實現(xiàn)和目的。 但是,有一些通用指南確實可以幫助編寫清晰易懂的方法。
可能最重要的一個是單一責(zé)任原則:嘗試以這種方式實現(xiàn)方法,即每個單一方法都只會做一件事情并且做得很好。 遵循此原理可能會炸毀許多類方法,因此找到合適的平衡很重要。
編碼和設(shè)計時的另一重要事項是保持方法實現(xiàn)的簡短(通常只需遵循單一職責(zé)原則,您就可以免費獲得它)。 簡短的方法很容易推論,而且它們通常適合一個屏幕,因此您的代碼讀者可以更快地理解它們。
最后(但并非最不重要)的建議與使用return語句有關(guān)。 如果某個方法返回某個值,請嘗試最大程度地減少調(diào)用return語句的位置(有些人走得更遠,建議在所有情況下僅使用單個return語句)。 更多的return語句方法變得很難遵循其邏輯流程并修改(或重構(gòu))實現(xiàn)。
4.方法重載
方法重載技術(shù)通常用于為不同的參數(shù)類型或組合提供方法的專用版本。 盡管方法名稱保持不變,但是編譯器會根據(jù)調(diào)用點的實際參數(shù)值選擇正確的替代方法(重載的最佳示例是Java中的構(gòu)造函數(shù):名稱始終相同,但參數(shù)集不同)或如果找不到,將引發(fā)編譯器錯誤。 例如:
public String numberToString( Long number ) {return Long.toString( number ); }public String numberToString( BigDecimal number ) {return number.toString(); }方法重載在某種程度上接近于泛型(關(guān)于泛型的更多詳細(xì)信息在本教程的第4部分“ 如何以及何時使用泛型”中進行了介紹 ),但是它在泛型方法不能很好地工作并且每種(或大多數(shù))泛型使用的情況下使用類型參數(shù)需要它們自己的專用實現(xiàn)。 盡管如此,將泛型和重載結(jié)合起來可能非常強大,但是由于類型擦除(在Java中通常是不可能的)(有關(guān)更多詳細(xì)信息,請參閱教程的第4部分 , 如何及何時使用泛型 )。 讓我們看一下這個例子:
public< T extends Number > String numberToString( T number ) {return number.toString(); }public String numberToString( BigDecimal number ) {return number.toPlainString(); }盡管可以在不使用泛型的情況下編寫此代碼段,但對于我們的演示目的而言,它不是重要的。 有趣的是,方法numberToString重載了BigDecimal的專門實現(xiàn),并為所有其他數(shù)字提供了通用版本。
5.方法覆蓋
在本教程的第3部分“ 如何設(shè)計類和接口”中 ,我們討論了很多方法重寫。 在本節(jié)中,當(dāng)我們已經(jīng)知道方法重載時,我們將展示為什么使用@Override批注如此重要。 我們的示例將演示簡單類層次結(jié)構(gòu)中方法重寫和重載之間的細(xì)微差別。
public class Parent {public Object toObject( Number number ) {return number.toString();} }Parent類只有一個toObject方法。 讓我們對該類進行子類化,并嘗試提出方法版本以將數(shù)字轉(zhuǎn)換為字符串(而不是原始對象)。
public class Child extends Parent {@Overridepublic String toObject( Number number ) {return number.toString();} }盡管如此, Child類中toObject方法的簽名還是有些不同(請參見Covariant方法返回類型以獲取更多詳細(xì)信息),它確實覆蓋了超類中的那個,并且Java編譯器對此沒有任何抱怨。 現(xiàn)在,讓我們向Child類添加另一個方法。
public class Child extends Parent {public String toObject( Double number ) {return number.toString();} }同樣,方法簽名之間只有細(xì)微的差別(用Double代替Number ),但是在這種情況下,它是方法的重載版本,它不會覆蓋父方法。 這就是Java編譯器和@Override注釋的幫助得到回報的時候:用@Override注釋上一個示例中的方法會引發(fā)編譯器錯誤。
6.內(nèi)聯(lián)
內(nèi)聯(lián)是Java JIT(即時)編譯器執(zhí)行的優(yōu)化,目的是消除特定的方法調(diào)用并將其直接替換為方法實現(xiàn)。 JIT編譯器使用的啟發(fā)式方法取決于方法的調(diào)用頻率和大小。 太大的方法不能有效地內(nèi)聯(lián)。 內(nèi)聯(lián)可以大大提高代碼的性能,這是使方法保持簡短的另一個好處,如我們在“ 方法正文 ”一節(jié)中所討論的。
7.遞歸
Java中的遞歸是一種方法,該方法在執(zhí)行計算時會調(diào)用自身。 例如,讓我們看下面的示例,該示例求和一個數(shù)組的數(shù)字:
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 ) );} }這是一個非常無效的實現(xiàn),但是它很好地證明了遞歸。 遞歸方法存在一個眾所周知的問題:根據(jù)調(diào)用鏈的深度,它們可能會炸毀堆棧并導(dǎo)致StackOverflowError異常。 但是事情并沒有聽起來那么糟糕,因為有一種技術(shù)可以消除棧溢出,稱為尾部調(diào)用優(yōu)化 。 如果方法是尾部遞歸的,則可以應(yīng)用此方法(尾部遞歸方法是所有遞歸調(diào)用都是尾部調(diào)用的方法)。 例如,讓我們以尾遞歸的方式重寫以前的算法:
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編譯器)不支持尾部調(diào)用優(yōu)化,但是當(dāng)您用Java編寫遞歸算法時,它仍然是了解和考慮的一種非常有用的技術(shù)。
8.方法參考
通過將功能性概念引入Java語言,Java 8向前邁出了一大步。 其基礎(chǔ)是將方法視為數(shù)據(jù),這是該語言以前所不支持的概念(但是,由于Java 7,JVM和Java標(biāo)準(zhǔn)庫已經(jīng)具有使之成為可能的某些功能)。 幸運的是,有了方法引用,現(xiàn)在就可以了。
| 參考類型 | 例 |
| 引用靜態(tài)方法 | SomeClass::staticMethodName |
| 引用特定對象的實例方法 | someInstance::instanceMethodName |
| 引用特定類型的任意對象的實例方法 | SomeType::methodName |
| 引用構(gòu)造函數(shù) | SomeClass::new |
表格1
讓我們看一個簡單的示例,該示例說明如何將方法作為參數(shù)傳遞給其他方法。
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方法的引用將字符串集合中的每個元素打印到控制臺,并將其作為參數(shù)傳遞給另一個方法forEach 。
9.不變性
如今,不變性已引起了很多關(guān)注,Java也不例外。 眾所周知,不變性在Java中很難實現(xiàn),但這并不意味著應(yīng)將其忽略。
在Java中,不變性就是改變內(nèi)部狀態(tài)。 作為示例,讓我們看一下JavaBeans規(guī)范( http://docs.oracle.com/javase/tutorial/javabeans/ )。 它非常清楚地表明,setter可以修改包含對象的狀態(tài),而這正是每個Java開發(fā)人員所期望的。
但是,替代方法不是修改狀態(tài),而是每次都返回一個新狀態(tài)。 它并不像聽起來那樣可怕,新的Java 8 Date / Time API (在JSR 310:Date and Time API框架下開發(fā))就是一個很好的例子。 讓我們看一下以下代碼片段:
final LocalDateTime now = LocalDateTime.now(); final LocalDateTime tomorrow = now.plusHours( 24 );final LocalDateTime midnight = now.withHour( 0 ).withMinute( 0 ).withSecond( 0 ).withNano( 0 );每次需要修改其狀態(tài)的LocalDateTime實例調(diào)用都將返回新的LocalDateTime實例,并使原始實例保持不變。 與舊的Calendar和Date相比,API設(shè)計范例發(fā)生了很大的變化(使用起來不太舒服,并且引起很多麻煩)。
10.方法文檔
在Java中,特別是如果您正在開發(fā)某種庫或框架,則應(yīng)使用Javadoc工具記錄所有公共方法( http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html ) 。 嚴(yán)格來說,沒有什么可以強迫您執(zhí)行此操作,但是好的文檔可以幫助其他開發(fā)人員了解特定方法的作用,所需的參數(shù),其實現(xiàn)所具有的假設(shè)或約束,異常的類型以及何時可以引發(fā)以及返回值(如果有)可能是(加上更多東西)。
讓我們看下面的例子:
/*** 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 '+'}.** <p>An exception of type {@code NumberFormatException} is thrown if* string is {@code null} or has length of zero.** <p>Examples:* <blockquote><pre>* parse( "0" ) returns 0* parse( "+42") returns 42* parse( "-2" ) returns -2* parse( "string" ) throws a NumberFormatException* </pre></blockquote>** @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工具提供的一些有用功能,包括對其他類的引用,示例代碼片段和高級格式化。 這是流行的Java IDE之一Eclipse反映此方法文檔的方式。
僅通過查看上面的圖片,任何初級到高級的Java開發(fā)人員都可以了解該方法的目的以及使用該方法的正確方法。
11.方法參數(shù)和返回值
文檔化方法是一件很了不起的事情,但是不幸的是,當(dāng)使用不正確或意外的參數(shù)值調(diào)用方法時,它不能阻止用例。 因此,根據(jù)經(jīng)驗,所有公共方法都應(yīng)驗證其自變量,并且永遠不應(yīng)相信將始終使用正確的值來指定它們(這種模式稱為健全性檢查)。
回到上一節(jié)的示例,方法parse應(yīng)該在對其執(zhí)行任何操作之前執(zhí)行其唯一參數(shù)的驗證:
public int parse( String str ) throws NumberFormatException {if( str == null ) {throw new IllegalArgumentException( "String should not be null" );}return Integer.parseInt( str ); }Java還有另一個選擇可以使用assert語句執(zhí)行驗證和健全性檢查。 但是,可以在運行時將其關(guān)閉,并且可能無法執(zhí)行。 最好始終執(zhí)行此類檢查并提出相關(guān)異常。
即使已經(jīng)記錄了方法并驗證了它們的參數(shù),仍然有一些與它們可以返回的值有關(guān)的注釋。 在Java 8之前,說“我目前沒有值”的方法的最簡單方法就是返回null 。 這就是Java對于NullPointerException異常如此臭名昭著的原因。 Java 8試圖通過引入Optional < T >類來解決此問題。 讓我們看一下這個例子:
public< T > Optional< T > find( String id ) {// Some implementation here }Optional < T >提供了許多有用的方法,并且完全消除了該方法返回null并在各處使用null檢查污染代碼的需要。 唯一的例外可能是集合。 每當(dāng)方法返回集合時,總是最好返回空的而不是null (甚至是Optional < T > ),例如:
public< T > Collection< T > find( String id ) { return Collections.emptyList(); }12.方法作為API入口點
即使您只是在組織內(nèi)構(gòu)建應(yīng)用程序的開發(fā)人員,還是對流行的Java框架或庫之一的貢獻者,您正在執(zhí)行的設(shè)計決策在如何使用代碼方面都起著非常重要的作用。
盡管API設(shè)計指南值得多本書籍,但本教程的這一部分涉及其中的許多內(nèi)容(因為方法成為API的切入點),因此快速總結(jié)將非常有幫助:
- 為方法及其參數(shù)使用有意義的名稱( 方法簽名 )
- 嘗試使參數(shù)的數(shù)量小于6(“ 方法簽名”部分)
- 保持方法簡短易讀(“ 方法主體”和“ 內(nèi)聯(lián)”一節(jié))
- 始終記錄您的公共方法,包括前提條件和示例(如果有必要的話)(“ 方法文檔”部分)
- 始終執(zhí)行參數(shù)驗證和完整性檢查(“ 方法參數(shù)和返回值”部分 )
- 嘗試轉(zhuǎn)義null作為方法的返回值(“ 方法參數(shù)和返回值”部分 )
- 每當(dāng)它是有道理的,嘗試設(shè)計不變的方法(這不影響內(nèi)部狀態(tài),部分不變性 )
- 使用可見性和可訪問性規(guī)則隱藏不應(yīng)該公開的方法(本教程的第3部分 , 如何設(shè)計類和接口 )
13.接下來是什么
本教程的這一部分討論的不是Java語言,而是更多關(guān)于如何有效地使用Java語言,特別是通過編寫可讀,干凈,有文檔的有效方法。 在下一節(jié)中,我們將繼續(xù)相同的基本思想,并討論通用的編程準(zhǔn)則,以幫助您成為更好的Java開發(fā)人員。
14.下載源代碼
這是關(guān)于如何有效地編寫方法的課程。 您可以在此處下載源代碼: advanced-java-part-6
翻譯自: https://www.javacodegeeks.com/2015/09/how-to-write-methods-efficiently.html
總結(jié)
- 上一篇: 在使用Gradle构建的Spring B
- 下一篇: Linux进程数据结构(linux进程数