理解Android中的注解與反射


反射

Java反射(Reflection)定義

Java反射機制是指在運行狀態中

對於任意一個類,都能知道這個類的所有屬性和方法;
對於任何一個對象,都能夠調用它的任何一個方法和屬性;

這樣動態獲取新的以及動態調用對象方法的功能就叫做反射。

比如像下面:

//獲取類
Class c = Class.forName("java.lang.String");
// 獲取所有的屬性
Field[] fields = c.getDeclaredFields();
StringBuffer sb = new StringBuffer();
sb.append(Modifier.toString(c.getModifiers()) + " class " + c.getSimpleName() + "{\n");
// 遍歷每一個屬性
for (Field field : fields) {
     sb.append("\t");// 空格
     sb.append(Modifier.toString(field.getModifiers()) + " ");// 獲得屬性的修飾符,例如public,static等等
     sb.append(field.getType().getSimpleName() + " ");// 屬性的類型的名字
     sb.append(field.getName() + ";\n");// 屬性的名字+回車
}
sb.append("}\n");
System.out.println(sb);

就可以獲得 String ,這個我們常用類的所有屬性:


string_property

再比如:

//獲取類
Class c = Class.forName("java.lang.String");
// 獲取所有的方法
Method[] ms = c.getDeclaredMethods();
 //遍歷輸出所有方法
for (Method method : ms) {
   //獲取方法所有參數
   Parameter[] parameters = method.getParameters();
   String params = "";
   if (parameters.length > 0) {
        StringBuffer stringBuffer = new StringBuffer();
        for (Parameter parameter : parameters) {
             stringBuffer.append(parameter.getType().getSimpleName() + " " + parameter.getName() + ",");
        }
        //去掉最后一個逗號
       params = stringBuffer.substring(0, stringBuffer.length() - 1);
}
System.err.println(Modifier.toString(method.getModifiers())
                    + " " + method.getReturnType().getSimpleName()
                    + " " + method.getName()
                    + " (" +params  + ")");
}

可以獲得String 類的所有方法(圖片只截取了部分方法,實際有很多就不占篇幅了):


string_method

Java反射機制API

主要的幾個類

Java中有關反射的類有以下這幾個:

用途
java.lang.Class 編譯后的class文件的對象
java.lang.reflect.Constructor 構造方法
java.lang.reflect.Field 類的成員變量(屬性)
java.lang.reflect.Method 類的成員方法
java.lang.reflect.Modifier 判斷方法類型
java.lang.annotation.Annotation 類的注解

具體實現

為了方便描述,這里我們創建一個類 TestClass

public class TestClass {
    private String address;
    private String port;
    private int number;

   public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }        
    private void myMethod(int number,String sex) {

    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }


}

這個類很簡單,包含三個成員變量address,port和number,以及它們各自的get,set方法。
兩個自定義的方法printInfo()和myMethod()。

下面我們就看一下如何通過反射,獲取這個TestClass的所有“信息”

  • 1.獲取Class
    關於Class的獲取有三種寫法:

  

//獲取類的三種方法:
Class c = Class.forName("java.lang.String");  //這里一定要用完整的包名
Class c1=String.class;
String str = new String();
Class c2=str.getClass();

 

  • 這里獲取的c,c1以及c2都是相等的。一般在反射中會用第一種寫法。

  • 2.獲取類的屬性(成員變量)

Field[] fields = c.getDeclaredFields();

這里返回的是一個數組 ,包含所有的屬性。獲取到的每一個屬性Filed,包含一系列的方法可以獲取及修改他的內容。
如下所示:

遍歷每一個屬性
for (Field field : fields) {
   sb.append("\t");// 空格
   sb.append(Modifier.toString(field.getModifiers()) + " ");// 獲得屬性的修飾符,例如public,static等等
   sb.append(field.getType().getSimpleName() + " ");// 屬性的類型的名字
   sb.append(field.getName() + ";\n");// 屬性的名字+回車
}

這里我們可以得到TestClass的所有屬性:


 
    • 3.獲取類的方法
// 獲取所有的方法
Method[] ms = c.getDeclaredMethods();
  • 和屬性類似,我們依然可以通過一系列的方法獲取到方法的返回值類型,名稱以及參數。下面的表格中總結了一些關鍵方法:

reflection

類似的獲取到TestClass的所有方法:


test_method

這里可以看到,獲取的TestClass的屬性和方法同我們定義的是完全一致的。

這里我們順便調用一下TestClass的printInfo方法:

new TestClass().printInfo();

用於所有屬性沒有做初始化,所以得到如下輸出:


null

可以看到,利用反射我們可以很方便的去“反編譯”一個class。那么我們用反射這么做的意義是什么呢?不要着急,下面我們先來了解一下注解

Java 注解(Annotation)

什么是注解

關於注解的定義網上有很多說法,就不再贅述。這里我們就說兩點

Annotation(注解)就是Java提供了一種源程序中的元素關聯任何信息或者任何元數據(metadata)的途徑和方法。

Annotation是被動的元數據,永遠不會有主動行為

既然是被動數據,對於那些已經存在的注解,比如Override,我們只能看看而已,並不知道它具體的工作機制是什么;所以想要理解注解,就直接從自定義注解開始。

自定義注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
@Inherited
public @interface Bind {
    int value() default 1;
    boolean canBeNull() default false;
}

這就是自定義注解的形式,我們用@interface 表明這是一個注解,Annotation只有成員變量,沒有方法。Annotation的成員變量在Annotation定義中以“無形參的方法”形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。比如上面的value和canBeNull。

元注解

可以看到自定義注解里也會有注解存在,給自定義注解使用的注解就是元注解。

@Rentention Rentention

@Rentention Rentention用來標記自定義注解的有效范圍,他的取值有以下三種:

RetentionPolicy.SOURCE: 只在源代碼中保留 一般都是用來增加代碼的理解性或者幫助代碼檢查之類的,比如我們的Override;

RetentionPolicy.CLASS: 默認的選擇,能把注解保留到編譯后的字節碼class文件中,僅僅到字節碼文件中,運行時是無法得到的;

RetentionPolicy.RUNTIME: ,注解不僅 能保留到class字節碼文件中,還能在運行通過反射獲取到,這也是我們最常用的。

@Target

@Target指定Annotation用於修飾哪些程序元素。
@Target也包含一個名為”value“的成員變量,該value成員變量類型為ElementType[ ],ElementType為枚舉類型,值有如下幾個:

  • ElementType.TYPE:能修飾類、接口或枚舉類型
  • ElementType.FIELD:能修飾成員變量
  • ElementType.METHOD:能修飾方法
  • ElementType.PARAMETER:能修飾參數
  • ElementType.CONSTRUCTOR:能修飾構造器
  • ElementType.LOCAL_VARIABLE:能修飾局部變量
  • ElementType.ANNOTATION_TYPE:能修飾注解
  • ElementType.PACKAGE:能修飾包

使用了@Documented的可以在javadoc中找到
使用了@Interited表示注解里的內容可以被子類繼承,比如父類中某個成員使用了上述@From(value),From中的value能給子類使用到。

好了,關於注解就說這么多。

反射&注解的使用

屬性值使用注解

下面我們首先自定義兩個注解:BindPort 和 BindAddress

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindPort {
    String value() default "8080";
}

指定BindPort 可以保留到運行時,並且可以修飾成員變量,包含一個成員變量默認值為”8080“。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindAddress {
    String value() default "127.0.0.0";
}

這個和上面類似,只是默認值為"127.0.0.0"。

同時,我們修改之前的TestClass

public class TestClass {
    @BindAddress()
    String address;
    @BindPort()
    private String port;

    private int number;

    public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }

   ........


}

這里我們將原先的address 和 port 兩個變量分別用這里定義的注解進行修飾,由於我們在定義注解時有默認值,所以這里的注解可以不寫參數。

使用反射獲取注解信息

前面已經說了,Annotation是被動的元數據,永遠不會有主動行為,所以我們需要通過使用反射,才能讓我們的注解產生意義。

通過反射可以獲取Class的所有屬性和方法,因此獲取注解信息也不在話下。我們看代碼:

//獲取類
Class c = Class.forName(className);
//實例化一個TestClass對象
TestClass tc= (TestClass) c.newInstance();

// 獲取所有的屬性
Field[] fields = c.getDeclaredFields();

for (Field field : fields) {
   if(field.isAnnotationPresent(BindPort.class)){
         BindPort port = field.getAnnotation(BindPort.class);
         field.setAccessible(true);
         field.set(tc,port.value());
   }

   if (field.isAnnotationPresent(BindAddress.class)) {
         BindAddress address = field.getAnnotation(BindAddress.class);
         field.setAccessible(true);        
         field.set(tc,address.value());
   }

}

tc.printInfo();

我們運行程序得到如下輸出:


output

我們對tc 對象並沒有做任何的set及初始化工作,輸出結果卻依然不再是null了,這就是反射與注解的功勞。

上面代碼的邏輯很簡單:

首先遍歷循環所有的屬性,如果當前屬性被指定的注解所修飾,那么就將當前屬性的值修改為注解中成員變量的值。

上面的代碼中,找到被BindPort修飾的屬性,然后將BindPort中value的值賦給該屬性。

這里setAccessible(true)的使用時因為,我們在聲明port變量時,其類型為private,為了確保可以訪問這個變量,防止程序出現異常。

理論上來說,這樣做是不安全的,不符合面向對象的思想,這里只是為了說明注解和反射舉例。

但是,你也會發現,反射給我們提供了一種在運行時改變對象的方法。

好了,下面我們繼續修改TestClass

public class TestClass {
    @BindAddress("http://www.google.com.cn")
    String address;
    @BindPort("8888")
    private String port;

    private int number;

    public void printInfo() {
        System.out.println("info is " + address + ":" + port);
    }
    .......
}

我們為注解設定了參數,再次運行,相信你已經猜到結果了。


output1

這時候由於我們在給成員變量設定注解時,寫了參數,反射時也取到了相應的值。

方法使用注解

上面對於類屬性(成員變量)設定注解,可能還不能讓你感受到注解&反射的優勢,我們再來看一下類的方法使用注解會怎樣。

我們還是先定義一個注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BindGet {
    String value() default "";
}

有效范圍至運行時,適用於方法。

再次修改TestClass 如下:

public class TestClass {
    @BindAddress("http://www.google.com.cn")
    String address;
    @BindPort("8888")
    private String port;

    private int number;

    @BindGet("mike")
    void getHttp(String param){
        String url="http://www.baidu.com/?username"+param;
        System.err.println("get------->"+url);
    }

    ...........
}

我們添加了一個名為getHttp的方法,而且這個方法由@BindGet注解。

然后看反射的使用:

//獲取類
Class c = Class.forName(className);
TestClass tc= (TestClass) c.newInstance();

// 獲取所有的方法
Method[] ms = c.getDeclaredMethods();

for (Method method : ms) {
  if(method.isAnnotationPresent(BindGet.class)){
     BindGet bindGet = method.getAnnotation(BindGet.class);
     String param=bindGet.value();
     method.invoke(tc, param);
  }
}

這里的邏輯和對屬性的解析相似,依舊是判斷當前方法是否被指定的注解(BindGet)所修飾,
如果是的話,就使用注解中的參數作為當前方法的參數去調用他自己。

這樣,我們在運行程序時,通過反射就回去主動調用getHttp方法,得到如下輸出:


output2

這里我們就可以通過注解動態的實現username參數的修改,甚至getHttp方法整個http url地址的修改。
(假設我們這里的getHttp方法是做網絡請求)

到這里,你應該已經明白了如何使用反射獲取注解的信息,但你一定會困惑這么做有什么用呢?

”動態“”動態“”動態“

這就是使用注解和反射最大的意義,我們可以動態的訪問對象。

說了這么多,下面我們看看,在Android開發中,我們遇到的注解和反射。

Android 中的注解&反射

Butterknife

如果你是一個Android開發者,相信在使用Butterknife插件之前,你一定寫了無數次的findViewById。

然而,如果使用了Butterknife 插件,我們就可以很方便的完成findViewById的工作,甚至是setOnClickListener 的工作。

public class ButtferknifeDemoActivity extends AppCompatActivity {
    @BindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_buttferknife);
        ButterKnife.bind(this);
        textView.setText("I'm not null");

    }
}

上面的代碼,應該不陌生。試想如果你的activity_bufferknife 布局文件中有很多控件時,這樣做不知道可以省多少時間了

我們看一下BindView的注解定義:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

這個注解用於修飾變量,有效范圍也是限定到了CLASS(即編譯階段),並沒有到運行時。
我們在Butterknife(8.4.0)的部分源碼中可以看到:

/** Simpler version of {@link View#findViewById(int)} which infers the target type. */
  @SuppressWarnings({ "unchecked", "UnusedDeclaration" }) // Checked by runtime cast. Public API.
  @CheckResult
  public static <T extends View> T findById(@NonNull View view, @IdRes int id) {
    return (T) view.findViewById(id);
  }

我們可以猜到的,編譯時最終的實現必然是到這里,實現view.findViewById(id)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM