Java進階知識點-反射和泛型
老規矩先放一張思維導圖鎮樓
Java中的進階知識點有很多,這一篇主要學習反射和泛型。可以說這2個知識點我們自己平時的開始用的可能不多,但是系統源碼和網上的第三方開源庫中用到的是非常多。如果rxjava ,熱修復,dagger2 ,等等都需要用到,還包括Hook點啊 動態代理 AOP APT 啊等等。所以掌握好反射 是學習這些框架的基礎。
反射
- 反射是什麼
- 反射怎麼使用
- 反射的優缺點
- 反射的原理是什麼
- 反射使用時機
反射是什麼?
反射(reflex)是指機體對內在或外在刺激有規律的反應。光沿着原路返回就叫反射,這個是物理的反射。java中的反射是指:程序在運行時能夠獲取自身的信息。
簡而言之,你可以在運行狀態中通過反射機制做到:
對於任意一個類,都能夠知道這個類的所有屬性和方法;
對於任意一個對象,都能夠調用它的任意一個方法和屬性;
這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
應該已經比較清楚了。就是非常規手段調用類中方法或者對象和字段。
反射怎麼使用
我們先看正常的調用方法流程。如狗叫
1、定義一個接口有叫方法
public interface IAnimalCall {
void call();
}
2、寫一個類Gog實現接口,實現方法叫
public class Dog implements IAnimalCall {
private String mName;
public Dog() {
}
public Dog(String name) {
this.mName = name;
}
@Override
public void call() {
System.out.println("狗:" + mName + "---汪汪汪");
}
@Override
public String toString() {
return "Dog [mName=" + mName + "]";
}
}
3、new出對象,調用方法。
public static void main(String[] args) {
IAnimalCall dog = new Dog("旺財");
dog.call();
}
4、運行結果
恩,結果不出我們所料(廢話這麼簡單,大家都會)
下面想一想如果我把構造函數改爲private私有的,這樣才能調用到call方法呢?
public class Dog implements IAnimalCall {
private String mName;
private Dog() {
}
private Dog(String name) {
this.mName = name;
}
@Override
public void call() {
System.out.println("狗:" + mName + "---汪汪汪");
}
@Override
public String toString() {
return "Dog [mName=" + mName + "]";
}
}
私有的構造函數是不能直接用New創建對象的。
編譯報錯了,提示說構造方法是不可見的。這麼辦? 先不要槓 這樣設計,就問你一句這麼辦,還怎麼調用Call方法。5分鐘考慮。。。
時間到了,想到辦法沒。哈哈 只要反射就可以了。
對於任意一個類,都能夠知道這個類的所有屬性和方法;
反射是Java寫好提供給我使用的。其實每個類class都有一個Class類去描述它。它會記錄這個類有什麼東西比如構造函數,字段,方法等等是對類的解釋和說明文檔,它由Java提供的在類被加載的時候就會存儲在方法區中。
平是我們接觸多的就是:
public static final String TAG=Main.class.getSimpleName();
這個Main.class 就是拿到Class注意是大寫,相當於:
Class main=Main.class;
String Tag=main.getSimpleName();
根據官方文檔我們獲取Class有3種方式:
- 通過 Object.getClass()
- 通過 .class 標識
- 通過 Class.forName() 方法
大家可以測試一下。
public static void main(String[] args) {
Dog dog=new Dog("旺財");
Class<? extends Dog> dog1=dog.getClass();
System.out.println("Object.getClass name="+dog1.getSimpleName());
Class<Dog> dog2=Dog.class;
System.out.println("Dog.class name="+dog2.getSimpleName());
try {
Class<?> dog3=Class.forName("com.zx.parse.annotation.Dog");
System.out.println("Class.forName name="+dog3.getSimpleName());
} catch (ClassNotFoundException e) {
System.out.println("Dog.class name="+e.toString());
}
}
運行結果:
這裏第三種方法就厲害了 你不需要有對象,甚至類都不需要知道,只要給全限定名稱就好了 其實就是包名加類名。因爲它要根據這個路徑去找你的類(文件類 真實的文件)。
現在我們根據第三種方式獲得了Dog類的 Class。然後就可以根據這個Class去獲取構造函數了:有條件的可以點進Class類中看看到底有什麼,它裏面有很多的getxxx()方法
如果我們的Dog類構造函數不是private就用下面代碼就可以了
Class<?> dogClass=Class.forName("com.zx.parse.annotation.Dog");
Dog dog=(Dog) dogClass.newInstance();
dog.call();
但是由於我們的構造函數是private所以這招行不通。我們再去看看官方文檔怎麼獲取構造函數:
首先用Class點一下發現有個getConstructor方法,看名字就知道應該是獲取構造函數的。好的 我們繼續。
Constructor<?> constructor=dogClass.getConstructor(dogClass);
Constructor這個其實也是一個類和Class類似。Class是用來描述類的,那Constructor就是來描述構造函數。比如構造函數的修飾符,參數啊什麼的。
測試用getConstructor獲取不到構造函數,要用getDeclaredConstructors
Class<?> dogClass = Class.forName("com.zx.parse.annotation.Dog");
Constructor<?>[] constructor = dogClass.getDeclaredConstructors();
println(constructor[1].getParameterCount());
Dog dog=(Dog) constructor[1].newInstance("旺財");
dog.call();
直接調用會報錯,檢測access說你這個是私有的,要你確保權限
怎麼確保,我們點擊看這個錯誤是怎麼產生的。
完,點不進去 eclipse就是這點不好,不要問我爲什麼還在用eclipse。這難不倒我們。我們可以去網上看在線源碼,還是什麼都看不見,
什麼只要把override 設置爲true就不會進來了。剛好就有一個方法
setAccessible
加上之後
其實只要你的是私有的就得加上這個,否則就會報使用異常,激動人心的時刻來了運行一下看看:
看到運行成功了。可以看到在構造函數是私有的情況下 我們也成功的獲取了對象調用了方法。
接下來 我們把call方法也至爲私有的。看怎麼通過反射調用到。
猜測原理應該類似,經過測試:
getDeclaredMethods 是獲取所有方法
getDeclaredMethod是獲取指定的方法
Class<?> dogClass = Class.forName("com.zx.parse.annotation.Dog");
Constructor<?>[] constructor = dogClass.getDeclaredConstructors();
println(constructor[1].getParameterCount());
constructor[1].setAccessible(true);
Dog dog = (Dog) constructor[1].newInstance("旺財-反射");
Method methodEat = dogClass.getDeclaredMethod("eat", String.class);
println(methodEat.getName());
methodEat.setAccessible(true);
methodEat.invoke(dog, "飯");
運行結果
哈哈 又調到了。
這裏之所以花這麼多功夫 自己一個一個的摸索而不是 直接去網上看別人的。就是想檢驗一下自己的動手能力 有沒有掉入
眼睛:我會了。大腦:我會了。 手:不,你不會。
果然知識點還是要自己動手實踐實踐的好,以前我看過幾遍關於反射,但是這次動手來 花了幾個小時,不過好在還弄出來了
接下來基本就是大同小異,就是熟練度了。修改變量等等,其實反射主要是爲了獲取註解,和調用方法。
反射的優缺點: 任何事物,都有兩面性,反射的優點,也同是就是它的缺點。
優點:
(1)能夠運行時動態獲取類的實例,大大提高系統的靈活性和擴展性。
(2)與Java動態編譯相結合,可以實現無比強大的功能
缺點:
(1)使用反射的性能較低
(2)使用反射相對來說不安全
(3)破壞了類的封裝性,可以通過反射獲取這個類的私有方法和屬性
仔細想想也是,你要只要一個路徑String就可以獲取到對象 還可以調用任何方法太變態了。
其實反射是Java提供好給我們用,你發生全程我們就是調用系統準備好的反射相關的API。所以會用反射沒什麼值得高興 滿足的,是個人對着反射說明書(官方文檔看兩遍就會了)。既然如此 那我們怎麼和別人區別開,那就要學習下一個問題了。
反射的實現原理是什麼?
反射就是把java類中的各種成分映射成一個個的Java對象
例如:一個類有:成員變量、方法、構造方法、包等等信息,利用反射技術可以對一個類進行解剖,把個個組成部分映射成一個個對象。
那這些描述的對象類 如Class Method Constructor 類在哪?
它們也是普通的類,只不過是JDK寫好的,在java.lang.reflect包下面。
它們什麼時候加載的?
類加載器負責根據一個類的全限定名來讀取此類的二進制字節流到JVM內部,並存儲在運行時內存區的方法區,然後將其轉換爲一個與目標類型對應的java.lang.Class對象實例
看看反射中最關鍵的一句:
Class.forName(“com.zx.parse.annotation.Dog”);
這個是怎麼拿到Class對象的。
這裏我們先插入一個知識點就是 類加載器 ClassLoader:這裏先簡單說明,後續學習虛擬機的時候會詳細講解。
看了這麼多 原理都是和類的加載有關,看來虛擬機的學習要提前。
反射使用時機: 或者說反射的作用,對於我來說完全就是爲了理解別人寫的第三方框架。
1.能用正常方法就不要用反射
2.看別人的開源框架
3.需要動態代理的時候
泛型
- 泛型是什麼
- 泛型怎麼使用
- 泛型的優缺點
- 泛型的原理是什麼
- 泛型使用時機
泛型是什麼
泛型這個東西大家可能用的不多,但是寫SDK的人 應該就會經常使用,我們接觸最多就是集合框架中 如ArrayList
我們是用的時候就必須先告訴它類型 否則會爆紅
ArrayList list = new ArrayList();
如上,我告訴了ArrayList 使用String字符串類型,之後只能list添加String。
仔細想想,這個有什麼用?
1.安全
2.減少重複代碼。
3.通用性強
否則ArrayList 要針對每種數據類型 建立一個數組,如果只是8大基本數據還好,那些自定義數據 還沒法玩了。
泛型官方定義:參數化類型。可以理解爲把 類型當做參數 傳給類,這一點明顯符合依賴倒置原則 和依賴注入。要什麼類型 客戶端自己決定。
但是也有不好的,因爲類型擦除的原因 類的行爲就被抹除了,就是說泛型T不能使用原類裏面自己的方法,只能用object中通用的方法。如:
上面我定義了一個泛型方法println打印,可以看到你傳什麼進來,這個msg只能用object中的方法。所以說泛型其實沒有想象中的那麼厲害,最大的作用就是存儲了。因爲 它調不了 你自己寫的方法。
泛型怎麼使用
泛型有3種:泛型類,泛型方法,泛型接口
泛型類:
public class Animal<T> {
private T mT;
public void setT(T t) {
this.mT = t;
}
public T getT() {
return mT;
}
public void call() {
// mT.call();
}
}
<>尖括號是關鍵。T 隨意 你可以用任意大寫字母,但是爲了規範還是用T吧
T 代表一般的任何類。
E 代表 Element 的意思,或者 Exception 異常的意思。
K 代表 Key 的意思。
V 代表 Value 的意思,通常與 K 一起配合使用。
使用:
public static void main(String[] args) {
Animal<Cat> animal=new Animal<>();
Cat cat=new Cat();
animal.setT(cat);
animal.getT().call();
}
比較簡單,好像什麼用都沒有,只能存儲一下,因爲
必須強制轉化爲Cat才能調用call。這是爲什麼呢,我們傳進來的明明就是Cat對象,這個我們反編譯一下Animal看看 傳進來的mT到底是什麼
使用javap進行反編譯:javap -c -l Animal.class
可以看到mT 類型是Object:所以不能當做Cat類,必須要先轉化。泛型只有在編譯器纔有用,編譯後類型都被擦出了。
那我們可不可以 不轉化直接使用。這個可以是可以 後面講解
泛型方法
之前面的那個println()方法就是泛型方法。
public static <T> void println(T msg) {
System.out.println("Main:" + msg.toString());
}
使用
public static void main(String[] args) {
println("泛型");
println(true);
println('H');
println(122);
println(12.12);
}
結果:
這就是泛型的好處,不然就要重載println方法參數爲String,int boolean 等。
泛型接口
泛型接口與泛型類基本一樣。
public interface IAnimal<T> {
public void setT(T t);
public T getT();
public void call();
}
下面我們想想有什麼辦法 可以不轉化直接調用。這裏就需要用到接口或者繼承了。我們先定義一個接口。
public interface ICall {
void call();
}
然後讓貓Cat實現ICall
public class Cat implements ICall {
private String name;
public Cat(String name) {
this.name = name;
}
public Cat() {
}
@Override
public void call() {
System.out.println(name + ":喵喵喵");
}
}
然後改造一下泛型類Animal,讓T繼承ICall
public class Animal<T extends ICall> {
private T mT;
public void setT(T t) {
this.mT = t;
}
public T getT() {
return mT;
}
public void call() {
mT.call();
}
}
然後運行:
public static void main(String[] args) {
Animal<Cat> mAnimal = new Animal<Cat>();
mAnimal.setT(new Cat("白嫖"));
mAnimal.call();
}
結果:
哈哈,我們沒有經過轉化也調用了call方法。我們再去看看此時mT編譯後是什麼類型-用javap反編譯:
看到mT不再是Object類型而是變成了ICall類型,所以可以直接調用接口中的call()方法了。是不是瞬間感覺 好厲害了。其實也沒有多大用。
這就叫做 泛型的通配符 嗎?
通配符通常有2種 <? extends T> 和<? super T>
我們 一個一個講解,先看<? extends T>
public <T extends ICall> void call(T t) {
t.call();
System.out.println(t.getClass().getName());
}
其實這個還不算通配符,也和簡單就是 你傳進來的類型必須是 繼承了ICall的子類,本來如果不加extends ICall 你可以傳任意類型,但是加了之後 就只能傳ICall子類。這個好像沒什麼用,失去了泛型的意義了。
我還不如直接寫成:
public void call(ICall t) {
t.call();
System.out.println(t.getClass().getName());
}
其實通配符這個東西是 給集合類使用的。舉個栗子:
其實還是網上的那個水果例子。那個例子就已經說的很好了。
首先我們有盤子放什麼東西不知道,所以是泛型:
public class Plate<T> {
private T mT;
public Plate(T t) {
this.mT = t;
}
public Plate() {
}
public void setT(T t) {
this.mT = t;
}
public T getT() {
return mT;
}
}
很簡單就是能夠set 和get 。接着定義2個類 水果和蘋果,繼承關係
public class Fruit {
public void describe() {
System.out.println("我是水果");
}
}
public class Apple extends Fruit {
public void describe() {
System.out.println("我是蘋果");
}
}
接着測試:
public static void main(String[] args) {
Plate<Fruit> plateFruit = new Plate<>(new Fruit());
plateFruit.getT().describe();
/////////////////////////////////////////////////
Plate<Apple> plateApple = new Plate<>(new Apple());
plateApple.getT().describe();
}
運行結果:
一切都沒什麼問題。但是如果我在放水果的盤子 放蘋果 可以嗎,試試看
Plate plateFruit = new Plate<>(new Apple());
這一句編譯不過,提示不能轉化。 那怎麼才能轉化呢?答案就是通配符
我們改造一下改爲:
Plate<? extends Fruit> plateFruit = new Plate<>(new Apple());
哈哈 可以編譯過去 也可以運行出結果:我是蘋果。
現在不管是蘋果還是橘子只要是 繼承於Fruit類都可以了。
別高興太早了,當加了通配符之後你會發現它不能調用set方法了
怎麼辦? 沒辦法 就是不能set了 原因就是 編譯器不知道 你要放什麼進來。我們反編譯看看Plate:看不出什麼
泛型的優缺點
優點:1.適配,簡潔,消除重複代碼
2.複用
3.類型安全
缺點:其實它的優點沒那麼強大,有點雞肋其實,缺點的話 由於優點不明顯,相應的缺點也不大,硬要說的話就是 不好看 不好理解。還有就是 類型擦除導致 原來定義的方法都不能用了。
泛型的原理
原理就是類型擦除了。而且泛型它只是在編譯器有用,在JVM運行時 類型都變成了Object。java 有編譯時 和運行時 Runtime 運行時。先放着,複習虛擬機時再來看看這個類是幹什麼的。
泛型使用時機
綜合來看,和容器相關的時候可以考慮使用,如果Adapter 集合框架 等等,這些不需要 類的具體方法 就可以使用了。其次就是看別人的框架 因爲框架就是一種廣泛適配的東西 就很適合使用泛型。
總結:泛型並不難,唯一難理解的地方就那個 通配符了。我們平時在框架中接觸的很多,多思考一下就好了。記住一點 泛型=參數化類型+類型擦除 類型擦除怎麼實現 那是編譯器實現的,定義好的語法規則。