編輯:關於Android編程
最近一段時間在研究EventBus和Retrofit 的過程中,都遇到了注解這個概念。由於在學習Java的時候對這方面沒有深入了解過,所以看起相關的代碼來,總會有點不知其所以然,這裡就注解和反射的使用做一下總結。
這裡我們先從反射說起,了解了反射的意義及用法後,我們在看看注解的使用,最後敘述一下在Android開發中是怎樣結合使用注解與反射。
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 ,這個我們常用類的所有屬性:
再比如:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> //獲取類 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 類的所有方法(圖片只截取了部分方法,實際有很多就不占篇幅了):
Java中有關反射的類有以下這幾個:
為了方便描述,這裡我們創建一個類 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 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();
和屬性類似,我們依然可以通過一系列的方法獲取到方法的返回值類型,名稱以及參數。下面的表格中總結了一些關鍵方法:
類似的獲取到TestClass的所有方法:
這裡可以看到,獲取的TestClass的屬性和方法同我們定義的是完全一致的。
這裡我們順便調用一下TestClass的printInfo方法:
new TestClass().printInfo();
用於所有屬性沒有做初始化,所以得到如下輸出:
可以看到,利用反射我們可以很方便的去“反編譯”一個class。那麼我們用反射這麼做的意義是什麼呢?不要著急,下面我們先來了解一下注解
關於注解的定義網上有很多說法,就不再贅述。這裡我們就說兩點
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用來標記自定義注解的有效范圍,他的取值有以下三種:
RetentionPolicy.SOURCE: 只在源代碼中保留 一般都是用來增加代碼的理解性或者幫助代碼檢查之類的,比如我們的Override;
RetentionPolicy.CLASS: 默認的選擇,能把注解保留到編譯後的字節碼class文件中,僅僅到字節碼文件中,運行時是無法得到的;
RetentionPolicy.RUNTIME: ,注解不僅 能保留到class字節碼文件中,還能在運行通過反射獲取到,這也是我們最常用的。
@Target指定Annotation用於修飾哪些程序元素。
@Target也包含一個名為”value“的成員變量,該value成員變量類型為ElementType[ ],ElementType為枚舉類型,值有如下幾個:
使用了@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();
我們運行程序得到如下輸出:
上面代碼的邏輯很簡單:
首先遍歷循環所有的屬性,如果當前屬性被指定的注解所修飾,那麼就將當前屬性的值修改為注解中成員變量的值。
上面的代碼中,找到被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); } ....... }
我們為注解設定了參數,再次運行,相信你已經猜到結果了。
這時候由於我們在給成員變量設定注解時,寫了參數,反射時也取到了相應的值。
上面對於類屬性(成員變量)設定注解,可能還不能讓感受到注解&反射的優勢,我們再來看一下類的方法使用注解會怎樣。
我們還是先定義一個注解
@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方法,得到如下輸出:
這裡我們就可以通過注解動態的實現username參數的修改,甚至getHttp方法整個http url地址的修改。
(假設我們這裡的getHttp方法是做網絡請求)
到這裡,你應該已經明白了如何使用反射獲取注解的信息,但你一定會困惑這麼做有什麼用呢?
”動態“,”動態“,”動態“
這就是使用注解和反射最大的意義,我們可以動態的訪問對象。
說了這麼多,下面我們看看,在Android開發中,我們遇到的注解和反射。
如果你是一個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 staticT findById(@NonNull View view, @IdRes int id) { return (T) view.findViewById(id); }
我們可以猜到的,編譯時最終的實現必然是到這裡,實現view.findViewById(id)。
在這裡,注解和反射的結合,使我們可以避免做很多重復的工作。
第一次使用Retrofit的時候,完全被接口定義的方式搞蒙圈了,完全搞不懂啊。
public interface UserBasicService { @GET("users/{user}") CallgetUsers(@Path("user") String uses); }
為什麼要這麼寫?參數是怎麼傳遞的?@ 是什麼意思?帶著曾經的這些疑問,我們首先看看這裡的兩個注解。
這裡使用了兩個注解GET 和Path ,我們看一下:
GET
/** Make a GET request. */ @Documented @Target(METHOD) @Retention(RUNTIME) public @interface GET { /** * A relative or absolute path, or full URL of the endpoint. This value is optional if the first * parameter of the method is annotated with {@link Url @Url}. *
* See {@linkplain retrofit2.Retrofit.Builder#baseUrl(HttpUrl) base URL} for details of how * this is resolved against a base URL to create the full endpoint URL. */ String value() default ""; }
Path
@Documented @Retention(RUNTIME) @Target(PARAMETER) public @interface Path { String value(); /** * Specifies whether the argument value to the annotated method parameter is already URL encoded. */ boolean encoded() default false; }
這兩個注解的生命周期都延續到了 RUNTIME,即運行時。GET用於方法,Path用於參數。這點和我們定義getUsers()方法是一致的。
關於Retrofit中反射和注解的使用,涉及到動態代理的相關概念,這裡就不展開來說。總的思路就是通過注解中使用的參數,動態的生成Request然後由OKHttp去調用。這個以後會做深入分析,這裡只是了解注解和反射用法。
好了,關於注解和反射的使用方法及意義就暫時總結到這裡。
以合適的方式使用反射,會讓我們寫代碼的方式更加靈活。反射使用不當,反而會適得其反,會對性能造成影響。
但是EventBus,Retrofit 的如此火爆,讓我們有理由相信,對性能的影響也許沒那麼大,或者說面對現如今硬件配置堪比電腦的手機,這點影響也許可以忽略不計。
所以關於反射的使用還是仁者見仁智者見智吧。