面試官: 談談你對Java反射的理解


轉載:https://juejin.im/post/5dac4edff265da5b5f7588d1#heading-4
Java提供的反射機制允許你在運行時檢查類的信息

Java的類加載
Java在真正需要使用一個類時才會去加載類,而不是在啟動程序時就載入所有的類,因為大多數使用者都只使用到程序的部分資源,在需要某些功能時再載入某些資源,可以讓系統資源運用的更高效。

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在Jvm的方法區內,然后在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

Java 中的所有類型包括基本類型(int, long, float等等),即使是數組都有與之關聯的 Class 類的對象。
Class對象是由Jvm自動生成的,每當一個類被載入時,Jvm就自動為其生成一個Class對象

Class對象
實例.getClass()
通過Object的getClass()獲取每一個實例對應的Class對象

String name = "hello";
Class stringClass = name.getClass();
System.out.println("類的名稱:" + stringClass.getName());
System.out.println("是否為接口:" + stringClass.isInterface());
System.out.println("是否為基本類型:" + stringClass.isPrimitive());
System.out.println("是否為數組:" + stringClass.isArray());
System.out.println("父類名稱:" + stringClass.getSuperclass().getName());
復制代碼
類的名稱:java.lang.String
是否為接口:false
是否為基本類型:false
是否為數組:false
父類名稱:java.lang.Object
復制代碼
類名.class
你也可以直接使用一下方式來獲取String類的Class對象

Class stringClass = String.class;
復制代碼
Class.forName()
在一些應用中,你無法事先知道使用者將載入什么類別,你可以使用Class的靜態方法forName()來動態加載類別

Class c = Class.forName(args[0]);
System.out.println("類的名稱:" + c.getName());
System.out.println("是否為接口:" + c.isInterface());
System.out.println("是否為基本類型:" + c.isPrimitive());
System.out.println("是否為數組:" + c.isArray());
System.out.println("父類名稱:" + c.getSuperclass().getName());
復制代碼
$ java Demo1 java.util.Scanner
類的名稱:java.util.Scanner
是否為接口:false
是否為基本類型:false
是否為數組:false
父類名稱:java.lang.Object
復制代碼
Class.forName()有兩個版本,上面的版本只指定了全限定類名,而另一個版本可以讓你指定類名,載入時是否執行靜態代碼塊,執行類加載器(ClassLoader)

static Class forName(String name, boolean initialize, ClassLoader loader)
復制代碼
ClassLoader loader = Thread.currentThread().getContextClassLoader();
// Class.forName() 加載類 默認會執行初始化塊
Class.forName("Test2");
// Class.forName() 加載類 第二個參數 可以控制是否執行初始化塊
Class.forName("Test2", false, loader);

class Test2 {
static {
System.out.println("靜態初始化塊執行了!");
}
}
復制代碼
從Class對象中獲取信息
Class對象表示所載入的類別,獲取Class對象后,你就可以獲取類別相關的信息,入 package, constructor, field, method等信息。
而每一種信息,都有相對應的類別

package: java.lang.reflect.Package
constructor: java.lang.reflect.Constructor
field: java.lang.reflect.Field
method: java.lang.reflect.Method
Class c = Class.forName(args[0]);
System.out.println("包信息package:" + c.getPackage());
System.out.println("類修飾符modifier:" + c.getModifiers());
System.out.println("構造方法constructor:");
Arrays.stream(c.getDeclaredConstructors()).forEach(System.out::println);
System.out.println("成員變量fields:");
Arrays.stream(c.getDeclaredFields()).forEach(System.out::println);
復制代碼
$ java Demo1 java.util.ArrayList
包信息package:package java.util
類修飾符modifier:1
構造方法constructor:
public java.util.ArrayList(java.util.Collection)
public java.util.ArrayList()
public java.util.ArrayList(int)
成員變量fields:
private static final long java.util.ArrayList.serialVersionUID
private static final int java.util.ArrayList.DEFAULT_CAPACITY
private static final java.lang.Object[] java.util.ArrayList.EMPTY_ELEMENTDATA
private static final java.lang.Object[] java.util.ArrayList.DEFAULTCAPACITY_EMPTY_ELEMENTDATA
transient java.lang.Object[] java.util.ArrayList.elementData
private int java.util.ArrayList.size
private static final int java.util.ArrayList.MAX_ARRAY_SIZE
復制代碼
ClassLoader 類加載器
Java在需要使用類的時候才會將類載入,Java中類的載入是由Class Loader來實現的.

當你嘗試執行java xxx命令時,java會嘗試找到JRE的安裝目錄,然后尋找
jvm.dll,接着啟動JVM並進行初始化操作,接着產生
BootstrapLoader,Bootstrap Loader會載入Extended Loader, 並設定Extended Loader 的parent 為 BootstrapLoader, 接着Bootstrap Loader 會載入 Application Loader, 並將Application Loader 的parent 設定為
Extended Loader

啟動類加載器
BootstrapLoader搜尋 sun.boot.library.path中指定的類, 你可以使用
System.getProperty("sun.boot.library.path")來獲取

/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib
復制代碼
擴展類加載器
Extended Loader(sun.misc.Launcher$ExtClassLoader) 是由Java編寫的,會搜尋系統參數java.ext.dirs中指定的類別,可以通過System.getProperty("java.ext.dirs")來獲取

/Users/dsying/Library/Java/Extensions:
/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:
/Network/Library/Java/Extensions:
/System/Library/Java/Extensions:/usr/lib/java
復制代碼
應用程序類加載器
Application Loader
(sun.misc.Launcher$AppClassLoader) 是由Java編寫的,會搜尋系統參數java.class.path中指定的類別,可以通過System.getProperty("java.class.path")來獲取, 在使用java xxx命令執行.class字節碼文件時,可以通過-cp參數設定classpath

java –cp ./classes SomeClass
復制代碼
類加載器之間的關系
ClassLoader loader Thread.currentThread().getContextClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2 應用類加載器
System.out.println(loader);
// sun.misc.Launcher$ExtClassLoader@610455d6 擴展類加載器
System.out.println(loader.getParent());
// Bootstrap ClassLoader 啟動類加載器(用C語言實現,所以此處返回null)
System.out.println(loader.getParent().getParent());
復制代碼
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
復制代碼

類加載有三種方式:
命令行啟動應用時候由JVM初始化加載
通過Class.forName()方法動態加載
通過ClassLoader.loadClass()方法動態加載
Class.forName()和ClassLoader.loadClass()區別
Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
ClassLoader.loadClass():只干一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法采用調用構造函數,創建類的對象。
JVM類加載機制
全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效
雙親委派模型
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,只有當父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。

雙親委派機制:
當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試加載;
若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。
ClassLoader源碼分析:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判斷該類型是否已經被加載
Class c = findLoadedClass(name);
if (c == null) {
//如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載
try {
if (parent != null) {
//如果存在父類加載器,就委派給父類加載器加載
c = parent.loadClass(name, false);
} else {
//如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
復制代碼
雙親委派模型意義:

系統類防止內存中出現多份同樣的字節碼
保證Java程序安全穩定運行
自定義加載器
自定義類加載器一般都是繼承自ClassLoader類,從上面對loadClass方法來分析來看,我們只需要重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類加載器的流程:

package com.github.hcsp.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
// 存放字節碼文件的目錄
private final File bytecodeFileDirectory;

public MyClassLoader(File bytecodeFileDirectory) {
    this.bytecodeFileDirectory = bytecodeFileDirectory;
}

// 還記得類加載器是做什么的么?
// "從外部系統中,加載一個類的定義(即Class對象)"
// 請實現一個自定義的類加載器,將當前目錄中的字節碼文件加載成為Class對象
// 提示,一般來說,要實現自定義的類加載器,你需要覆蓋以下方法,完成:
//
// 1.如果類名對應的字節碼文件存在,則將它讀取成為字節數組
//   1.1 調用ClassLoader.defineClass()方法將字節數組轉化為Class對象
// 2.如果類名對應的字節碼文件不存在,則拋出ClassNotFoundException
//
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] classData = getByteArrayFromFile(name);
    if (classData == null) {
        throw new ClassNotFoundException();
    }
    return defineClass(name, classData, 0, classData.length);
}

byte[] getByteArrayFromFile(String className) throws ClassNotFoundException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    File file = new File(bytecodeFileDirectory, className + ".class");
    int len = 0;
    try {
        byte[] bufferSize = new byte[1024];
        FileInputStream fis = new FileInputStream(file);
        while ((len = fis.read(bufferSize)) != -1) {
            bos.write(bufferSize, 0, len);
        }
    } catch (FileNotFoundException e) {
        throw new ClassNotFoundException();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return bos.toByteArray();
}

public static void main(String[] args) throws Exception {
    File projectRoot = new File(System.getProperty("basedir", System.getProperty("user.dir")));
    MyClassLoader myClassLoader = new MyClassLoader(projectRoot);

    Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
    Object testClassInstance = testClass.getConstructor().newInstance();
    String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
    System.out.println(message);
}

}
復制代碼
自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。由於這里只是演示,我並未對class文件進行加密,因此沒有解密的過程.

其它
使用反射創建對象
你可以使用Class的newInstance()方法來實例化

Class c = Class.forName(className);
Object obj = c.newInstance();
復制代碼
調用方法
使用反射可以取回類中的方法,方法對應的類為
java.lang.reflect.Method, 你可以使用它的
invoke()方法來調用指定的方法

Class testClass = myClassLoader.loadClass("com.github.hcsp.MyTestClass");
Object testClassInstance = testClass.getConstructor().newInstance();
String message = (String) testClass.getMethod("sayHello").invoke(testClassInstance);
復制代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM