在計算機程序運行的過程當中,老是會出現各類各樣的錯誤。java
有一些錯誤是用戶形成的,好比,但願用戶輸入一個int
類型的年齡,可是用戶的輸入是abc
:程序員
// 假設用戶輸入了abc: String s = "abc"; int n = Integer.parseInt(s); // NumberFormatException!
程序想要讀寫某個文件的內容,可是用戶已經把它刪除了:數組
// 用戶刪除了該文件: String t = readFile("C:\\abc.txt"); // FileNotFoundException!
Java內置了一套異常處理機制,老是使用異常來表示錯誤。安全
異常是一種class
,所以它自己帶有類型信息。異常能夠在任何地方拋出,但只須要在上層捕獲,這樣就和方法調用分離了:網絡
try { String s = processFile(「C:\\test.txt」); // ok: } catch (FileNotFoundException e) { // file not found: } catch (SecurityException e) { // no read permission: } catch (IOException e) { // io error: } catch (Exception e) { // other error: }
由於Java的異常是class
,它的繼承關係以下:框架
┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Throwable │ └───────────┘ ▲ ┌─────────┴─────────┐ │ │ ┌───────────┐ ┌───────────┐ │ Error │ │ Exception │ └───────────┘ └───────────┘ ▲ ▲ ┌───────┘ ┌────┴──────────┐ │ │ │ ┌─────────────────┐ ┌─────────────────┐┌───────────┐ │OutOfMemoryError │... │RuntimeException ││IOException│... └─────────────────┘ └─────────────────┘└───────────┘ ▲ ┌───────────┴─────────────┐ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │NullPointerException │ │IllegalArgumentException │... └─────────────────────┘ └─────────────────────────┘
從繼承關係可知:Throwable
是異常體系的根,它繼承自Object
。Throwable
有兩個體系:Error
和Exception
,Error
表示嚴重的錯誤,程序對此通常無能爲力,例如:ide
OutOfMemoryError
:內存耗盡NoClassDefFoundError
:沒法加載某個ClassStackOverflowError
:棧溢出而Exception
則是運行時的錯誤,它能夠被捕獲並處理。工具
某些異常是應用程序邏輯處理的一部分,應該捕獲並處理。例如:單元測試
NumberFormatException
:數值類型的格式錯誤FileNotFoundException
:未找到文件SocketException
:讀取網絡失敗還有一些異常是程序邏輯編寫不對形成的,應該修復程序自己。例如:測試
NullPointerException
:對某個null
的對象調用方法或字段IndexOutOfBoundsException
:數組索引越界Exception
又分爲兩大類:
RuntimeException
以及它的子類;RuntimeException
(包括IOException
、ReflectiveOperationException
等等)Java規定:
Exception
及其子類,但不包括RuntimeException
及其子類,這種類型的異常稱爲Checked Exception。Error
及其子類,RuntimeException
及其子類。捕獲異常使用try...catch
語句,把可能發生異常的代碼放到try {...}
中,而後使用catch
捕獲對應的Exception
及其子類
public class Main { public static void main(String[] args) { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) { try { // 用指定編碼轉換String爲byte[]: return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 若是系統不支持GBK編碼,會捕獲到UnsupportedEncodingException: System.out.println(e); // 打印異常信息 return s.getBytes(); // 嘗試使用用默認編碼 } } }
若是咱們不捕獲UnsupportedEncodingException
,會出現編譯失敗的問題
編譯器會報錯,錯誤信息相似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,而且準確地指出須要捕獲的語句是return s.getBytes("GBK");
。意思是說,像UnsupportedEncodingException
這樣的Checked Exception,必須被捕獲。
這是由於String.getBytes(String)
方法定義是:
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { ... }
在方法定義的時候,使用throws Xxx
表示該方法可能拋出的異常類型。調用方在調用的時候,必須強制捕獲這些異常,不然編譯器會報錯。
Java使用異常來表示錯誤,並經過try ... catch
捕獲異常;
Java的異常是class
,而且從Throwable
繼承;
Error
是無需捕獲的嚴重錯誤,Exception
是應該捕獲的可處理的錯誤;
RuntimeException
無需強制捕獲,非RuntimeException
(Checked Exception)需強制捕獲,或者用throws
聲明;
不推薦捕獲了異常但不進行任何處理。
在Java中,凡是可能拋出異常的語句,均可以用try ... catch
捕獲。把可能發生異常的語句放在try { ... }
中,而後使用catch
捕獲對應的Exception
及其子類。
可使用多個catch
語句,每一個catch
分別捕獲對應的Exception
及其子類。JVM在捕獲到異常後,會從上到下匹配catch
語句,匹配到某個catch
後,執行catch
代碼塊,而後_再也不_繼續匹配。
簡單地說就是:多個catch
語句只有一個能被執行。例如:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println(e); } catch (NumberFormatException e) { System.out.println(e); } }
存在多個catch
的時候,catch
的順序很是重要:子類必須寫在前面。例如:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println("IO error"); } catch (UnsupportedEncodingException e) { // 永遠捕獲不到 System.out.println("Bad encoding"); } }
對於上面的代碼,UnsupportedEncodingException
異常是永遠捕獲不到的,由於它是IOException
的子類。當拋出UnsupportedEncodingException
異常時,會被catch (IOException e) { ... }
捕獲並執行。
finally
語句塊保證有無錯誤都會執行。上述代碼能夠改寫以下:
public static void main(String[] args) { try { process1(); process2(); process3(); } catch (UnsupportedEncodingException e) { System.out.println("Bad encoding"); } catch (IOException e) { System.out.println("IO error"); } finally { System.out.println("END"); } }
當某個方法拋出了異常時,若是當前方法沒有捕獲異常,異常就會被拋到上層調用方法,直到遇到某個try ... catch
被捕獲爲止:
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { process2(); } static void process2() { Integer.parseInt(null); // 會拋出NumberFormatException } }
經過printStackTrace()
能夠打印出方法的調用棧,相似:
java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:614) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.process2(Main.java:16) at Main.process1(Main.java:12) at Main.main(Main.java:5)
printStackTrace()
對於調試錯誤很是有用,上述信息表示:NumberFormatException
是在java.lang.Integer.parseInt
方法中被拋出的,從下往上看,調用層次依次是:
main()
調用process1()
;process1()
調用process2()
;process2()
調用Integer.parseInt(String)
;Integer.parseInt(String)
調用Integer.parseInt(String, int)
。查看Integer.java
源碼可知,拋出異常的方法代碼以下:
public static int parseInt(String s, int radix) throws NumberFormatException { if (s == null) { throw new NumberFormatException("null"); } ... }
而且,每層調用均給出了源代碼的行號,可直接定位。
當發生錯誤時,例如,用戶輸入了非法的字符,咱們就能夠拋出異常。
如何拋出異常?參考Integer.parseInt()
方法,拋出異常分兩步:
Exception
的實例;throw
語句拋出。下面是一個例子:
void process2(String s) { if (s==null) { NullPointerException e = new NullPointerException(); throw e; } }
實際上,絕大部分拋出異常的代碼都會合並寫成一行:
void process2(String s) { if (s==null) { throw new NullPointerException(); } }
若是一個方法捕獲了某個異常後,又在catch
子句中拋出新的異常,就至關於把拋出的異常類型「轉換」了:
void process1(String s) { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(); } } void process2(String s) { if (s==null) { throw new NullPointerException(); } }
Java標準庫定義的經常使用異常包括:
Exception │ ├─ RuntimeException │ │ │ ├─ NullPointerException │ │ │ ├─ IndexOutOfBoundsException │ │ │ ├─ SecurityException │ │ │ └─ IllegalArgumentException │ │ │ └─ NumberFormatException │ ├─ IOException │ │ │ ├─ UnsupportedCharsetException │ │ │ ├─ FileNotFoundException │ │ │ └─ SocketException │ ├─ ParseException │ ├─ GeneralSecurityException │ ├─ SQLException │ └─ TimeoutException
當咱們在代碼中須要拋出異常時,儘可能使用JDK已定義的異常類型。例如,參數檢查不合法,應該拋出IllegalArgumentException
:
static void process1(int age) { if (age <= 0) { throw new IllegalArgumentException(); } }
在一個大型項目中,能夠自定義新的異常類型,可是,保持一個合理的異常繼承體系是很是重要的。
一個常見的作法是自定義一個BaseException
做爲「根異常」,而後,派生出各類業務類型的異常。
BaseException
須要從一個適合的Exception
派生,一般建議從RuntimeException
派生:
public class BaseException extends RuntimeException { }
其餘業務類型的異常就能夠從BaseException
派生:
public class UserNotFoundException extends BaseException { } public class LoginFailedException extends BaseException { } ...
自定義的BaseException
應該提供多個構造方法:
public class BaseException extends RuntimeException { public BaseException() { super(); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(String message) { super(message); } public BaseException(Throwable cause) { super(cause); } }
上述構造方法實際上都是原樣照抄RuntimeException
。這樣,拋出異常的時候,就能夠選擇合適的構造方法。經過IDE能夠根據父類快速生成子類的構造方法。
拋出異常時,儘可能複用JDK已定義的異常類型;
自定義異常體系時,推薦從RuntimeException
派生「根異常」,再派生出業務異常;
自定義異常時,應該提供多種構造方法。
在全部的RuntimeException
異常中,Java程序員最熟悉的恐怕就是NullPointerException
了。
NullPointerException
即空指針異常,俗稱NPE。若是一個對象爲null
,調用其方法或訪問其字段就會產生NullPointerException
,這個異常一般是由JVM拋出的,例如:
public class Main { public static void main(String[] args) { String s = null; System.out.println(s.toLowerCase()); } }
指針這個概念實際上源自C語言,Java語言中並沒有指針。咱們定義的變量其實是引用,Null Pointer更確切地說是Null Reference,不過二者區別不大。
若是遇到NullPointerException
,咱們應該如何處理?首先,必須明確,NullPointerException
是一種代碼邏輯錯誤,遇到NullPointerException
,遵循原則是早暴露,早修復,嚴禁使用catch
來隱藏這種編碼錯誤:
// 錯誤示例: 捕獲NullPointerException try { transferMoney(from, to, amount); } catch (NullPointerException e) { }
好的編碼習慣能夠極大地下降NullPointerException
的產生,例如:
成員變量在定義時初始化:
public class Person { private String name = ""; }
使用空字符串""
而不是默認的null
可避免不少NullPointerException
,編寫業務邏輯時,用空字符串""
表示未填寫比null
安全得多。
返回空字符串""
、空數組而不是null
:
public String[] readLinesFromFile(String file) { if (getFileSize(file) == 0) { // 返回空數組而不是null: return new String[0]; } ... }
這樣可使得調用方無需檢查結果是否爲null
。
斷言(Assertion)是一種調試程序的方式。在Java中,使用assert
關鍵字來實現斷言。
咱們先看一個例子:
public static void main(String[] args) { double x = Math.abs(-123.45); assert x >= 0; System.out.println(x); }
語句assert x >= 0;
即爲斷言,斷言條件x >= 0
預期爲true
。若是計算結果爲false
,則斷言失敗,拋出AssertionError
。
使用assert
語句時,還能夠添加一個可選的斷言消息:
assert x >= 0 : "x must >= 0";
這樣,斷言失敗的時候,AssertionError
會帶上消息x must >= 0
,更加便於調試。
Java斷言的特色是:斷言失敗時會拋出AssertionError
,致使程序結束退出。所以,斷言不能用於可恢復的程序錯誤,只應該用於開發和測試階段。
對於可恢復的程序錯誤,不該該使用斷言。例如:
void sort(int[] arr) { assert arr != null; }
應該拋出異常並在上層捕獲:
void sort(int[] arr) { if (x == null) { throw new IllegalArgumentException("array cannot be null"); } }
斷言是一種調試方式,斷言失敗會拋出AssertionError
,只能在開發和測試階段啓用斷言;
對可恢復的錯誤不能使用斷言,而應該拋出異常;
斷言不多被使用,更好的方法是編寫單元測試。
在編寫程序的過程當中,發現程序運行結果與預期不符,怎麼辦?固然是用System.out.println()
打印出執行過程當中的某些變量,觀察每一步的結果與代碼邏輯是否符合,而後有針對性地修改代碼。
代碼改好了怎麼辦?固然是刪除沒有用的System.out.println()
語句了。
若是改代碼又改出問題怎麼辦?再加上System.out.println()
。
反覆這麼搞幾回,很快你們就發現使用System.out.println()
很是麻煩。
怎麼辦?
解決方法是使用日誌。
那什麼是日誌?日誌就是Logging,它的目的是爲了取代System.out.println()
。
輸出日誌,而不是用System.out.println()
,有如下幾個好處:
"ERROR: " + var
;由於Java標準庫內置了日誌包java.util.logging
,咱們能夠直接用。先看一個簡單的例子:
public class Hello { public static void main(String[] args) { Logger logger = Logger.getGlobal(); logger.info("start process..."); logger.warning("memory is running out..."); logger.fine("ignored."); logger.severe("process will be terminated..."); } }
運行上述代碼,獲得相似以下的輸出:
Mar 02, 2019 6:32:13 PM Hello main INFO: start process... Mar 02, 2019 6:32:13 PM Hello main WARNING: memory is running out... Mar 02, 2019 6:32:13 PM Hello main SEVERE: process will be terminated...
對比可見,使用日誌最大的好處是,它自動打印了時間、調用類、調用方法等不少有用的信息。
再仔細觀察發現,4條日誌,只打印了3條,logger.fine()
沒有打印。這是由於,日誌的輸出能夠設定級別。JDK的Logging定義了7個日誌級別,從嚴重到普通:
由於默認級別是INFO,所以,INFO級別如下的日誌,不會被打印出來。使用日誌級別的好處在於,調整級別,就能夠屏蔽掉不少調試相關的日誌輸出。
和Java標準庫提供的日誌不一樣,Commons Logging是一個第三方日誌庫,它是由Apache建立的日誌模塊。
Commons Logging的特點是,它能夠掛接不一樣的日誌系統,並經過配置文件指定掛接的日誌系統。默認狀況下,Commons Loggin自動搜索並使用Log4j(Log4j是另外一個流行的日誌系統),若是沒有找到Log4j,再使用JDK Logging。
什麼是反射?
反射就是Reflection,Java的反射是指程序在運行期能夠拿到一個對象的全部信息。
正常狀況下,若是咱們要調用一個對象的方法,或者訪問一個對象的字段,一般會傳入對象實例:
// Main.java import com.itranswarp.learnjava.Person; public class Main { String getFullName(Person p) { return p.getFirstName() + " " + p.getLastName(); } }
可是,若是不能得到Person
類,只有一個Object
實例,好比這樣:
String getFullName(Object obj) { return ??? }
怎麼辦?有童鞋會說:強制轉型啊!
String getFullName(Object obj) { Person p = (Person) obj; return p.getFirstName() + " " + p.getLastName(); }
強制轉型的時候,你會發現一個問題:編譯上面的代碼,仍然須要引用Person
類。否則,去掉import
語句,你看能不能編譯經過?
因此,反射是爲了解決在運行期,對某個實例一無所知的狀況下,如何調用其方法。
JVM爲每一個加載的class
及interface
建立了對應的Class
實例來保存class
及interface
的全部信息;
獲取一個class
對應的Class
實例後,就能夠獲取該class
的全部信息;
經過Class實例獲取class
信息的方法稱爲反射(Reflection);
JVM老是動態加載class
,能夠在運行期根據條件來控制加載class。
Java的反射API提供的Field
類封裝了字段的全部信息:
經過Class
實例的方法能夠獲取Field
實例:getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
;
經過Field實例能夠獲取字段信息:getName()
,getType()
,getModifiers()
;
經過Field實例能夠讀取或設置某個對象的字段,若是存在訪問限制,要首先調用setAccessible(true)
來訪問非public
字段。
經過反射讀寫字段是一種很是規方法,它會破壞對象的封裝。
當咱們獲取到一個Method
對象時,就能夠對它進行調用。咱們如下面的代碼爲例:
String s = "Hello world"; String r = s.substring(6); // "world"
若是用反射來調用substring
方法,須要如下代碼:
// String對象: String s = "Hello world"; // 獲取String substring(int)方法,參數爲int: Method m = String.class.getMethod("substring", int.class); // 在s對象上調用該方法並獲取結果: String r = (String) m.invoke(s, 6); // 打印調用結果: System.out.println(r);
Java的反射API提供的Method對象封裝了方法的全部信息:
經過Class
實例的方法能夠獲取Method
實例:getMethod()
,getMethods()
,getDeclaredMethod()
,getDeclaredMethods()
;
經過Method
實例能夠獲取方法信息:getName()
,getReturnType()
,getParameterTypes()
,getModifiers()
;
經過Method
實例能夠調用某個對象的方法:Object invoke(Object instance, Object... parameters)
;
經過設置setAccessible(true)
來訪問非public
方法;
經過反射調用方法時,仍然遵循多態原則。
咱們一般使用new
操做符建立新的實例:
Person p = new Person();
若是經過反射來建立新的實例,能夠調用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
Constructor
對象封裝了構造方法的全部信息;
經過Class
實例的方法能夠獲取Constructor
實例:getConstructor()
,getConstructors()
,getDeclaredConstructor()
,getDeclaredConstructors()
;
經過Constructor
實例能夠建立一個實例對象:newInstance(Object... parameters)
; 經過設置setAccessible(true)
來訪問非public
構造方法。
註解是放在Java源碼的類、方法、字段、參數前的一種特殊「註釋」:
// this is a component: @Resource("hello") public class Hello { @Inject int n; @PostConstruct public void hello(@Param String name) { System.out.println(name); } @Override public String toString() { return "Hello"; } }
註釋會被編譯器直接忽略,註解則能夠被編譯器打包進入class文件,所以,註解是一種用做標註的「元數據」。
從JVM的角度看,註解自己對代碼邏輯沒有任何影響,如何使用註解徹底由工具決定。
Java的註解能夠分爲三類:
第一類是由編譯器使用的註解,例如:
@Override
:讓編譯器檢查該方法是否正確地實現了覆寫;@SuppressWarnings
:告訴編譯器忽略此處代碼產生的警告。這類註解不會被編譯進入.class
文件,它們在編譯後就被編譯器扔掉了。
第二類是由工具處理.class
文件使用的註解,好比有些工具會在加載class的時候,對class作動態修改,實現一些特殊的功能。這類註解會被編譯進入.class
文件,但加載結束後並不會存在於內存中。這類註解只被一些底層庫使用,通常咱們沒必要本身處理。
第三類是在程序運行期可以讀取的註解,它們在加載後一直存在於JVM中,這也是最經常使用的註解。例如,一個配置了@PostConstruct
的方法會在調用構造方法後自動被調用(這是Java代碼讀取該註解實現的功能,JVM並不會識別該註解)。
定義一個註解時,還能夠定義配置參數。配置參數能夠包括:
由於配置參數必須是常量,因此,上述限制保證了註解在定義時就已經肯定了每一個參數的值。
註解的配置參數能夠有默認值,缺乏某個配置參數時將使用默認值。
此外,大部分註解會有一個名爲value
的配置參數,對此參數賦值,能夠只寫常量,至關於省略了value參數。
若是隻寫註解,至關於所有使用默認值。
舉個栗子,對如下代碼:
public class Hello { @Check(min=0, max=100, value=55) public int n; @Check(value=99) public int p; @Check(99) // @Check(value=99) public int x; @Check public int y; }
@Check
就是一個註解。第一個@Check(min=0, max=100, value=55)
明肯定義了三個參數,第二個@Check(value=99)
只定義了一個value
參數,它實際上和@Check(99)
是徹底同樣的。最後一個@Check
表示全部參數都使用默認值。
註解(Annotation)是Java語言用於工具處理的標註:
註解能夠配置參數,沒有指定配置的參數使用默認值;
若是參數名稱是value
,且只有一個參數,那麼能夠省略參數名稱。
Java語言使用@interface
語法來定義註解(Annotation
),它的格式以下:
public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
註解的參數相似無參數方法,能夠用default
設定一個默認值(強烈推薦)。最經常使用的參數應當命名爲value
。
有一些註解能夠修飾其餘註解,這些註解就稱爲元註解(meta annotation)。Java標準庫已經定義了一些元註解,咱們只須要使用元註解,一般不須要本身去編寫元註解。
最經常使用的元註解是@Target
。使用@Target
能夠定義Annotation
可以被應用於源碼的哪些位置:
ElementType.TYPE
;ElementType.FIELD
;ElementType.METHOD
;ElementType.CONSTRUCTOR
;ElementType.PARAMETER
。例如,定義註解@Report
可用在方法上,咱們必須添加一個@Target(ElementType.METHOD)
:
@Target(ElementType.METHOD) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
定義註解@Report
可用在方法或字段上,能夠把@Target
註解參數變爲數組{ ElementType.METHOD, ElementType.FIELD }
:
@Target({ ElementType.METHOD, ElementType.FIELD }) public @interface Report { ... }
實際上@Target
定義的value
是ElementType[]
數組,只有一個元素時,能夠省略數組的寫法。
另外一個重要的元註解@Retention
定義了Annotation
的生命週期:
RetentionPolicy.SOURCE
;RetentionPolicy.CLASS
;RetentionPolicy.RUNTIME
。若是@Retention
不存在,則該Annotation
默認爲CLASS
。由於一般咱們自定義的Annotation
都是RUNTIME
,因此,務必要加上@Retention(RetentionPolicy.RUNTIME)
這個元註解:
@Retention(RetentionPolicy.RUNTIME) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
使用@Repeatable
這個元註解能夠定義Annotation
是否可重複。這個註解應用不是特別普遍。
使用@Inherited
定義子類是否可繼承父類定義的Annotation
。@Inherited
僅針對@Target(ElementType.TYPE)
類型的annotation
有效,而且僅針對class
的繼承,對interface
的繼承無效:
@Inherited @Target(ElementType.TYPE) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
在使用的時候,若是一個類用到了@Report
:
@Report(type=1) public class Person { }
則它的子類默認也定義了該註解:
public class Student extends Person { }
咱們總結一下定義Annotation
的步驟:
第一步,用@interface
定義註解:
public @interface Report { }
第二步,添加參數、默認值:
public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
把最經常使用的參數定義爲value()
,推薦全部參數都儘可能設置默認值。
第三步,用元註解配置註解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
其中,必須設置@Target
和@Retention
,@Retention
通常設置爲RUNTIME
,由於咱們自定義的註解一般要求在運行期讀取。通常狀況下,沒必要寫@Inherited
和@Repeatable
。
Java使用@interface
定義註解:
可定義多個參數和默認值,核心參數使用value
名稱;
必須設置@Target
來指定Annotation
能夠應用的範圍;
應當設置@Retention(RetentionPolicy.RUNTIME)
便於運行期讀取該Annotation
。
能夠在運行期經過反射讀取RUNTIME
類型的註解,注意千萬不要漏寫@Retention(RetentionPolicy.RUNTIME)
,不然運行期沒法讀取到該註解。
能夠經過程序處理註解來實現相應的功能:
@Test
標記的測試方法。註解如何使用,徹底由程序本身決定。例如,JUnit是一個測試框架,它會自動運行全部標記爲@Test
的方法。
咱們來看一個@Range
註解,咱們但願用它來定義一個String
字段的規則:字段長度知足@Range
的參數定義:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Range { int min() default 0; int max() default 255; }
在某個JavaBean中,咱們可使用該註解:
public class Person { @Range(min=1, max=20) public String name; @Range(max=10) public String city; }
可是,定義了註解,自己對程序邏輯沒有任何影響。咱們必須本身編寫代碼來使用註解。這裏,咱們編寫一個Person
實例的檢查方法,它能夠檢查Person
實例的String
字段長度是否知足@Range
的定義:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException { // 遍歷全部Field: for (Field field : person.getClass().getFields()) { // 獲取Field定義的@Range: Range range = field.getAnnotation(Range.class); // 若是@Range存在: if (range != null) { // 獲取Field的值: Object value = field.get(person); // 若是值是String: if (value instanceof String) { String s = (String) value; // 判斷值是否知足@Range的min/max: if (s.length() < range.min() || s.length() > range.max()) { throw new IllegalArgumentException("Invalid field: " + field.getName()); } } } } }
這樣一來,咱們經過@Range
註解,配合check()
方法,就能夠完成Person
實例的檢查。注意檢查邏輯徹底是咱們本身編寫的,JVM不會自動給註解添加任何額外的邏輯。
泛型是一種「代碼模板」,能夠用一套代碼套用各類類型。
在講解什麼是泛型以前,咱們先觀察Java標準庫提供的ArrayList
,它能夠看做「可變長度」的數組,由於用起來比數組更方便。
實際上ArrayList
內部就是一個Object[]
數組,配合存儲一個當前分配的長度,就能夠充當「可變數組」:
public class ArrayList { private Object[] array; private int size; public void add(Object e) {...} public void remove(int index) {...} public Object get(int index) {...} }
若是用上述ArrayList
存儲String
類型,會有這麼幾個缺點:
例如,代碼必須這麼寫:
ArrayList list = new ArrayList(); list.add("Hello"); // 獲取到Object,必須強制轉型爲String: String first = (String) list.get(0);
很容易出現ClassCastException,由於容易「誤轉型」:
list.add(new Integer(123)); // ERROR: ClassCastException: String second = (String) list.get(1);
要解決上述問題,咱們能夠爲String
單獨編寫一種ArrayList
:
public class StringArrayList { private String[] array; private int size; public void add(String e) {...} public void remove(int index) {...} public String get(int index) {...} }
這樣一來,存入的必須是String
,取出的也必定是String
,不須要強制轉型,由於編譯器會強制檢查放入的類型:
StringArrayList list = new StringArrayList(); list.add("Hello"); String first = list.get(0); // 編譯錯誤: 不容許放入非String類型: list.add(new Integer(123));
問題暫時解決。
然而,新的問題是,若是要存儲Integer
,還須要爲Integer
單獨編寫一種ArrayList
:
public class IntegerArrayList { private Integer[] array; private int size; public void add(Integer e) {...} public void remove(int index) {...} public Integer get(int index) {...} }
實際上,還須要爲其餘全部class單獨編寫一種ArrayList
:
這是不可能的,JDK的class就有上千個,並且它還不知道其餘人編寫的class。
爲了解決新的問題,咱們必須把ArrayList
變成一種模板:ArrayList<T>
,代碼以下:
public class ArrayList<T> { private T[] array; private int size; public void add(T e) {...} public void remove(int index) {...} public T get(int index) {...} }
T
能夠是任何class。這樣一來,咱們就實現了:編寫一次模版,能夠建立任意類型的ArrayList
:
// 建立能夠存儲String的ArrayList: ArrayList<String> strList = new ArrayList<String>(); // 建立能夠存儲Float的ArrayList: ArrayList<Float> floatList = new ArrayList<Float>(); // 建立能夠存儲Person的ArrayList: ArrayList<Person> personList = new ArrayList<Person>();
所以,泛型就是定義一種模板,例如ArrayList<T>
,而後在代碼中爲用到的類建立對應的ArrayList<類型>
:
ArrayList<String> strList = new ArrayList<String>();
由編譯器針對類型做檢查:
strList.add("hello"); // OK String s = strList.get(0); // OK strList.add(new Integer(123)); // compile error! Integer n = strList.get(0); // compile error!
這樣一來,既實現了編寫一次,萬能匹配,又經過編譯器保證了類型安全:這就是泛型。
在Java標準庫中的ArrayList<T>
實現了List<T>
接口,它能夠向上轉型爲List<T>
:
public class ArrayList<T> implements List<T> { ... } List<String> list = new ArrayList<String>();
即類型ArrayList<T>
能夠向上轉型爲List<T>
。
要_特別注意_:不能把ArrayList<Integer>
向上轉型爲ArrayList<Number>
或List<Number>
。
這是爲何呢?假設ArrayList<Integer>
能夠向上轉型爲ArrayList<Number>
,觀察一下代碼:
// 建立ArrayList<Integer>類型: ArrayList<Integer> integerList = new ArrayList<Integer>(); // 添加一個Integer: integerList.add(new Integer(123)); // 「向上轉型」爲ArrayList<Number>: ArrayList<Number> numberList = integerList; // 添加一個Float,由於Float也是Number: numberList.add(new Float(12.34)); // 從ArrayList<Integer>獲取索引爲1的元素(即添加的Float): Integer n = integerList.get(1); // ClassCastException!
咱們把一個ArrayList<Integer>
轉型爲ArrayList<Number>
類型後,這個ArrayList<Number>
就能夠接受Float
類型,由於Float
是Number
的子類。可是,ArrayList<Number>
實際上和ArrayList<Integer>
是同一個對象,也就是ArrayList<Integer>
類型,它不可能接受Float
類型, 因此在獲取Integer
的時候將產生ClassCastException
。
實際上,編譯器爲了不這種錯誤,根本就不容許把ArrayList<Integer>
轉型爲ArrayList<Number>
。
ArrayList<Integer>和ArrayList<Number>二者徹底沒有繼承關係。
泛型就是編寫模板代碼來適應任意類型;
泛型的好處是使用時沒必要對類型進行強制轉換,它經過編譯器對類型進行檢查;
注意泛型的繼承關係:能夠把ArrayList<Integer>
向上轉型爲List<Integer>
(T
不能變!),但不能把ArrayList<Integer>
向上轉型爲ArrayList<Number>
(T
不能變成父類)。
使用ArrayList
時,若是不定義泛型類型時,泛型類型實際上就是Object
:
// 編譯器警告: List list = new ArrayList(); list.add("Hello"); list.add("World"); String first = (String) list.get(0); String second = (String) list.get(1);
此時,只能把<T>
看成Object
使用,沒有發揮泛型的優點。
當咱們定義泛型類型<String>
後,List<T>
的泛型接口變爲強類型List<String>
:
// 無編譯器警告: List<String> list = new ArrayList<String>(); list.add("Hello"); list.add("World"); // 無強制轉型: String first = list.get(0); String second = list.get(1);
當咱們定義泛型類型<Number>
後,List<T>
的泛型接口變爲強類型List<Number>
:
List<Number> list = new ArrayList<Number>(); list.add(new Integer(123)); list.add(new Double(12.34)); Number first = list.get(0); Number second = list.get(1);
編譯器若是能自動推斷出泛型類型,就能夠省略後面的泛型類型。例如,對於下面的代碼:
List<Number> list = new ArrayList<Number>();
編譯器看到泛型類型List<Number>
就能夠自動推斷出後面的ArrayList<T>
的泛型類型必須是ArrayList<Number>
,所以,能夠把代碼簡寫爲:
// 能夠省略後面的Number,編譯器能夠自動推斷泛型類型: List<Number> list = new ArrayList<>();
除了ArrayList<T>
使用了泛型,還能夠在接口中使用泛型。例如,Arrays.sort(Object[])
能夠對任意數組進行排序,但待排序的元素必須實現Comparable<T>
這個泛型接口:
public interface Comparable<T> { /** * 返回負數: 當前實例比參數o小 * 返回0: 當前實例與參數o相等 * 返回正數: 當前實例比參數o大 */ int compareTo(T o); }
使用泛型時,把泛型參數<T>
替換爲須要的class類型,例如:ArrayList<String>
,ArrayList<Number>
等;
能夠省略編譯器能自動推斷出的類型,例如:List<String> list = new ArrayList<>();
;
不指定泛型參數類型時,編譯器會給出警告,且只能將<T>
視爲Object
類型;
能夠在接口中定義泛型類型,實現此接口的類必須實現正確的泛型類型。
編寫泛型類比普通類要複雜。一般來講,泛型類通常用在集合類中,例如ArrayList<T>
,咱們不多須要編寫泛型類。
若是咱們確實須要編寫一個泛型類,那麼,應該如何編寫它?
能夠按照如下步驟來編寫一個泛型類。
首先,按照某種類型,例如:String
,來編寫類:
public class Pair { private String first; private String last; public Pair(String first, String last) { this.first = first; this.last = last; } public String getFirst() { return first; } public String getLast() { return last; } }
最後,把特定類型String
替換爲T
,並申明<T>
:
public class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
熟練後便可直接從T
開始編寫。
編寫泛型類時,要特別注意,泛型類型<T>
不能用於靜態方法。
泛型還能夠定義多種類型。例如,咱們但願Pair
不老是存儲兩個類型同樣的對象,就可使用類型<T, K>
:
public class Pair<T, K> { private T first; private K last; public Pair(T first, K last) { this.first = first; this.last = last; } public T getFirst() { ... } public K getLast() { ... } }
使用的時候,須要指出兩種類型:
Pair<String, Integer> p = new Pair<>("test", 123);
Java標準庫的Map<K, V>
就是使用兩種泛型類型的例子。它對Key使用一種類型,對Value使用另外一種類型。
編寫泛型時,須要定義泛型類型<T>
;
靜態方法不能引用泛型類型<T>
,必須定義其餘類型(例如<K>
)來實現靜態泛型方法;
泛型能夠同時定義多種類型,例如Map<K, V>
。
Java的泛型是由編譯器在編譯時實行的,編譯器內部永遠把全部類型T
視爲Object
處理,可是,在須要轉型的時候,編譯器會根據T
的類型自動爲咱們實行安全地強制轉型。
瞭解了Java泛型的實現方式——擦拭法,咱們就知道了Java泛型的侷限:
侷限一:<T>
不能是基本類型,例如int
,由於實際類型是Object
,Object
類型沒法持有基本類型:
Pair<int> p = new Pair<>(1, 2); // compile error!
侷限二:沒法取得帶泛型的Class
。
Java的泛型是採用擦拭法實現的;
擦拭法決定了泛型<T>
:
int
;Class
,例如:Pair<String>.class
;x instanceof Pair<String>
;T
類型,例如:new T()
。泛型方法要防止重複定義方法,例如:public boolean equals(T obj)
;
子類能夠獲取父類的泛型類型<T>
。
集合類型是Java標準庫中被使用最多的類型。
什麼是集合(Collection)?集合就是「由若干個肯定的元素所構成的總體」。例如,5只小兔構成的集合。
在數學中,咱們常常遇到集合的概念。例如:
有限集合:
無限集合:
爲何要在計算機中引入集合呢?這是爲了便於處理一組相似的數據,例如:
在Java中,若是一個Java對象能夠在內部持有若干其餘Java對象,並對外提供訪問接口,咱們把這種Java對象稱爲集合。很顯然,Java的數組能夠看做是一種集合:
String[] ss = new String[10]; // 能夠持有10個String對象 ss[0] = "Hello"; // 能夠放入String對象 String first = ss[0]; // 能夠獲取String對象
既然Java提供了數組這種數據類型,能夠充當集合,那麼,咱們爲何還須要其餘集合類?這是由於數組有以下限制:
所以,咱們須要各類不一樣類型的集合類來處理不一樣的數據,例如:
Java標準庫自帶的java.util
包提供了集合類:Collection
,它是除Map
外全部其餘集合類的根接口。Java的java.util
包主要提供瞭如下三種類型的集合:
List
:一種有序列表的集合,例如,按索引排列的Student
的List
;Set
:一種保證沒有重複元素的集合,例如,全部無重複名稱的Student
的Set
;Map
:一種經過鍵值(key-value)查找的映射表集合,例如,根據Student
的name
查找對應Student
的Map
。Java集合的設計有幾個特色:一是實現了接口和實現類相分離,例如,有序表的接口是List
,具體的實現類有ArrayList
,LinkedList
等,二是支持泛型,咱們能夠限制在一個集合中只能放入同一種數據類型的元素,例如:
List<String> list = new ArrayList<>(); // 只能放入String類型
最後,Java訪問集合老是經過統一的方式——迭代器(Iterator)來實現,它最明顯的好處在於無需知道集合內部元素是按什麼方式存儲的。
因爲Java的集合設計很是久遠,中間經歷過大規模改進,咱們要注意到有一小部分集合類是遺留類,不該該繼續使用:
Hashtable
:一種線程安全的Map
實現;Vector
:一種線程安全的List
實現;Stack
:基於Vector
實現的LIFO
的棧。還有一小部分接口是遺留接口,也不該該繼續使用:
Enumeration<E>
:已被Iterator<E>
取代。Java的集合類定義在java.util
包中,支持泛型,主要提供了3種集合類,包括List
,Set
和Map
。Java集合使用統一的Iterator
遍歷,儘可能不要使用遺留接口。
在集合類中,List
是最基礎的一種集合:它是一種有序列表。
List
的行爲和數組幾乎徹底相同:List
內部按照放入元素的前後順序存放,每一個元素均可以經過索引肯定本身的位置,List
的索引和數組同樣,從0
開始。
數組和List
相似,也是有序結構,若是咱們使用數組,在添加和刪除元素的時候,會很是不方便。例如,從一個已有的數組{'A', 'B', 'C', 'D', 'E'}
中刪除索引爲2
的元素:
┌───┬───┬───┬───┬───┬───┐ │ A │ B │ C │ D │ E │ │ └───┴───┴───┴───┴───┴───┘ │ │ ┌───┘ │ │ ┌───┘ │ │ ▼ ▼ ┌───┬───┬───┬───┬───┬───┐ │ A │ B │ D │ E │ │ │ └───┴───┴───┴───┴───┴───┘
這個「刪除」操做其實是把'C'
後面的元素依次往前挪一個位置,而「添加」操做其實是把指定位置之後的元素都依次向後挪一個位置,騰出來的位置給新加的元素。這兩種操做,用數組實現很是麻煩。
所以,在實際應用中,須要增刪元素的有序列表,咱們使用最多的是ArrayList
。實際上,ArrayList
在內部使用了數組來存儲全部元素。例如,一個ArrayList
擁有5個元素,實際數組大小爲6
(即有一個空位):
size=5 ┌───┬───┬───┬───┬───┬───┐ │ A │ B │ C │ D │ E │ │ └───┴───┴───┴───┴───┴───┘
當添加一個元素並指定索引到ArrayList
時,ArrayList
自動移動須要移動的元素:
size=5 ┌───┬───┬───┬───┬───┬───┐ │ A │ B │ │ C │ D │ E │ └───┴───┴───┴───┴───┴───┘
而後,往內部指定索引的數組位置添加一個元素,而後把size
加1
:
size=6 ┌───┬───┬───┬───┬───┬───┐ │ A │ B │ F │ C │ D │ E │ └───┴───┴───┴───┴───┴───┘
繼續添加元素,可是數組已滿,沒有空閒位置的時候,ArrayList
先建立一個更大的新數組,而後把舊數組的全部元素複製到新數組,緊接着用新數組取代舊數組:
size=6 ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ A │ B │ F │ C │ D │ E │ │ │ │ │ │ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
如今,新數組就有了空位,能夠繼續添加一個元素到數組末尾,同時size
加1
:
size=7 ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ A │ B │ F │ C │ D │ E │ G │ │ │ │ │ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
可見,ArrayList
把添加和刪除的操做封裝起來,讓咱們操做List
相似於操做數組,卻不用關心內部元素如何移動。
咱們考察List<E>
接口,能夠看到幾個主要的接口方法:
boolean add(E e)
boolean add(int index, E e)
int remove(int index)
int remove(Object e)
E get(int index)
int size()
可是,實現List
接口並不是只能經過數組(即ArrayList
的實現方式)來實現,另外一種LinkedList
經過「鏈表」也實現了List接口。在LinkedList
中,它的內部每一個元素都指向下一個元素:
┌───┬───┐ ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │ │ └───┴───┘ └───┴───┘ └───┴───┘ └───┴───┘
一般狀況下,咱們老是優先使用ArrayList
。