泛型和反射——讀《編寫高質(zhì)量代碼:改善Java程序的151個(gè)建議》(七)

讀書,收獲,分享
建議后面的五角星僅代表筆者個(gè)人需要注意的程度。
Talk is cheap.Show me the code

建議93:Java的泛型是類型擦除的★☆☆☆☆

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

//下面這種方法的重載,編輯器會(huì)報(bào)錯(cuò),提示方法沖突....
//'listMethod(List<String>)' clashes with 'listMethod(List<Integer>)'; both methods have same erasure
    public void listMethod(List<String> stringList){
    }
    public void listMethod(List<Integer> intList) {
    }

這就是Java泛型擦除引起的問題:在編譯后所有的泛型類型都會(huì)做相應(yīng)的轉(zhuǎn)化。轉(zhuǎn)換規(guī)則如下:

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

Java之所以如此處理,有兩個(gè)原因:

  • 避免JVM的大換血。C++的泛型生命期延續(xù)到了運(yùn)行期,而Java是在編譯器擦除掉的,如果JVM也把泛型類型延續(xù)到運(yùn)行期,那么JVM就需要進(jìn)行大量的重構(gòu)工作了。
  • 版本兼容。在編譯期擦除可以更好地支持原生類型(Raw Type),在Java 1.5或1.6平臺(tái)上,即使聲明一個(gè)List這樣的原生類型也是可以正常編譯通過的,只是會(huì)產(chǎn)生警告信息而已。

我們就可以解釋類似如下的問題了:

  1. 泛型的class對(duì)象是相同的

    public static void main(String[] args) {  
        List<String> ls = new ArrayList<String>();  
        List<Integer> li = new ArrayList<Integer>();  
        System.out.println(ls.getClass() == li.getClass());  
        //運(yùn)行結(jié)果:true
    }  
    

    每個(gè)類都有一個(gè)class屬性,泛型化不會(huì)改變class屬性的返回值

  2. 泛型數(shù)組初始化時(shí)不能聲明泛型類型

    //如下代碼編譯時(shí)通不過:
    List<String>[] list = new List<String>[];  
    

    可以聲明一個(gè)帶有泛型參數(shù)的數(shù)組,但是不能初始化該數(shù)組,因?yàn)閳?zhí)行了類型擦除操作,List<Object>[]List<String>[]就是同一回事了,編譯器拒絕如此聲明。

  3. instanceof不允許存在泛型參數(shù)

    //以下代碼不能通過編譯,原因一樣,泛型類型被擦除了:
    List<String> list = new ArrayList<String>();  
    System.out.println(list instanceof List<String>)
    

建議94:不能初始化泛型參數(shù)和數(shù)組★☆☆☆☆

示例如下:

//這段代碼是編譯通不過的,因?yàn)榫幾g器在編譯時(shí)需要獲得T類型,但泛型在編譯期類型已經(jīng)被擦除了
//所以new T()和new T[5]都會(huì)報(bào)錯(cuò)
public class Client {
    private T t = new T();
    private T[] tArray = new T[5];
    private List<T> list = new ArrayList<T>();
}

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

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

    }

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

建議95:強(qiáng)制聲明泛型的實(shí)際類型★☆☆☆☆

示例:

class ArrayUtils{
    //把一個(gè)變長參數(shù)轉(zhuǎn)變?yōu)榱斜?    public  static <T> List<T> asList(T...t){
        List<T> list = new ArrayList<T>();
        Collections.addAll(list, t);
        return list;
    }
}

public class Client {
    public static void main(String[] args) {
     //強(qiáng)制聲明泛型類型
    //asList方法要求的是一個(gè)泛型參數(shù),在輸入前定義這是一個(gè)Integer類型的參數(shù),當(dāng)然,輸出也是Integer類型的集合了
        List<Integer> list = ArrayUtils.<Integer>asList();
    }
}

注意:無法從代碼中推斷出泛型類型的情況下,即可強(qiáng)制聲明泛型類型。

建議96:不同的場(chǎng)景使用不同的泛型通配符★★☆☆☆

Java泛型支持通配符(Wildcard),可以單獨(dú)使用一個(gè)“?”表示任意類,也可以使用extends關(guān)鍵字表示某一個(gè)類(接口)的子類型,還可以使用super關(guān)鍵字表示某一個(gè)類(接口)的父類型,但問題是什么時(shí)候該用extends,什么時(shí)候該用super呢?

  1. 泛型結(jié)構(gòu)只參與“”操作則限定上界(使用extends關(guān)鍵字)

       public static <E> void read(List<? extends E> list){
            for(E e:list){
                System.out.println(e.getClass());
                //業(yè)務(wù)邏輯處理
            }
        }
    
  2. 泛型結(jié)構(gòu)只參與“”操作則限定下界(使用super關(guān)鍵字)

        public static void write(List<? super Number> list) {
            list.add(123);
            list.add(3.14);
        }
    

對(duì)于是要限定上界還是限定下界,JDKCollections.copy方法是一個(gè)非常好的例子,它實(shí)現(xiàn)了把源列表中的所有元素拷貝到目標(biāo)列表中對(duì)應(yīng)的索引位置上,代碼如下:

    //源列表是用來提供數(shù)據(jù)的,所以src變量需要限定上界,帶有extends關(guān)鍵字。
    //目標(biāo)列表是用來寫入數(shù)據(jù)的,所以dest變量需要界定上界,帶有super關(guān)鍵字。
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

如果一個(gè)泛型結(jié)構(gòu)即用作“讀”操作又用作“寫”操作,那該如何進(jìn)行限定呢?不限定,使用確定的泛型類型即可,如List<E>。

建議97:警惕泛型是不能協(xié)變和逆變的★★☆☆☆

什么叫協(xié)變(covariance)和逆變(contravariance)?

在編程語言的類型框架中,協(xié)變和逆變是指寬類型和窄類型在某種情況下(如參數(shù)、泛型、返回值)替換或交換的特性,簡(jiǎn)單地說,協(xié)變是用一個(gè)窄類型替換寬類型,而逆變則是用寬類型覆蓋窄類型。

泛型既不支持協(xié)變,也不支持逆變:

    public static void main(String[] args) {
        //數(shù)組支持協(xié)變
        Number[] n = new Integer[10];
        //編譯不通過,泛型不支持協(xié)變
        List<Number> ln = new ArrayList<Integer>();
        //報(bào)錯(cuò):Type mismatch: cannot convert from ArrayList<Integer> to List<Number>
    }
  1. 可以使用通配符(Wildcard)模擬協(xié)變,代碼如下所示:

       //Number的子類型都可以是泛型參數(shù)類型
       List<? extends Number> ln = new ArrayList<Integer>();
    
  2. 可以使用super關(guān)鍵字來模擬逆變,代碼如下所示:

       //Integer的父類型(包括Integer)都可以是泛型參數(shù)類型
      List<? super Integer>  li = new ArrayList<Number>();
    
泛型通配符的QA

注意:Java的泛型是不支持協(xié)變和逆變的,只是能夠?qū)崿F(xiàn)協(xié)變和逆變。

建議98:建議采用的順序是List<T>、List<?>、List<Object>★★☆☆☆

原因如下:

  1. List<T>是確定的某一個(gè)類型

    List<T>表示的是List集合中的元素都為T類型,具體類型在運(yùn)行期決定;

    List<?>表示的是任意類型,與List<T>類似,

    List<Object>則表示List集合中的所有元素為Object類型,因?yàn)?code>Object是所有類的父類,所以List<Object>也可以容納所有的類類型,

    從這一字面意義上分析,List<T>更符合習(xí)慣:編碼者知道它是某一個(gè)類型,只是在運(yùn)行期才確定而已。

  2. List<T>可以進(jìn)行讀寫操作

    List<T>可以進(jìn)行如add、remove等操作,因?yàn)樗念愋褪枪潭ǖ?code>T類型,在編碼期不需要進(jìn)行任何的轉(zhuǎn)型操作。

    List<?>是只讀類型的,不能進(jìn)行增加、修改操作,因?yàn)榫幾g器不知道List中容納的是什么類型的元素,也就無法校驗(yàn)類型是否安全了,而且List<?>讀取出的元素都是Object類型的,需要主動(dòng)轉(zhuǎn)型,所以它經(jīng)常用于泛型方法的返回值。注意,List<?>雖然無法增加、修改元素,但是卻可以刪除元素,比如執(zhí)行removeclear等方法,那是因?yàn)樗膭h除動(dòng)作與泛型類型無關(guān)。

    List<Object>也可以讀寫操作,但是它執(zhí)行寫入操作時(shí)需要向上轉(zhuǎn)型(Up cast),在讀取數(shù)據(jù)后需要向下轉(zhuǎn)型(Downcast),而此時(shí)已經(jīng)失去了泛型存在的意義了。

建議99:嚴(yán)格限定泛型類型采用多重界限★★★☆☆

比如在公交車費(fèi)優(yōu)惠系統(tǒng)中,對(duì)部分人員(如工資低于2500元的上班族并且是站立著的乘客)車費(fèi)打8折,該如何實(shí)現(xiàn)呢?

//職員
interface Staff{
    //工資
    public int getSalary();
}

//乘客
interface Passenger{
    //是否是站立狀態(tài)
    public boolean isStanding();
}

class Me implements Staff,Passenger{
    public boolean isStanding(){
        return true;
    }

    public int getSalary() {
        return 2000;
    }
}

//使用多重限定
public class Client {
    //工資低于2500元的上斑族并且站立的乘客車票打8折
    public static <T extends Staff & Passenger> void discount(T t){
        if(t.getSalary()<2500 && t.isStanding()){
            System.out.println("恭喜你!您的車票打八折!");
        }
    }
    public static void main(String[] args) {
        discount(new Me());
    }
}

在Java的泛型中,可以使用“&”符號(hào)關(guān)聯(lián)多個(gè)上界并實(shí)現(xiàn)多個(gè)邊界限定,而且只有上界才有此限定,下界沒有多重限定的情況。

使用多重邊界可以很方便地解決問題,而且非常優(yōu)雅,建議在開發(fā)中考慮使用多重限定

建議100:數(shù)組的真實(shí)類型必須是泛型類型的子類型★★★☆☆

期望輸入的是一個(gè)泛型化的List,轉(zhuǎn)化為泛型數(shù)組,代碼如下:

public class Client<T> {
    public static <T> T[] toArray(List<T> list) {
        T[] t = (T[]) new Object[list.size()];
        for (int i = 0, n = list.size(); i < n; i++) {
            t[i] = list.get(i);
        }
        return t;
    }

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        for (String str : toArray(list)) {//這一句報(bào)錯(cuò),Object數(shù)組不能向下轉(zhuǎn)型為String數(shù)組
            System.out.println(str);
        }
    }
}

運(yùn)行異常如下:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at com.jyswm.demo.Client.main(Client.java:17)

因?yàn)榉盒褪穷愋筒脸模?code>toArray方法經(jīng)過編譯后與如下代碼相同:

    public static Object[] toArray(List list){
        //此處的強(qiáng)制類型沒必要存在,只是為了保持與源代碼對(duì)比
        Object[] t = (Object[])new Object[list.size()];
        for(int i=0,n=list.size();i<n;i++){
            t[i] = list.get(i);
        }
        return t;
    }

那該如何解決呢?

其實(shí)要想把一個(gè)Obejct數(shù)組轉(zhuǎn)換為String數(shù)組,只要Object數(shù)組的實(shí)際類型(Actual Type)也是String就可以了,例如:

public class Client<T> {
    public static void main(String[] args) {
        //objArray的實(shí)際類型和表面類型都是String數(shù)組
        Object[] objArray = {"A","B"};
        //拋出ClassCastException
        String[] strArray = (String[])objArray;

        String[] ss = {"A","B"};
        //objs的真實(shí)類型是String數(shù)組,顯示類型為Object數(shù)組
        Object[] objs = ss;
        //順利轉(zhuǎn)換為String數(shù)組
        String[] strs = (String[])objs;
    }
}

如此,那就把泛型數(shù)組聲明為泛型類的子類型吧!代碼如下:

public class Client<T> {

    public static <T> T[] toArray(List<T> list, Class<T> tClass) {
        //聲明并初始化一個(gè)T類型的數(shù)組
        //通過反射類Array聲明了一個(gè)T類型的數(shù)組,
        //由于我們無法在運(yùn)行期獲得泛型類型的參數(shù),因此就需要調(diào)用者主動(dòng)傳入T參數(shù)類型
        T[] t = (T[]) Array.newInstance(tClass, list.size());
        for(int i=0,n=list.size();i<n;i++){
            t[i] = list.get(i);
        }
        return t;
    }
    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B");
        for (String str : toArray(list,String.class)) {
            System.out.println(str);
        }
    }
}

注意:當(dāng)一個(gè)泛型類(特別是泛型集合)轉(zhuǎn)變?yōu)榉盒蛿?shù)組時(shí),泛型數(shù)組的真實(shí)類型不能是泛型類型的父類型(比如頂層類Object),只能是泛型類型的子類型(當(dāng)然包括自身類型),否則就會(huì)出現(xiàn)類型轉(zhuǎn)換異常。

建議101:注意Class類的特殊性★☆☆☆☆

Java語言是先把Java源文件編譯成后綴為class的字節(jié)碼文件,然后再通過ClassLoader機(jī)制把這些類文件加載到內(nèi)存中,最后生成實(shí)例執(zhí)行的,這是Java處理的基本機(jī)制,但是加載到內(nèi)存中的數(shù)據(jù)是如何描述一個(gè)類的呢?

Java使用一個(gè)元類(MetaClass)來描述加載到內(nèi)存中的類數(shù)據(jù),這就是Class類,它是一個(gè)描述類的類對(duì)象。

Class類特殊的地方:

  • 無構(gòu)造函數(shù)。Java中的類一般都有構(gòu)造函數(shù),但是Class類卻沒有構(gòu)造函數(shù),不能實(shí)例化,Class對(duì)象是在加載類時(shí)由 Java 虛擬機(jī)通過調(diào)用類加載器中的defineClass方法自動(dòng)構(gòu)造的。

  • 可以描述基本類型。雖然8個(gè)基本類型在JVM中并不是一個(gè)對(duì)象,它們一般存在于棧內(nèi)存中,但是Class類仍然可以描述它們,例如可以使用int.class表示int類型的類對(duì)象。

  • 其對(duì)象都是單例模式。一個(gè)Class的實(shí)例對(duì)象描述一個(gè)類,并且只描述一個(gè)類,反過來也成立,一個(gè)類只有一個(gè)Class實(shí)例對(duì)象,如下代碼返回的結(jié)果都為true

    public class Client {
        public static void main(String[] args) throws Exception {
            //類的屬性class所引用的對(duì)象與實(shí)例對(duì)象的getClass返回值相同
            String.class.equals(new String().getClass());
            "ABC".getClass().equals(String.class);
            //class實(shí)例對(duì)象不區(qū)分泛型
            ArrayList.class.equals(new ArrayList<String>().getClass());
        }
    }
    

建議102:適時(shí)選擇getDeclared×××get×××★☆☆☆☆

Java的Class類提供了很多的getDeclared×××方法和get×××方法,如下:

public static void main(String[] args) throws Exception {
        //方法名稱
        String methodName = "doStuff";
        Method m1 = Foo.class.getDeclaredMethod(methodName);
        Method m2 = Foo.class.getMethod(methodName);
}

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

其他的getDeclaredConstructorsgetConstructorsgetDeclaredFieldsgetFields等與此相似。

建議103:反射訪問屬性或方法時(shí)將Accessible設(shè)置為true★★☆☆☆

Java中通過反射執(zhí)行一個(gè)方法的過程如下:獲取一個(gè)方法對(duì)象,然后根據(jù)isAccessible返回值確定是否能夠執(zhí)行,如果返回值為false則需要調(diào)用setAccessible(true),最后再調(diào)用invoke執(zhí)行方法。如下:

Method method= ...;
        //檢查是否可以訪問
        if(!method.isAccessible()){
            method.setAccessible(true);
        }
        //執(zhí)行方法
        method.invoke(obj, args);

那為什么要這么寫呢?

首先,Accessible的屬性并不是訪問權(quán)限,而是指是否要更容易獲得,是否進(jìn)行安全檢查。

AccessibleObject類的源代碼如下:

//它提供了取消默認(rèn)訪問控制檢查的功能
public class AccessibleObject implements AnnotatedElement {
      //定義反射的默認(rèn)操作權(quán)限suppressAccessChecks
      static final private java.security.Permission ACCESS_PERMISSION =
        new ReflectPermission("suppressAccessChecks");
      //是否重置了安全檢查,默認(rèn)為false
      boolean override;
      //構(gòu)造函數(shù)
      protected AccessibleObject() {}
      //是否可以快速獲取,默認(rèn)是不能
      public boolean isAccessible() {
        return override;
    }
}

Accessible屬性只是用來判斷是否需要進(jìn)行安全檢查的,如果不需要?jiǎng)t直接執(zhí)行,這就可以大幅度的提升系統(tǒng)性能了(注意:取消了安全檢查,也可以運(yùn)行private方法、訪問private屬性的)。經(jīng)過測(cè)試,在大量的反射情況下,設(shè)置Accessibletrue可以提高性能20倍左右。

建議104:使用forName動(dòng)態(tài)加載類文件★★☆☆☆

動(dòng)態(tài)加載(Dynamic Loading)是指在程序運(yùn)行時(shí)加載需要的類庫文件。

對(duì)Java程序來說,一般情況下,一個(gè)類文件在啟動(dòng)時(shí)或首次初始化時(shí)會(huì)被加載到內(nèi)存中,而反射則可以在運(yùn)行時(shí)再?zèng)Q定是否要加載一個(gè)類。

比如從Web上接收一個(gè)String參數(shù)作為類名,然后在JVM中加載并初始化,這就是動(dòng)態(tài)加載,此動(dòng)態(tài)加載通常是通過Class.forName(String)實(shí)現(xiàn)的,只是為什么要使用forName方法動(dòng)態(tài)加載一個(gè)類文件呢?

因?yàn)槲覀儾恢缹⒁傻膶?shí)例對(duì)象是什么類型(如果知道就不用動(dòng)態(tài)加載),而且方法和屬性都不可訪問。

動(dòng)態(tài)加載的意義在什么地方呢?示例如下:

class Utils{
    //靜態(tài)代碼塊
    static{
        System.out.println("Do Something.....");
    }
}
public class Client {
    public static void main(String[] args) throws ClassNotFoundException {
        //動(dòng)態(tài)加載
        Class.forName("Utils");
        //此時(shí)輸出了:Do Something.....
    }
}

如上,并沒有對(duì)Utils做任何初始化,只是通過forName方法加載了Utils類,但是卻產(chǎn)生了一個(gè)Do Something的輸出,這就是因?yàn)?code>Utils類被加載后,JVM會(huì)自動(dòng)初始化其static變量和static代碼塊,這是類加載機(jī)制所決定的。

經(jīng)典的應(yīng)用:數(shù)據(jù)庫驅(qū)動(dòng)程序的加載片段

   //加載驅(qū)動(dòng)
  Class.forName("com.mysql..jdbc.Driver");
  String url="jdbc:mysql://localhost:3306/db?user=&password=";
  Connection conn =DriverManager.getConnection(url);
  Statement stmt =conn.createStatement();

Class.forName("com.mysql..jdbc.Driver");這一句的意義,示例如下:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
   
    //靜態(tài)代碼塊
    static {
        try {
            //把自己注冊(cè)到DriverManager中
            DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            //異常處理
            throw new RuntimeException("Can't register driver!");
        }
    }
    //構(gòu)造函數(shù)
    public Driver() throws SQLException {
    }

}

程序邏輯如下:當(dāng)程序動(dòng)態(tài)加載該驅(qū)動(dòng)時(shí),也就是執(zhí)行到Class.forName("com.mysql.jdbc.Driver")時(shí),Driver類會(huì)被加載到內(nèi)存中,也就是把自己注冊(cè)到DriverManager中。

forName只是把一個(gè)類加載到內(nèi)存中,并不保證由此產(chǎn)生一個(gè)實(shí)例對(duì)象,也不會(huì)執(zhí)行任何方法,之所以會(huì)初始化static代碼,那是由類加載機(jī)制所決定的,而不是forName方法決定的。也就是說,如果沒有static屬性或static代碼塊,forName就只是加載類,沒有任何的執(zhí)行行為。

注意:forName只是加載類,并不執(zhí)行任何代碼。

建議105:動(dòng)態(tài)加載不適合數(shù)組★☆☆☆☆

在Java中,數(shù)組是一個(gè)非常特殊的類,雖然它是一個(gè)類,但沒有定義類路徑。

示例:

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

運(yùn)行異常,如下:

Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[]
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:186)

因?yàn)榫幾g器編譯后為不同的數(shù)組類型生成不同的類,具體如下表所示:


數(shù)組編譯對(duì)應(yīng)關(guān)系表

所以動(dòng)態(tài)加載一個(gè)對(duì)象數(shù)組只要加載編譯后的數(shù)組對(duì)象就可以了,修改代碼如下:

  //加載一個(gè)String數(shù)組
  Class.forName("[Ljava.lang.String;");
  //加載一個(gè)Long數(shù)組
  Class.forName("[J");

但是這種操作沒有什么意義,因?yàn)樗荒苌梢粋€(gè)數(shù)組對(duì)象,只是把一個(gè)String類型的數(shù)組類和long類型的數(shù)組類加載到了內(nèi)存中,它沒有定義數(shù)組的長度,在Java中數(shù)組是定長的,沒有長度的數(shù)組是不允許存在的。

因?yàn)閿?shù)組的特殊性,所以Java專門定義了一個(gè)Array數(shù)組反射工具類來實(shí)現(xiàn)動(dòng)態(tài)探知數(shù)組的功能,如下:

        // 動(dòng)態(tài)創(chuàng)建數(shù)組
        String[] strs = (String[]) Array.newInstance(String.class, 8);
        // 創(chuàng)建一個(gè)多維數(shù)組
        int[][] ints = (int[][]) Array.newInstance(int.class, 2, 3);

注意:通過反射操作數(shù)組使用Array類,不要采用通用的反射處理API。

建議106:動(dòng)態(tài)代理可以使代理模式更加靈活★★★☆☆

Java的反射框架提供了動(dòng)態(tài)代理(Dynamic Proxy)機(jī)制,允許在運(yùn)行期對(duì)目標(biāo)類生成代理,避免重復(fù)開發(fā)。

首先,簡(jiǎn)單的靜態(tài)代理實(shí)現(xiàn)示例如下:

/**
 * 抽象角色-廚師
 */
interface Chef {
    
    /**
     * 提供餃子
     */
    String dumplings();

    /**
     * 提供面條
     */
    String noodles();

}

/**
 * 具體角色-廚師老張
 */
class RealChef implements Chef {

    @Override
    public String dumplings() {
        return "老張秘制酸湯水餃";
    }

    @Override
    public String noodles() {
        return "老張秘制蘭州牛肉面";
    }
}

/**
 * 代理角色(proxy)-幸福餐廳
 */
public class HappyRestaurant implements Chef {

    /**
     * 要代理哪個(gè)實(shí)現(xiàn)類(要讓哪個(gè)廚師做)
     */
    private Chef chef = null;

    /**
     * 默認(rèn)被代理者(默認(rèn)的廚師老張)
     */
    public HappyRestaurant() {
        chef = new RealChef();
    }

    /**
     * 通過構(gòu)造函數(shù)傳遞被代理者(客戶點(diǎn)名哪個(gè)廚師做)
     */
    public HappyRestaurant(Chef _chef) {
        chef = _chef;
    }

    @Override
    public String dumplings() {
        before();
        return chef.dumplings();
    }

    @Override
    public String noodles() {
        before();
        return chef.noodles();
    }

    /**
     * 預(yù)處理
     */
    private void before() {
        // 先收銀
    }

}
    //調(diào)用
    public static void main(String[] args) {
        //來到幸福餐廳
        HappyRestaurant happyRestaurant = new HappyRestaurant();
        //點(diǎn)了一份餃子
        String food = happyRestaurant.dumplings();
        System.out.println(food);
        //得到:老張秘制酸湯水餃
    }

代理:"你去餐廳吃飯,并沒有見給你真正做飯的廚師老張,而是由餐廳的服務(wù)人員端到你面前的。"

改為動(dòng)態(tài)代理示例如下:

/**
 * 抽象角色-廚師
 */
interface Chef {
    
    /**
     * 提供餃子
     */
    String dumplings();

    /**
     * 提供面條
     */
    String noodles();

}

/**
 * 具體角色-廚師老張
 */
class RealChef implements Chef {

    @Override
    public String dumplings() {
        return "老張秘制酸湯水餃";
    }

    @Override
    public String noodles() {
        return "老張秘制蘭州牛肉面";
    }
}

/**
 * 委托處理(不是具體的哪一家餐廳,而是美團(tuán)了)
 */
public class ChefHandler implements InvocationHandler {

    /**
     * 被代理的對(duì)象(廚師)
     */
    private Chef chef;

    public ChefHandler(Chef _chef) {
        chef = _chef;
    }

    /**
     * 委托處理方法(點(diǎn)外賣)
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 預(yù)處理
        System.out.println("預(yù)處理...");
        //直接調(diào)用被代理的方法
        Object obj = method.invoke(chef, args);
        // 后處理
        System.out.println("后處理...");
        return obj;
    }

}

注意看,這里沒有了餐廳這個(gè)角色,取而代之的是ChefHandler作為主要的邏輯委托處理,其中invoke方法是接口InvocationHandler定義必須實(shí)現(xiàn)的,它完成了對(duì)真實(shí)方法的調(diào)用。

InvocationHanlder接口:動(dòng)態(tài)代理是根據(jù)被代理的接口生成所有方法的,也就是說給定一個(gè)(或多個(gè))接口,動(dòng)態(tài)代理會(huì)宣稱“我已經(jīng)實(shí)現(xiàn)該接口下的所有方法了”

動(dòng)態(tài)代理的場(chǎng)景類,代碼如下:

    public static void main(String[] args) {
        //被代理類(想吃老張做的飯,確定目標(biāo))
        Chef chef = new RealChef();
        //代理實(shí)例的處理Handler(打開美團(tuán)app搜索老張)
        InvocationHandler handler = new ChefHandler(chef);
        //當(dāng)前加載器(美團(tuán)開始搜索并加載老張的信息)
        ClassLoader classLoader = chef.getClass().getClassLoader();
        //動(dòng)態(tài)代理(美團(tuán)已經(jīng)擁有了老張的所有能力,比如提供一份水餃等)
        Chef proxy = (Chef) Proxy.newProxyInstance(classLoader, chef.getClass().getInterfaces(), handler);
        //調(diào)用具體方法(點(diǎn)一份酸湯水餃)
        String food = proxy.dumplings();
        System.out.println(food);
        //得到: 老張秘制酸湯水餃
    }

此時(shí)就實(shí)現(xiàn)了不用顯式創(chuàng)建代理類即實(shí)現(xiàn)代理的功能。例如可以在被代理角色執(zhí)行前進(jìn)行權(quán)限判斷,或者執(zhí)行后進(jìn)行數(shù)據(jù)校驗(yàn)。

建議107:使用反射增加裝飾模式的普適性★★★☆☆

裝飾模式(Decorator Pattern)的定義是 動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就增加功能來說,裝飾模式相比于生成子類更為靈活,

使用Java的動(dòng)態(tài)代理也可以實(shí)現(xiàn)裝飾模式的效果,而且其靈活性、適應(yīng)性都會(huì)更強(qiáng)。

裝飾一只小老鼠,讓它更強(qiáng)大,示例如下:

interface Animal{
    public void doStuff();
}

class Rat implements Animal{
    @Override
    public void doStuff() {
        System.out.println("Jerry will play with Tom ......");
    }
    
}

/**
 * 使用裝飾模式,給老鼠增加一些能力,比如飛行,鉆地等能力
 */

//定義某種能力
interface Feature{
    //加載特性
    public void load();
}
//飛行能力
class FlyFeature implements Feature{

    @Override
    public void load() {
        System.out.println("增加一對(duì)翅膀...");
    }
}
//鉆地能力
class DigFeature implements Feature{
    @Override
    public void load() {
        System.out.println("增加鉆地能力...");
    }
    
}

/**
 * 要把這兩種屬性賦予到老鼠身上,那需要一個(gè)包裝動(dòng)作類
 */

class DecorateAnimal implements Animal {
    // 被包裝的動(dòng)物
    private Animal animal;
    // 使用哪一個(gè)包裝器
    private Class<? extends Feature> clz;

    public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
        animal = _animal;
        clz = _clz;
    }

    @Override
    public void doStuff() {
        InvocationHandler handler = new InvocationHandler() {
            // 具體包裝行為
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                Object obj = null;
                if (Modifier.isPublic(method.getModifiers())) {
                    obj = method.invoke(clz.newInstance(), args);
                }
                animal.doStuff();
                return obj;
            }
        };
        //當(dāng)前加載器
        ClassLoader cl = getClass().getClassLoader();
        //動(dòng)態(tài)代理,又handler決定如何包裝
        Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
        proxy.load();
    }

}

    /**
     * 注意看doStuff方法,
     * 一個(gè)裝飾類型必然是抽象構(gòu)建(Component)的子類型,它必須實(shí)現(xiàn)doStuff方法,此處的doStuff方法委托給了動(dòng)態(tài)代理執(zhí)行,
     * 并且在動(dòng)態(tài)代理的控制器Handler中還設(shè)置了決定裝飾方式和行為的條件(即代碼中InvocationHandler匿名類中的if判斷語句),
     * 當(dāng)然,此處也可以通過讀取持久化數(shù)據(jù)的方式進(jìn)行判斷,這樣就更加靈活了。
     */

/**
 * 客戶端進(jìn)行調(diào)
 */
public static void main(String[] args) {
        //定義Jerry這只老鼠
        Animal jerry = new Rat();
        //為Jerry增加飛行能力
        jerry = new DecorateAnimal(jerry, FlyFeature.class);
        //jerry增加挖掘能力
        jerry = new DecorateAnimal(jerry, DigFeature.class);
        //Jerry開始戲弄貓了
        jerry.doStuff();
}
// 裝飾行為由動(dòng)態(tài)代理實(shí)現(xiàn),實(shí)現(xiàn)了對(duì)裝飾類和被裝飾類的完全解耦,提供了系統(tǒng)的擴(kuò)展性。

建議108:反射讓模板方法模式更強(qiáng)大★★★☆☆

模板方法模式(Template Method Pattern)的定義是:定義一個(gè)操作中的算法骨架,將一些步驟延遲到子類中,使子類不改變一個(gè)算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。簡(jiǎn)單地說,就是父類定義抽象模板作為骨架,其中包括基本方法(是由子類實(shí)現(xiàn)的方法,并且在模板方法被調(diào)用)和模板方法(實(shí)現(xiàn)對(duì)基本方法的調(diào)度,完成固定的邏輯),它使用了簡(jiǎn)單的繼承和覆寫機(jī)制。

普通模板方法,示例如下:

public abstract class AbsPopulator {
    // 模板方法
    public final void dataInitialing() throws Exception {
        // 調(diào)用基本方法
        doInit();
    }

    // 基本方法
    protected abstract void doInit();
}

//子類實(shí)現(xiàn)
public class UserPopulator extends AbsPopulator{
    @Override
    protected void doInit() {
        //初始化用戶表,如創(chuàng)建、加載數(shù)據(jù)等
    }

}

改造,使用反射增強(qiáng)模板方法模式,使模板方法實(shí)現(xiàn)對(duì)一批固定的規(guī)則的基本方法的調(diào)用。如下:

public abstract class AbsPopulator {
    // 模板方法
    public final void dataInitialing() throws Exception {
        // 獲得所有的public方法
        Method[] methods = getClass().getMethods();
        for (Method m : methods) {
            // 判斷是否是數(shù)據(jù)初始化方法
            if (isInitDataMethod(m)) {
                m.invoke(this);
            }
        }
    }

    // 判斷是否是數(shù)據(jù)初始化方法,基本方法鑒定器
    private boolean isInitDataMethod(Method m) {
        return m.getName().startsWith("init")// init開始
                && Modifier.isPublic(m.getModifiers())// 公開方法
                && m.getReturnType().equals(Void.TYPE)// 返回值是void
                && !m.isVarArgs()// 輸出參數(shù)為空
                && !Modifier.isAbstract(m.getModifiers());// 不能是抽象方法
    }
}

//子類實(shí)現(xiàn)
public class UserPopulator extends AbsPopulator {

    public void initUser() {
        /* 初始化用戶表,如創(chuàng)建、加載數(shù)據(jù)等 */
    }

    public void initPassword() {
        /* 初始化密碼 */
    }

    public void initJobs() {
        /* 初始化工作任務(wù) */
    }
}

在一般的模板方法模式中,抽象模板(這里是AbsPopulator類)需要定義一系列的基本方法,一般都是protected訪問級(jí)別的,并且是抽象方法,這標(biāo)志著子類必須實(shí)現(xiàn)這些基本方法,這對(duì)子類來說既是一個(gè)約束也是一個(gè)負(fù)擔(dān)。但是使用了反射后,不需要定義任何抽象方法,只需定義一個(gè)基本方法鑒別器(例子中isInitDataMethod)即可加載符合規(guī)則的基本方法。鑒別器在此處的作用是鑒別子類方法中哪些是基本方法,模板方法(例子中的dataInitialing)則根據(jù)基本方法鑒別器返回的結(jié)果通過反射執(zhí)行相應(yīng)的方法。

注意:決定使用模板方法模式時(shí),請(qǐng)嘗試使用反射方式實(shí)現(xiàn),它會(huì)讓你的程序更靈活、更強(qiáng)大。

建議109:不需要太多關(guān)注反射效率★★☆☆☆

反射的效率相對(duì)于正常的代碼執(zhí)行確實(shí)低很多(經(jīng)過測(cè)試,相差15倍左右),但是它是一個(gè)非常有效的運(yùn)行期工具類,只要代碼結(jié)構(gòu)清晰、可讀性好那就先開發(fā)起來,等到進(jìn)行性能測(cè)試時(shí)證明此處性能確實(shí)有問題時(shí)再修改也不遲(一般情況下反射并不是性能的終極殺手,而代碼結(jié)構(gòu)混亂、可讀性差則很可能會(huì)埋下性能隱患)。

對(duì)于反射效率問題,不要做任何的提前優(yōu)化和預(yù)期,這基本上是杞人憂天,很少有項(xiàng)目是因?yàn)榉瓷鋯栴}引起系統(tǒng)效率故障的,而且根據(jù)二八原則,80%的性能消耗在20%的代碼上,這20%的代碼才是我們關(guān)注的重點(diǎn),不要單單把反射作為重點(diǎn)關(guān)注對(duì)象。

注意:反射效率低是個(gè)真命題,但因?yàn)檫@一點(diǎn)而不使用它就是個(gè)假命題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容