改善 Java 程序的151個建議之泛型和反射

1.Java泛型是類型擦除的

Java的泛型在編譯期有效,在運行期被刪除,也就是說所有的泛型參數(shù)類型在編譯后都會被清除掉。

public class Foo{
//listMethod接收數(shù)組參數(shù)并進行重載
    public void arrayMethod(String[] strArray){
    }
    public void arrayMethod(Integer[] strArray){
    }
    //listMethod接收泛型list參數(shù)并進行重載
    public void listMethod(List<String> strArray){
    }
    public void listMethod(List<Integer> strArray){
    }
}

這個程序是無法編譯的,List<String>和List<Integer>在編譯時查出類型后的都是List<E>,造成方法簽名重復。這就是Java泛型擦除引起的問題。在編譯后所有的泛型類型都會做相應的轉化,轉換規(guī)則如下:

  • List<String>、List<Integer>、List<T>擦除后的類型是List
  • List<String>[]擦除后的類型為List[]
  • List<? extends E>、List<? super E>擦除后的類型為List<E>
  • List<T extends Seriailzable &Cloneable>擦除后為List<Seriailzable>

Java編譯后的字節(jié)碼中沒有泛型的任何信息。比如Foo<T>類只有一份Foo.class,不管是Foo<String>還是Foo<Integer>引用的都是同一字節(jié)碼。

2.不能初始化泛型參數(shù)和數(shù)組

泛型類型在編譯期被擦除,我們在類初始化時將無法獲得泛型的,比如這樣的代碼:

class Foo<T>{
    private T t = new T();//1.編譯不通過
    private T[] tArr = new T[5];//2.編譯不通過
    private List<T> list = new ArrayList<T>();//3.編譯通過
}

1,2編譯不通過是因為編譯期在編譯時需要獲得T類型,但泛型在編譯期類型已經被擦除了,所以new T(),new T[5]都會報錯,那為什么3編譯通過呢?其實ArrayList表面是泛型的,其實已經在編譯期轉型為Object了,詳細可以查看ArrayList的源代碼。

在某些情況下,我們確實需要泛型數(shù)組,那該怎么處理呢?代碼如下:

class Foo<T>{
    //不在初始化,由構造函數(shù)初始化
    private T t;
    private T[] tArray;
    private List<T> list = new ArrayList<T>();
    //構造函數(shù)初始化
    public Foo(){
        try{
            Class<?> tType = Class.forName("");
            t = (T)tType.newInstance();
            tArray = (T[])Array.newInstance(tType,5);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

此時運行就沒有任何問題了。剩下的問題就是怎么在運行期獲得T的類型,也就是tType參數(shù),一般情況下泛型類型是無法獲取的,不過在客戶端調用時多傳輸一個T類型的class就會解決問題。

類的成員變量是在類初始化前初始化的,所以要求在初始化前它必須具有明確的類型,否則就只能聲明,不能初始化

3.不同場景使用不同的泛型通配符

java泛型支持通配符,可以單獨使用一個?,也可以使用extends關鍵字表示一個類(接口)的子類型,也可以使用super關鍵字表示一個類(接口)的父類型,使用規(guī)則如下:

  • 泛型結構只支持讀操作則限定上界(extends關鍵字)
public static <E> void read(List<? extends E> list){
    for(E e : list){
        //業(yè)務邏輯操作
    }
}
  • 泛型結構只支持寫操作則限定下界(super關鍵字)
public static <E> void write(List<? super E> list){
    list.add(1);
    list.add(1.2);
}

如果一個泛型結構既用作“讀操作”,又用作“寫操作”,直接使用確定的泛型類即可,如List<E>

4.適時選擇getDeclaredxxx和getxxx

Java的Class類提供了很多的getDeclaredxxx方法和getxxx方法,例如getDeclaredMethod和getMethod成對出現(xiàn),getDeclaredConstructors也是成對出現(xiàn),兩者的區(qū)別如下:

  • getMethod方法獲得的是所有public訪問級別的方法,包括父類繼承的方法
  • getDeclaredMethod獲得的是自身類的所有方法,包括公用(public)方法,私有(private)方法等,而且不受限于訪問權限

5.反射訪問屬性或方法時將Accessible設置為true

Accessible的屬性并不是我們語法層次理解的訪問權限,而是指是否更容易獲得,是否進行安全檢查。我們知道,修改一個類或方法或執(zhí)行方法時受Java安全體系的制約,而安全的處理是非常消耗資源的(性能非常低),因此對于運行期要執(zhí)行的方法或要修改的屬性就提供了Accessible可選項;由開發(fā)者決定是否要逃避安全體系的檢查

  • 設置Accessible為true可以提升性能20倍以上,但可以運行private方法,訪問private私有屬性等

6.動態(tài)加載不適合數(shù)組

如果forName要加載一個類,那它首先必須是一個類(8個基本類型排除在外),它們不是一個具體的類,其次,它必須具有可追索的類路徑。

在Java中,數(shù)組是一個非常特殊的類,雖然它是一個類,但沒有定義路徑,例如這樣的代碼:

public static void main(String[] args)throw Exception{
    String[] strs = new String[10];
    Class.forName("java.lang.String[]");
}

運行結果報錯!雖然數(shù)組是一個類,但編譯器編譯后會為不同的數(shù)組類型產生不同的類:

元素類型 編譯后的類型
byte[] [B
char[] [C
Double[] [D
Float[] [F
Int[] [I
Long[] [J
Short[] [S
Boolean[] [Z
引用類型(如String[]) [L 引用類型(如:[Ljava.lang.String)

反射不能定義一個數(shù)組,可以使用Array數(shù)組反射類來動態(tài)加載,代碼如下:

String[] strs = (String[])Array.newInstance(String.class,8)
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容