單元測試、反射 一、單元測試 1.1 單元測試快速入門 所謂單元測試,就是針對最小的功能單元,編寫測試代碼對其進行正確性測試。 我們想想,咱們之前是怎麼進行測試的呢? 比如說我們寫了一個學生管理系統,有添加學生、修改學生、刪除學生、查詢學生等這些功能。要對這些功能這幾個功能進行測試,我們是在main ...
一、單元測試
1.1 單元測試快速入門
我們想想,咱們之前是怎麼進行測試的呢?
比如說我們寫了一個學生管理系統,有添加學生、修改學生、刪除學生、查詢學生等這些功能。要對這些功能這幾個功能進行測試,我們是在main方法中編寫代碼來測試的。
但是在main方法中寫測試代碼有如下的幾個問題,如下圖所示:
為了測試更加方便,有一些第三方的公司或者組織提供了很好用的測試框架,給開發者使用。這裡給同學們介紹一種Junit測試框架。
Junit是第三方公司開源出來的,用於對代碼進行單元測試的工具(IDEA已經集成了junit框架)。相比於在main方法中測試有如下幾個優點。
我們知道單元測試是什麼之後,接下來帶領同學們使用一下。由於Junit是第三方提供的,所以我們需要把jar包導入到我們的項目中,才能使用,具體步驟如下圖所示:
接下來,我們就按照上面的步驟,來使用一下.
先準備一個類,假設寫了一個StringUtil工具類,代碼如下
public class StringUtil{ public static void printNumber(String name){ System.out.println("名字長度:"+name.length()); } }
接下來,寫一個測試類,測試StringUtil工具類中的方法能否正常使用。
public class StringUtilTest{ @Test public void testPrintNumber(){ StringUtil.printNumber("admin"); StringUtil.printNumber(null); } }
寫完代碼之後,我們會發現測試方法左邊,會有一個綠色的三角形按鈕。點擊這個按鈕,就可以運行測試方法。
1.2 單元測試斷言
接下來,我們學習一個單元測試的斷言機制。所謂斷言:意思是程式員可以預測程式的運行結果,檢查程式的運行結果是否與預期一致。
我們在StringUtil類中新增一個測試方法
public static int getMaxIndex(String data){ if(data == null){ return -1; } return data.length(); }
接下來,我們在StringUtilTest類中寫一個測試方法
public class StringUtilTest{ @Test public void testGetMaxIndex(){ int index1 = StringUtil.getMaxIndex(null); System.out.println(index1); int index2 = StringUtil.getMaxIndex("admin"); System.out.println(index2); //斷言機制:預測index2的結果 Assert.assertEquals("方法內部有Bug",4,index2); } }
運行測試方法,結果如下圖所示,表示我們預期值與實際值不一致
1.3 Junit框架的常用註解
同學們,剛纔我們以及學習了@Test註解,可以用來標記一個方法為測試方法,測試才能啟動執行。
除了@Test註解,還有一些其他的註解,我們要知道其他註解標記的方法什麼時候執行,以及其他註解在什麼場景下可以使用。
接下來,我們演示一下其他註解的使用。我們在StringUtilTest測試類中,再新增幾個測試方法。代碼如下
public class StringUtilTest{ @Before public void test1(){ System.out.println("--> test1 Before 執行了"); } @BeforeClass public static void test11(){ System.out.println("--> test11 BeforeClass 執行了"); } @After public void test2(){ System.out.println("--> test2 After 執行了"); } @AfterClass public static void test22(){ System.out.println("--> test22 AfterCalss 執行了"); } }
執行上面的測試類,結果如下圖所示,觀察執行結果特點如下
1.被
我們現在已經知道每一個註解的作用了,那他們有什麼用呢?應用場景在哪裡?
我們來看一個例子,假設我想在每個測試方法中使用Socket對象,並且用完之後,需要把Socket關閉。代碼就可以按照下麵的結構來設計
public class StringUtilTest{ private static Socket socket; @Before public void test1(){ System.out.println("--> test1 Before 執行了"); } @BeforeClass public static void test11(){ System.out.println("--> test11 BeforeClass 執行了"); //初始化Socket對象 socket = new Socket(); } @After public void test2(){ System.out.println("--> test2 After 執行了"); } @AfterCalss public static void test22(){ System.out.println("--> test22 AfterCalss 執行了"); //關閉Socket socket.close(); } }
二、反射
什麼是反射。其實API文檔中對反射有詳細的說明,我們去瞭解一下。在java.lang.reflect包中對反射的解釋如下圖所示
翻譯成人話就是:反射技術,指的是載入類的位元組碼到記憶體,並以編程的方法解刨出類中的各個成分(成員變數、方法、構造器等)。
反射有啥用呢?其實反射是用來寫框架用的,但是現階段同學們對框架還沒有太多感覺。為了方便理解,我給同學們看一個我們見過的例子:平時我們用IDEA開發程式時,用對象調用方法,IDEA會有代碼提示,idea會將這個對象能調用的方法都給你列舉出來,供你選擇,如果下圖所示
問題是IDEA怎麼知道這個對象有這些方法可以調用呢? 原因是對象能調用的方法全都來自於類,IDEA通過反射技術就可以獲取到類中有哪些方法,並且把方法的名稱以提示框的形式顯示出來,所以你能看到這些提示了。
為反射獲取的是類的信息,那麼反射的第一步首先獲取到類才行。由於Java的設計原則是萬物皆對象,獲取到的類其實也是以對象的形式體現的,叫位元組碼對象,用Class類來表示。獲取到位元組碼對象之後,再通過位元組碼對象就可以獲取到類的組成成分了,這些組成成分其實也是對象,其中每一個成員變數用Field類的對象來表示、每一個成員方法用Method類的對象來表示,每一個構造器用Constructor類的對象來表示。
如下圖所示:
1.1 獲取類的位元組碼
反射的第一步:是將位元組碼載入到記憶體,我們需要獲取到的位元組碼對象。
比如有一個Student類,獲取Student類的位元組碼代碼有三種寫法。不管用哪一種方式,獲取到的位元組碼對象其實是同一個。
public class Test01_Class { public static void main(String[] args) throws ClassNotFoundException { /* 位元組碼文件對象獲取三種方式 */ //1:根據類名.class Class c1 = Student.class; System.out.println(c1); System.out.println(c1.getName());// 獲取全類名 包名+類名 System.out.println(c1.getSimpleName());// 獲取簡單類名 //2:根據類的全路徑 (包名+類名)獲取 Class c2 = Class.forName("com.itheima.d2_reflect.Student"); // c1 c2 是不是一個對象? 類只載入一次!! System.out.println(c1==c2); //3:得到對象之後,通過對象的方法獲取 運行時類 Student stu = new Student(); Class c3= stu.getClass(); System.out.println(c1==c3); } }
1.2 獲取類的構造器
同學們,上一節我們已經可以獲取到類的位元組碼對象了。接下來,我們學習一下通過位元組碼對象獲取構造器,並使用構造器創建對象。
獲取構造器,需要用到Class類提供的幾個方法,如下圖所示:
想要快速記住這個方法的區別,給同學們說一下這些方法的命名規律,按照規律來記就很方便了。
get:獲取
Declared: 有這個單詞表示可以獲取任意一個,沒有這個單詞表示只能獲取一個public修飾的
Constructor: 構造方法的意思
尾碼s: 表示可以獲取多個,沒有尾碼s只能獲取一個
話不多少,上代碼。假設現在有一個Cat類,裡面有幾個構造方法,代碼如下
public class Cat{ private String name; private int age; public Cat(){ } private Cat(String name, int age){ } }
-
接下來,我們寫一個測試方法,來測試獲取類中所有的構造器
-
@Test public void testGetConstructors(){ // 反射第一步 獲取位元組碼對象 Class c= Cat.class; // 獲取 所有的 public 修飾的 構造器 // Constructor[] constructors = c.getConstructors(); // System.out.println(constructors.length); // for (Constructor constructor : constructors) { // System.out.println("構造器全稱:"+constructor); // System.out.println("構造器名稱:"+constructor.getName());//獲取構造器的名字 // System.out.println("參數個數:"+constructor.getParameterCount()); // } // 獲取 所有存在的 構造器 (無所謂修飾符) Constructor[] constructors = c.getDeclaredConstructors(); System.out.println("總共:"+constructors.length+"個構造器"); for (Constructor constructor : constructors) { System.out.println("構造器全稱:"+constructor); System.out.println("構造器名稱:"+constructor.getName());//獲取構造器的名字 System.out.println("參數個數:"+constructor.getParameterCount()); System.out.println("=============================="); } }
運行測試方法列印結果如下
-
剛纔演示的是獲取Cat類中所有的構造器,接下來,我們演示單個構造器試一試
-
//獲取指定的構造器 @Test public void testContructor() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // 獲取構造器(方法 ) 是不需要指定名字的 因為構造方法名字是類名!!! //第一步 獲取 位元組碼對象 Class c = Cat.class; //獲取 無參這個構造器 public Constructor constructor1 = c.getConstructor(); //列印構造器 相關信息 System.out.println("構造器全稱:"+constructor1); System.out.println("構造器名稱:"+constructor1.getName());//獲取構造器的名字 System.out.println("參數個數:"+constructor1.getParameterCount()); Cat cat1 = (Cat)constructor1.newInstance(); System.out.println("查看對象:"+cat1); System.out.println("=============================="); // String類型?String.class //獲取兩個參數的構造器 忽略修飾符 Constructor constructor2 = c.getDeclaredConstructor(String.class, int.class); //() 參數的順序和類型來匹配指定的構造器 System.out.println("構造器全稱:"+constructor2); System.out.println("構造器名稱:"+constructor2.getName());//獲取構造器的名字 System.out.println("參數個數:"+constructor2.getParameterCount()); //針對私有的成員 需要繞過許可權訪問 constructor2.setAccessible(true); Cat cat2 =(Cat)constructor2.newInstance("小花",3);//參數 倆 第一個參數String,第二個是int System.out.println(cat2); }
列印結果如下
1.3 反射獲取構造器的作用
同學們,剛纔上一節我們已經獲取到了Cat類中的構造器。獲取到構造器後,有什麼作用呢?
其實構造器的作用:初始化對象並返回。
這裡我們需要用到如下的兩個方法,註意:這兩個方法時屬於Constructor的,需要用Constructor對象來調用。
如下圖所示,constructor1和constructor2分別表示Cat類中的兩個構造器。現在我要把這兩個構造器執行起來
由於構造器是private修飾的,先需要調用setAccessible(true)
表示禁止檢查訪問控制,然後再調用newInstance(實參列表)
就可以執行構造器,完成對象的初始化了。
代碼如下:為了看到構造器真的執行, 故意在兩個構造器中分別加了兩個列印語句
代碼的執行結果如下圖所示:
1.4 反射獲取成員變數&使用
同學們,上一節我們已經學習了獲取類的構造方法並使用。接下來,我們再學習獲取類的成員變數,並使用。
其實套路是一樣的,在Class類中提供了獲取成員變數的方法,如下圖所示。
這些方法的記憶規則,如下
get:獲取
Declared: 有這個單詞表示可以獲取任意一個,沒有這個單詞表示只能獲取一個public修飾的
Field: 成員變數的意思
尾碼s: 表示可以獲取多個,沒有尾碼s只能獲取一個
-
假設有一個Cat類它有若幹個成員變數,用Class類提供 的方法將成員變數的對象獲取出來。
執行完上面的代碼之後,我們可以看到控制臺上列印輸出了,每一個成員變數的名稱和它的類型。
public class Test03_Field { @Test public void getFields() throws NoSuchFieldException, IllegalAccessException { //獲取所有的屬性 // 1:獲取位元組碼對象 Class c = Cat.class; //2:獲取所有的成員屬性 Field[] fields = c.getDeclaredFields(); //3:遍歷得到每個成員屬性的 對象形式 for (Field field : fields) { System.out.println("全稱:"+field+" 查小名:"+field.getName()+" 屬性的類型:"+field.getType()); } System.out.println("========================="); // 需求 對某個對象的某個屬性完成 賦值 取值 Cat cat = new Cat(); // 給cat對象的 name 屬性 賦值 "加菲貓" // 給cat對象的 age 屬性 賦值 3 // 通過反射方式 找到指定的屬性對象 Field fName = c.getDeclaredField("name"); Field fAge= c.getDeclaredField("age"); // 只要是在反射形式調用 可以繞過許可權檢查 fName.setAccessible(true); fAge.setAccessible(true); // 完成 賦值 // fName fAge 屬性的對象形式 fName.set(cat,"加菲貓"); fAge.set(cat,3); System.out.println(cat); // 獲取到 指定對象的指定屬性的值? cat.getName() cat.name //屬性對象反向調用 String name = (String) fName.get(cat);//cat.name System.out.println(name); } }
-
獲取到成員變數的對象之後該如何使用呢?
在Filed類中提供給給成員變數賦值和獲取值的方法,如下圖所示。
再次強調一下設置值、獲取值的方法時Filed類的需要用Filed類的對象來調用,而且不管是設置值、還是獲取值,都需要依賴於該變數所屬的對象。代碼如下
執行代碼,控制台會有如下的列印
1.5 反射獲取成員方法
各位同學,上面幾節我們已經學習了反射獲取構造方法、反射獲取成員變數,還剩下最後一個就是反射獲取成員方法並使用了。
在Java中反射包中,每一個成員方法用Method對象來表示,通過Class類提供的方法可以獲取類中的成員方法對象。如下下圖所示
接下來我們還是用代碼演示一下:假設有一個Cat類,在Cat類中紅有若幹個成員方法
public class Cat{ private String name; private int age; public Cat(){ System.out.println("空參數構造方法執行了"); } private Cat(String name, int age){ System.out.println("有參數構造方法執行了"); this.name=name; this.age=age; } private void run(){ System.out.println("(>^ω^<)喵跑得賊快~~"); } public void eat(){ System.out.println("(>^ω^<)喵愛吃貓糧~"); } private String eat(String name){ return "(>^ω^<)喵愛吃:"+name; } public void setName(String name){ this.name=name; } public String getName(){ return name; } public void setAge(int age){ this.age=age; } public int getAge(){ return age; } }
接下來,通過反射獲取Cat類中所有的成員方法,每一個成員方法都是一個Method對象
package com.itheima.d2_reflect; import org.junit.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Test04_Method { @Test public void testGetMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { //1:獲取位元組碼對象 Class c = Cat.class; //2:獲取所有的方法 Method[] methods = c.getDeclaredMethods(); //3:遍歷 for (Method method : methods) { System.out.println("全稱:"+method+" 簡稱:"+method.getName()+" 返回值類型:"+method.getReturnType() +" 參數個數:"+method.getParameterCount()); } System.out.println("=============="); //獲取指定的方法 // run Method run = c.getDeclaredMethod("run");// 因為類中 是這麼區分方法的 方法名和參數列表 System.out.println(" 簡稱:"+run.getName()+" 返回值類型:"+run.getReturnType() +" 參數個數:"+run.getParameterCount()); Method eat = c.getDeclaredMethod("eat",String.class);// 因為類中 是這麼區分方法的 方法名和參數列表 System.out.println(" 簡稱:"+eat.getName()+" 返回值類型:"+eat.getReturnType() +" 參數個數:"+eat.getParameterCount()); } }
執行上面的代碼,運行結果如下圖所示:列印輸出每一個成員方法的名稱、參數格式、返回值類型
也能獲取單個指定的成員方法,如下圖所示
獲取到成員方法之後,有什麼作用呢?
在Method類中提供了方法,可以將方法自己執行起來。
下麵我們演示一下,把run()
方法和eat(String name)
方法執行起來。看分割線之下的代碼
package com.itheima.d2_reflect; import org.junit.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Test04_Method { @Test public void testGetMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { //1:獲取位元組碼對象 Class c = Cat.class; //2:獲取所有的方法 Method[] methods = c.getDeclaredMethods(); //3:遍歷 for (Method method : methods) { System.out.println("全稱:"+method+" 簡稱:"+method.getName()+" 返回值類型:"+method.getReturnType() +" 參數個數:"+method.getParameterCount()); } System.out.println("=============="); //獲取指定的方法 // run Method run = c.getDeclaredMethod("run");// 因為類中 是這麼區分方法的 方法名和參數列表 System.out.println(" 簡稱:"+run.getName()+" 返回值類型:"+run.getReturnType() +" 參數個數:"+run.getParameterCount()); Method eat = c.getDeclaredMethod("eat",String.class);// 因為類中 是這麼區分方法的 方法名和參數列表 System.out.println(" 簡稱:"+eat.getName()+" 返回值類型:"+eat.getReturnType() +" 參數個數:"+eat.getParameterCount()); Cat cat = new Cat(); // 方法對象是主體 由方法對象完成調用 // run.invoke(cat,實際參數); run.setAccessible(true); //繞過許可權檢查 Object returnValue1 = run.invoke(cat);//run方法執行 // run方法被 cat 對象調用執行 System.out.println("run方法調用之後的返回值:"+returnValue1); eat.setAccessible(true); Object returnValue2 = eat.invoke(cat,"魚兒");//eat方法執行 System.out.println("eat方法調用之後的返回值:"+returnValue2); } }
列印結果如下圖所示:run()方法執行後列印貓跑得賊快~~
,返回null
; eat()方法執行完,直接返回貓最愛吃:魚兒
1.6 反射的應用
各位小伙伴,按照前面我們學習反射的套路,我們已經充分認識了什麼是反射,以及反射的核心作用是用來獲取類的各個組成部分並執行他們。但是由於同學們的經驗有限,對於反射的具體應用場景還是很難感受到的(這個目前沒有太好的辦法,只能慢慢積累,等經驗積累到一定程度,就會豁然開朗了)。
我們一直說反射使用來寫框架的,接下來,我們就寫一個簡易的框架,簡單窺探一下反射的應用。反射其實是非常強大的,這個案例也僅僅值小試牛刀。
需求是讓我們寫一個框架,能夠將任意一個對象的屬性名和屬性值寫到文件中去。不管這個對象有多少個屬性,也不管這個對象的屬性名是否相同。
分析一下該怎麼做
1.先寫好兩個類,一個Student類和Teacher類
2.寫一個ObjectFrame類代表框本架
在ObjectFrame類中定義一個saveObject(Object obj)方法,用於將任意對象存到文件中去
參數:Object obj: 就表示要存入文件中的對象
3.編寫方法內部的代碼,往文件中存儲對象的屬性名和屬性值
1)參數obj對象中有哪些屬性,屬性名是什麼實現值是什麼,中有對象自己最清楚。
2)接著就通過反射獲取類的成員變數信息了(變數名、變數值)
3)把變數名和變數值寫到文件中去
寫一個ObjectFrame表示自己設計的框架,代碼如下圖所示
public class ObjectFrame { /** * 告訴對象 我幫你解剖 * 屬性 屬性值解析出來 並寫到指定文件中。 * 1: 根據傳遞的對象 即系 屬性名 和屬性值 * 2: 通過流的形式把內容寫到文件中 * 方法三要素: 方法名 參數列表 返回值類型 */ public static void saveTxt(Object obj) throws IllegalAccessException, FileNotFoundException { //已知條件 obj對象 PrintStream ps = new PrintStream(