java反射运用及优化

目录

一、什么是Java的反射:

二、反射的运用实例:

三、什么情况下需要反射

1、动态加载和执行代码

3、构建灵活的框架

4、序列化和反序列化

5、插件架构

四、反射的优化

1、避免频繁地调用反射

2、缓存反射操作

3、使用 setAccessible(true)

4、尽可能使用 public 方法和字段

5、通过MethodHandle实现反射

  6、 通过CallSite与MethodHandle结合来实现反射

 五、MethodHandle实例讲解

1、创建 MethodHandle

2、调用 MethodHandle

3、InvokeExact 和 Invoke 的区别

4、MethodHandles 与 Java反射API的比较

5、完整实例

六、CallSite实例讲解

1、基本概念

2、CallSite类型

3、完整实例

七、 普通reflection与methodHandle的效率对比


一、什么是Java的反射:

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

Java反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

反射技术大量用于Java设计模式和框架技术,最常见的设计模式就是工厂模式和单例模式。

单例模式(Singleton):这个模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。这样做就是为了节省内存空间,保证我们所访问到的都是同一个对象。

工厂模式(Factory):工厂模式利用Java反射机制和Java多态的特性可以让我们的程序更加具有灵活性。用工厂模式进行大型项目的开发,可以很好的进行项目并行开发。


二、反射的运用实例:

我们将创建一个名为 "Person" 的类,然后使用Java反射来获取它的 Class 对象,以及调用它的方法。

public class Person {

    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void sayHello() {
        System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
    }

    private void sayBye() {
        System.out.println("Goodbye!");
    }

    // getters and setters...
}

 使用反射API进行功能展示:

public class ReflectionExample {

    public static void main(String[] args) throws Exception {
        // 通过完全限定类名获取 Class 对象
        Class cls = Class.forName("your.package.name.Person");

        // 构造一个具有特定参数类型的新 Person 实例
        Constructor constructor = cls.getDeclaredConstructor(String.class, int.class);
        Object person = constructor.newInstance("John Doe", 20);

        // 获取并调用公开方法
        Method sayHelloMethod = cls.getDeclaredMethod("sayHello");
        sayHelloMethod.invoke(person);

        // 获取并调用私有方法
        Method sayByeMethod = cls.getDeclaredMethod("sayBye");
        sayByeMethod.setAccessible(true);
        sayByeMethod.invoke(person);

        // 通过反射操作属性
        Field nameField = cls.getDeclaredField("name");
        nameField.setAccessible(true);
        // 获取属性值
        System.out.println("Name: " + nameField.get(person));
        // 设置属性值
        nameField.set(person, "Jane Doe");
        System.out.println("Changed name: " + nameField.get(person));
    }
}

这段代码首先获取 Person 类的 Class 对象,然后使用 Class 对象获取 Constructor 对象。Constructor 对象被用来创建 Person 类的新实例。

然后它获取 sayHello 和 sayBye 方法,并调用它们。请注意,sayBye 是私有方法,所以我们需要通过 setAccessible(true) 这行代码来操作它。

最后,我们通过反射获取 name 属性,得到它的值,再设置新的值。

这就是如何使用Java反射操作类、方法和属性的基本用法。


三、什么情况下需要反射

反射是面向对象编程中的一个重要特性,尽管它可能有一些性能开销和安全性问题,但在某些情况下,使用反射是必要的,下面是一些例子:

1、动态加载和执行代码

反射可以用于动态地加载和执行代码,这意味着程序不需要知道它需要使用的所有类和方法的确切名称就可以执行。这在编写高度灵活和可拓展的代码时非常有用。

2、调试和测试工具

        反射可以使开发人员在运行时访问和修改私有字段、方法和构造函数,这对编写调试和测试工具特别有用。JUnit 这样的测试框架就是利用反射来发现和执行测试方法的。

3、构建灵活的框架

许多流行的 Java 框架,如 Spring 和 Hibernate,都使用反射来创建灵活的代码。这些框架通常在运行时动态构建和装配对象,而不需要知道在编译时所有可能使用的类和方法。

4、序列化和反序列化

在将对象转换为可存储或传输的格式(如 JSON 或 XML)以及从这种格式转换回对象的过程中,反射被广泛使用。

5、插件架构

当创建可接受外部插件的应用程序时,可以使用反射来动态加载和使用插件,而无需应用程序本身在编译时了解插件的具体实现。

请注意:

虽然反射是一个强大的工具,但它也有一些不足之处。它可能带来性能问题,因为反射操作通常比非反射操作更耗时。此外,反射可能会绕过语言的访问控制,这可能导致安全问题。因此,除非有必要,否则应该避免使用反射。


四、反射的优化

Java 反射的性能有时可能较差,因此可能需要进行一定的优化。以下是一些可行的优化策略:

1、避免频繁地调用反射

        尽可能地减少反射使用的频率。当频繁调用时,可能会导致性能下降。如果有静态的编码方法可以替代,那么应该优先选择这些方法。

2、缓存反射操作

        如果你需要多次反射调用一个方法,那么你应该将该方法的 Method 对象缓存起来。不要每次都去调用 Class.getMethod(),因为这个操作的性能开销较大。

Method methodToCall = null;
if (cachedMethods.containsKey(methodName)) {
  methodToCall = cachedMethods.get(methodName);
} else {
  methodToCall = clazz.getMethod(methodName, paramClasses);
  cachedMethods.put(methodName, methodToCall);
}

3、使用 setAccessible(true)

        在反射调用方法之前,你可以调用 Method.setAccessible(true)。这将关闭一些Java的访问检查,从而提高反射的性能。但是,需要注意的是这会停用Java语言的访问控制检查,所以只有在你确定这样做不会导致安全问题时,才应使用这种方法。 

Method method = myObject.getClass().getMethod("myMethod");
method.setAccessible(true);
method.invoke(myObject);

4、尽可能使用 public 方法和字段

        访问非公开字段和方法通常比访问公开字段和方法需要更多的处理,尽可能使用公开字段和方法可以提升性能。

5、通过MethodHandle实现反射

   MethodHandle 是一个型态化的,直接对方法,构造函数,或者字段的引用。其设计的目标是让 JVM 对它进行与常规 Java 方法调用相同的优化。

MethodHandle 在某些情形下可能比反射的性能更好。首先,MethodHandle 的创建和链接过程更为高效;其次,MethodHandle 支持更加强力的编译器优化,因为运行时更清楚的知道包含方法句柄的代码所做的事情。

MethodType mt = MethodType.methodType(String.class, int.class, int.class);
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "substring", mt);
String output = (String) mh.invokeExact("Hello World!", 0, 5);

  6、 通过CallSite与MethodHandle结合来实现反射

   在Java中,CallSite是一个连接方法调用信息和方法调用实现的中介。它是 Java 7 引入的一部分,用于支持动态语言的实现。CallSite 是指向一种方法的引用,可以动态更改引用的方法。

import java.lang.invoke.*;

public class CallSiteDemo {
    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findStatic(Math.class, "sqrt", MethodType.methodType(double.class, double.class));
        CallSite site = new ConstantCallSite(mh);
        MethodHandle invoker = site.dynamicInvoker();
        Double result = (Double) invoker.invoke(9d);
        System.out.println(result);  // should print '3.0'
    }
}

 五、MethodHandle实例讲解

在 Java 7 中,方法句柄(MethodHandle)被引入为强大的底层构造,用于表达和执行常见的 Java Virtual Machine(JVM)操作。MethodHandle基本上是对一个方法的直接参考,无论这个方法是静态的,私有的,或者是任何其他类型。 它是 JVM 中的一个基本数据类型,就像 int 或 String 一样。

1、创建 MethodHandle

要获取 MethodHandle,需要使用一个MethodHandles.Lookup实例,这是 MethodHandles 类的内部类。一般来说,具有取方法句柄能力的查找对象本身应该被看作是名字和类型的元组。 此查找对象可以用以下方式获得:

MethodHandles.Lookup lookup = MethodHandles.lookup();

 之后,就可以使用 lookup 对象来查找方法句柄:

MethodHandle handle = lookup.findStatic(Receiver.class, "methodName", methodType(String.class));

这里,“Receiver.class”是包含我们要查找方法的类,“methodName”是方法的名字,methodType定义了方法类型。

2、调用 MethodHandle

MethodHandle 一旦创建,都可以通过invoke() 或者 invokeExact()方法来调用。

String result = (String) handle.invoke("argument");

3、InvokeExact 和 Invoke 的区别

  • invokeExact() 更加类型安全。在调用方法的时候,它会检查你传递的参数和方法的类型签名完全匹配。否则,它会抛出一个WrongMethodTypeException

  • invoke()则在这点宽松。在类型不匹配情况下,它会将参数适应到方法的类型(如果可能)。

这体现了 MethodHandle 的类型特性。每个方法句柄都有一种类型,这是由它接受的参数类型和它返回的结果类型定义的。

注意:严格情况下,你应始终使用 invokeExact(),只有在确定某些参数类型可能会发生变化,并且你确实希望 JVM 在这些情况下“尽力而为”,时,才应该使用 invoke()

4、MethodHandles 与 Java反射API的比较

与常规的反射API相比,方法句柄有几个优点:

  • 性能:在某些场景下,相较于传统的反射API,MethodHandles 表现出更好的性能。
  • 类型安全:MethodHandles 提供了类型安全的操作,可以利用编译时类型检查。
  • 灵活性:MethodHandle 提供了一些高级特性,例如方法句柄组合和转换。

其中缺点是,由于 MethodHandles 在 Java 7 中引入,因此在早期的 Java 版本中无法使用。

MethodHandles 是 Java 提供的一个强大工具,使得动态方法调用和控制流操作变得简单。然而,对于大多数业务和应用程序编程,传统的 OOP 和接口/实现模式可能更合适。MethodHandle 更常见的用例是在创建一个需要高度动态性或能够直接操作 JVM 指令集的库(如某些类型的任务派发,或者复杂的 DSL 实现)。

5、完整实例

import java.lang.invoke.*;

public class MethodHandlesExample {
    
    // 定义一个简单的方法
    static class ClassA {
        public void println(String str) {
            System.out.println(str);
        }
    }

    public static void main(String[] args) throws Throwable {
        
        Object obj = new ClassA();
        // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
        getPrintlnMethodHandle(obj).invokeExact("Hello, MethodHandle!");
    }

    private static MethodHandle getPrintlnMethodHandle(Object reveiver) throws NoSuchMethodException, IllegalAccessException {
        /* MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数).*/
        MethodType mt = MethodType.methodType(void.class, String.class);
        
        /*
         * findVirtual:在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
         * 因为这里调用的是一个虚方法,按照Java语言的规矩,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象。这个参数以前是放在参数列表中进行传递的,而现在提供了bindTo()方法来完成这件事情。
         */
        return MethodHandles.lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }

 在这个例子中,getPrintlnMethodHandle() 方法返回一个 MethodHandle 实例,这个实例封装了println方法的调用。这个方法的调用与普通的Java方法调用看起来非常类似,唯一的区别在于 MethodHandle 提供了更为丰富和灵活的调用方式。


六、CallSite实例讲解

1、基本概念

Java的CallSite一词主要源自invokedynamic(Java 7引入的新的字节码指令)。这是一个独特的指令,因为它允许JVM在运行时之后或运行时确定一个方法调用应该如何分派。

那么CallSite在其中的作用是什么呢?简单来说,CallSite表示一个方法调用点(即一个特定的调用位置)。然而,与invokedynamic相关的Java核心类库提供了三种不同种类的CallSite:

  1. ConstantCallSite
  2. MutableCallSite
  3. VolatileCallSite

这些类别的区别在于,我们在运行时可以改变方法调用的分派方式,这是通过改变CallSite的状态来实现的。

2、CallSite类型

以下是三种类型的CallSite的详细描述:

  1. ConstantCallSite: 这是最简单的一种,它表示一旦被初始化,它的行为就不会再改变。这意味着调用位置的分派方式不会在后面再发生改变。

  2. MutableCallSite: 这个类型的CallSite允许我们在运行时改变其行为。我们可以更改它的分派方式,但这个操作不是线程安全的。

  3. VolatileCallSite: 这也是一个可变类型的CallSite。如果有多个线程并发修改VolatileCallSite,改变分派方式的时候,它保证这个操作是线程安全的。

3、完整实例

使用CallSite必须依赖MethodHandle以及相关的Bootstrap方法。Bootstrap方法的作用是返回一个CallSite实例,该实例链接到想要适当调用的方法实现。java CallSite的实例使用需要对Bootstrap方法和MethodHandle有较深的理解。这里我们试图展示如何使用MutableCallSite,这是一种CallSite,在运行时能够修改行为的类型:

import java.lang.invoke.*;

public class Main {
    static class MyCallSite extends MutableCallSite {
        MethodHandle fallback;

        public MyCallSite() throws NoSuchMethodException, IllegalAccessException {
            super(MethodType.methodType(void.class, String.class));
            fallback = lookup().findVirtual(MyCallSite.class, "fallback", MethodType.methodType(void.class, String.class))
                    .bindTo(this);
            setTarget(fallback);
        }

        private void fallback(String arg) throws Throwable {
            System.out.println("Fallback " + arg);
            setTarget(lookup().findVirtual(Main.class, "print", MethodType.methodType(void.class, String.class)));
            fallback.invoke(arg);
        }
    }

    public static void main(String[] args) throws Throwable {
        MyCallSite callSite = new MyCallSite();
        MethodHandle target = callSite.dynamicInvoker();
        target.invoke("Hello World 1");  // 第一次回调,之后更新目标
        target.invoke("Hello World 2");  // 这次调用更新过的目标方法
    }

    public static void print(String arg) {
        System.out.println("Print " + arg);
    }
}

在上面的代码片段中:

  1. 我们创建了一个MyCallSite类,它继承自MutableCallSite类。
  2. 在MyCallSite的构造函数中,我们定义了一个fallback MethodHandle,并将其设置为当前MyCallSite的目标。
    • 这里的fallback方法只会在第一次调用的时候执行,后面的调用将直接执行我们设置的新的target方法。
  3. 在fallback方法内,我们修改了MyCallSite的target来指向我们自定义的print方法,并将此修改后的MethodHandle再次进行调用。
  4. 在main方法内,我们创建了一个MyCallSite的实例和它的dynamic invoker,然后我们尝试执行这个invoker两次:
    • 第1次执行invoke,它会执行预设的fallback方法,并将下一次的target修改为print方法。
    • 第2次执行invoke,它会直接执行修改过的target,也就是print方法。

注意:以上例子展示了如何在运行时修改一个方法的行为。使用这种功能需要谨慎,因为操作不当可能引起意外的行为或错误。总是在彻底理解invokedynamic和CallSite机制后再使用。


七、 普通reflection与methodHandle的效率对比

以下是一个简单的Java代码实例,我们将使用分别调用反射和方法句柄,以观察其性能差异。

请注意,这仅仅是一个基本的例子,并不能完全代表所有使用场景的性能差异。具体差异取决于许多因素,包括JVM的优化以及特定的用例等。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodHandleAndReflectTest {
    static class Test {
        public void hello() {
            // Method body
        }
    }
    
    public static void main(String[] args) throws Throwable {
         Test t = new Test();
            Method method = Test.class.getMethod("hello");

            // Reflection
            long lastTime = System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                method.invoke(t);
            }

            System.out.println("Reflection Time: " + (System.currentTimeMillis() - lastTime));

            // MethodHandle
            MethodHandle handle = MethodHandles.lookup().findVirtual(Test.class, "hello", MethodType.methodType(void.class)).bindTo(t);
            lastTime= System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                handle.invoke();
            }

            System.out.println("MethodHandle Time: " + (System.currentTimeMillis() - lastTime));

             lastTime = System.currentTimeMillis();
            for (int i = 0; i < 1_000_000; i++) {
                t.hello();
            }

            System.out.println("native Time: " + (System.currentTimeMillis() - lastTime));
}

输出结果为:

Reflection Time: 92
MethodHandle Time: 5
native Time: 0

大家看到结果还是非常明显。在测试中我们发现,Reflection 和MethodHandle不做缓存的情况下,MethodHandle效率反而比Refection比较低


你可能感兴趣的:(java,java,开发语言)