19.java类的加载和反射

文章目录

  • `类的加载`
  • `类初始化`
  • `类加载器`
  • `反射`
  • `获取反射对象`
  • `获取反射详细信息`
  • `获取反射操作对象`
  • `type接口`

类的加载

当一个Java程序启动时,JVM会启动,并且负责执行Java字节码。类加载过程是在程序运行期间动态加载类的字节码到JVM中,使得程序能够调用这些类的方法和访问其字段。下面是类加载过程的详细步骤:

  1. 加载(Loading):

    • 类加载器的选择: 类加载器根据类的全限定名(fully qualified name)定位并加载类的字节码文件。Java中有三种主要的类加载器:启动类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)、应用程序类加载器(Application Class Loader)。
    • 查找类文件: 类加载器根据类的全限定名查找对应的类文件。这个过程可以在本地文件系统、网络中,或者其他来源中进行。
    • 创建类的二进制表示: 读取类文件的二进制数据,然后将其转换为内部表示形式,通常存储在方法区(Method Area)中。
  2. 连接(Linking):

    • 验证(Verification): 验证类的字节码是否符合Java虚拟机规范,以确保其是合法且安全的。验证的过程包括对类结构、字段、方法等方面的检查。
    • 准备(Preparation): 为类的静态变量分配内存空间,并设置默认初始值。这里并不包括对变量赋初值的过程,赋值操作将在后面的初始化阶段进行。
    • 解析(Resolution): 将类中的符号引用转化为直接引用。符号引用包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。解析的过程是将这些符号引用转换为在内存中直接指向目标的引用。
  3. 初始化(Initialization):

    • 在这个阶段,类的静态变量将会被赋予程序中定义的值。
    • 执行静态代码块(static block)中的代码。这些代码块通常包含一些类初始化时需要执行的逻辑。

需要注意的是,类的初始化是被延迟执行的。具体来说,只有在对类的静态变量进行赋值或静态方法/块被调用时,才会触发类的初始化。这种延迟加载的机制有助于提高程序性能,因为不是所有的类都在程序启动时被加载和初始化。

总体而言,类加载过程是Java虚拟机在运行时动态加载类的字节码文件,连接阶段将符号引用解析为直接引用,并在初始化阶段为类的静态变量赋值,执行静态代码块。这个过程使得Java程序能够在运行时动态加载和使用类。

类初始化

在Java中,类的初始化是一个懒加载的过程,只有在需要的时候才会进行。以下是一些操作会触发类的初始化的情况:

  1. 创建类的实例:

    • 当通过 new 关键字创建类的实例时,将触发类的初始化。这包括直接实例化对象或者通过反射创建对象。
    MyClass obj = new MyClass(); // MyClass 类的初始化将会被触发
    
  2. 访问类的静态变量:

    • 当访问一个类的静态变量时,如果该类尚未被初始化,将触发其初始化。
    int value = MyClass.staticVariable; // MyClass 类的初始化将会被触发
    
  3. 调用类的静态方法:

    • 当调用一个类的静态方法时,如果该类尚未被初始化,将触发其初始化。
    int result = MyClass.calculate(); // MyClass 类的初始化将会被触发
    
  4. 使用反射操作类:

    • 通过反射机制访问类的方法、字段、构造器等,如果类尚未被初始化,将触发其初始化。
    Class<?> clazz = Class.forName("MyClass"); // MyClass 类的初始化将会被触发
    
  5. 初始化子类:

    • 当一个类的子类被初始化时,其父类也将被初始化。
    class SubClass extends MyClass {
        // MyClass 类和 SubClass 类都会被初始化
    }
    

需要注意的是,对于一个类而言,它的初始化是被延迟的,即只有在真正需要初始化的时候才会进行。这种懒加载的机制有助于提高程序的性能,因为不是所有的类都在程序启动时被加载和初始化。

在Java中,并非所有使用类的操作都会导致类的初始化。以下是一些使用类的操作,不会触发类的初始化的情况:

  1. 访问类的常量:

    • 访问类的常量(使用 final 修饰的基本类型或字符串),不会触发类的初始化。
    public class MyClass {
        public static final int CONSTANT = 42;
    }
    
    // 访问常量不会触发类的初始化
    int value = MyClass.CONSTANT;
    
  2. 通过数组定义类引用:

    • 通过数组定义类引用,不会触发类的初始化。
    MyClass[] array = new MyClass[5]; // 不会触发 MyClass 类的初始化
    
  3. 引用类的静态变量,但不对其进行赋值操作:

    • 仅仅引用类的静态变量而不对其进行赋值操作,不会触发类的初始化。
    int value = MyClass.staticVariable; // 不会触发 MyClass 类的初始化
    
  4. 使用类的类字面常量:

    • 使用类的类字面常量(MyClass.class),不会触发类的初始化。
    Class<?> clazz = MyClass.class; // 不会触发 MyClass 类的初始化
    
  5. 调用父类的静态变量或静态方法:

    • 当只访问父类的静态变量或静态方法时,并不会触发子类的初始化。
    int parentValue = ParentClass.staticVariable; // 不会触发 ParentClass 类的初始化
    

总体来说,访问类的静态常量、通过数组定义类引用、引用类的静态变量而不赋值、使用类的类字面常量、调用父类的静态变量或静态方法等操作,并不会导致类的初始化。这种行为使得Java虚拟机能够按需加载和初始化类,提高程序的性能。

类加载器

Java类加载器(Class Loader)是Java运行时环境的一部分,负责将类的字节码加载到内存中,使得Java程序能够运行。类加载器通常负责从文件系统、网络或其他来源加载类的字节码,并将其转换为Java虚拟机可用的类对象。

Java虚拟机中的类加载器体系是一个层次结构,主要包括以下几种类加载器:

  1. 启动类加载器(Bootstrap Class Loader):

    • 它是Java虚拟机的一部分,是用本地代码实现的加载器,负责加载Java核心库(JAVA_HOME/jre/lib/rt.jar或者modules)。
    • 启动类加载器是虚拟机的一部分,它不是一个Java对象,因此在Java中无法直接获取对启动类加载器的引用。
  2. 扩展类加载器(Extension Class Loader):

    • 扩展类加载器是sun.misc.Launcher$ExtClassLoader的实例,负责加载Java扩展库(JAVA_HOME/jre/lib/ext或者由系统变量java.ext.dirs指定的目录)。
    • 扩展类加载器是系统类加载器的父类。
  3. 应用程序类加载器(Application Class Loader):

    • 也被称为系统类加载器,是sun.misc.Launcher$AppClassLoader的实例。
    • 应用程序类加载器负责加载应用程序类路径上指定的类库。应用程序类路径通常通过环境变量CLASSPATH或者通过 -classpath(或 -cp)命令行选项指定。
  4. 自定义类加载器(Custom Class Loader):

    • Java允许用户自定义类加载器,继承自ClassLoader类,通过重写findClass方法实现自定义的类加载逻辑。
    • 自定义类加载器通常用于加载非标准的类文件,例如从数据库、网络或其他非传统来源加载类。

类加载器之间形成了一个层次结构,每个类加载器都有一个父加载器。当需要加载类时,先尝试使用当前类加载器加载,如果加载失败,则委托给其父加载器,依次递归,直到达到启动类加载器。这种委托机制被称为“双亲委派模型”。

类加载器的工作可以被简化为以下几个步骤:

  1. 加载(Loading): 查找并加载类的字节码。
  2. 连接(Linking): 将类的二进制数据合并到虚拟机中,包括验证、准备和解析三个阶段。
  3. 初始化(Initialization): 执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。

通过类加载器的灵活使用,Java实现了动态加载和运行时扩展的能力,使得程序具有更高的灵活性和可维护性。

反射

反射是Java语言的一项强大特性,它提供了在运行时检查和操作类的能力。虽然反射使用不当可能导致性能问题和类型安全问题,但它在一些情况下是非常有用的,以下是一些使用反射的典型场景:

  1. 动态加载类:

    • 可以在运行时加载一些未知的类,这对于实现插件系统、动态配置和模块化设计是非常有用的。
  2. 运行时类型检查:

    • 在编译时无法确定要使用的类时,可以使用反射来在运行时进行类型检查和操作。
  3. 工具和框架设计:

    • 许多框架和工具(例如Spring框架、Hibernate ORM)使用反射来实现通用的、灵活的解决方案。例如,Spring的依赖注入就是通过反射来实现的。
  4. 编写通用代码:

    • 有时候需要编写通用的代码,能够处理多个不同类型的类。反射提供了一种通用的访问类信息的方式。
  5. 调试和开发工具:

    • 反射允许在运行时查看和修改类的结构,这对于调试和开发工具的开发非常有帮助。
  6. 动态代理:

    • 反射使得实现动态代理变得更加容易,这在AOP(面向切面编程)等方面非常有用。
  7. 读取注解信息:

    • 通过反射可以读取类、字段、方法等上的注解信息,从而实现一些元编程的功能。
  8. 序列化和反序列化:

    • 对象的序列化和反序列化通常需要动态地了解对象的结构,反射提供了一种机制来实现这一点。

虽然反射提供了很大的灵活性,但它也应该谨慎使用。由于反射涉及到在运行时才能获得的信息,因此可能导致性能下降和类型安全问题。在不需要时,最好避免使用反射,而应该优先选择更静态、类型安全的编程方式。

获取反射对象

获取 Class 对象的方式主要有三种:

  1. 通过类的静态属性 class

    • 对于已知类,可以通过该类的 .class 静态属性获取 Class 对象。
    Class<MyClass> clazz = MyClass.class;
    
  2. 通过对象的 getClass 方法:

    • 对于已有对象,可以调用该对象的 getClass 方法获取其 Class 对象。
    MyClass myObject = new MyClass();
    Class<?> clazz = myObject.getClass();
    
  3. 通过 Class.forName 方法:

    • 通过类的全限定名(包括包名)使用 Class.forName 方法获取 Class 对象。这种方式在编写通用代码时比较有用,因为可以在运行时指定类名。
    try {
        Class<?> clazz = Class.forName("com.example.MyClass");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    

这三种方式在不同的情况下都有用。第一种和第二种方式适用于已知类的情况,而第三种方式适用于在运行时动态指定类名的情况。在实际应用中,选择获取 Class 对象的方式取决于具体的需求和设计。

获取反射详细信息

获取类型的所有详细信息可以通过 Class 类的方法以及相关的反射 API 实现。以下是一些获取类型详细信息的常用方法:

  1. 获取类名、包名、修饰符等基本信息:

    • 使用 Class 类的方法获取基本的类信息。
    Class<MyClass> clazz = MyClass.class;
    
    // 获取类名
    String className = clazz.getName();
    
    // 获取包名
    Package packageName = clazz.getPackage();
    
    // 获取修饰符
    int modifiers = clazz.getModifiers();
    
  2. 获取父类和实现的接口信息:

    • 使用 Class 类的方法获取类的继承关系信息。
    Class<?> superClass = clazz.getSuperclass();
    
    Class<?>[] interfaces = clazz.getInterfaces();
    
  3. 获取字段信息:

    • 使用 Class 类的方法获取类的字段信息。
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        String fieldName = field.getName();
        Class<?> fieldType = field.getType();
        int fieldModifiers = field.getModifiers();
    }
    
  4. 获取方法信息:

    • 使用 Class 类的方法获取类的方法信息。
    Method[] methods = clazz.getDeclaredMethods();
    for (Method method : methods) {
        String methodName = method.getName();
        Class<?> returnType = method.getReturnType();
        Class<?>[] parameterTypes = method.getParameterTypes();
        int methodModifiers = method.getModifiers();
    }
    
  5. 获取构造方法信息:

    • 使用 Class 类的方法获取类的构造方法信息。
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();
    for (Constructor<?> constructor : constructors) {
        Class<?>[] parameterTypes = constructor.getParameterTypes();
        int constructorModifiers = constructor.getModifiers();
    }
    
  6. 获取注解信息:

    • 使用 Class 类的方法获取类、字段、方法上的注解信息。
    Annotation[] classAnnotations = clazz.getAnnotations();
    
    // 获取字段上的注解
    Field field = clazz.getDeclaredField("fieldName");
    Annotation[] fieldAnnotations = field.getAnnotations();
    
    // 获取方法上的注解
    Method method = clazz.getDeclaredMethod("methodName");
    Annotation[] methodAnnotations = method.getAnnotations();
    

这些方法提供了丰富的信息,可以用于动态地了解和操作类的结构。需要注意,部分方法(如 getDeclaredFieldsgetDeclaredMethods 等)返回的是当前类声明的字段、方法,不包括父类的字段、方法。如果需要获取所有的字段、方法,可以使用相应的递归方法或结合父类的信息。

获取反射操作对象

使用反射创建对象并操作类的属性和方法主要分为以下步骤:

  1. 获取 Class 对象:通过已知类的静态属性 .class、对象的 getClass 方法,或者使用 Class.forName 方法获取 Class 对象。

  2. 创建对象:通过反射创建类的实例对象。

  3. 操作属性:通过 Field 对象获取或设置类的属性值。

  4. 操作方法:通过 Method 对象调用类的方法。

以下是一个包含这些步骤的示例:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) {
        try {
            // 获取 Class 对象的方式
            Class<?> clazz = MyClass.class;

            // 创建对象
            Object myObject = clazz.newInstance();

            // 操作属性
            Field nameField = clazz.getDeclaredField("name");
            nameField.setAccessible(true); // 设置私有属性可访问
            nameField.set(myObject, "John Doe");

            // 操作方法
            Method getNameMethod = clazz.getMethod("getName");
            Object result = getNameMethod.invoke(myObject);

            // 输出结果
            System.out.println("Name: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyClass {
    private String name;

    public String getName() {
        System.out.println("GetName method called.");
        return name;
    }
}

在上述示例中,我们通过反射创建 MyClass 类的对象,获取 name 属性的 Field 对象,设置私有属性可访问后,通过 set 方法设置属性值。然后,通过获取 getName 方法的 Method 对象,并调用 invoke 方法获取方法的返回值。最终,输出了获取到的属性值。

需要注意的是,使用反射操作属性和方法时,要注意访问权限。如果属性或方法是私有的,需要调用 setAccessible(true) 来设置访问权限。另外,要处理可能抛出的异常,如 NoSuchFieldExceptionIllegalAccessExceptionNoSuchMethodExceptionInvocationTargetException

type接口

当我们谈论Java的Type接口时,我们实际上是在说关于处理泛型(Generic)的一些工具。泛型是为了让我们写更灵活、通用的代码。而Type接口则是帮助我们在程序运行时理解和操作泛型信息的工具。

示例:

  1. Class 类:

    • Class 表示普通的类,比如 String.class 表示字符串类。这就是我们平常用的类。
    Class<?> stringClass = String.class;
    
  2. ParameterizedType 接口:

    • ParameterizedType 表示带有泛型参数的类型,比如 List。这帮助我们了解和获取泛型参数的信息。
    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.util.List;
    
    Type type = MyClass.class.getField("list").getGenericType();
    
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Type[] typeArguments = parameterizedType.getActualTypeArguments();
        // 在这里你可以获取 List 中的 String 类型信息
    }
    
  3. GenericArrayType 接口:

    • GenericArrayType 表示泛型数组类型,例如 T[]。这帮助我们处理这样的泛型数组类型。
    import java.lang.reflect.GenericArrayType;
    import java.lang.reflect.Type;
    
    Type type = MyClass.class.getField("array").getGenericType();
    
    if (type instanceof GenericArrayType) {
        GenericArrayType genericArrayType = (GenericArrayType) type;
        Type componentType = genericArrayType.getGenericComponentType();
        // 在这里你可以获取 T[] 中的 T 类型信息
    }
    
  4. WildcardType 接口:

    • WildcardType 表示通配符类型,例如 ? extends Number。这帮助我们了解通配符的上下界信息。
    import java.lang.reflect.WildcardType;
    import java.lang.reflect.Type;
    
    Type type = MyClass.class.getField("wildcard").getGenericType();
    
    if (type instanceof WildcardType) {
        WildcardType wildcardType = (WildcardType) type;
        Type[] upperBounds = wildcardType.getUpperBounds();
        Type[] lowerBounds = wildcardType.getLowerBounds();
        // 在这里你可以获取通配符的上下界信息
    }
    
  5. TypeVariable 接口:

    • TypeVariable 表示类型变量,例如 。这是我们在泛型类或方法中定义的变量,可以代表任何类型。
    import java.lang.reflect.TypeVariable;
    import java.lang.reflect.Type;
    
    Type type = MyClass.class.getTypeParameters()[0];
    
    if (type instanceof TypeVariable) {
        TypeVariable<?> typeVariable = (TypeVariable<?>) type;
        Type[] bounds = typeVariable.getBounds();
        // 在这里你可以获取类型变量的上界信息
    }
    

总的来说,Type 接口及其实现类帮助我们在编写更加灵活、通用的代码时,了解和处理泛型类型的信息。如果你写的是相对简单的代码。

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