【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之动态性技术原理指南(反射技术专题)

简介: 学习一门新的动态类型语言可能需要花费较长的时间,使得已经熟悉Java的开发人员更希望继续使用Java来解决问题。然而,Java本身也支持动态性,在一些需要灵活性的场合可以发挥作用。反射API就是Java中的一个例子,它能够在运行时通过方法名称查找并调用方法。Java语言也在不断更新版本,提高对动态性和灵活性的支持。

带你攻破你很可能存在的Java技术盲点之动态性技术原理指南

本系列技术专题的相关技术指南主要有以下三个方面:
在这里插入图片描述

编程语言的类型

学习一门新的动态类型语言可能需要花费较长的时间,使得已经熟悉Java的开发人员更希望继续使用Java来解决问题。然而,Java本身也支持动态性,在一些需要灵活性的场合可以发挥作用。反射API就是Java中的一个例子,它能够在运行时通过方法名称查找并调用方法。Java语言也在不断更新版本,提高对动态性和灵活性的支持。

整体的编程语言分为三大类:静态类型语言和动态类型语言、半静态半动态类型语言。
在这里插入图片描述

静态类型语言

Java语言是一种静态类型的编程语言,即要在编译时进行类型检查。在Java中,每个变量的类型需要在声明时显式指定;所有变量、方法的参数和返回值的类型必须在程序运行之前就已经确定。这种静态类型特性使得编译器能够在编译时进行大量的类型检查,从而发现代码中明显的类型错误。然而,这也意味着代码中包含了大量不必要的类型声明,使代码显得过于冗长且不够灵活。相对应的,动态类型语言(如JavaScript和Ruby等)的类型检查则是在运行时进行的。在这类语言中,源代码中的变量类型可以在运行时动态确定。

动态类型语言

相比于静态类型语言,动态类型语言(如JavaScript和Ruby等)的类型检查是在运行时进行的。在这类语言中,源代码中不需要显式地声明类型,因此,使用动态类型语言编写的代码更加简洁。近年来,动态类型语言的流行也反映了语言中动态性的重要性。适当的动态性对于提高开发效率非常有帮助,因为它可以减少开发人员需要编写的代码量。

技术核心方向

虽然Java是一种静态类型语言,但是它也提供了使代码更具灵活性的动态性特性。这些特性包括脚本语言支持API、反射API、动态代理和JSR292中引入的动态语言支持。开发人员可以选择不同的方式来提高代码的灵活性。例如,可以使用脚本语言支持API将脚本语言集成到Java程序中,使用反射API在运行时动态调用方法,使用动态代理拦截接口方法调用,或使用JSR292中的方法句柄来实现更多的功能。方法句柄支持多种变换操作,并能满足不同场合的需求。在这里插入图片描述

反射API

反射API是Java语言提供的动态性支持,它允许程序在运行时获取Java类的内部结构,如构造方法、域和方法等,并与它们进行交互。反射API也能实现许多动态语言常用的实用功能。按照面向对象的思路,应该通过方法来改变对象的状态,而不是直接修改属性的值。Java类中的属性设置和获取方法名通常遵循JavaBeans规范,以setXxx和getXxx命名。因此,可以编写一个工具类,用于设置和获取任何符合JavaBeans规范的对象的属性。

可以使用Java的反射API实现与JavaScript语言的实现类似的功能,代码量上并不太有差别。实现思路是先从对象的类中查找方法,再调用该方法并传入参数。这个静态方法可以被作为一个实用工具方法在程序中使用。

public class ReflectSetter
   public static void invokeSetter(Object obj,String field,Object value) throws NoSuchMethodException,InvocationTargetException,IllegalAccessException{
     String methodName "set"+field.substring(0,1).toUppercase() + field.substring(1);
     class<?>clazz obj.getclass();
     Method method clazz.getMethod (methodName,value.getclass ())
     method.invoke (obj,value);
 }
}

从上述示例可以看出,反射API可以实现Java语言的灵活使用。实际上,反射API定义了提供者和使用者之间的松散契约,这种契约可以在方法调用时只需要建立在名称和参数类型上,而不需要在代码中首先声明变量。这种方式提供了更大的灵活性和动态性,但也需要开发者自己保证调用的合法性。如果方法调用不合法,相关的异常会在运行时抛出。

反射案例介绍

反射API常用于方法名或属性名按照特定规则变化的情况:

  • 在Servlet中,利用反射API可以遍历HTTP请求中的所有参数,然后用invokeSetter方法填充领域对象的属性值。
  • 在数据库操作中,也通过反射API实现从查询结果集中创建并填充领域对象的场景。这些对应关系都可以通过反射API来建立。

反射功能操作

反射API虽然能为Java程序带来灵活性,但其实现机制也会带来性能代价。通过反射调用方法一般比直接在源代码中编写的方式慢一到两个数量级。虽然随着Java虚拟机的改进,反射API的性能得到了提升,但在一些对性能要求高的应用中,需要慎用反射API。
在这里插入图片描述

获取构造器

可以通过反射API获取Java类中的构造方法,从而在运行时动态地创建Java对象。具体步骤如下:

  1. 获取Class类的对象,可以使用Class.forName方法或者类的.class属性。
  2. 通过Class类的getConstructors方法获取所有的公开构造方法的列表,或者使用getConstructor方法根据参数类型获取公开的构造方法。如果需要获取类中真正声明的构造方法,可以使用getDeclaredConstructors和getDeclaredConstructor方法。
  3. 得到表示构造方法的java.lang.reflect.Constructor对象之后,可以通过其getName方法获取构造方法的名称,getParameterTypes方法获取构造方法的参数类型,getModifiers方法获取构造方法的修饰符等信息。
  4. 最后,可以使用newInstance方法创建出新的对象,该方法接受一个可变参数列表,用于传递构造方法的参数值。如果构造方法没有参数,则可以直接调用newInstance方法。

需要注意的是,使用反射API创建对象的效率较低,应该尽量避免在性能要求较高的场景中使用。

一般的构造方法的获取和使用并没有什么特殊之处,需要特别说明的是对参数长度可变的构造方法和嵌套类(nested class)的构造方法的使用。
长度可变的参数 - 构造方法

如果一个构造方法声明了长度可变的参数,需要使用对应的数组类型的 Class 对象来获取该构造方法,因为长度可变的参数实际上是通过数组来实现的。

使用反射 API 获取参数长度可变的构造方法

例如,如果一个类 VarargsConstructor 的构造方法包含 String 类型的可变长度参数,调用getDeclaredConstructor 方法时需要使用 String[].class,否则会找不到该构造方法。在调用newInstance 方法时,需要将作为实际参数的字符串数组先转换为 Object 类型,以避免方法调用时的歧义,这样编译器就知道将该字符串数组作为一个可变长度的参数来传递。


public class VarargsConstructor {
    public VarargsConstructor(String... names) {}
}

public void useVarargsConstructor() throws Exception { 
    Constructor<VarargsConstructor> constructor = VarargsConstructor.class.
        getDeclaredConstructor(String[].class);
    constructor.newInstance((Object) new String[]{"A", "B", "C"});
}

获取嵌套类的构造方法时,需要区分静态和非静态两种情况。
在这里插入图片描述静态嵌套类,可以按照一般的方式来使用。

非静态嵌套类,其特殊之处在于它的对象实例中都有一个隐含的对象引用,指向包含它的外部类对象。这个隐含的对象引用的存在,使得非静态嵌套类中的代码可以直接引用外部类中包含的私有域和方法。因此,在获取非静态嵌套类的构造方法时,类型参数列表的第一个值必须是外部类的 Class 对象。

例如,对于非静态嵌套类 NestedClass,获取其构造方法时需要传入外部类的 Class 对象作为第一个参数,以便在创建新对象时传递外部对象的引用。

static class StaticNestedClass {
    public StaticNestedClass(String name) {}
}
class NestedClass {
    public NestedClass(int count) {}
}
public void useNestedClassConstructor() throws Exception {
    Constructor< StaticNestedClass> sncc = StaticNestedClass.class. getDeclaredConstructor(String.class);
    sncc.newInstance("Alex");
    Constructor<NestedClass> ncc = NestedClass.class.getDeclaredConstructor(ConstructorUsage.class, int.class);
    NestedClass ic = ncc.newInstance(this, 3);
}

获取Field域

通过反射 API,可以获取类中的域(field),包括公开的静态域和对象中的实例域。获取表示域的 java.lang.reflect.Field 类的对象之后,就可以获取和设置域的值。与获取构造方法的方法类似,Class 类中也有 4 个方法用来获取域,分别是 getFields、getField、getDeclaredFields 和 getDeclaredField。
在这里插入图片描述

  • getFields 方法返回公开的静态域和对象中的实例域;
  • getField 方法返回指定名称的公开的静态域或对象中的实例域;
  • getDeclaredFields 方法返回类中所有的域,包括私有的静态域和对象中的实例域;
  • getDeclaredField 方法返回指定名称的域,包括私有的静态域和对象中的实例域。
使用反射 API 获取和使用静态域和实例域
获取和使用静态域和实例域的示例,两者的区别在于使用静态域时不需要提供具体的对象实例,使用 null 即可

Field 类中除了操作 Object 的 get 和 set 方法之外,还有操作基本类型的对应方法,包括 getBoolean / setBoolean、getByte / setByte、getChar / setChar、getDouble / setDouble、getFloat / setFloat、getInt / setInt 和 getLong / setLong 等

public void useField() throws Exception {
    Field fieldCount = FieldContainer.class.getDeclaredField("count");
    fieldCount.set(null, 3);
    Field fieldName = FieldContainer.class.getDeclaredField("name"); 
    FieldContainer fieldContainer = new FieldContainer(); 
    fieldName.set(fieldContainer, "Bob");
}

总的来说,获取和设置类中的公开域比较简单,但是无法通过反射 API 获取或操作私有域。

获取Method方法

最常使用反射 API 的场景是获取对象中的方法,并在运行时调用该方法。Class 类中有 4 个方法用来获取方法,分别是 getMethods、getMethod、getDeclaredMethods 和 getDeclaredMethod。这些方法的作用类似于获取构造方法和域的对应方法。通过获取表示方法的 java.lang.reflect.Method 类的对象,可以查询该方法的详细信息,例如方法的参数和返回值的类型等。使用 invoke 方法可以传入实际参数并调用该方法。

获取和调用对象中的公开和私有方法的示例
public void useMethod() throws Exception {         
    MethodContainer mc = new MethodContainer();
    Method publicMethod = MethodContainer.class.getDeclaredMethod("publicMethod");
    publicMethod.invoke(mc);
    Method privateMethod = MethodContainer.class.getDeclaredMethod("privateMethod");
    privateMethod.setAccessible(true);
    privateMethod.invoke(mc);
}

需要注意的是,在调用私有方法之前,需要先调用 Method 类的setAccessible方法来设置可以访问的权限。与构造方法和域不同的是,通过反射 API 可以获取到类中的私有方法。

操作数组

利用反射API对数组进行操作的方式有所不同于一般的Java对象。需要使用java.lang.reflect.Array这个实用工具类来实现。该类提供了创建数组和操作数组元素的方法。newInstance方法用来创建新的数组。第一个参数是数组中元素的类型,后面的参数是数组的维度信息。

String[] names = ( Array.newInstance(int.class, 3, 3, 3);
double[][][] arrays= (double[][][]) Array.newInstance(double[][].class, 2, 2);
使用反射 API 操作数组

例如,可以使用下面的示例代码创建一个长度为10的一维String数组和一个3x3x3的三维数组:

public void useArray() {
    String[] names = (String[]) Array.newInstance(String.class, 10);
    names[0] = "Hello"; 
    Array.set(names, 1, "World");
    String str = (String) Array.get(names, 0);
    int[][][] matrix1 = (int[][][]) Array.newInstance(int.class, 3, 3, 3);
    matrix1[0][0][0] = 1;
    int[][][] matrix2 = (int[][][]) Array.newInstance(int[].class, 3, 4);
    matrix2[0][0] = new int[10]; 
    matrix2[0][1] = new int[3]; 
    matrix2[0][0][1] = 1;
}

需要注意的是,尽管在创建时只声明了两个维度,但是matrix2实际上也是一个三维数组,因为它的元素类型是double。

访问权限与异常处理

使用反射 API 可以绕过 Java 语言中默认的访问控制权限,例如访问在另一个类中声明的私有方法。这是通过调用继承自 java.lang.reflect.AccessibleObject 的 setAccessible 方法来实现的。在使用 invoke 方法调用方法时,如果方法本身抛出异常,invoke 方法会抛出 InvocationTargetException 异常来表示这种情况。可以通过 InvocationTargetException 异常的 getCause 方法获取真正的异常信息来进行调试。

在 Java 7 中,所有与反射操作相关的异常类都添加了一个新的父类 java.lang.ReflectiveOperationException,可以直接捕获这个新的异常。

内容总结

Java反射技术允许程序在运行时动态地获取类的信息、调用类的方法、访问类的属性等,从而提高程序的灵活性和可扩展性。它可以获取类的名称、包名、父类、接口、构造方法、方法、属性等信息,创建对象,调用方法,访问属性,实现动态代理等功能。Java反射技术在框架开发、ORM框架、动态代理、单元测试等方面都有着重要的应用。但是,由于使用反射技术需要额外的开销,因此在性能要求较高的场景下,应该尽量避免使用。

相关文章
|
9天前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
18 1
|
14天前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
30 3
|
14天前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
16 1
|
14天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
29 1
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
15天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
2天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
15 9
|
5天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
2天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
5天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
15 3