天天看點

Java基礎 -- 05反射reflect反射:

引序:開始反射概念學習之前,先來段引序。

請牢記:計算機的三大程式結構;順序執行結構、條件分支結構、循環處理結構。

有時我們寫的程式會自我感覺這裡寫了代碼,它是怎麼被排程執行的呢,按照程式的順序執行來考究,有點說不通。我舉幾個例子說明看起來說不通的現象:

  • 說不通1:注解
import java.lang.reflect.*;
import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnno{	                                     //注解定義
    String name() default "12345";
    int value();
}
 
@MyAnno(name="haha",value=123)                              //注解釘點
class B {
 
    public static void main(String[] args){
		
	if( B.class.isAnnotationPresent(MyAnno.class) ){
	    Annotation annos[] = B.class.getAnnotations();  //消費注解
	    for(Annotation anno : annos){
		System.out.println(anno);
		System.out.println( ((MyAnno)anno).name() );
		System.out.println( ((MyAnno)anno).value() );
                System.out.println( anno.annotationType().getName() );
}}}}
           

上面注解的小示例:如果看《注解定義》《注解釘點》 這兩個地方,能搞懂我們寫這兩處的意思嘛?完全是懵圈的,這是什麼代碼,又什麼時候被排程執行,咋執行的呢?正是因為有 《注解消費》 這個梗,我們才恍然大悟,注解是啥時用起來的。(這也是很多架構利用大量注解的底層技術手段,第三方架構有其自己的《注解定義》以及《注解消費》底層,開放給我們開發時用的就是“注解釘點”,我們利用好《注解釘點》這一點就OK)

  • 說不通2:匿名内部類

匿名内部類:作為方法的入參來使用,例如:

1:事件監聽處理器類:處理事件時作為監聽處理器方法的形參的Handler類

event.addEventListener( new Handler() { //這裡的Handler就是匿名内部類

    public void xxxMethed(Event event){ //這是匿名類中的方法xxxMethod();
    }

}); //這個xxxMtd()是由事件排程線程來執行的。
           

以上代碼我們隻是調用了addEventListener(Handler hd); 傳進來一個hd對象而已,那麼Handler類中的xxxMethod()方法怎麼會被調用的呢?答:是被事件排程線程來執行的。事件排程線程是JVM管理的,不受我們的代碼控制。

2:多線程程式設計時的任務類:往線程池當中送出一個實作了Runnable接口的類

threadPoolExecutor.execute( new Runnable(){ //這裡的Runnable就是匿名内部類

    public void run(){                      //這個匿名類中有一個方法run();
    }

}); //這個run()是由線程池排程工作線程來執行的。
           

以上代碼隻是調用了execute(Runnable task); 傳進來一個task任務而已,那麼Runnable類中的run()方法怎麼會被調用的呢?答:是被線程池排程工作線程來執行的。線程池是一個架構,它怎麼排程和管理工作線程,不受我們的代碼控制。我們“寫好匿名内部類中的方法業務邏輯”這一點就OK。了解匿名内部類很重要,它是靜态代理模式的一種展現。之是以匿名内部類很重要,是因為它是了解lambda函數式程式設計思想的基礎。

  • 說不通3:動态代理
import java.lang.reflect.*;

//接口
interface InterfaceHello {
    void sayHello();
    void read();
}

//業務類:實作接口
class HelloImpl implements InterfaceHello {
    public void sayHello() {
        System.out.println("hello world");
    }
    public void read() {
        System.out.println("I am reading...");
    }
}

//我是代理:業務類真正業務邏輯的前後,我搞點事(比如:打個廣告)
class ProxyHello implements InvocationHandler{

    private Object target;

    public ProxyHello(Object target) {
        this.target=target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("我是代理,看視訊之前我打個廣告,haha!");

        Object obj = method.invoke(target,args); // 真正的業務邏輯

	System.out.println("我是代理,視訊看完給評價下呗,多謝!\r\n");
        
        return obj;
    }
}

//測試
public class HelloTest {

    public static void main(String[]args) {
 
        try{
	    InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(
		    HelloTest.class.getClassLoader(),
                    new Class<?>[]{InterfaceHello.class},
                    new ProxyHello(new HelloImpl())
	    );
        
	    h.sayHello(); //調用InterfaceHello接口中的sayHello()方法
	    h.read(); //調用InterfaceHello接口中的read()方法
	}
	catch(Exception e){}
    }
}
           

程式輸出:

我是代理,看視訊之前我打個廣告,haha!

hello world

我是代理,視訊看完給評價下呗,多謝!

我是代理,看視訊之前我打個廣告,haha!

I am reading...

我是代理,視訊看完給評價下呗,多謝!

如果單看以上的測試類:h.sayHello(); 這句代碼,是蒙圈的,因為按照我們以往的程式經驗,這不就是實作了InterfaceHello接口的HelloImpl幹的事嘛,但是程式的輸出卻不是這樣,前有廣告,中間是正事,後有評論。為什麼這樣呢?是因為:h是Proxy.newProxyInstance()出來的,下面簡要分析下:

Proxy.newProxyInstance(loader,intfs[],handler){

 Class<?> cl = getProxyClass0(loader, intfs); //記憶體中生成一個類$Proxy0,實作intfs所有接口

 final Constructor<?> cons = cl.getConstructor(constructorParams); //類$Proxy0的構造函數

 return cons.newInstance(new Object[]{handler}); //引入handler,new $Proxy0對象傳回

}

那這個記憶體中動态生成的類$Proxy0到底長什麼樣子呢?我寫個簡易版的樣子:

    public class $Proxy0{

        private InvocationHandler handler;

        public $Proxy0(InvocationHandler handler){

            this.handler = handler;

        }

        public void sayHello(){ //這就是動态代理,【魔術揭秘】的地方

            handler.invoke();

        }

        public void read(){

            handler.invoke();

        }

    }

看:InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(loader,intfs[],handler);

我們恍然大悟,我們拿到的是$Proxy0對象,這個對象實作了所有的接口,是以向下類型強轉後,我們拿到的變量h可以調用接口中的任意方法。我們代碼随後編寫的 h.sayHello(); 這下徹底明白,它的内部邏輯是調用handler.invoke();//看上方的【魔術揭秘】。是以我的總結是:程式的順序執行結構永遠是這麼個套路,大家别以為這不是廢話嘛,程式不順序執行難道還來回胡亂的亂跑執行嘛?不不不,要深刻體會,有助于碰到新技術時的了解的。在用很多第三方架構的時候,我們寫好“業務實作邏輯”就OK,代理都是架構幫我們做好的,比如我們的方法業務邏輯裡面并沒有事務管理,但CRUD增删查改時都是能反應到資料庫并送出/復原的。

總結:以上三個小示例不過是解釋了随着程式設計技術的更多手段,讓我們對程式總是順序執行結構在感官上的認知有所偏差。其實呢?說到底,程式的執行永遠是從上至下的順序執行結構,隻不過有些代碼比較顯而易見,有些代碼并不顯而易見是由于架構/JVM等有一些我們看不見的底層機制,來排程順序執行我們的代碼。是以很多時候我們是了解底層原理即可,(依托底層機制抛出來的)面向應用高層面的一些API來編寫好代碼即可,必定我們不是java公司要制作jvm或者api,我們隻是利用api寫應用程式。

反射:

官方定義,反射是程式在運作時分析代碼的能力(更貼切的說,就是知曉類結構資訊的能力)。結合上述三個小示例的示範和說明,反射不過就是一種程式設計手段而已,就這麼簡單。我給反射下的定義是:“知曉類結構”

  • 強調:

類對象 Class<?> clazz = myObj.getClass(); 這個clazz就是類對象。每一個類檔案被JVM加載後,JVM就自動的建立好了一個類對象clazz,這個類對象包裝了這個類的結構資訊,一個類大緻的結構:就是它實作了什麼接口,它繼承了哪個超類,它的構造函數,它的資料成員,它的方法成員,方法成員的入參有幾個都是什麼類型,大緻一個類的結構就這麼點玩意。正是由于JVM自動建立出來一個類對象,是以我們才能寫出來clazz.getFields(); //知道類的資料成員有哪些,clazz.getMethods(); //知道類的方法成員有哪些,等等API。如果這種技術手段給一個更貼切的漢字含義,其實應該叫“知曉類結構”,不過這個名字太土了點,java就給它起了個高大上的名字:反射。

  • 又強調:

知道了類對象的存在,那麼現在大家都應該了解所謂的靜态成員為什麼都是 類名.靜态成員名 的通路手法了,這看起來像什麼呢?不就是 對象.方法名 的寫法嘛,這裡的類名其實類對象的意思。

  • 再三強調:

請看下方示例代碼,為什麼靜态方法上的synchronized和非靜态方法上的synchronized 并不會互相幹擾呢?因為它們的内置鎖不同。靜态方法的内置鎖是類對象,而非靜态方法(/也叫對象方法)的内置鎖是具體的new出來的對象,它們不是同步一把鎖,是以靜态方法和對象方法并不會互斥。

class A{

    //我是靜态方法,我的内置鎖是A類對象
    public synchronized static void a1(){

        /**我在執行
         * 此時想調用a2(), a2()被互斥
         * 此時想調用b1()或者b2()是可以的
         */
    }

    //我是靜态方法,我的内置鎖是A類對象
    public synchronized static void a2(){}

    //我是對象方法,我的内置鎖是目前調用我的對象
    public synchronized void b1(){
        
        /**我在執行
         * 此時想調用b2(), b2()被互斥
         * 此時想調用a1()或者a2()是可以的
         */
    }

    //我是對象方法,我的内置鎖是目前調用我的對象
    public synchronized void b2(){}    
}
           

以上是對Class類對象的一個淺顯的總結,下面我們接着回到話題:反射。

類對象:知曉類結構

類對象clazz 知曉類結構資訊(包括繼承父類的)public修飾的

Class<?> clazz = MyClass.class;                           //得到類對象clazz 第1種手段

Class<?> clazz = myObj.getClass();                      //得到類對象clazz 第2種手段

Class<?> clazz = Class.forName(pkg.MyClass);   //得到類對象clazz 第3種手段

方法 方法傳回值 說明(/知曉類結構)

實作接口 getInterfaces() Class<?>[ ] 類實作的所有接口
繼承父類 getSuperclass() Class<? extends T> 類所繼承的父類
構造函數 getConstructors() Constructor<?>[ ] 類所有的構造函數
getConstructor(Class<?>...paramTypes) Constructor<T> 類的某個構造函數
資料成員 getFields() Field[ ] 類所有的資料成員
getField(String name) Field 類的某個資料成員
方法成員 getMethods() Method[ ] 類所有的方法成員
getMethod(String name,Class<?>...paramTypes) Method 類的某個方法成員
注解成員

最下方有個注解的單獨表格。

Class類、Field字段、Method方法、Constructor構造函數、都支援注解的。

類對象clazz 僅僅知曉類自身,不包括繼承來的,Declared*方法

類自身的,不包括繼承 方法 方法傳回值

内部成員

所屬的聲明類

getDeclaringClass() Class<?>
構造函數 getDeclaredConstructors() Constructor<?>[ ]
getDeclaredConstructor(Class<?>...paramTypes) Constructor<T>
資料成員 getDeclaredFields() Field[ ]
getDeclaredField(String name) Field
方法成員 getDeclaredMethods() Method[ ]
getDeclaredMethod(String name,Class<?>...paramTypes) Method

通過以上對Class類的部分方法(這裡集中出了在編寫反射代碼時常用的一些API)的總結,可以知曉,通過clazz類對象調用這些API我們可以做到知曉類結構的資訊。比如:知道類中有哪些屬性,哪些方法等。

類結構之:内部成員

類對象 Class<?> clazz = myObj.getClass(); 這個clazz就是類對象,通過調用類對象的API,我們可以獲知類的結構,其内部成員我主要從以下三個着手講解:構造函數、資料成員、方法成員。如圖:

Java基礎 -- 05反射reflect反射:

内部成員--構造函數

Constructor<T> cons = clazz.getConstructor(Class<?>...paramTypes);

T obj = cons.newInstance(initargs[]); //平時主要用構造函數的這句代碼來生成一個對象

内部成員--資料成員

Field f = clazz.getDeclaredField(String fieldName);

f.setAccessible(true); //意指能通路f字段的值(預設反射隻能通路public修飾的)

f.getAnnotationByType(MyAnno.class); //字段除了值以外能攜帶額外資訊,注解

内部成員--方法成員

Method m = clazz.getMethod(String mtdName, Class<?>...paramTypes); //參數的類型

m.invoke(Object target, Object...paramArgs); //實際的參數

靜态代理:

靜态代理要求:接口,實作該接口的兩貨色:真實角色,代理角色;代理角色要持有真實角色的引用。靜态代理的本質是運用java的多态性的展現模式一,方法重寫。java多态性,請參考我的部落格 Java基礎 -- 02類 /接口 /繼承

靜态代理模式:匿名内部類就是一種靜态代理模式的展現。lambda表達式又是匿名内部類的一種“文法糖”的簡易版本,是以lambda表達式也是一種靜态代理模式的展現。

靜态代理這個簡單,一個小示例:

//接口
interface Hello {
    void say();
}

// 真實角色
class HelloImpl implements Hello {
    
    public void say() {
        System.out.println("..我是業務實作方!");
    }
}

// 代理角色
class ProxyH implements Hello {
    
    private Hello hImpl; // 持有真實業務方的引用
    
    public ProxyH(){}
    public ProxyH(Hello hImpl){ this.hImpl = hImpl; }

    public void say() {

        System.out.println("代理幹的事:業務開始前,來段廣告~~");

        hImpl.say(); //真實業務執行

        System.out.println("代理幹的事:業務結束後,下評論呗^^");
    }
}


//測試
public class StaticProxyTest {

    public static void main(String[] args) {
        
        Hello hImpl = new HelloImpl(); //建立真實業務
        
        Hello proxyH = new ProxyH(hImpl); //建立靜态代理

        proxyH.say(); //代理執行
    }
}
           

靜态代理小示例執行結果:

代理幹的事:業務開始前,來段廣告~~

..我是業務實作方!

代理幹的事:業務結束後,下評論呗^^

動态代理:

Java基礎 -- 05反射reflect反射:

在本文剛開始的引序裡面的3,動态代理,已經說的很詳細。這裡隻是總結下:

動态代理--核心技術:

運作時JVM動态生成記憶體中的代理類 $Proxy0

    public class $Proxy0{

        private InvocationHandler handler;

        public $Proxy0(InvocationHandler handler){

            this.handler = handler;

        }

        public void sayHello(){ //這就是動态代理,【魔術揭秘】的地方

            handler.invoke();

        }

    }

動态代理--解決問題:

在不影響原有代碼結構的情況下(有的時候也影響不了,比如:别人封裝好的jar包,無法反編譯的,我們就無法窺探class檔案所對應的源碼java檔案到底長什麼樣,自然無法對其進行修改擴容。當然此話不絕對,比如Hibernate架構就是通過CGLIB技術分析class檔案的位元組碼來動态的在記憶體中生成代理類的,隻是這種手段一般程式員無法掌握而已,太複雜),我們又想在原有代碼做業務行為的前後橫加幹涉一下,比如穿插廣告,事後提醒評論,記錄日志等,那這時用動态代理就很合适。

動态代理--使用場景:

日志系統,Spring的AOP技術,聲明式事務管理,等等。Hibernate用CGLIB技術實作動态代理,Spring用Proxy技術實作動态代理。

反射配套注解,更強悍:

舉一個簡單的說明大家即可明白,我們通過反射能知道類的資料成員有 id,name,age等等,但是通過反射Field的若幹方法,我們能知道哪個是主鍵嘛?這玩意單單靠反射Field的API達不到要求。我們回想一下,注解,幹嘛的呢?注解就是個标簽(标簽可以攜帶額外資訊),注解釘點在哪裡,目前被釘點的元素(可以是類/字段/方法/構造函數等)就能攜帶更多的額外資訊,然後我們簡單的針對目前元素調用出注解field.getAnnotations(MyAnno.class)來消費,就能很好的在這麼多的字段中,判别出哪個字段才是主鍵。注解不了解的話,可以參考我的部落格 Java基礎 -- 06注解Annotation

反射結合泛型再配套注解,更更更強悍:

Java基礎 -- 05反射reflect反射:
Java基礎 -- 05反射reflect反射:

以上兩個圖,是jdk5之後,java為了迎合到底怎麼處理泛型而擴充出來的類型。從jdk5之後,java有了這些類型:

  • 8種基本資料類型、
  • Class類類型、
  • ParameterizedType泛型、
  • TypeVariable泛型變量類型、
  • AnnotatedArrayType泛型(/泛型變量)數組類型

上圖中的配套小示例以及不了解泛型的話,請參考我的部落格 Java基礎 -- 04泛型

為什麼說,更更更強悍呢?先看小示例:

繼續閱讀