文章目錄
由於借鑑的較多,這裏就直接作爲轉載了。Java 反射由淺入深 | 進階必備
泛型基礎
泛型的好處
在使用泛型之前,使用的是繼承,會有兩個問題:1.強制轉換,2.沒有錯誤檢查。泛型提供了類型參數,具有更好的可讀性。在使用get方法時,不需要強制轉換。在add時會出現編譯錯誤,以提示使用者。
//不使用泛型需要強制轉換
List list = new ArrayList();
list.add("hello");
list.add(5);
System.out.println(list.get(0));
System.out.println((String)list.get(1));% class java.lang.Integer cannot be cast to class java.lang.String
//使用泛型可避免強制轉換
List<String> list = new ArrayList<String>();
list.add("hello");
list.add(5)%error
String s = list.get(0);
泛型使用
使用<類型參數>代表這是一個泛型類、泛型方法、泛型接口。其實 是爲了說明類型參數
泛型類:
public class ObjectTool<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
泛型方法:
public class ObjectTool {
public <T> T show(T t) {
if (t != null){
return t;
}else{
return null;
}
}
泛型接口:
注意在具體類實現泛型接口的時候不能在使用類型參數T了,而是要具體的類型。
public class Ttry {
public static void main(String[]args){
showTry showTry = new showTry();
System.out.println(showTry.show(20));
}
}
interface show<T>{
T show(T t);
}
class showTry implements show<Integer>{
public showTry(){
}
@Override
public Integer show(Integer num){
return num;
}
}
類型變量的限定
例如我們有一個泛型方法min,其中的形參是也是一個泛型數組。我們要使用泛型變量smallest的comparTo方法,就要求類型變量T是可以實現Comparable接口的類。
爲了解決這種問題,我們可以使用這種方式。雖然是接口,但這裏也使用了extends。因爲這裏的含義表示T是綁定類型的子類型。這裏T和綁定類型可以是類,也可以是接口。同時一個類型變量可以有多個限定,使用’&'來分隔。
public static<T extends Comparable> T min(T[]a)
T extends Comparable & Serializable
類型擦除
類型擦除概述
虛擬機沒有泛型類型對象,所有對象都屬於普通類。無論何時定義一個泛型類型,都會有一個原始類型(raw type)。會將無限定類型(T)轉換爲限定類型(如果沒有的話默認爲Object)。一個簡單的例子如下代碼,這是一個泛型類在虛擬機中的原始類型。
class Pair<T>{
private T first;
public Pair(T first) this.first = first;
public T getFirst(){return first;}
public void setFirst(T newvalue)first = newvalue;
}
class Pair{
private Object first;
public Pair(Object first) this.first = first;
public Object getFirst(){return first;}
public void setFirst(Object newvalue)first = newvalue;
}
假如類型變量有限定的話,就會用第一個限定的類型來作爲原始類型,如下圖。
當程序調用泛型方法時,如果沒有擦除返回類型的話,編輯器插入強制轉換,下述代碼一共分兩步,首先通過原始方法getFirst獲得一個Object對象,再強制轉換爲Employee對象。
類型擦除也會出現在泛型方法中:
public static <T extends Comparable> T min(T[]a)
public static Comparable min(Comparable a)
類型擦除帶來的影響
1.不能使用基本類型作爲類型變量,類型擦除之後List含有Object的域,而Object域不能存儲int值。
List<int>list = new ArrayList<>();//error
List<Integer>list = new ArrayList<>();
2.運行時類型查詢僅適用於原始類型
虛擬機中的對象有一個特定的非泛型類型,因此所有的類查詢都只會產生原始類型。
if (list instanceof ArrayList<Integer>)//error
if (list instanceof ArrayList<T>)//error
//其實查詢的只是判斷list是否是任意一個類型的ArrayList
if(list instanceof ArrayList<?>)//ok
ArrayList<String>()list1 = new ArrayList<>();
ArrayList<Integer>list2 = new ArrayList<>();
list1.getClass()==list2.getClass();//return true,都是ArrayList.class
3.不能創建參數化類型的數組
List和 List在 jvm 中等同於List,所有的類型信息都被擦除,程序也無法分辨一個數組中的元素類型具體是 List類型還是 List類型。
List<Integer>[] li2 = new ArrayList<Integer>[10];//error
List<Boolean> li3 = new ArrayList<Boolean>[10];//error
List<List<Integer>>lists = new ArrayList;//ok
4.不能實例化類型變量
public Pair(){first = new T();second = new T();}//error
5.不能拋出或捕獲泛型類的實例
//不能創建、捕捉或拋出參數化類型的對象
class MathException<T> extends Exception {} // error
class QueueFullException<T> extends Throwable {} // error
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
} catch (T e) { // error
}
}
6.不能重載參數類型爲相同原始類型的方法
在擦除後都是List list。
public class Example { // error
public void print(List<String> list) {}
public void print(List<Integer> list) {}
}
通配符類型
1.?extends Employee,而當前代碼代表它的參數類型是Employee的子類,但一定不是其他的比如String。
Pair<? extends Employee>
我理解通配符類型可以做到如下事情:當泛型類MyChar中的類型變量可以是類A,類B時,我們有一個函數想要使用類型變量中的一個元素。如果我們這個方法傳入的形參中的類型變量是A,就只能使用A的元素.但類B是類A的子類,也同樣擁有這個元素,我們想要讓A的子類都可以被使用。就需要使用通配符
public static void returnNum(MyChar<A> c)
public static void returnNum(MyChar<? extends A> c)
public class Ttry {
public static void main(String[]args){
//showTry showTry = new showTry();
//System.out.println(showTry.show(20));
A a = new A(10);
B b = new B(20,30);
MyChar<A> aMyChar = new MyChar<>(a);
MyChar<B> bMyChar = new MyChar<>(b);
returnNum(aMyChar);
returnNum(bMyChar);
}
public static void returnNum(MyChar<? extends A> c){
A a = c.getT();
System.out.println(a.getNum());
}
}
class MyChar<T>{
private T first;
public MyChar(T t)
{
this.first = t;
}
public T getT(){
return first;
}
}
class A{
private int num;
public A(int num)
{
this.num = num;
}
public int getNum(){
return num;
}
}
class B extends A{
private int size;
public B(int num){
super(num);
}
public B(int num,int size)
{
super(num);
this.size = size;
}
public int getNum()
{
return super.getNum();
}
}
2.?super Employee代表當前通配符限制爲Employee的父類。
3.還可以使用無限定通配符:“?”
反射
java反射機制:在程序運行時,能知道任意一個類的所有屬性和方法,對於任意一個對象都能調用它的任意一個方法和屬性。這種動態的獲取信息或動態的調用對象的方法稱之爲反射。
這裏最重要的一點是運行時,反射允許我們可以在程序運行時加載使用探索運行時生成的.class
文件。換句話說,Java 程序可以加載一個運行時才得知名稱的 .class
文件,然後獲悉其完整構造,並生成其對象實體、或對其 fields(變量)設值、或調用其 methods(方法)。
反射基礎
我們可以很輕鬆的通過下述代碼獲取一個Java類的class對象。
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
我們通過可以動態的創建一個類的對象。可以創建一個與testClass具有相同構造的對象,newInstance方法是使用默認方法(無參)初始化新創建的對象。
TestClass testClass = new TestClass();
testClass.getName().newInstance();
使用反射獲取類的信息
這裏設置兩個類(Father.class與Son.class)來體現效果。當然實際開發中變量域應該是private,但這裏爲了體現效果寫出public。
FatherClass.java
public class FatherClass {
public String mFatherName;
public int mFatherAge;
public void printFatherMsg(){}
}
SonClass.java
public class SonClass extends FatherClass{
private String mSonName;
protected int mSonAge;
public String mSonBirthday;
public void printSonMsg(){
System.out.println("Son Msg - name : "
+ mSonName + "; age : " + mSonAge);
}
private void setSonName(String name){
mSonName = name;
}
private void setSonAge(int age){
mSonAge = age;
}
private int getSonAge(){
return mSonAge;
}
private String getSonName(){
return mSonName;
}
}
獲取類的所有變量信息
/**
* 通過反射獲取類的所有變量
*/
private static void printFields(){
//1.獲取並輸出類的名稱
Class mClass = SonClass.class;
System.out.println("類的名稱:" + mClass.getName());
//2.1 獲取所有 public 訪問權限的變量
// 包括本類聲明的和從父類繼承的
Field[] fields = mClass.getFields();
//2.2 獲取所有本類聲明的變量(不問訪問權限)
//Field[] fields = mClass.getDeclaredFields();
//3. 遍歷變量並輸出變量信息
for (Field field :
fields) {
//獲取訪問權限並輸出
int modifiers = field.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//輸出變量的類型及變量名
System.out.println(field.getType().getName()
+ " " + field.getName());
}
}
值得注意的是:
調用 getFields() 方法,輸出 SonClass 類以及其所繼承的父類( 包括 FatherClass 和 Object ) 的 public 成員變量。注:Object 類中沒有成員變量,所以沒有輸出。
類的名稱:obj.SonClass
public java.lang.String mSonBirthday
public java.lang.String mFatherName
public int mFatherAge
調用 getDeclaredFields() , 輸出 SonClass 類的所有成員變量,不問訪問權限。
類的名稱:obj.SonClass
private java.lang.String mSonName
protected int mSonAge
public java.lang.String mSonBirthday
獲取類的所有方法信息
可以更好的體現getMethods()方法是獲取所有包括自己聲明與父類繼承的public權限方法。
/**
* 通過反射獲取類的所有方法
*/
private static void printMethods(){
//1.獲取並輸出類的名稱
Class mClass = SonClass.class;
System.out.println("類的名稱:" + mClass.getName());
//2.1 獲取所有 public 訪問權限的方法
//包括自己聲明和從父類繼承的
Method[] mMethods = mClass.getMethods();
//2.2 獲取所有本類的的方法(不問訪問權限)
//Method[] mMethods = mClass.getDeclaredMethods();
//3.遍歷所有方法
for (Method method :
mMethods) {
//獲取並輸出方法的訪問權限(Modifiers:修飾符)
int modifiers = method.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//獲取並輸出方法的返回值類型
Class returnType = method.getReturnType();
System.out.print(returnType.getName() + " "
+ method.getName() + "( ");
//獲取並輸出方法的所有參數
Parameter[] parameters = method.getParameters();
for (Parameter parameter:
parameters) {
System.out.print(parameter.getType().getName()
+ " " + parameter.getName() + ",");
}
//獲取並輸出方法拋出的異常
Class[] exceptionTypes = method.getExceptionTypes();
if (exceptionTypes.length == 0){
System.out.println(" )");
}
else {
for (Class c : exceptionTypes) {
System.out.println(" ) throws "
+ c.getName());
}
}
}
}
調用 getMethods() 方法
**獲取 SonClass 類所有 public 訪問權限的方法,包括從父類繼承的。**打印信息中,printSonMsg() 方法來自 SonClass 類, printFatherMsg() 來自 FatherClass 類,其餘方法來自 Object 類。
類的名稱:obj.SonClass
public void printSonMsg( )
public void printFatherMsg( )
public final void wait( ) throws java.lang.InterruptedException
public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException
public final native void wait( long arg0, ) throws java.lang.InterruptedException
public boolean equals( java.lang.Object arg0, )
public java.lang.String toString( )
public native int hashCode( )
public final native java.lang.Class getClass( )
public final native void notify( )
public final native void notifyAll( )
訪問以及操作類的私有變量及方法(重要)
通過反射我們可以訪問類的私有方法,以及更改私有變量或常量(以final修飾)。
這是一個TestClass。
public class TestClass {
private String MSG = "Original";
private void privateMethod(String head , int tail){
System.out.print(head + tail);
}
public String getMsg(){
return MSG;
}
}
訪問私有方法
以訪問 TestClass 類中的私有方法 privateMethod(...)
爲例
/**
* 訪問對象的私有方法
* 爲簡潔代碼,在方法上拋出總的異常,實際開發別這樣
*/
private static void getPrivateMethod() throws Exception{
//1. 獲取 Class 類實例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有方法
//第一個參數爲要獲取的私有方法的名稱
//第二個爲要獲取方法的參數的類型,參數爲 Class...,沒有參數就是null
//方法參數也可這麼寫 :new Class[]{String.class , int.class}
Method privateMethod =
mClass.getDeclaredMethod("privateMethod", String.class, int.class);
//3. 開始操作方法
if (privateMethod != null) {
//獲取私有方法的訪問權
//只是獲取訪問權,並不是修改實際權限
privateMethod.setAccessible(true);
//使用 invoke 反射調用私有方法
//privateMethod 是獲取到的私有方法
//testClass 要操作的對象
//後面兩個參數傳實參
privateMethod.invoke(testClass, "Java Reflect ", 666);
}
}
這裏的重點就是privateMethod.setAccessible(true)
方法與privateMethod.invoke
方法,我們想要調用私有方法必須要要將這個方法通過setAccessible(true)
來獲取訪問權。而我們想要調用時,就要使用invoke
方法來調用,首先傳入要操作的對象(就是這個類的實例),之後傳入privateMethod
的形參。
訪問(更改)私有變量
/**
* 修改對象私有變量的值
* 爲簡潔代碼,在方法上拋出總的異常
*/
private static void modifyPrivateFiled() throws Exception {
//1. 獲取 Class 類實例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有變量
Field privateField = mClass.getDeclaredField("MSG");
//3. 操作私有變量
if (privateField != null) {
//獲取私有變量的訪問權
privateField.setAccessible(true);
//修改私有變量,並輸出以測試
System.out.println("Before Modify:MSG = " + testClass.getMsg());
//調用 set(object , value) 修改變量的值
//privateField 是獲取到的私有變量
//testClass 要操作的對象
//"Modified" 爲要修改成的值
privateField.set(testClass, "Modified");
System.out.println("After Modify:MSG = " + testClass.getMsg());
}
}
與訪問私有方法類似,同樣需要privateField.setAccessible(true)
,給我們的私有變量權限,之後通過set(object , value)
修改私有變量的值。
Before Modify:MSG = Original
After Modify:MSG = Modified
修改私有常量
常規情況
私有常量就是用final修飾的成員變量。jvm會在編譯.java
文件得到.class
文件時,會進行優化代碼。簡單來說會把引用常量的代碼直接更改成具體的常量值。
編譯前:
//注意是 String 類型的值
private final String FINAL_VALUE = "hello";
if(FINAL_VALUE.equals("world")){
//do something
}
編譯後:
private final String FINAL_VALUE = "hello";
//替換爲"hello"
if("hello".equals("world")){
//do something
}
因此我們可以知道即使我們通過反射更改了私有常量的值,更改後的值也不會對程序造成影響,因爲jvm在編譯階段得到的.class文件時,已經將引用私有常量的位置都直接改成了私有常量的具體值。
爲了驗證結果,我們更改測試類
//String 會被 JVM 優化
private final String FINAL_VALUE = "FINAL";
public String getFinalValue(){
//劇透,會被優化爲: return "FINAL" ,拭目以待吧
return FINAL_VALUE;
}
之後修改私有常量,其實代碼與修改私有變量類似。
/**
* 修改對象私有常量的值
* 爲簡潔代碼,在方法上拋出總的異常,實際開發別這樣
*/
private static void modifyFinalFiled() throws Exception {
//1. 獲取 Class 類實例
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有常量
Field finalField = mClass.getDeclaredField("FINAL_VALUE");
//3. 修改常量的值
if (finalField != null) {
//獲取私有常量的訪問權
finalField.setAccessible(true);
//調用 finalField 的 getter 方法
//輸出 FINAL_VALUE 修改前的值
System.out.println("Before Modify:FINAL_VALUE = "
+ finalField.get(testClass));
//修改私有常量
finalField.set(testClass, "Modified");
//調用 finalField 的 getter 方法
//輸出 FINAL_VALUE 修改後的值
System.out.println("After Modify:FINAL_VALUE = "
+ finalField.get(testClass));
//使用對象調用類的 getter 方法
//獲取值並輸出
System.out.println("Actually :FINAL_VALUE = "
+ testClass.getFinalValue());
}
}
我們可以根據程序的結果看出,我們雖然已經更改了FINAL_VALUE,但在getFinalValue()
中的結果仍然是之前的FINAL
。
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = FINAL
下圖是編譯後的.class文件。
非常規情況
java允許我們不在聲明時賦值,可以在構造函數再賦值。
public class TestClass {
//......
private final String FINAL_VALUE;
//構造函數內爲常量賦值
public TestClass(){
this.FINAL_VALUE = "FINAL";
}
//......
}
此時再運行結果就會得到想要的效果:
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = Modified
具體原因在於FINAL_VALUE
不是聲明時賦值而是構造函數後賦值,就會讓jvm在.class
中就不將類中使用該常量的位置直接更改爲該常量的具體值,而是該常量的引用,因此就可以得到我們想要的效果。
泛型與反射的結合
當泛型類被類型擦除後,可以通過反射API(Type接口)來確定信息。
使用反射編寫泛型數組
java.lang.reflect包中的Array允許動態的創建數組。例如CopyOf方法,可以將數組擴充至想要的長度。
int[]num = new int[]{5,4,3,2};
num = Arrays.copyOf(num,10);
System.out.println(num.length);%10
我們希望將int[]num可以變成object[]num,以便更多的類型都可以使用該方法。但下述方法直接創建了一個Object的數組,之後就無法再轉換成我們想要的int數組了。
public static Object[] badCopyOf(Object[]num,int length){
Object[] xin = new Object[length];
System.arraycopy(num,0,xin,0,length);
return xin;
}
正確的方法:核心思想就是將要擴充的數組比如int類型,暫時轉換爲Object類型,之後再轉換回來是可以的。
我們可以通過反射獲取到這個類的Class對象,並通過getComponentType
獲取數組的元素類型,之後我們通過newInstance(commpentType,length)
創建Object對象。
這裏使用Object對象而不是用Object數組是爲了讓比如int[]數組使用,因爲int[]數組可以轉換成Object,而不可以轉成Object數組。
public static Object goodCopyOf(Object num,int length){
Class c1 = num.getClass();
//獲取數組的元素類型
Class commpentType = c1.getComponentType();
int oldlength = Array.getLength(num);
Object newArray = Array.newInstance(commpentType,length);
System.arraycopy(num,0,newArray,0,Math.min(length,oldlength));
return newArray;
}
泛型與反射的總結(複習必看)
1.泛型總結
1.泛型的最大好處是可以省去強制轉換,也可以增加代碼的清晰度。同時當執行add操作時就會報錯,而原始的繼承方法只會在get時出錯。
2.泛型類:class A<T>
泛型方法: public T <T> A(T t)
泛型接口:inteface A<T>
,泛型接口在具體類實現接口時要具體的類型比如:class showTry implements show<Integer>
3.類型變量T可以有限定,比如當我們想調用類型變量的compare方法,就要求類型變量T所對應的那些類都實現了Comparable接口,雖然是接口,我們也使用extends關鍵詞。<T extends Comparable>
4.類型擦除,虛擬機中沒有泛型類型對象,會對其進行類型擦除。如果只是類型變量T,則會將其擦除爲Object,如果<T extends Comparable>
,就會擦除爲Comparabale。
//例子1
class Pair<T>{
private T first;
public Pair(T first) this.first = first;
public T getFirst(){return first;}
public void setFirst(T newvalue)first = newvalue;
}
class Pair{
private Object first;
public Pair(Object first) this.first = first;
public Object getFirst(){return first;}
public void setFirst(Object newvalue)first = newvalue;
}
//例子2
public static <T extends Comparable> T min(T[]a)
public static Comparable min(Comparable a)
5.類型擦除帶來的影響:
1)不能使用基本類型作爲類型變量,而要使用其包裝類。
List<int>list = new ArrayList<>();//error
List<Integer>list = new ArrayList<>();
2)不能使用instanceOf
或getclass
,運行時類型查詢僅適用於原始類型。虛擬機中的對象有一個特定的非泛型類型,因此所有的類查詢都只會產生原始類型。
if (list instanceof ArrayList<Integer>)//error
ArrayList<String>()list1 = new ArrayList<>();
ArrayList<Integer>list2 = new ArrayList<>();
list1.getClass()==list2.getClass();//return true,都是ArrayList.class
3)不能創建參數化類型數組
虛擬機無法知道傳入的數據類型究竟是什麼。
List<Integer>[] li2 = new ArrayList<Integer>[10];//error
List<Boolean> li3 = new ArrayList<Boolean>[10];//error
List<List<Integer>>lists = new ArrayList;//ok
4)不能實例化類型變量
public Pair(){first = new T();second = new T();}//error
5)不能拋出或捕獲泛型類的實例
//不能創建、捕捉或拋出參數化類型的對象
class MathException<T> extends Exception {} // error
class QueueFullException<T> extends Throwable {} // error
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
} catch (T e) { // error
}
}
6)不能重載參數類型爲相同原始類型的方法。
類型擦除後都是List list,分不清其中的數據類型。
public class Example { // error
public void print(List<String> list) {}
public void print(List<Integer> list) {}
}
6.通配符,可以使用通配符來讓約束類型變量,<? extends A>
,則要求傳入的數據類型必須是A的子類,
? super A
,則要求傳入的數據類型必須是A的父類。
反射總結
1.反射的作用:可以通過反射在運行時動態的獲取任意類的任意成員變量及任意方法,以及動態的進行訪問任意成員變量及任意方法(包括private方法與private對象)。當類中存在私有常量,反射也可以進行更改,但如果私有常量在聲明時賦值,則無法對其相關的方法產生影響。(因爲虛擬機會在編譯時將.java文件得到.class文件時,會將靜態常量的引用代碼都直接更改爲靜態常量的具體數值)。
2.獲取類的變量信息與獲取類的方法信息類似。創建Class對象,並分別通過getFields()
與getMethods()
方法可以獲取到類的變量與方法。注意這裏獲取到的只有當前類及其父類的public方法與public的成員變量。
3.反射可以訪問私有方法和私有對象。都需要通過privateMethod.setAccessible(true)
獲取訪問權,對應私有方法就使用privateMethod.invoke(對象,形參)
方法來調用privateMethod;對應私有對象就使用`privateField.set(對象, 要修改成的值);
4.修改私有常量的方法需要在構造函數中對私有常量賦值纔可以,否則則無法對引用私有常量的方法起到作用。
5.由於泛型會被類型擦除,使用反射的type接口,可以來確定信息。同時,使用反射可以構建泛型數組。因爲通過Class commpentType = c1.getComponentType();
可以獲取到數組的元素類型,進而創建相應的數組Object newArray =newInstance(commpentType,length)
。只暫時讓數組爲Object,之後再轉換類型num = (int)Arrays.goodcopyOf(num,10);
。