基礎2

異常處理

Java的異常

在計算機程序運行的過程當中,老是會出現各類各樣的錯誤。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是異常體系的根,它繼承自ObjectThrowable有兩個體系:ErrorExceptionError表示嚴重的錯誤,程序對此通常無能爲力,例如:ide

  • OutOfMemoryError:內存耗盡
  • NoClassDefFoundError:沒法加載某個Class
  • StackOverflowError:棧溢出

Exception則是運行時的錯誤,它能夠被捕獲並處理。工具

某些異常是應用程序邏輯處理的一部分,應該捕獲並處理。例如:單元測試

  • NumberFormatException:數值類型的格式錯誤
  • FileNotFoundException:未找到文件
  • SocketException:讀取網絡失敗

還有一些異常是程序邏輯編寫不對形成的,應該修復程序自己。例如:測試

  • NullPointerException:對某個null的對象調用方法或字段
  • IndexOutOfBoundsException:數組索引越界

Exception又分爲兩大類:

  1. RuntimeException以及它的子類;
  2. RuntimeException(包括IOExceptionReflectiveOperationException等等)

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語句,每一個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方法中被拋出的,從下往上看,調用層次依次是:

  1. main()調用process1()
  2. process1()調用process2()
  3. process2()調用Integer.parseInt(String)
  4. 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()方法,拋出異常分兩步:

  1. 建立某個Exception的實例;
  2. 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派生「根異常」,再派生出業務異常;

自定義異常時,應該提供多種構造方法。

NullPointerException

在全部的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是一種代碼邏輯錯誤,遇到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,只能在開發和測試階段啓用斷言;

對可恢復的錯誤不能使用斷言,而應該拋出異常;

斷言不多被使用,更好的方法是編寫單元測試。

使用JDK Logging

在編寫程序的過程當中,發現程序運行結果與預期不符,怎麼辦?固然是用System.out.println()打印出執行過程當中的某些變量,觀察每一步的結果與代碼邏輯是否符合,而後有針對性地修改代碼。

代碼改好了怎麼辦?固然是刪除沒有用的System.out.println()語句了。

若是改代碼又改出問題怎麼辦?再加上System.out.println()

反覆這麼搞幾回,很快你們就發現使用System.out.println()很是麻煩。

怎麼辦?

解決方法是使用日誌。

那什麼是日誌?日誌就是Logging,它的目的是爲了取代System.out.println()

輸出日誌,而不是用System.out.println(),有如下幾個好處:

  1. 能夠設置輸出樣式,避免本身每次都寫"ERROR: " + var
  2. 能夠設置輸出級別,禁止某些級別輸出。例如,只輸出錯誤日誌;
  3. 能夠被重定向到文件,這樣能夠在程序運行結束後查看日誌;
  4. 能夠按包名控制日誌級別,只輸出某些包打的日誌;
  5. 能夠……

由於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個日誌級別,從嚴重到普通:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

由於默認級別是INFO,所以,INFO級別如下的日誌,不會被打印出來。使用日誌級別的好處在於,調整級別,就能夠屏蔽掉不少調試相關的日誌輸出。

使用Commons Logging

和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語句,你看能不能編譯經過?

因此,反射是爲了解決在運行期,對某個實例一無所知的狀況下,如何調用其方法。

Class類

JVM爲每一個加載的classinterface建立了對應的Class實例來保存classinterface的全部信息;
獲取一個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並不會識別該註解)。

定義一個註解時,還能夠定義配置參數。配置參數能夠包括:

  • 全部基本類型;
  • String;
  • 枚舉類型;
  • 基本類型、String、Class以及枚舉的數組。

由於配置參數必須是常量,因此,上述限制保證了註解在定義時就已經肯定了每一個參數的值。

註解的配置參數能夠有默認值,缺乏某個配置參數時將使用默認值。

此外,大部分註解會有一個名爲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。使用@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定義的valueElementType[]數組,只有一個元素時,能夠省略數組的寫法。

@Retention

另外一個重要的元註解@Retention定義了Annotation的生命週期:

  • 僅編譯期:RetentionPolicy.SOURCE
  • 僅class文件: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

使用@Repeatable這個元註解能夠定義Annotation是否可重複。這個註解應用不是特別普遍。

@Inherited

使用@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

咱們總結一下定義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),不然運行期沒法讀取到該註解。

能夠經過程序處理註解來實現相應的功能:

  • 對JavaBean的屬性值按規則進行檢查;
  • JUnit會自動運行@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不會自動給註解添加任何額外的邏輯。

泛型

泛型是一種「代碼模板」,能夠用一套代碼套用各類類型。

image.png

什麼是泛型

在講解什麼是泛型以前,咱們先觀察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

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList
  • ...

這是不可能的,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類型,由於FloatNumber的子類。可是,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,由於實際類型是ObjectObject類型沒法持有基本類型:

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只小兔構成的集合。

在數學中,咱們常常遇到集合的概念。例如:

  • 有限集合:

    • 一個班全部的同窗構成的集合;
    • 一個網站全部的商品構成的集合;
    • ...
  • 無限集合:

    • 全體天然數集合:1,2,3,……
    • 有理數集合;
    • 實數集合;
    • ...

爲何要在計算機中引入集合呢?這是爲了便於處理一組相似的數據,例如:

  • 計算全部同窗的總成績和平均成績;
  • 列舉全部的商品名稱和價格;
  • ……

在Java中,若是一個Java對象能夠在內部持有若干其餘Java對象,並對外提供訪問接口,咱們把這種Java對象稱爲集合。很顯然,Java的數組能夠看做是一種集合:

String[] ss = new String[10]; // 能夠持有10個String對象
ss[0] = "Hello"; // 能夠放入String對象
String first = ss[0]; // 能夠獲取String對象

既然Java提供了數組這種數據類型,能夠充當集合,那麼,咱們爲何還須要其餘集合類?這是由於數組有以下限制:

  • 數組初始化後大小不可變;
  • 數組只能按索引順序存取。

所以,咱們須要各類不一樣類型的集合類來處理不一樣的數據,例如:

  • 可變大小的順序鏈表;
  • 保證無重複元素的集合;
  • ...

Collection

Java標準庫自帶的java.util包提供了集合類:Collection,它是除Map外全部其餘集合類的根接口。Java的java.util包主要提供瞭如下三種類型的集合:

  • List:一種有序列表的集合,例如,按索引排列的StudentList
  • Set:一種保證沒有重複元素的集合,例如,全部無重複名稱的StudentSet
  • Map:一種經過鍵值(key-value)查找的映射表集合,例如,根據Studentname查找對應StudentMap

Java集合的設計有幾個特色:一是實現了接口和實現類相分離,例如,有序表的接口是List,具體的實現類有ArrayListLinkedList等,二是支持泛型,咱們能夠限制在一個集合中只能放入同一種數據類型的元素,例如:

List<String> list = new ArrayList<>(); // 只能放入String類型

最後,Java訪問集合老是經過統一的方式——迭代器(Iterator)來實現,它最明顯的好處在於無需知道集合內部元素是按什麼方式存儲的。

因爲Java的集合設計很是久遠,中間經歷過大規模改進,咱們要注意到有一小部分集合類是遺留類,不該該繼續使用:

  • Hashtable:一種線程安全的Map實現;
  • Vector:一種線程安全的List實現;
  • Stack:基於Vector實現的LIFO的棧。

還有一小部分接口是遺留接口,也不該該繼續使用:

  • Enumeration<E>:已被Iterator<E>取代。

小結

Java的集合類定義在java.util包中,支持泛型,主要提供了3種集合類,包括ListSetMap。Java集合使用統一的Iterator遍歷,儘可能不要使用遺留接口。

使用List

在集合類中,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 │
└───┴───┴───┴───┴───┴───┘

而後,往內部指定索引的數組位置添加一個元素,而後把size1

size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

繼續添加元素,可是數組已滿,沒有空閒位置的時候,ArrayList先建立一個更大的新數組,而後把舊數組的全部元素複製到新數組,緊接着用新數組取代舊數組:

size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

如今,新數組就有了空位,能夠繼續添加一個元素到數組末尾,同時size1

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

相關文章
相關標籤/搜索