看了Java的Class的原始碼,我自閉了

chenweicool發表於2020-06-20

java原始碼之Class

​ 原始碼的重要性不言而喻,雖然枯燥,但是也有拍案叫絕。這是我的原始碼系列第二彈,後續還會一直更新,歡迎交流。String原始碼可以看我的Java原始碼之String,如有不足,希望指正。

1.class這個類是什麼

Class的本質也是一個類,只不過它是將我們定義類的共同的部分進行抽象,比如我們常定義的類都含有構造方法,類變數,函式,而Class這個類就是來操作這些屬性和方法的。當然我們常定義的類包含的型別都可以通過Class間接的來操作。而類的型別包含一般的類,介面,列舉型別,註解型別等等。這麼說可能有點太理論,我們看下面這個例子:

我們將生活中的一類事物抽象為一個類的時候,往往是因為他們具有相同的共性和不同的個性。定義一個類的作用就是將相同的共性抽離出來。一般的類都包含屬性和方法(行為),下面我們定義水果和汽車這兩個大類:

程式碼如下:

汽車類:

class Car{

    // 定義屬性
    private String name;
    private String color;

    /**
     * 定義兩個構造方法
     */
    public Car(){

    }

    public Car(String name,String color){
        this.name = name;
        this.color = color;
    }

    /**
     * 定義兩個普通方法(行為)
     */
    public void use(){
        
    }
    
    public void run(){
        
    }

    /**
     * 屬性的get和set方法
     * @return
     */
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
         this.color = color;
    }
}


水果類:

class Fruit{

    // 定義屬性
    private String name;
    private int size;

    /**
     * 定義兩個構造方法
     */
    public Fruit(){

    }

    public Fruit(String name,int size){
        this.name = name;
        this.size =size;
    }

    /**
     * 定義兩個方法(行為)
     */
    public void use(){
        
    }
    
    public void doFruit(){
        
    }

    /**
     * 屬性的get和set方法
     * @return
     */
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }
}

可以看到水果和汽車這兩個類都有共同的部分,也就是一個類共同的部分,那就是屬性和方法,而Class就是來操作我們定義類的屬性和方法。

​小試牛刀:通過Class這個類來獲取Fruit這個類中定義的方法;

public static void main(String[] args) {

        Fruit fruit = new Fruit();
        Class fruitClass = fruit.getClass();

        Method[] fruitMethods = fruitClass.getMethods();
        System.out.println("方法個數:" + fruitMethods.length);

        for (Method method : fruitMethods) {
            //得到返回型別
            System.out.print("方法名稱和引數:" + method.getName() + "(");
            //取得某個方法對應的引數型別陣列
            Class[] paramsType = method.getParameterTypes();
            for (Class paramType : paramsType) {
                System.out.print(paramType.getTypeName() + " ");
            }
            System.out.print(")");

            Class returnType = method.getReturnType();
            System.out.println("返回型別:" + returnType.getTypeName());
        }
    }

執行結果:

方法個數:15
方法名稱和引數:getName()返回型別:java.lang.String
方法名稱和引數:setName(java.lang.String )返回型別:void
方法名稱和引數:getSize()返回型別:int
方法名稱和引數:setSize(int )返回型別:void
方法名稱和引數:use()返回型別:void
方法名稱和引數:doFruit()返回型別:void
方法名稱和引數:wait()返回型別:void
方法名稱和引數:wait(long int )返回型別:void
方法名稱和引數:wait(long )返回型別:void
方法名稱和引數:equals(java.lang.Object )返回型別:boolean
方法名稱和引數:toString()返回型別:java.lang.String
方法名稱和引數:hashCode()返回型別:int
方法名稱和引數:getClass()返回型別:java.lang.Class
方法名稱和引數:notify()返回型別:void
方法名稱和引數:notifyAll()返回型別:void

這裡可能有人疑惑了,Fruit類並沒有定義的方法為什麼會出現,如wait(),equals()方法等。這裡就有必要說一下java的繼承和反射機制。在繼承時,java規定每個類預設繼承Object這個類,上述這些並沒有在Fruit中定義的方法,都是Object中的方法,我們看一下Object這個類的原始碼就會一清二楚:

 public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

 public final native void wait(long timeout) throws InterruptedException;

 public final void wait() throws InterruptedException {
        wait(0);
    }

而Class類中的getMethods()方法預設會獲取父類中的公有方法,也就是public修飾的方法。所以Object中的公共方法也出現了。

注: 要想獲得父類的所有方法(public、protected、default、private),可以使用apache commons包下的FieldUtils.getAllFields()可以獲取類和父類的所有(public、protected、default、private)屬性。

是不是感覺非常的強大 ,當然,使用Class來獲取一些類的方法和屬性的核心思想就是利用了Java反射特性。萬物皆反射,可見反射的強大之處,至於反射的原理,期待我的下一個部落格。

2.常用方法的使用以及原始碼分析

2.1構造方法

原始碼如下:

 private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

可以看到Class類只有一個建構函式,並且是私有的。也就是說不能通過new來建立這個類的例項。官方文件的解釋:私有建構函式,僅Java虛擬機器建立Class物件。我想可能就是為了安全,具體原因不是很瞭解。如果有了解的話,可以在評論區內共同的交流。

Class是怎麼獲取一個例項的。

那麼既然這個class構造器私有化,那我們該如何去構造一個class例項呢,一般採用下面三種方式:

1.運用.class的方式來獲取Class例項。對於基本資料型別的封裝類,還可以採用.TYPE來獲取相對應的基本資料型別的Class例項,如下的示例。

 // 普通類獲取Class的例項。介面,列舉,註解,都可以通過這樣的方式進行獲得Class例項
Class fruitClass = Fruit.class;

// 基本型別和封裝型別獲得Class例項的方式,兩者等效的
Class intClass = int.class;
Class intClass1 = Integer.TYPE;

下面的表格兩邊等價:

boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

但是這種方式有一個不足就是對於未知的類,或者說不可見的類是不能獲取到其Class物件的。

2.利用物件.getClass()方法獲取該物件的Class例項;

這是利用了Object提供的一個方法getClass() 來獲取當著例項的Class物件,這種方式是開發中用的最多的方式,同樣,它也不能獲取到未知的類,比如說某個介面的實現類的Class物件。

Object類中的getClass()的原始碼如下:

public final native Class<?> getClass();

原始碼說明:

可以看到,這是一個native方法(一個Native Method就是一個java呼叫非java程式碼的介面),並且不允許子類重寫,所以理論上所有型別的例項都具有同一個 getClass 方法。

使用:

 Fruit fruit = new Fruit();
 Class fruitClass = fruit.getClass();

3.使用Class類的靜態方法forName(),用類的名字獲取一個Class例項(static Class forName(String className) ),這種方式靈活性最高,根據類的字串全名即可獲取Class例項,可以動態載入類,框架設計經常用到;

原始碼如下:

    /*
     由於方法區 Class 型別資訊由類載入器和類全限定名唯一確定,所以引數name必須是全限定名,
     引數說明   name:class名,initialize是否載入static塊,loader 類載入器
     */
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        
        // 1.進行安全檢查
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
           ....
            }
        }
        // 2.呼叫本地的方法
        return forName0(name, initialize, loader, caller);
    }
   
    // 3.核心的方法
    private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
      throws ClassNotFoundException;

   /* 
    這個 forName是上述方法的過載,平時一般都使用這個 方法預設使用呼叫者的類載入器,將類的.class檔案載入     到 jvm中
    這裡傳入的initialize為true,會去執行類中的static塊
    */
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

原始碼說明已在註釋中說明,有些人會疑惑, static native Class<?> forName0()這個方法的實現。

這就要說到java的不完美的地方了,Java的不足除了體現在執行速度上要比傳統的C++慢許多之外,Java無法直接訪問到作業系統底層(如系統硬體等),為此Java使用native方法來擴充套件Java程式的功能。有關native的方法請移步這裡

基本使用:

 Class fruitClass = Class.forName("cn.chen.test.util.lang.Fruit");

: 這種方式必須使用類的全限定名,,這是因為由於方法區 Class 型別資訊由類載入器和類全限定名唯一確定,否則會丟擲ClassNotFoundException的異常。

2.2一般方法以及原始碼分析:

Class類的一般的方法總共有六十多種,其實看到這麼多方法我們也不要慫,這裡面還有很多過載的方法,根據二八原則,我們平時用的也就那麼幾個方法,所以這裡只對以下幾個方法的使用和實現進行交流,其他的方法可以移步Java官方文件

2.2.1 獲得類的構造方法

這個方法主要是用來了解一個類的構造方法有哪些,包含那些引數,特別是在單例的模式下。一般包含的方法如下:

  • public Constructor[] getConstructors() :獲取類物件的所有可見的建構函式

  • public Constructor[] getDeclaredConstructors():獲取類物件的所有的建構函式

  • public Constructor getConstructor(Class... parameterTypes): 獲取指定的可見的建構函式,引數為:指定建構函式的引數型別陣列,如果該建構函式不可見或不存在,會丟擲 NoSuchMethodException 異常

  • public Constructor getDeclaredConstructor(Class... parameterTypes) :獲取指定的建構函式,引數為:指定建構函式的引數型別陣列,無論建構函式可見性如何,均可獲取

基本使用:

Constructor[] constructors = fruitClass.getConstructors();
 for (Constructor constructor : constructors) {
            System.out.println("獲得共有的構造方法:"+constructor);
        }

輸出結果:

獲得共有的構造方法:public cn.chen.test.util.lang.Fruit()
獲得共有的構造方法:public cn.chen.test.util.lang.Fruit(java.lang.String,int)

可以看到我們前面定義的來個構造方法,都被列印出來了。注意getConstructors()只能獲得被public修飾的構造方法,如果要獲得被(protected,default,private)修飾的構造方法,就要使用的getDeclaredConstructors()這個方法了。接下來,修改Fruit中的一個構造方法為private:

 private  Fruit(String name,int size){
        this.name = name;
        this.size =size;
    }

使用getConstructors()和getDeclaredConstructors()著兩個方法進行測試:

       Class fruitClass = Fruit.class;       
       Constructor[] constructors = fruitClass.getConstructors();
       Constructor[] constructors1 = fruitClass.getDeclaredConstructors();

        for (Constructor constructor : constructors) {
            System.out.println("獲得共有的構造方法:"+constructor);
        }

        System.out.println("=================================================");
        for (Constructor constructor : constructors1) {
            System.out.println("獲得所有的構造方法:"+constructor);
        }

輸出結果:

獲得共有的構造方法:public cn.chen.test.util.lang.Fruit()
===================分隔線=============================
獲得所有的構造方法:public cn.chen.test.util.lang.Fruit()
獲得所有的構造方法:private cn.chen.test.util.lang.Fruit(java.lang.String,int)

可以看到兩者的區別。所以,反射在一定程度上破壞了java的封裝特性。畢竟人無完人,語言亦是一樣。

getConstructors()的原始碼分析:

public Constructor<?>[] getConstructors() throws SecurityException {
          
        // 1.檢查是否允許訪問。如果訪問被拒絕,則丟擲SecurityException。
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        return copyConstructors(privateGetDeclaredConstructors(true));
    }
    
 private static <U> Constructor<U>[] copyConstructors(Constructor<U>[] arg) {
      // 2.使用克隆,得到當前類的所有建構函式   
      Constructor<U>[] out = arg.clone();
     // 3.使用ReflectionFactory構造一個物件,也是不使用構造方法構造物件的一種方式。
        ReflectionFactory fact = getReflectionFactory();
     // 4.遍歷,將建構函式進行拷貝返回,注意在呼叫fact.copyConstructor(out[i])這個方法的時候,還會進行安全檢查,用的就是下面的LangReflectAccess() 這個方法。
        for (int i = 0; i < out.length; i++) {
            out[i] = fact.copyConstructor(out[i]);
        }
        return out;
    }



 private static LangReflectAccess langReflectAccess() {
        if (langReflectAccess == null) {
            Modifier.isPublic(1);
        }

        return langReflectAccess;
    } 

通過打斷點除錯,可以看到下面的資訊:

程式碼的呼叫邏輯在註釋裡已進行說明。

2.2.2 獲得屬性

主要獲取類的屬性欄位,瞭解這個類宣告瞭那些欄位。

一般有四個方法:

  • public Field[] getFields():獲取所有可見的欄位資訊,Field陣列為類中宣告的每一個欄位儲存一個Field 例項
  • public Field[] getDeclaredFields():獲取所有的欄位資訊
  • public Field getField(String name) :通過欄位名稱獲取字元資訊,該欄位必須可見,否則丟擲異常
  • public Field getDeclaredField(String name) :通過欄位名稱獲取可見的字元資訊

基本使用:

首先我們在Fruit的類中加入一個public修飾的屬性:

    public double weight;
Class fruitClass = Fruit.class; 
Field[] field2 = fruitClass.getFields();
        for (Field field : field2) {
            System.out.println("定義的公有屬性:"+field);
        }

        Field[] fields = fruitClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println("定義的所有屬性:"+field);
        }

輸出結果:

定義的公有屬性:public double cn.chen.test.util.lang.Fruit.weight
========================分隔線============================
定義的所有屬性:private java.lang.String cn.chen.test.util.lang.Fruit.name
定義的所有屬性:private int cn.chen.test.util.lang.Fruit.size
定義的所有屬性:public double cn.chen.test.util.lang.Fruit.weight

原始碼分析,就以getFileds()這個方法為例,涉及以下幾個方法:

public Field[] getFields() throws SecurityException {
        // 1.檢查是否允許訪問。如果訪問被拒絕,則丟擲SecurityException。
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
        return copyFields(privateGetPublicFields(null));
    }
 
 private static Field[] copyFields(Field[] arg) {
         // 2. 宣告一個Filed的陣列,用來儲存類的欄位 
        Field[] out = new Field[arg.length];
        //  3.使用ReflectionFactory構造一個物件,也是不使用構造方法構造物件的一種方式。
        ReflectionFactory fact = getReflectionFactory();
       // 4.遍歷,將欄位複製後返回。
        for (int i = 0; i < arg.length; i++) {
            out[i] = fact.copyField(arg[i]);
        }
        return out;
    }
    
 public Field copyField(Field var1) {
        return langReflectAccess().copyField(var1);
    }
 
// 再次檢查屬性的訪問許可權
  private static LangReflectAccess langReflectAccess() {
        if (langReflectAccess == null) {
            Modifier.isPublic(1);
        }

        return langReflectAccess;
    }

2.2.3 獲得一般方法

就是獲取一個類中的方法,一般有以下幾個方法:

  • public Method[] getMethods(): 獲取所有可見的方法

  • public Method[] getDeclaredMethods() :獲取所有的方法,無論是否可見

  • public Method getMethod(String name, Class... parameterTypes)

    引數說明:

  1. 通過方法名稱、引數型別獲取方法
  2. 如果你想訪問的方法不可見,會丟擲異常
  3. 如果你想訪問的方法沒有引數,傳遞 null作為引數型別陣列,或者不傳值)
  • public Method getDeclaredMethod(String name, Class... parameterTypes)
  1. 通過方法名稱、引數型別獲取方法
  2. 如果你想訪問的方法沒有引數,傳遞 null作為引數型別陣列,或者不傳值)

基本使用:

//在fruit中定義一個這樣的方法
 private  void eat(String describe){
        System.out.println("通過getMethod()方法呼叫了eat()方法:  "+describe);
    }

呼叫這個方法:

        Class fruitClass = Fruit.class;
        Method method = fruitClass.getDeclaredMethod("eat",String.class);
        method.setAccessible(true);
        method.invoke(fruitClass.newInstance(),"我是該方法的引數值");

輸出結果:

  通過getMethod()方法呼叫了eat()方法:我是該方法的引數值

分析getDeclaredMethod()涉及的原始碼:

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        // 1.檢查方法的修飾符
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        // 2.searchMethods()方法的第一個引數確定這個方法是不是私有方法,第二個引數我們定義的方法名,第三個引數就是傳入的方法的引數型別
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }

// 這個方法就是通過傳入的方法名找到我們定義的方法,然後使用了Method的copy()方法返回一個Method的例項,我們通過操作mehtod這個例項就可以操作我們定義的方法。
 private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (m.getName() == internedName
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }

        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }

 public Method copyMethod(Method var1) {
        return langReflectAccess().copyMethod(var1);
    }

 
// 檢查屬性的訪問許可權
  private static LangReflectAccess langReflectAccess() {
        if (langReflectAccess == null) {
            Modifier.isPublic(1);
        }

        return langReflectAccess;
    }

2.2.4 判斷類的型別的方法

這型別的方法顧名思義,就是來判斷這個類是什麼型別,是介面,註解,列舉,還是一般的類等等。部分方法如下表

boolean isAnnotation()判斷是不是註解
boolean isArray() 判斷是否為陣列
boolean isEnum()判斷是否為列舉型別
boolean isInterface() 是否為介面型別
boolean isMemberClass()當且僅當基礎類是成員類時,返回“true”
boolean isPrimitive()確定指定的“類”物件是否表示原始型別。
boolean isSynthetic()如果這個類是合成類,則返回' true ';否則返回“false”。

基本用法:

// 定義一個介面:
interface  Animal{
    public void run();
}

判斷是不是一個介面:

Class AnimalClass = Animal.class;
 boolean flag = AnimalClass.isInterface();
 System.out.println(flag);

輸出結果:

true

原始碼分析isInterface():

 public native boolean isInterface();

這是一個native方法,大家都知道native方法是非Java語言實現的程式碼,供Java程式呼叫的,因為Java程式是執行在JVM虛擬機器上面的,要想訪問到比較底層的與作業系統相關的就沒辦法了,只能由靠近作業系統的語言來實現。

2.2.5 toString()方法

將物件轉換為字串。字串表示形式是字串“類”或“介面”,後跟一個空格,然後是該類的全限定名。

基本使用:

// 這是前面定義的兩個類Fruit和Car,Car是一個介面
 Class fruitClass = Fruit.class;
 Class AnimalClass = Animal.class;
 System.out.println(AnimalClass.toString());
 System.out.println(fruitClass.toString());

輸出結果:

// 格式  字串“類”或“介面”,後跟一個空格,然後是該類的全限定名
interface cn.chen.test.util.lang.Animal
class cn.chen.test.util.lang.Fruit

原始碼如下:

 public String toString() {
       // 先是判斷是介面或者類,然後呼叫getName輸出類的全限定名
        return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
            + getName();
    }

  public native boolean isInterface();
  public native boolean isPrimitive();

追本溯源,方能闊步前行。

參考資料

https://blog.csdn.net/x_panda/article/details/17120479

https://juejin.im/post/5d4450fbe51d4561ce5a1be1

JavaSE的官方文件

相關文章