Java动态性有:反射机制,动态编译/代理,字节码操作。常见的是反射和字节码操作。


Java让我们在运行时识别对象和类的信息,主要有2种方式:一种是传统的RTTI,它假定我们在编译时已经知道了所有的类型信息;另一种是反射机制,它允许我们在运行时发现和使用类的信息。


类的生命周期

Java——动态性、反射机制、类加载、动态编译、脚本引擎、字节码操作....._java

类加载初始化阶段,必须对类进行初始化的情况:

1、使用new关键字实例化对象时、读取或者设置一个类的静态字段(除final常量)以及调用静态方法的时候。

2、使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。

3、当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。

4、当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类

5、当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时。

不会被初始化的情况:

    当访问静态域,只有真正声明这个域的类菜会被初始化。

        通过子类引用父类的静态变量,不会导致子类初始化。

    通过数组定义引用类,不会触发此类初始化

    引用常量不会触发此类的初始化。


Class对象

Class类的对象作用是运行时提供或获得某个对象的类型信息。

  理解RTTI在Java中的工作原理,首先需要知道类型信息在运行时是如何表示的,这是由Class对象来完成的,它包含了与类有关的信息。Class对象就是用来创建所有“常规”对象的,Java使用Class对象来执行RTTI,即使你正在执行的是类似类型转换这样的操作。


  每个类都会产生一个对应的Class对象,也就是保存在.class文件。

比如创建一个Shapes类,编译Shapes类后就会创建其包含Shapes类相关类型信息的Class对象,并保存在Shapes.class字节码文件中。

所有类都是在对其第一次使用时,动态加载到JVM的,当程序创建一个对类的静态成员的引用时,就会加载这个类。Class对象仅在需要的时候才会加载,static初始化是在类加载时进行的。


类加载器首先会检查这个类的Class对象是否已被加载过,如果尚未加载,默认的类加载器就会根据类名查找对应的.class文件。


  想在运行时使用类型信息,必须获取对象的Class对象的引用,使用功能Class.forName(“Base”)可以实现该目的,或者使用base.class。

注:使用.class来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象。为了使用类而做的准备工作一般有以下3个步骤:

加载:由类加载器完成,找到对应的字节码,创建一个Class对象

链接:验证类中的字节码,为静态域分配空间

初始化:如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块

获取Class类的四种方式


1.调用运行时类本身的.class属性

Class clazz = String.class;


2.通过运行时类的对象获取

Person p = new Person();

Class clazz = p.getClass();


3.通过Class的静态方法获取:体现反射的动态性

Class clazz = Class.forName("com.util.xxx");


4.通过类的加载器

ClassLoader classLoader = this.getClass().getClassLoader();

Class clazz = classLoader.loadClass("com.util.xxx");

这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.


反射

     Class类和java.lang.reflect类库一起对反射进行了支持,该类库包含Field、Method和Constructor类,这些类的对象由JVM在启动时创建,以表示未知类里对应的成员。这就就可以使用Contructor创建新的对象,用get()和set()方法获取和修改类中与Field对象关联的字段,用invoke()方法调用与Method对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()等许多便利的方法,以返回表示字段、方法、以及构造器对象的数组,这样,对象信息可以在运行时被完全确定下来,而在编译时不需要知道关于类的任何事情。


  通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类。因此,那个类的.class对于JVM来说必须是可获取的,要么在本地机器上,要么从网络获取。所以对于RTTI和反射之间的真正区别只在于:

RTTI:编译器在编译时打开和检查.class文件

反射:运行时打开和检查.class文件


instanceof 关键字与isInstance方法


        if(obj instanceof Animal){

            Animal animal= (Animal) obj;

        }

//isInstance方法则是Class类中的一个Native方法

        if(Animal.class.isInstance(obj)){

            Animal animal= (Animal) obj;

        }


参考:https://blog.csdn.net/javazejian/article/details/70768369


动态编译

就是运行时动态生成java代码, JAVA 6.0引入了动态编译机制。

• 动态编译的应用场景

比如浏览器端编写java代码,上传服务器编译和运行。

服务器动态加载某些类文件进行编译。

• 动态编译的两种做法:

通过Runtime调用javac,启动新的进程去操作,这是一种模拟操作。

Runtime run = Runtime.getRuntime();

Process process = run.exec("javac -cp d:/myjava/ HelloWorld.java");

通过JavaCompiler动态编译

public static int compileFile(String sourceFile){

// 动态编译

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

int result = compiler.run(null, null, null,sourceFile);

System.out.println(result==0?" 编译成功 ":" 编译失败 ");

return result;

}

第一个参数: 为java编译器提供参数

第二个参数: 得到 Java 编译器的输出信息

第三个参数: 接收编译器的 错误信息

第四个参数: 可变参数(是一个String数组)能传入一个或多个 Java 源文件

返回值: 0表示编译成功,非0表示编译失败


动态编译运行好的类:

1、通过Runtime.getRuntime() 运行启动新的进程运行

Runtime run = Runtime.getRuntime();

Process process = run.exec("java -cp d:/myjava HelloWorld");

// Process process = run.exec("java -cp "+dir+" "+classFile);

2、通过反射运行编译好的类

public static void runJavaClassByReflect(String dir,String classFile) throws Exception{

try {

URL[] urls = new URL[] {new URL("file:/"+dir)};

URLClassLoader loader = new URLClassLoader(urls);

Class c = loader.loadClass(classFile);

//调用加载类的main方法

c.getMethod("main",String[].class).invoke(null, (Object)new String[]{});

} catch (Exception e) {

e.printStackTrace();

}

}


脚本引擎

 JAVA脚本引擎是从JDK6.0之后添加的新功能。

脚本引擎介绍:

使得 Java 应用程序可以通过一套固定的接口与各种脚本引擎交互,从而达到在 Java 平台上调用各种脚本语言的目的。

Java 脚本 API 是连通 Java 平台和脚本语言的桥梁。

可以把一些复杂异变的业务逻辑交给脚本语言处理。

获得脚本引擎对象


ScriptEngineManager sem = new ScriptEngineManager();

ScriptEngine engine = sem.getEngineByName("javascript");


Java 脚本 API 为开发者提供了如下功能:

获取脚本程序输入,通过脚本引擎运行脚本并返回运行结果,这是最核心的接口。

Java可以使用各种不同的实现,从而通用的调用js、python等脚本。

– 使用Js脚本引擎:Rhino

https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino

Rhino 是一种使用 Java 语言编写的 JavaScript 的开源实现,现在被集成进入JDK 6.0。

– 通过脚本引擎的运行上下文在脚本和 Java 平台间交换数据。

– 通过 Java 应用程序调用脚本函数。


字节码操作

就是在运行时对字节码操作,可以让我们实现如下功能:

动态生成新的类

动态改变某个类的结构(添加、删除、修改  新的属性和方法)


字节码操作的优势:比反射开销小,性能高

常见的字节码类库如下;

-BECL :是java classworking广泛使用的一种框架,可以深入理解JVM汇编语言进行类的操作细节。

基于JVM底层指令操作,比较难学。哈哈


-ASM :轻量级的java字节码操作框架类库,直接涉及JVM底层操作和指令


-CGLIB  :是基于ASM的的实现,强大性能高


-Javassist :开源框架,较简单。

支持类库和bytecode来操作字节码,性能相对BECL,ASM性能低下,跟CGLIB差不多,很多开源框架都在使用它,常见。