在上篇文章《一篇文章全面了解Java反射機制》中我們學習了Java反射機制的基本使用,留心的朋友可能已經注意到了,在文中提到了三種獲取Class對象的方法。
如果面試中涉及到Java反射,那么遇到該面試題的概率將大大增加。
以下三種獲取Class對象的方式有什么不同?
1、new Object().getClass 2、Object.class 3、 Class.forName("java.util.String")
本篇文章就通過實例帶大家來了解一下這三種獲取Class對象的區別。示例基於JDK8。
實例演示場景一
為了更好的演示,我們先創建一個對象Person,對象內部定義了一些靜態的方法。
public class Person {
static {
System.out.println("Person:靜態代碼塊");
}
{
System.out.println("Person:動態代碼塊");
}
public Person(){
System.out.println("Person:構造方法");
}
}
在Person對象中分別定義了:靜態代碼塊、動態代碼塊、構造方法。
針對上面的實例,我們構建了三個單元測試的場景,對應代碼如下:
public class GetClassTest {
@Test
public void test1(){
Class<?> clz = Person.class;
}
@Test
public void test2() throws ClassNotFoundException {
Class<?> clz = Class.forName("com.choupangxia.reflect.Person");
}
@Test
public void test3() {
Class<?> clz = new Person().getClass();
}
}
分別執行三個單元測試發現,第一個單元測試沒打印任何內容;第二個單元測試打印了“靜態方法”中的內容;第三個單元測試打印出了全部內容:
Person:靜態代碼塊
Person:動態代碼塊
Person:構造方法
也就是說通過Person.class的方法獲取對象的Class對象,根本不會調用對象中任何的代碼塊或代碼。而Class.forName()會調用靜態代碼塊的內容。
而第三種方式打印所有內容的原因很顯然,就因為要先實例化對象。
實例演示場景二
下面再組合一下這三種方式,看看一些其他的效果。
首先,依次調用三個獲取Class對象的方法:
@Test
public void test4() throws ClassNotFoundException {
Class<?> clz = Person.class;
System.out.println("---------------");
clz = Class.forName("com.choupangxia.reflect.Person");
System.out.println("---------------");
clz = new Person().getClass();
}
test4打印日志如下:
---------------
Person:靜態代碼塊
---------------
Person:動態代碼塊
Person:構造方法
通過日志說明,Class.forName()方法執行過靜態代碼塊之后,new Person().getClass()就不再會執行同樣的靜態代碼塊了。這也證明靜態代碼塊只會被初始化一次。
再調整組合第二種場景:
@Test
public void test5() throws ClassNotFoundException {
Class<?> clz = new Person().getClass();
System.out.println("---------------");
clz = Class.forName("com.choupangxia.reflect.Person");
}
test5打印日志如下:
Person:靜態代碼塊
Person:動態代碼塊
Person:構造方法
---------------
同樣只打印一次靜態代碼塊的操作。再次證明類中的靜態代碼塊只會被初始化一次。
實例演示場景三
這里,我們比較一下三種形式獲得的Class對象是否是相同的。測試代碼如下:
@Test
public void test6() throws ClassNotFoundException {
Class<?> clz1 = Person.class;
Class<?> clz2 = Class.forName("com.choupangxia.reflect.Person");
Class<?> clz3 = new Person().getClass();
System.out.println(clz1 == clz2);
System.out.println(clz2 == clz3);
}
注意,上面我們使用的是等號,也就是說比較的是引用。猜猜打印的結果?
true
true
三種形式獲得的Class對象是同一個對象。這是為什么呢?
這要涉及到類的加載過程,我們知道類加載過程分:加載階段、連接階段和初始化階段。
類的加載階段是將class文件中的二進制數據讀取到內存中,然后將該字節流所代表的靜態存儲結構轉化為方法區中運行時的數據結構,並且在堆內存中生成一個該類的java.lang.class對象,作為方法區數據結構的入口。
類加載階段的最終產物是堆內存中的class對象,對於同一個Classloader對象,不管某個類被加載多少次,對應堆內存中的class對象始終只有一個。
也就是說無論通過哪種形式來獲取Class對象,獲得的都是堆內存中對應的Class對象。
回顧三種形式
(1)類名.class:JVM將使用類裝載器,將類裝入內存(前提是:類還沒有裝入內存),不做類的初始化工作,返回Class的對象。
(2)Class.forName("類名字符串"):裝入類,並做類的靜態初始化,返回Class的對象。
(3)實例對象.getClass():對類進行靜態初始化、非靜態初始化;返回引用運行時真正所指的對象(子對象的引用會賦給父對象的引用變量中)所屬的類的Class的對象。