Java注解与反射

249 阅读54分钟

注解 Annotaion

基本概念

  • Annotation是JDK 5.0及以后版本引入的,它可以用于创建文档、代码分析、编译检查以及编译时和运行时的处理。
  • Annotation是接口的一种特殊实现,程序可以通过反射机制相关的API来访问Annotation信息。
  • Annotation不会影响程序代码的执行。无论Annotation如何变化,代码都始终如一地执行。Java语言解释器在工作时会忽略这些Annotation,因此在JVM中这些Annotation是“不起作用”的,只能通过配套的工具才能对这些Annotation类型的信息进行访问和处理。

用途

  1. 提供编译时的静态检查:例如,@Override注解用于指示方法应该覆盖父类中的方法,如果没有正确覆盖,编译器会产生警告或错误。此外,某些注解还可以强制要求某个方法必须在特定的条件下调用,否则编译器会报错。
  2. 自动生成代码:例如,Lombok库使用注解来生成getter、setter和构造函数等代码,通过这种方式,开发者可以减少样板代码的编写,提高代码的可读性和可维护性。另外,通过注解还可以自动生成序列化和反序列化的代码,简化对象的持久化操作。
  3. 提供运行时的动态处理:注解可以在运行时通过反射机制获取并处理,从而实现一些动态的功能。例如,Spring框架中的@Autowired注解用于自动注入依赖,而JPA(Java Persistence API)使用注解来定义实体类和表之间的映射关系。此外,许多框架和库也使用注解来配置和管理程序的运行时行为。
  4. 代码文档和辅助:注解可以作为一种在代码中提供额外信息的方式,帮助开发者了解代码的用途和行为。例如,JUnit测试框架使用@Test注解来标识测试方法。此外,通过代码里标识的元数据还可以生成文档,如JavaDoc文档。

格式
使用“@”符号作为标记,后面跟着注解的名称,再后面可以包含一对括号,括号中用于提供注解的参数。参数的类型可以是基本数据类型、枚举类型、字符串类型等。
语法格式

@AnnotationName(parameter1 = value1, parameter2 = value2, ...)
//@[注解的名称]([注解的参数名1]=[相应参数的值1],[注解的参数名2]=[相应参数的值2], ...)
如果注解没有参数,那么可以省略括号:@AnnotationName
如果注解只有一个参数,并且该参数名为 `value`,那么可以省略参数名,只写参数值:@AnnotationName("value1")

应用位置
Annotation可以像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量(从Java 8开始)的声明中,相当于给它们添加了额外的辅助信息,可以通过反射机制编程实现对这些元数据的访问

内置注解

定义:预定义在Java语言中的一组注解,提供丰富的元数据信息,用于改善代码质量、增强代码的可读性和可维护性,以及优化编译器的行为。

重要的内置注解

  1. @Override
  • 作用:此注解只适用于方法,表示一个方法打算重写(override)父类中的另一个方法。
  • 使用场景:当子类想要覆盖父类中的方法时,使用此注解可以确保在编译时检查方法的签名是否与父类中被覆盖的方法的签名相匹配。
  • 示例:@Override public String toString() { ... }
  1. @Deprecated
  • 作用:此注解可以应用于方法、属性、类,表示该元素已经被弃用,不建议程序员使用。
  • 使用场景:用来标记那些已经过时或不再建议使用的API、方法、属性或类。当开发者在代码中使用了被标记为@Deprecated的元素时,编译器会发出警告,提示开发者该元素已经过时,建议寻找替代的方法或实现。
  • 示例:@Deprecated public void oldMethod() { ... }
  1. @SuppressWarnings
  • 作用:此注解用于抑制编译器产生的特定警告信息。
  • 使用场景:当开发者确信某段代码产生的警告是安全的,或者想要忽略某些不关心的警告时,可以使用此注解。
  • 参数:此注解需要一个参数来指定要抑制的警告类型,如"unchecked"(忽略“未检查的类型转换”), "deprecation"(忽略“已弃用API”的警告)等,或者使用"all"来抑制所有警告。
    • 同时忽略多种类型的警告:@SuppressWarnings({"[警告类型1]", "[警告类型2]"})或者@SuppressWarnings(value={"[警告类型1]", "[警告类型2]"})
  • 示例:@SuppressWarnings("unchecked") public void methodWithWarnings() { ... }

元注解 meta-annotation

定义:一种特殊的注解,提供关于这些注解的额外信息或说明,用于对其他注解进行注解,帮助开发者定义注解的行为和特性。

JDK 1.5引入了四个标准的元注解类型

  1. @Target:用于指定被修饰的注解能用于哪些地方。
    例如,它可以用于修饰一个注解,表明这个注解只能用于方法、类、字段等。常见的参数值有:
  • ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明。
  • ElementType.FIELD:用于描述实例变量。
  • ElementType.METHOD:用于描述方法。
  1. @Retention:用于指定被修饰的注解的生命周期。有三个参数值:
  • RetentionPolicy.SOURCE:注解只保留在源文件中,当Java文件编译成class文件的时候,注解被遗弃。
  • RetentionPolicy.CLASS:注解被保留到class文件,但JVM加载class文件时候被遗弃,这是默认的生命周期。
  • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,JVM加载class文件之后,仍然存在。
  1. @Documented:用于指定被修饰的注解将被javadoc工具提取成文档。使用该元注解修饰的注解,其信息可以生成到javadoc文档中。
  2. @Inherited:用于指定被修饰的注解具有继承性。如果一个类使用了这个注解,那么其子类也会继承这个注解。

自定义注解 Custom Annotations

定义:Java编程语言中的一种特性,允许开发人员根据自己的需求定义一些标记,用于标识 类、方法、变量等代码元素,并使用元注解来描述其使用方式。这些自定义的注解可以在代码编写和执行的过程中被读取,以实现特定的功能。自定义注解是一种元数据(Metadata)形式,它不直接影响程序代码的执行,但可以为代码提供额外的信息或指示,从而增加代码的灵活性和可扩展性。

使用范围

用于创建文档、代码分析、编译检查、运行时检查等场景。例如,可以创建一个自定义注解来标记某个方法已经过时,然后在编译时检查是否还有代码调用了这个方法,如果有则发出警告。或者,可以创建一个自定义注解来标识某个类实现了特定的接口,然后在运行时检查这个类是否确实实现了该接口。

使用流程

  1. 定义注解:这相当于定义标记,定义注解的语法格式为@interface [自定义注解名] {}。注解还可以定义属性,语法格式为@interface [自定义注解名] {[参数类型] [参数名]() default [参数默认值];}
  2. 配置注解:将定义的标记应用到需要用到的程序代码中,即把注解写在类、方法、变量等关键节点上。
  3. 解析注解:在编译期或运行时检测到这些标记,并根据标记进行特殊操作。这通常需要编写代码来读取注解信息,并根据这些信息执行相应的逻辑。

使用示例

无参数的自定义注解
首先,需要定义一个注解。在Java中,注解是一个特殊的接口,它使用@interface关键字来定义。
下面,定义一个名为AuthToken的注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthToken {
}

在上面的代码中,使用了三个元注解:

  • @Target:这个元注解定义了我们的AuthToken注解可以被应用到哪些元素上。在这个例子中,我们的注解可以被应用到方法或类上。
  • @Retention:这个元注解定义了我们的AuthToken注解的生命周期。RUNTIME意味着这个注解在运行时仍然可见,因此我们可以使用反射来获取它。
  • @Documented:这个元注解意味着当我们在代码中使用这个注解时,它会被包含在生成的Javadoc中。

然后,可以在方法或类上使用这个注解:

@AuthToken
public class MyService {

    @AuthToken
    public void myMethod() {
        // ...
    }
}

在这个例子中,我们在MyService类和myMethod方法上都使用了AuthToken注解。

最后,可以在运行时获取并使用这个注解。例如,可以使用反射来获取 类、方法上的注解,并检查它是否存在

带参数的自定义注解
自定义注解的语法可以定义带有参数的注解。这些参数可以有默认值,也可以在使用注解时显式指定。当定义一个带有参数的注解时,可以指定这些参数的类型和名称。这些参数在注解的生命周期中可以被读取和使用。

下面是一个带有参数的自定义注解的示例:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
    // 定义注解参数
    String value() default "";   // 默认值为空字符串
    int number() default 0;      // 默认为0
    boolean enabled();           // 没有默认值
}

在这个例子中,定义了一个名为MyCustomAnnotation的自定义注解,它有三个参数:

  • value:类型为String,默认值为空字符串。
  • number:类型为int,默认值为0。
  • enabled:类型为boolean,默认值为true

当使用这个注解时,可以选择性地提供这些参数的值,或者使用它们的默认值。下面是使用这个注解的不同方式:

public class AnnotationExample {

    // 使用注解,不使用任何参数,所有参数都将使用默认值
    @MyCustomAnnotation
    public void methodWithDefaultValues() {
        // ...
    }

    // 使用注解,只指定value参数
    @MyCustomAnnotation(value = "SomeValue")
    public void methodWithValue() {
        // ...
    }

    // 使用注解,指定所有参数
    @MyCustomAnnotation(value = "AnotherValue", number = 42, enabled = false)
    public void methodWithAllValues() {
        // ...
    
    //如果参数没有默认值,那么使用这个注解参数就必须明确为其赋值。否则,编译器会报错  
    @MyCustomAnnotation(enabled = false)  // 必须为enabled参数赋值  
    public void methodWithAllValues() {
        // ...
    }
}

当想要读取这些注解参数的值时,可以使用反射API。

反射概述

动态语言与静态语言

动态语言

  • 运行时结构变化:动态语言允许在运行时改变代码的结构。这包括添加新的函数、对象或代码片段,以及删除或修改已有的函数。
  • 灵活性:由于能够在运行时改变结构,动态语言通常更加灵活,更容易适应不同的场景和需求。
  • 主要语言:常见的动态语言包括 Object-C、C#、JavaScript、PHP、Python 等。

静态语言

  • 固定结构:静态语言在编译时确定代码的结构,并在运行时保持不变。这意味着你不能在运行时添加、删除或修改函数和对象。
  • 性能:由于结构在编译时就已经确定,静态语言通常比动态语言具有更好的性能,尤其是在执行大量计算任务时。
  • 主要语言:常见的静态语言包括 Java、C、C++ 等。
  • 在 Java 中,所有的代码都必须在编译时确定,不能在运行时改变。尽管 Java 是一种静态语言,但它通过反射机制提供了一定程度的动态性。通过反射,Java 程序可以在运行时获取和操作类的元数据,从而模拟一些动态语言的特性。

总结

  • 静态语言:结构固定,编译时确定,运行时不变,性能通常更好。
  • 动态语言:结构灵活,可以在运行时改变,更加适应快速变化的需求,但可能牺牲一些性能。

反射概念

原理:加载完类之后,在堆内存的方法区中就产生了一个Class类的对象(即Class对象,对象的类型为Class,一个类只有一个Class对象),这个对象包含了完整的类的结构信息。可以通过这个对象看到类的结构,这个Class对象就被视为一个“镜子”。所以,透过这个镜子 查看和操作 类的结构被形象地称之为——反射。

用途:Reflection(反射)是Java被视为动态语言的关键,反射(Reflection)机制允许程序在运行时对任何类的内部细节进行访问和操作。通过反射API,可以获取类的结构信息,包括其属性、方法和构造器等,并可以在运行时创建对象、调用方法、访问和修改属性等。

用法:使用反射的一个常见方式是通过 Class类 的对象来获取和操作类的信息。 Class类 是Java所有类型信息的元类,每个类都有一个与之对应的 Class对象 。这个 Class对象 包含了关于该类的所有信息,包括其名称、父类、实现的接口、字段、方法等。

  • 元类:一种用于定义类的类,通过元类,你可以控制类的创建过程,动态地修改类的结构,或者实现类的特定行为。
  • 类型信息:方法,构造方法,成员变量,类名,继承关系,注解,访问修饰符,常量等。

语法格式

动态加载目标类并获取其 Class 对象

Class [变量名] = Class.forName("[类的完全限定名]")  //如果类加载成功,[变量名]就会持有该类的 Class 对象

Class.forName() :这是java.lang.Class类中的一个静态方法,接受一个字符串参数,即要加载的类的完全限定名(包括包名)。用于在运行时动态加载类,并返回与该类对应的 Class 对象。如果该类或接口尚未被加载和初始化,那么forName()方法会执行这些操作。

示例

public class Test02 {
    public static void main(String[] args) throws ClassNotFoundException {
        
        Class c1 = Class.forName("com.blue.reflection.User");  //第一次调用,加载类并通过反射获取Class对象
        System.out.println(c1);  //打印c1的toString()结果,通常显示类的全名

        Class c2 = Class.forName("com.blue.reflection.User");  //第二次调用,从缓存中通过反射获取已加载的类的Class对象,不会重新加载类
        System.out.println(c2.hashCode());  //打印c2的hashCode值
    }
}

//输出
class com.blue.reflection.User  // 这是c1的toString()输出,显示类的全名  
[hashCode的值]                  // 这是c2的hashCode()输出,由于c1和c2引用同一个Class对象,它们的hashCode将相同

说明:JVM使用类加载器(ClassLoader)来加载类,并且类加载器会缓存已经加载的类。一旦类被加载到JVM中,后续的Class.forName()调用(对于相同的类名)将直接从缓存中获取类的Class对象,而不会重新加载类。

反射功能

在运行时

  • 判断任意一个对象所属的类
  • 构造任意一个类的对象
  • 判断任意一个类所具有的成员变量和方法
  • 获取泛型信息
  • 调用任意一个对象的成员变量和方法
  • 处理注解
  • 生成动态代理

反射利弊

优点:增大程序的灵活性和可扩展性。允许程序在运行时动态地创建对象、调用方法、访问和修改成员变量等
缺点:降低性能,破坏类的封装性。反射需要在运行时进行解析和调用,这通常会比直接代码执行慢,因此在性能敏感的场景中应尽量避免使用反射

反射API

反射相关的API主要位于java.lang.reflect包中。这个包提供了一组类和接口,用于在运行时对任意类进行内省(introspection),即查看类的结构、属性和行为。
主要的API有:

  • java.lang.Class:代表一个类
  • java.lang.reflect.Method:代表类的方法
  • java.lang.reflect.Field:代表类的成员变量
  • java.lang.reflect.Constructor:代表类的构造器

Class类及对象

Class类:一个用于描述其他类的类型信息的特殊的类,是Java反射的源头,针对任何想要动态加载、运行的类,唯有先获得相应的Class对象

Class对象
生成与储存:当编写一个类定义时,Java编译器会为该类生成一个对应的Class对象。这个Class对象在程序运行时被存储在Java虚拟机(JVM)的方法区中,是Class类的一个实例。
与类的关系:如上所述,每个类在JVM中都有唯一且对应的Class对象,这个对象详细描述了对应类的内部结构和行为,包含了关于该类的所有元信息,如类的名称、继承的父类、实现的接口、成员变量、方法以及构造器等,可以视作类的“元数据容器”或“说明书”。
用途:可以通过Class对象访问类的元数据,甚至可以在运行时动态地创建类的实例、访问其成员变量和调用其方法,这是Java反射机制的基础。

getClass()方法

调用:在Java中,Object 类是所有类的根类,它定义了一些基本的方法,其中之一就是 getClass()。由于所有类都继承自 Object 类,因此(无论它们属于哪个类)所有对象实例都可以调用 getClass() 方法。

用途:对象的引用指向了内存中的该对象实例,通过对象的引用 调用getClass() 方法可以查询这个对象实例属于哪个类,并返回一个代表该类的 Class 对象

理解
运行时类:getClass() 返回的是对象实例的运行时类的 Class 对象。这意味着,即使有一个父类类型的引用指向了一个子类对象,getClass() 也会返回子类的 Class 对象。这是动态分派(dynamic dispatch)的一个例子,它允许在运行时确定实际的对象类型。
对象与类的关系:每个对象都有一个与之关联的类,这个类定义了对象的结构和行为。getClass() 方法允许查询这个关联关系,获取到对象的类信息。
反射的入口:getClass() 方法是进入反射API的入口之一。通过它,可以获取到对象的 Class 对象,进而使用反射API提供的各种方法来查询和修改对象的属性和行为。

示例

class Animal {
    // ...
}

class Dog extends Animal {
    // ...
}

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog(); // myDog 是一个 Animal 类型的引用,但它指向一个 Dog 对象

        // 使用 getClass() 方法获取对象的运行时类的 Class 对象
        Class<?> dogClass = myDog.getClass();

        // 输出结果应该是 "Dog",因为 myDog 实际上指向的是一个 Dog 对象
        System.out.println(dogClass.getName());
    }
}

说明:在这个例子中,尽管 myDog 是一个 Animal 类型的引用,但它实际上指向了一个 Dog 类型的对象。当调用 myDog.getClass() 时,返回的是 Dog 类的 Class 对象,而不是 Animal 类的 Class 对象。这是因为 getClass() 方法返回的是对象实例的实际运行时类的 Class 对象。

Class类的常用方法

详述穿插在 反射操作的步骤 部分

语法格式:[Class对象名].[方法名]

一个规律

  • getXXX() 方法通常返回所有公共的(public)XXX(字段、构造器或方法)且包括从父类继承的
  • getDeclaredXXX() 方法返回当前类声明的所有XXX,无论其访问级别如何,但不包括从父类继承的

Java中,首先需要一个 Class 类型的对象,然后调用该对象上的方法来获取有关类的信息和执行反射操作

可以有Class对象的类型

  • class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类。
  • interface:接口
  • []:数组
  • enum:枚举
  • annotation:注解@interface
  • 基本内置类型的包装类
  • void
  • Class类

演示如何获取上述各种元素的Class对象

import java.util.ArrayList;  
import java.util.List;  
  
public class MainClass {  
  
    // 静态内部类  
    public static class StaticNestedClass {}  
  
    // 成员内部类  
    public class InnerClass {}  
  
    public static void main(String[] args) {  
    
        //创建了一个存储 Class 对象的 空列表,其中这些 Class 对象可以表示任何类型
        List<Class<?>> classes = new ArrayList<>();  
        //List<Class<?>>:是一个泛型接口的声明,表示一个列表(List),该列表的元素是某种未知的Class类型
        //Class<?> 表示未知类型的 Class 对象。这意味着列表可以存储任何类型的 Class 对象
        //classes 是一个变量,引用一个可以存储 Class对象的列表
        //new ArrayList<>():是ArrayList类的一个构造方法的调用,用于创建一个新的ArrayList实例。ArrayList是List接口的一个常用实现
        // = :用于将新创建的ArrayList实例分配给classes变量
        
        // 外部类的Class对象  
        classes.add(MainClass.class);  
  
        // 成员内部类的Class对象(需要通过外部类实例来获取)  
        classes.add(MainClass.InnerClass.class);  
  
        // 静态内部类的Class对象  
        classes.add(MainClass.StaticNestedClass.class);  
  
        // 局部内部类(在方法中定义)  
        class LocalInnerClass {}  
        classes.add(LocalInnerClass.class);  
  
        // 匿名内部类(没有名字,但可以通过类型推断来获取其Class对象)  
        Runnable runnable = new Runnable() {  
            @Override  
            public void run() {}  
        };  
        classes.add(runnable.getClass());  
  
        // 接口的Class对象  
        interface MyInterface {}  
        classes.add(MyInterface.class);  
  
        // 数组的Class对象  
        int[] array = new int[0];  
        classes.add(array.getClass());  
  
        // 枚举的Class对象  
        enum MyEnum {VALUE1, VALUE2}  
        classes.add(MyEnum.class);  
  
        // 注解的Class对象(@interface)  
        @interface MyAnnotation {}  
        classes.add(MyAnnotation.class);  
  
        // 基本类型的包装类的Class对象  
        classes.add(Integer.class); // int的包装类  
        // 类似地,还可以添加其他基本类型的包装类,如Double.class, Character.class等  

        // Class类的Class对象  
        classes.add(Class.class);  
        
        //以上程序:
        //调用 classes 列表的 add 方法
        //将 某一元素的 Class 对象作为参数传递给 add 方法
        //add 方法将 该元素的 Class 对象添加到 classes 列表的末尾
   
        // 注意:这里没有使用System.out.println来输出,但在实际应用中可能需要某种形式的输出来验证结果  
    }  
}
// 另外:获取void类型的Class对象表示(实际上是通过Void.TYPE获取)Class<Void> voidClass = Void.TYPE; 

反射操作的步骤

1. 获取Class对象

这是反射的第一步,我们需要先获取到目标类的Class对象。可以通过三种返回Class对象的方式获取:使用Class类的forName()静态方法,使用对象的getClass()方法,或者直接使用.class语法。

  • Class.forName("[类名]"):需要传入类的全名(包括包名),这种方法在运行时加载类,具有动态性,扩展性最强
  • [对象名].getClass():通过已经创建的对象来获取其 Class对象,这种方法需要在运行时才能确定对象的类型
  • [类名].class:直接通过类名字符串来获取其 Class对象,这种方法在编译时就确定了类的类型。
  • TYPE属性:为了通过反射等机制来统一处理基本类型和对象类型,Java为每个基本类型都提供了一个“包装类”,并且每个包装类都有一个静态的 TYPE 字段,该字段引用了表示该基本类型的 Class对象
    • byte 对应 Byte.TYPE;short 对应 Short.TYPE;int 对应 Integer.TYPE;long 对应 Long.TYPE;
    • float 对应 Float.TYPE;double 对应 Double.TYPE;char 对应 Character.TYPE;boolean 对应 Boolean.TYPE
  • [子类Class对象].getSuperclass():通过子类Class对象获取其父类的Class对象
     Class<?> variableName = Class.forName(fullyQualifiedClassName);
     //variableName是一个变量名,用于存储 获取的Class对象
     //fullyQualifiedClassName 是一个字符串,表示类的完全限定名(包含包名的类名)
    
     Class<? extends ObjectType> variableName = object.getClass();
     //object 是想要获取其运行时类的对象名
     //ObjectType 是object对象的类型(即所属类名)或它的任何超类型(包括Object类本身)。在实际编程中,通常不需要明确地提供类型参数,因为编译器可以进行类型推断
     
     Class<?> variableName = ClassName.class;
     //ClassName 是目标类的名称
     
     Class<?> variableName = [TYPE属性];
     
     Class<?> parentvariableName = childvariableName.getSuperclass();
     //parentvariableName 是一个变量名,用于存储 获取的父类的Class对象
     //childvariableName 是一个变量名,表示 已获取的子类的Class对象
    
    如何选择
    若已知类的确切名称,则使用 [类名].class,该方法最为安全可靠,程序性能最高
    若已知类的实例,则使用[对象名].getClass()
    若 需要动态加载类;已知一个类的全类名,且该类在类路径下,则使用Class.forName("[类名]")
    对于内置基本数据类型,则使用 TYPE属性

2. 获取类的成员

获取到了Class对象后,就可以通过Class对象来反射获取运行时类的完整结构:Field、Method、Constructor、 Superclass、 Interface、 Annotation,包括构造方法、成员方法、成员变量等。

  1. 获取构造方法:使用Class类中的getConstructorgetDeclaredConstructorgetConstructorsgetDeclaredConstructors方法

    getConstructor、getDeclaredConstructor

    getConstructor 返回一个表示 MethodClass类中 接受指定类型参数的 public构造方法的 Constructor对象
    Constructor<?> constructorObject = [Class对象].getConstructor(String.class, int.class);
    // constructorObject 是一个Constructor<?>类型的变量,它持有一个已经通过反射获取到的 Constructor对象的引用,即 [Constructor对象]
    //String.class和int.class是构造方法参数的类型
    //有多个参数用逗号隔开;字符串参数用双引号;这里参数的类型和顺序必须与方法声明时一致
    

    getConstructors、getDeclaredConstructors

    getConstructors 返回一个 包含所有表示 MethodClass类中 public构造方法的 Constructor对象的数组
    Constructor<?>[] constructorObjectArray = [Class对象].getConstructors();
    //constructorObjectArray是一个数组类型的变量,它持有一个已经通过反射获取到的 Constructor对象的数组的引用
    

    获取全部的构造方法:

    Constructor<?>[] constructors = [Class对象].getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor.toString());
        }
    
  2. 获取成员方法:使用Class对象的getMethod()或getDeclaredMethod()方法

    Method variableName = [Class对象].getMethod(methodName, parameterTypes);
    //variableName是一个Method类型的变量,它持有一个已经通过反射获取到的 Method对象的引用
    //methodName 是一个字符串,指定要获取的方法的名称
    //parameterTypes 是一个Class对象的数组,表示想要获取的方法的参数类型。
    //如果方法没有参数,则此数组应为空(但不是null,而是长度为0的数组,如 new Class[0])
    //需要传参数的情况:存在重载,通过参数可找到指定的方法
    关于()内的内容:
    ...("add", int.class, String.class) // 获取有一个int类型参数和的一个String类型参数的方法add  
    ...("myMethod", new Class[0]) // 获取没有参数的方法myMethod 
    //有多个参数用逗号隔开;字符串参数用双引号;这里参数的类型和顺序必须与方法声明时一致
    
    [Class对象]的获取:常见的是直接使用类字面量(如 MethodClass.class)来获取
    

    获取全部的方法:

    Method[] methods = [Class对象].getMethods();
    for (Method method : methods) {
        System.out.println(method.toString());
    }
    
  3. 获取成员变量:使用Class对象的getField()或getDeclaredField()方法

     Field variableName = [Class对象].getField("fieldName"); 
     
     获取[Class对象]类中声明的所有公共字段,包含在返回的 arrayName 数组中
     Field[] arrayName = [Class对象].getFields();
     
     //variableName 是一个Field类型的变量,它持有一个已经通过反射获取到的Field对象的引用
     //fieldName是要获取的字段的名称
    

    获取全部的Field:

    Field[] fields = [Class对象].getFields();
    for (Field field : fields) {
        System.out.println(field.toString());
    }
    
  4. 获取类名
    通过Class对象的getSimpleName()getName()getCanonicalName()方法,可以获取类的名称

    String className = clazz.getSimpleName(); // 获取简单的类名(不包含包名)
    String fullName = clazz.getName(); // 获取完整的类名(包含包名) 
    String canonicalName = clazz.getCanonicalName(); // 获取规范的类名(去除重复包名)
    
  5. 获取实现的全部接口: 使用getInterfaces()方法可以获取类实现的所有接口。

    Class<?>[] interfaces = [Class对象].getInterfaces();
    for (Class<?> anInterface : interfaces) {
        System.out.println(anInterface.getName());
    }
    
  6. 获取所继承的父类: 使用getSuperclass()方法可以获取类的直接父类。

    Class<?> superclass = [Class对象].getSuperclass();
    if (superclass != null) {
        System.out.println(superclass.getName());
    }
    
  7. 获取注解: 使用getAnnotations()getDeclaredAnnotations()方法可以获取类上声明的所有注解。

    Annotation[] annotations = [Class对象].getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation.toString());
    }
    

    此外,还可以获取特定类型的注解,如:

    MyAnnotation myAnnotation = [Class对象].getAnnotation(MyAnnotation.class);
    if (myAnnotation != null) {
        System.out.println(myAnnotation.value());
    }
    

3. 访问类的成员

获取到了类中成员的对象后,我们就可以通过相应的方法来访问这些成员了。

  • 创建类对象:对于构造方法,我们可以通过调用构造方法对象的newInstance()方法创建类对象。如果构造方法需要参数,则需要先获取对应参数的构造器对象,然后通过该构造器对象的newInstance([实际参数])进行对象的初始化

    使用无参构造方法
    [ClassName] objectname = constructorObject.newInstance();  
    //ClassName是要创建类对象的类的名称
    //objectname是一个对象名,实质是一个 存储了新创建的类对象的 引用变量
    //constructorObject 即[Constructor对象],即[Class对象].getDeclaredConstructor()
    
    使用有参构造方法获取指定类中接受特定参数列表构造器,并创建该类的一个实例
    //1. 通过getDeclaredConstructor 方法获取指定类中接受特定参数列表构造器,并将其引用 赋值给名为 constructorObject 的 Constructor<?> 变量
    Constructor<?> constructorObject = [Class对象].getDeclaredConstructor(String.class, int.class);
    //2. 通过先前获取的构造器 constructorObject 创建该类的一个新实例,并将其引用 赋值给名为 objectname 的 ClassName 类型变量
    ClassName objectname = (ClassName) constructorObject.newInstance([实参列表]); 
    或者
    //1.创建参数数组
    Object[] arrayName = new Object[]{[实参列表]};
    //2.调用构造器创建实例
    ClassName objectname = (ClassName) constructorObject.newInstance(arrayName);
    
    //ClassName是要创建类对象的类的名称
    //[实参列表]:有多个参数用逗号隔开;字符串参数用双引号;这里参数的类型和顺序必须与方法声明时一致
    
    如果构造器不是 public 的,则需要设置可访问性为 true,即 在第1步与第2步之间进行如下操作:  
    variableName.setAccessible(true);  
    //这种做法可能会破坏封装性,并且可能带来安全风险。在可能的情况下,最好通过类的公共接口与对象进行交互。
    //使用反射和setAccessible(true)应该是一种最后的手段,当你无法通过其他方式实现所需功能时才考虑使用。
    
  • 调用方法:通过调用方法对象的invoke(Object obj, Object... args)方法执行类对象对应的方法。需要注意的是,如果方法是静态的,那么可以通过Class对象直接调用,不需要创建类的实例。另外,如果方法的访问权限是private的,那么需要先通过setAccessible(true)方法设置访问权限

    调用静态方法  
    variableName.invoke(null, [实参列表]);
    //variableName是一个Method类型的变量,它持有一个已经通过反射获取到的Method对象的引用
    // 静态方法可以通过Class对象直接调用,所以第一个参数是null
    
    
    调用非静态方法
    variableName.invoke(methodInstance, [实参列表]);
    //variableName是一个Method类型的变量,它持有一个已经通过反射获取到的Method对象的引用
    //methodInstance是variableName代表的方法(即要调用的方法)所属的类的 一个实例,表示要在这个实例上调用方法
    
    //[实参列表]:有多个参数用逗号隔开;字符串参数用双引号;这里参数的类型和顺序必须与方法声明时一致
    //如果没有参数,()中就只写null(静态方法)或实例名(非静态方法)
    
    调用私有方法
     privateMethod.setAccessible(true); // 设置访问权限,以便  
     
    如果方法不是 public 的,则需要设置可访问性为 true,即 在第1步与第2步之间进行如下操作:  
    variableName.setAccessible(true);  
    //这种做法可能会破坏封装性,并且可能带来安全风险。在可能的情况下,最好通过类的公共接口与对象进行交互。
    //使用反射和setAccessible(true)应该是一种最后的手段,当你无法通过其他方式实现所需功能时才考虑使用
    
  • 对于成员变量,我们可以使用set()和get()方法来设置和获取变量的值

    使用get()方法获取指定实例对象classInstance上由Field对象表示的字段的当前值  
    FieldType fieldValue = (FieldType) variableName.get(classInstance);  
    //FieldType 是字段的实际类型
    //fieldValue 是一个局部变量,用于存储从get()方法返回的字段值
    //(FieldType) 是一个强制类型转换,它将get()方法返回的Object类型的值转换为FieldType类型
    //variableName 是一个Field类型的变量,它持有一个已经通过反射获取到的Field对象的引用
    //classInstance 是要获取其值的字段所在类的对象
    //在大多数情况下,进行(FieldType)强制类型转换是必要的、不可省略的。除非FieldType是Object类型或者有其他方式来确保类型的正确性(比如使用泛型并已经确保了类型安全)
     
    使用set()方法设置指定实例对象classInstance上由Field对象表示的字段的新值 
    variableName.set(classInstance, newValue);
    //variableName 是一个Field类型的变量,它持有一个已经通过反射获取到的Field对象的引用
    //classInstance 是要设置其值的字段所在类的对象
    //newValue 是要设置的新值,它的类型必须与字段的类型兼容
    
    私有字段:如果目标字段是私有的(private),则需要设置可访问性为 true,即 在设置和获取变量的值之前进行如下操作: 
    variableName.setAccessible(true);  
    //variableName 是一个Field类型的变量,它持有一个已经通过反射获取到的私有字段的Field对象的引用
    
    静态字段:如果目标字段是静态的(static),那么classInstance可以是null,因为静态字段不属于任何特定实例,而是属于类本身
    //不必要的是,传递类的任何实例或类的Class对象 作为classInstance参数也是可以的
    

setAccessible

  • setAccessible作用是启动和禁用访问安全检查的开关
  • Method和Field、Constructor对象都有setAccessible()方法
  • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查
  • 参数值为false则指示反射的对象在使用时应该实施Java语言访问检查
  • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true,使得原本无法访问的私有成员也可以访问

类加载内存分析

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

  1. 加载(Loading) 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,此过程由类加载器完成
  2. 链接(Linking) 将类的字节码文件(二进制数据)加载到JVM中,并经过验证、准备和解析等阶段,以便该类能够在JRE提供的环境中正确执行的过程
  • 验证:确保加载的类信息符合JVM规范,没有安全方面的隐患
  • 准备:为类的静态变量分配内存,并将其初始化为默认值(如基本类型的0或引用类型的null),这些内存都将在方法区中进行分配
  • 解析:JVM虚拟机常量池内的符号引用(常量名)转换为直接引用(地址)
  1. 初始化(Initialization) 为类的静态变量赋予正确的初始值,执行静态代码块
  • 初始化过程是由JVM负责的,通过执行一个特殊的类构造器方法<clinit>()来完成。
    <clinit>()方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并产生的。这个方法不是用来构造类对象的,而是用来初始化类信息的。当一个类需要初始化时,如果它的父类还没有初始化,那么会先触发父类的初始化。这是为了确保父类中的静态变量和静态代码块在子类之前被正确执行。
  • 在多线程环境中,虚拟机保证了()方法的正确加锁和同步,以确保只有一个线程能够执行类的初始化

示例

public class Test05 {                                 //1.加载Test05类  
    public static void main(String[] args) {
    A a = new A();                                    //5.创建A类实例
        System.out.println(A.m);                      //7.访问A类静态变量m,并打印A类静态变量m的值 (300)
    }
}
class A{                                              //2.加载A类
    static{                                           //3.A类静态代码块初始化(static m = 300),并打印输出语句
        System.out.println("A 类静态代码块初始化");
        m=300;
    }
    static int m =100;                                //4.A类静态变量m初始化(m = 100)

    public A() {                                      //6.A类的无参构造初始化,并打印输出语句
        System.out.println("A类的无参构造初始化");
    }
}
    
//输出:
A 类静态代码
A类的无参构造初始化
100

从内存和类加载的角度来逐步分析这个程序:

  1. 类加载
  • 当程序开始运行时,JVM首先加载Test05类和A类。加载过程包括读取类的字节码文件(.class文件),并将其转换为内部的数据结构,以便JVM能够识别和使用
  • 在加载A类时,会注意到A类有一个静态代码块和一个静态变量m。静态变量会在类加载时进行初始化
  • 静态变量和静态代码块只会在类首次被加载到JVM时初始化一次,无论创建多少个对象实例,静态变量的值都是相同的,且静态代码块只执行一次
  1. 静态变量和静态代码块初始化
  • 在类加载过程中,JVM会执行静态代码块。因此,当A类被加载时,它的静态代码块会被执行,输出“A 类静态代码块初始化”
  • 在静态代码块中,静态变量m被赋值为300。由于这是在静态代码块中进行的,因此m的初始化发生在静态代码块执行期间
  1. 创建对象
  • Test05类的main方法中,通过A a = new A();创建了一个A类的实例
  • 当创建对象时,首先会触发A类的初始化(如果尚未初始化)。由于静态变量和静态代码块已经在类加载时初始化,因此这一步不会再次初始化静态变量或执行静态代码块
  • 接着,会调用A类的无参构造器来初始化新创建的对象。构造器执行时,会输出“A类的无参构造初始化”
  1. 访问静态变量
  • 在对象创建后,System.out.println(A.m);语句输出静态变量m的值。由于静态变量在类加载时已经被初始化为300,因此将输出300

图示 屏幕截图 2024-02-12 102938.png 图析

1.加载, 将class文件加载到内存,在堆中产生一个类对应的Class对象,方法区有对应类的方法属性等  
2.链接, 为static分配内存并设置默认初始化,链接结束后 m=0  
3.初始化,会合并静态代码块和属性  
  调用<clinit>(){  
  //会按顺序收集并执行类中的所有静态变量的初始化语句,包括直接赋值语句和静态代码块中的语句  
      System.out.println("A类静态代码块初始化");  
      m = 300;  
      static int m = 100;  
  }  
  m=100  

类初始化

何时发生类初始化

类的主动引用 (一定会发生类的初始化)

  • 虚拟机启动,先初始化main方法所在的类
  • 创建类的实例(通过new关键字或反序列化)
  • 访问或修改一个类的非static final静态字段
  • 调用一个类的非static final静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当初始化一个类, 如果其父类没有被初始化,则先会初始化它的父类

类的被动引用 (不会发生类的初始化)

  • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化 (因为所调数据为父类的,只要父类初始化即可)
  • 通过数组定义类引用,不会触发此类的初始化 (new 数组的话只是声明其实还没有指向具体对象,所以没有初始化,即没有初始化这个类的实例,只是预先申请了空间)
  • 引用常量不会触发此类的初始化 (在编译时就已经分配了值,不需要加载类(执行<clinit>()方法)来获取这些值)

类加载器的作用

类加载器的作用:负责执行类加载过程,把类(class)装载进内存,它是JVM的重要组成部分,确保了类的正确加载和初始化。

类加载的作用:将class文件字节码内容加载到JVM的内存中,并将这些静态数据转换成方法区中的运行时数据结构。同时,JVM会在堆内存中创建一个对应的java.lang.Class对象,该对象作为该类在方法区中数据的访问入口。通过这个Class对象,JVM可以获取类的所有静态数据和方法,从而能够创建类的实例、访问静态字段和调用静态方法。

类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某企类被加载到类加载器中,它将维持加载(缓存) 一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

图示 屏幕截图 2024-02-12 113238.png

JVM规范定义了如下类型的类的加载器

  1. 启动类加载器(Bootstrap Class Loader) :用C++语言编写,嵌套在JVM内部。这是最顶层的加载类,用于加载Java核心库,如rt.jarresources.jarcharsets.jar等。它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。
  2. 扩展类加载器(Extension Class Loader) :它是java.lang.ClassLoader的子类,由sun.misc.Launcher$ExtClassLoader实现,父类加载器是启动类加载器。。它负责加载java.ext.dirs系统属性所指定的目录下的类库,或者从JDK的安装目录的jre/lib/ext子目录(扩展目录)下的类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
  3. 系统类加载器(System Class Loader) :它也是java.lang.ClassLoader的子类,由sun.misc.Launcher$AppClassLoader实现,父类加载器为扩展类加载器。负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库。通过ClassLoader.getSystemClassLoader()方法可以获取到它的实例。如果没有特别指定,它就是程序中默认的类加载器。

此外,除了上述三种系统类加载器,用户还可以根据需要定义自己的类加载器,这些自定义的类加载器通常继承自java.lang.ClassLoader或其子类。自定义类加载器可以实现更加灵活和复杂的类加载策略,以满足特定的需求。

20210413212009756.png

双亲委派机制:当一个类加载器收到了类加载请求时,它不会自己立即去加载这个类,而是先将这个请求委派给它的父类加载器去完成。当父类加载器无法完成这个加载请求(即它的搜索范围中没有找到所需的类)时,加载请求会逐层返回给子类加载器,此时,子类加载器才会尝试自己加载这个类

public class Demo06 {
    public static void main(String[] args) throws ClassNotFoundException {
        //获取系统的类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
        //获取系统类加载器的父类加载器-->扩展类加载器
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@4554617c
    
        //获取扩展类加载器的父类加载器- ->根加载器(C/C++)
        ClassLoader grantparent = parent.getParent();
        System.out.println(grantparent);//null
    
        //测试当前类是哪个加载器加载的
        ClassLoader classLoader = Class.forName("com.reflection.Demo06").getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
    
        //测试JDK内置的类是谁加载的
        ClassLoader classLoader1 = Class.forName("java.lang.Object").getClassLoader();
        System.out.println(classLoader1);//null
    
        //如何获得系统类加载器可以加载的路径
        System.out.println(System.getProperty("java.class.path"));
        
        //类加载器存在双亲委派机制 双亲委派机制大概就创建对象后先向上(父类加载器)找包,找不到了在用自己的
    }
}

分析

P4
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); 获取系统的类加载器,并将其赋值给systemClassLoader变量

下面是对代码的逐部分解释:

ClassLoader
ClassLoader 是Java中的一个核心类,它是所有类加载器的超类。类加载器的主要任务是将类的字节码文件加载到JVM中,并转换为 Class 对象,这样JVM才能识别和使用这些类。

getSystemClassLoader()
getSystemClassLoader() 是 ClassLoader 类中的一个静态方法。它返回系统类加载器,即用于加载应用程序类路径(class path)上的类和资源的类加载器。系统类加载器通常是 sun.misc.Launcher$AppClassLoader 的实例,它是 URLClassLoader 的一个子类。

系统类加载器通常负责加载以下位置的类:

  • Java命令的-classpath-cp选项指定的目录和ZIP/JAR包
  • 环境变量CLASSPATH指定的目录和ZIP/JAR包
  • 当前工作目录(即用户执行Java命令时所在的目录)

ClassLoader systemClassLoader
systemClassLoader是一个 ClassLoader 类型的变量,用于存储通过 getSystemClassLoader() 方法获取的系统类加载器的引用。之后,可以使用这个变量来进一步查询或操作类加载器,例如加载类、获取已加载的类列表等。

P8
systemClassLoader.getParent()
getParent()
getParent() 是 ClassLoader 类的一个实例方法。该方法返回调用它的类加载器的父类加载器。
systemClassLoader 是通过 ClassLoader.getSystemClassLoader() 获取的系统类加载器的引用。

对于系统类加载器(通常是 sun.misc.Launcher$AppClassLoader 的实例),其父类加载器通常是扩展类加载器(sun.misc.Launcher$ExtClassLoader 的实例)。

结果:执行P8行代码后,parent 变量将持有系统类加载器的父类加载器(即扩展类加载器)的引用。这可以用于进一步查询或操作这个父类加载器,例如了解它加载了哪些类或资源。

P16
Class.forName("com.reflection.Demo06").getClassLoader();
getClassLoader()

  • 一旦 Class.forName() 成功加载了com.reflection.Demo06类,它会返回一个代表该类的 Class 对象
  • getClassLoader() 是 Class类的一个方法,用于获取加载该 Class 对象的类加载器。这个方法返回的是一个 ClassLoader 对象,代表了加载该类的类加载器

P24
System.out.println(System.getProperty("java.class.path"));:作用是打印出Java虚拟机(JVM)的类路径(classpath)。

下面是对这行代码的详细分析:

  1. System 类
    System 是Java中的一个核心类,提供了许多有用的类字段和方法。它包含了与Java虚拟机交互的静态方法,例如获取系统属性、环境变量等。
  2. out 静态成员System.out 是 System 类的一个静态成员,代表标准输出流,通常是控制台。它是一个 PrintStream 类型的对象,提供了各种输出方法,如 println()
  3. getProperty()
    System.getProperty() 是 System 类中的一个静态方法,用于获取指定键的系统属性。系统属性是一组键值对,可以通过JVM命令行选项设置,或者在运行时通过 System.setProperty() 方法设置。
  4. "java.class.path" 键:
    "java.class.path" 是一个特定的系统属性键,它代表了Java类路径(classpath)。类路径是JVM用来搜索类文件(.class 文件)和资源的路径列表。当JVM启动时,它会查找这个路径列表来加载需要的类。

整体流程: 这行代码首先通过 System.getProperty("java.class.path") 获取了JVM的类路径系统属性。然后,它将这个属性值传递给 System.out.println 方法,以便在标准输出(通常是控制台)上打印出这个类路径。

currentTimeMillis() 性能分析

currentTimeMillis() 方法属于 System 类,返回当前时间与1970年1月1日00:00:00 GMT(格林威治标准时间)之间的毫秒数(长整型数值 long),也被称为 Unix 时间戳。
用途:可以用来计算某一操作的执行时长。通过记录操作开始和结束的时间戳,然后计算它们之间的差值,从而得到操作的执行时长。

示例

public class TimeMeasurement {
    public static void main(String[] args) {
        // 记录操作开始的时间戳
        long startTime = System.currentTimeMillis();

        // 执行你的操作
        performSomeOperation();

        // 记录操作结束的时间戳
        long endTime = System.currentTimeMillis();

        // 计算操作的执行时长(毫秒)
        long elapsedTime = endTime - startTime;

        // 输出执行时长
        System.out.println("操作执行时长: " + elapsedTime + " 毫秒");
    }

    // 模拟某个操作
    private static void performSomeOperation() {
        // 这里放置要测量的代码,例如,可以是一个循环、数据库查询、文件读写等
        //也可以使用循环for (int i = 0; i < 100000000; i++) {要测量的代码}延长极快代码的用时以进行比较
    }
}

获取泛型信息

泛型:在Java中,有许多集合接口和类都是泛型的,例如 List、Set、Map、ArrayList、HashSet 等。这些泛型集合都允许开发者指定它们应该存储的对象类型,以创建一个特定类型的接口或类,这个类型被称为泛型参数,从而提供更强的类型安全性和灵活性。
例如,当声明一个 List 变量时,List<String> 表示一个存储字符串对象的列表,而 List<Integer> 表示一个存储整数对象的列表。

泛型实现原理:Java的泛型是通过类型擦除来实现的,这意味着在编译之后,关于泛型类型的具体信息(如List<String>中的String)会被擦除,只留下原始类型(如List)。然而,在编译时,编译器javac 确实使用了这些泛型信息来确保类型安全,并在必要时自动插入强制类型转换。

Type 接口:Java的反射API提供了一个基础接口TypeType接口有多个子接口来表示泛型类型,这些接口(Type对象)包括 Class 、 ParameterizedType 、 GenericArrayType 、 TypeVariable 和 WildcardType 。这些接口允许我们在一定程度上查询和操作泛型类型,尽管这些信息在运行时并不完整。
Type 接口用途:这些接口主要用于高级反射场景,当需要处理泛型类型参数或编写泛型框架时。在常规应用程序代码中,通常不需要直接使用这些接口。

  1. ParameterizedType:
  • 当一个类、接口或方法使用了泛型参数时,其类型就是参数化类型。这种类型使用 ParameterizedType 来表示。
  • 例如,List<String> 是一个参数化类型,其中 List 是原始类型,而 String 是泛型参数
  • 通过 ParameterizedType 的 getRawType() 方法,可以返回调用该方法的对象所表示的 泛型类型被定义时 使用的原始类型(List.class
  • 通过 getActualTypeArguments() 方法,可以获取泛型参数的类型数组,表示此类型参数的实际类型参数([String.class]) - 在Java中,泛型可以包含多个类型参数。例如,对于Map<String, Integer>这样的类型,实际类型参数就有两个:StringInteger。因此,getActualTypeArguments()方法返回的是一个Type对象的数组,每个元素表示一个具体的类型参数。
  1. GenericArrayType:
  • 当一个类型表示泛型数组(即 该类型是一个数组,但是它的元素类型是参数化类型或者类型变量)时,它的类型是泛型数组类型。这种类型使用 GenericArrayType 来表示。
  • 这种类型在涉及到泛型和数组结合的情况下会出现,例如 List<String>[] 或者 T[] 这样的形式,其中 T 是数组元素的类型
  • 通过 GenericArrayType 的 getGenericComponentType() 方法,可以获取数组元素的类型,该类型本身可以是参数化类型、类型变量或通配符类型
  1. TypeVariable:
  • 表示泛型类或方法中定义的占位符类型,即 使用类型变量来代表一种未知的具体类型。这种类型使用 TypeVariable 来表示。
  • TypeVariable 对象:每个 TypeVariable 对象都代表了一个特定的泛型类型参数(类型变量),并且包含了以下信息:
    1. 名称:类型变量的名称,如 TE 等。
    2. 上界:类型变量的上界,即它扩展或实现的类型。如果没有显式声明上界,则默认上界为 Object。在 Java 泛型中,类型变量只能有一个上界,不能有下界。
    3. 泛型声明:类型变量被声明的地方,通常是一个类、接口、方法或构造函数的泛型声明。
  • 类型变量:指在泛型类、接口或方法中定义的参数化类型。它们通常用大写字母表示,比如 TEK 等。类型变量可以用于编写可以处理多种类型的通用代码,使得代码更加灵活和可重用。
    在实际使用中,当实例化泛型类或调用泛型方法时,可以将类型变量替换为具体的类或接口类型,从而使得泛型代码能够适用于不同的数据类型。这样的特性使得我们能够编写更加通用和灵活的代码,同时提高了代码的可复用性。
  • 例如:定义一个类时, [修饰符] class ClassName<T> 中,T 就是一个类型变量,它表示一个未知的具体类型。
  • getBounds() 方法,返回一个Type数组,用于获取类型变量的上界。返回一个 Type 对象的数组,这个数组包含了类型变量的所有上界。对于简单的情况(只有一个上界),数组将只有一个元素。对于复杂的情况(多个上界,通常是通过 & 符号组合在一起的),数组将包含多个元素。这些上界限定了类型变量可以代表的具体类型。
  • 上界 通常是一个类或接口的 Class 对象,或者是另一个 TypeVariable,或者是 Object.class(如果没有显式指定上界)
    上界通过 extends 关键字指定,例如public class Box<T extends Number>T 是一个类型参数,它的上界是 Number 类。这意味着 T 可以是 Number 或其任何子类(如 Integer, Double 等)。如果类型变量没有显式指定上界,那么它的上界默认为 Object 类。因此,即使你没有为类型参数指定上界,调用 getBounds() 方法也将至少返回一个元素,即 Object.class
  • 下界 通常是 null,因为 Java 不支持为类型变量设置下界(除了 null 和 Object 之外)
  1. WildcardType:
  • 通配符类型是表示未知类型的泛型类型。这种类型使用 WildcardType 来表示。
  • 如 ?? extends Number, 或 ? super Integer
  • 通过 WildcardType 的 getUpperBounds() 和 getLowerBounds() 方法,可以获取通配符的上界和下界
  • 上界 是一个 Type 对象的数组,表示通配符可以匹配的类型。通常,这个数组只包含一个元素,除非使用了多个上界(这是不常见的)。
  • 下界 也是一个 Type 对象的数组,表示通配符可以匹配的类型。与类型变量不同,通配符可以有非 null 的下界。

示例

要使用这些接口来获取和操作泛型类型信息,通常需要在反射上下文中操作,比如获取一个类的 Type 信息,或者处理一个方法的泛型参数。

  1. 获取类的泛型信息

    假设有一个泛型类,如:

    public class Box<T> {
        private T t;
        // ...
    }
    

    可以通过类的 Class 对象来获取泛型信息:

    Class<Box> boxClass = Box.class;  // boxClass 是一个 Class 类型的变量,它持有 泛型类Box 的元信息
    
    // 获取表示泛型类声明的Type对象
    Type genericSuperclass = boxClass.getGenericSuperclass();
    
    // 检查是否是参数化类型
    if (genericSuperclass instanceof ParameterizedType) {
        //将genericSuperclass强制转换为ParameterizedType,并赋值给parameterizedType变量
        ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
    
        // 获取原始类型(Box.class)
        Type rawType = parameterizedType.getRawType();
    
        // 获取泛型参数类型(T的实际类型)
        Type[] typeArguments = parameterizedType.getActualTypeArguments();
    
        // 处理泛型参数...
    }
    

    P4
    getGenericSuperclass() 是 java.lang.Class 类中的一个方法,它返回表示此 Class 对象所表示的实体(类或接口)的父类的 Type 对象。如果此 Class 对象表示一个接口、基本类型、void 类型或 java.lang.Object,则返回 null。它可以用于访问泛型类的父类的泛型信息。

    P7
    if (genericSuperclass instanceof ParameterizedType) { ... }
    这个 if 语句检查genericSuperclass是否是ParameterizedType的实例。如果Box类有一个泛型超类,那么genericSuperclass就会是ParameterizedType的一个实例。

    instanceof 是一个关键字(A instanceof B),用于测试一个对象是否是指定类型的实例或该类型的子类型(对于类)的实例。如果 对象A 是给 定类型B 或其子类型的一个实例,那么 instanceof  运算符将返回 true,否则返回 false

  2. 获取方法的泛型信息

    如果有一个泛型方法,如:

    public class Utility {
        public static <U> U getId(U object) {
            // ...
        }
    }
    

    可以通过方法的 Method 对象来获取泛型信息:

    //从Utility类中获取一个名为getId的方法,该方法接受一个Object类型的参数
    Method method = Utility.class.getMethod("getId", Object.class);
    
    // 获取表示泛型方法声明的Type对象
    Type genericReturnType = method.getGenericReturnType();
    
    // 检查是否是参数化类型
    if (genericReturnType instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
    
        // 获取原始返回类型(这里是U)
        Type rawReturnType = parameterizedType.getRawType();
    
        // 获取泛型参数类型(Object.class,因为我们传入了Object.class作为参数)
        Type[] typeArguments = parameterizedType.getActualTypeArguments();
    
        // 处理泛型参数...
    }
    

    P4
    getGenericReturnType() 是 java.lang.reflect.Method 类中的一个方法。它返回由此 Method 对象表示的方法的声明中用于标识方法返回值类型的部分的 Type 对象。这个返回类型可能是一个类类型(Class)、一个参数化类型(ParameterizedType)、一个类型变量(TypeVariable)、一个基本类型(如 int.classfloat.class 等)、void.class,或者是表示数组类型的 GenericArrayType

    这个方法特别有用在泛型编程和反射中,因为它可以用于获取到方法的返回类型的完整信息,包括泛型参数。这对于理解和操作方法的返回类型非常重要,尤其是在处理泛型方法时。

  3. 处理泛型数组

    如果有一个泛型数组,如 T[],可以使用 GenericArrayType 来处理它:

    // 创建一个表示String类型数组的GenericArrayType对象 ,GenericArrayType是Java反射API中的类,用于表示泛型数组的类型
    Type genericArrayType = new GenericArrayType(String.class);
    
    if (genericArrayType instanceof GenericArrayType) {
        GenericArrayType genericArrayTypeInstance = (GenericArrayType) genericArrayType;
    
        // 获取数组元素的类型
        Type genericComponentType = genericArrayTypeInstance.getGenericComponentType();
    
        // 处理数组元素的泛型类型...
    }
    
  4. 处理类型变量

    类型变量通常出现在泛型类或方法的声明中,如 Tclass MyClass<T> 中。可以通过 TypeVariable 来获取和处理类型变量的信息:

    //调用MyClass类的Class对象的getTypeParameters()方法,用于获取MyClass类中定义的第一个类型参数
    //并将其存储在 原始类型是Class<MyClass>的 TypeVariable类型的 变量typeVariable中
    TypeVariable<Class<MyClass>> typeVariable = MyClass.class.getTypeParameters()[0];
    
    // 获取类型变量的上界
    Type[] bounds = typeVariable.getBounds();
    
    // 处理类型变量的上界...
    

    P2
    getTypeParameters() 是 Java 反射 API 中的一个方法,它属于 Class 类和 ParameterizedType 接口。这个方法用于获取表示类、接口或方法的泛型类型参数的 TypeVariable 对象的数组(数组的每个元素都是一个 TypeVariable 对象)。

  5. 处理通配符类型

    通配符类型通常在泛型方法或类声明中出现,如 ?, ? extends Number, 或 ? super Integer。可以通过 WildcardType 来获取和处理通配符类型的信息:

     Class<Box> boxClass = Box.class;  
    
     // 获取表示泛型类声明的Type对象  
     Type genericSuperclass = boxClass.getGenericSuperclass();  
    
     //检查是否成功获取到了泛型超类信息,并且这个超类是否是参数化类型(即泛型类)  
     if (genericSuperclass != null && genericSuperclass instanceof ParameterizedType) {  
         ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;  
    
         // 获取类型参数  
         Type[] typeArguments = parameterizedType.getActualTypeArguments();  
    
         // 处理类型参数  
         for (Type typeArgument : typeArguments) {         //遍历每个类型参数
             if (typeArgument instanceof WildcardType) {   //检查当前类型参数是否是通配符类型
                 WildcardType wildcardType = (WildcardType) typeArgument;  //如果是,则将其强制转换为 WildcardType
    
                 // 获取通配符的上界和下界  
                 Type[] upperBounds = wildcardType.getUpperBounds();  
                 Type[] lowerBounds = wildcardType.getLowerBounds();  
    
                 // 处理通配符类型参数,遍历通配符类型的上界和下界,并打印它们的类型名称
                 for (Type upperBound : upperBounds) {
                     System.out.println("Upper bound: " + upperBound.getTypeName());  
                 }  
                 for (Type lowerBound : lowerBounds) {  
                     System.out.println("Lower bound: " + lowerBound.getTypeName());  
                 }  
             } else {  
                 // 处理非通配符类型参数,打印非通配符类型参数的类型名称
                 System.out.println("Non-wildcard type argument: " + typeArgument.getTypeName());  
             }  
         }  
     } else {
         // 如果初始条件不满足(即没有获取到泛型超类信息或者超类不是泛型类),则打印一条消息表示没有获取到参数化类型信息
         System.out.println("The superclass is not a parameterized type.");  
     }
    

获取注解信息

注解(Annotation)在Java中是一种元数据机制,它为代码提供了一种附加信息的方式,这些信息可以在编译时或运行时被读取。注解不会直接影响代码的执行,但它们可以被编译器用来生成额外的源代码、生成文档、代码分析等。

1. 通过反射获得注解

在Java中,可以使用反射来获取类的注解信息。这通常是通过Class对象来实现的。每个类都有一个与之关联的Class对象,可以通过这个对象来获取注解。

示例

2. 获取注解的value值

当获取了一个注解对象后,可以调用该对象的方法来获取注解的值。通常,注解会有一个名为value的元素,可以通过调用value()方法来获取它的值。

// 假设ClassName类中有一个注解 AnnotationClassName,它有一个value元素
AnnotationClassName annotationInstance = (AnnotationClassName) ClassName.class.getAnnotation(AnnotationClassName.class);
//ClassName.class 获取该类的 Class 对象,然后调用 getAnnotation(AnnotationClassName.class) 方法来获取ClassName类中指定注解的实例
//最后,使用强制类型转换将返回的注解实例分配给一个注解变量annotationInstance
    
if (annotationInstance != null) {   //检查是否成功获取到了AnnotationClassName注解实例
    String value = annotationInstance.value();   //调用value()方法来获取注解的value元素的值
    ...// 使用value
}

3. 获取类指定的注解

如果想获取类指定的注解,需要先获取到类的方法、属性或构造器的对象,然后对这些对象调用 getAnnotation方法,返回一个注解类的对象,可以调用其方法来获取value值

示例

获取注解中的元数据

通过反射获取类中每个字段上的 Fieldw 注解,再获取每个注解中的元数据【columName(数据库表中的列名)、type(字段的数据类型)、length(字段的长度)】,并将其打印出来

// 导入java.lang.annotation包下的注解类,这些类用于定义自定义注解的属性。  
import java.lang.annotation.ElementType;    
import java.lang.annotation.Retention;    
import java.lang.annotation.RetentionPolicy;    
import java.lang.annotation.Target;    
  
// 导入java.lang.reflect包下的Field类,这个类用于获取类的字段信息。  
import java.lang.reflect.Field;    
  
// 定义Fieldw注解,它用于标注类的字段,以提供额外的元数据信息。  
@Retention(RetentionPolicy.RUNTIME)    // 指定该注解在运行时仍然可见,因此可以通过反射读取
@Target(ElementType.FIELD)    // 指定该注解只能应用于类的字段上
public @interface Fieldw {    
    String columnName() default "";    // 定义注解的属性columnName,默认值为空字符串
    String type() default "";    // 定义注解的属性type,默认值为空字符串
    int length() default 0;    // 定义注解的属性length,默认值为0
}    
  
// MyClass是一个普通的Java类,它使用Fieldw注解来标注其字段  
public class MyClass {    
    // 在id字段上使用Fieldw注解,指定columnName为"id",type为"int",length为10。  
    @Fieldw(columnName = "id", type = "int", length = 10)    
    private int id;    
    
    // 在name字段上使用Fieldw注解,指定columnName为"name",type为"varchar",length为50。  
    @Fieldw(columnName = "name", type = "varchar", length = 50)    
    private String name;    
    
    // MyClass的构造方法、getter和setter方法  
    // getId方法返回id字段的值  
    public int getId() {    
        return id;    
    }    
    
    // setId方法设置id字段的值  
    public void setId(int id) {    
        this.id = id;    
    }    
    
    // getName方法返回name字段的值  
    public String getName() {    
        return name;    
    }    
    
    // setName方法设置name字段的值  
    public void setName(String name) {    
        this.name = name;    
    }    
}    
  
// AnnotationReader类用于通过反射获取MyClass类字段上的Fieldw注解,并打印其属性
public class AnnotationReader {      
    public static void main(String[] args) {    
        // 获取MyClass类的Class对象
        Class<MyClass> myClass = MyClass.class;    
    
        // 使用getDeclaredFields方法获取MyClass类声明的所有字段,包括私有字段  
        Field[] fields = myClass.getDeclaredFields();    
    
        // 遍历MyClass的字段  
        for (Field field : fields) {    
            // 检查字段上是否存在Fieldw注解  
            if (field.isAnnotationPresent(Fieldw.class)) {    
                // 如果存在Fieldw注解,则获取该注解的实例  
                Fieldw fieldw = field.getAnnotation(Fieldw.class);    
                // 打印字段名  
                System.out.println("Field Name: " + field.getName());    
                // 打印注解的columnName属性值  
                System.out.println("Column Name: " + fieldw.columnName());    
                // 打印注解的type属性值  
                System.out.println("Type: " + fieldw.type());    
                // 打印注解的length属性值  
                System.out.println("Length: " + fieldw.length());    
                // 打印一个空行,以便分隔不同字段的注解信息  
                System.out.println();    
            }    
        }    
    }    
}

ORM (Object Relationship Mapping)

ORM 是一种技术,它将对象(通常是Java对象)与关系数据库中的表结构进行映射。这种映射使得开发者可以使用面向对象的方式来操作数据库,而不需要写原始的SQL语句。

  • 类和表结构对应:每个类通常对应一个数据库表,类的属性对应表的字段
  • 属性和字段对应:类的属性(成员变量)通常对应数据库表中的字段
  • 对象和记录对应:类的一个实例(对象)通常对应表中的一条记录

通过这种方式,开发者可以更加直观地操作数据库,同时减少SQL语句的编写量。然而,ORM也有其缺点,比如可能引入额外的性能开销和复杂度,以及在处理复杂查询时可能不如原生SQL灵活。