跳至主要內容

注解与反射

LincZero大约 16 分钟

注解与反射

CONTENTS

反射 Java.Reflection

参考:https://www.bilibili.com/video/BV1p4411P7V3(进度:p7 end)

目录

  • Java反射机制概述
  • 理解Class类并获取Class实例
  • 类的加载与ClassLoader
  • 创建运行时类的对象
  • 获取运行时类的完整结构
  • 调用运行时类的指定结构

其他

可以获取注解(与注解配合)

JVM(Java虚拟机)

Hook

静态 VS 动态语言

  • 动态语言
    • 是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。 通俗点说就是在运行时代码可以根据某些条件改变自身结构。
    • 主要动态语言:Object-C、C#、JavaScript、PHP、Python等。
  • 静态语言
    • 与动态语言相对应的,运行时结构不可变的语言就是
    • 主要静态语言:Java、C、C++等
    • Java不是动态语言,但Java可以称之为 “准动态语言” 。 即Java有一定的动态性,我们可以==利用反射机制获得类似动态语言的特性==。Java的动态性让编程的时候更加灵活!

Java反射

介绍

  • 大概原理

    • Reflection (反射) 是Java被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

      Class c = Class.forName("java.lang.String")
      
    • 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。 我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射

      • 正常方式:引入需要的"包类”名称 ==> 通过new实例化 ==> 取得实例化对象
      • 反射方式:实例化对象 ==> getClass()方法 ==> 得到完整的“包类”名称
  • 功能

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

    • 可以实现动态创建对象和编译,体现出很大的灵活性
  • 缺点

    • 对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。
  • 反射主要API

    java.lang.Class
        // 代表一个类。`Class` 是 Reflection API 中的一个核心类
        
    java.lang.reflect.Method
        // 代表类的方法
    
    java.lang.reflect.Field
        // 代表类的成员变量
        
    java.lang.reflect.Constructor
        // 代表类的构造器
        
    ......
    

获得反射对象

// 什么叫反射
public class Test02 extends Object{
    public static void main(String[] args) throws ClassNotFoundException{
        // 通过反射获取类的Class对象
        // 一个类在内存中只有一个Class对象
        // 一个类被加载后,类的整个结构都会被封装在Class对象中
        Class c1 = Class.forName("com.kuang.reflection,User");
        System.out.println(c1);
    }
}

// 实体类: pojo, entity
class User{
    private String name;
    private int id;
    private int age;
    
    public User(){}
    
    public User(String name, int id, int age){
        this.name = name;
        this.id = id;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getId() {
        return id;
    }
    
    public int setId() {
        this.id = id;
    }
    
    public int getAge(){
        return age;
    }
    
    public void setAge(int age){
        this.age = age;
    }
}

得到Class类的几种方式

对象"照镜子"后可以得到的信息:

  • 某个类的属性
  • 方法和构造器
  • 某个类到底实现了哪些接口

对于每个类而言,JRE都为其保留一个不变的Class类型的对象。 一个Class对象包含了特定某个结构(classlinterfacelenum/annotation/primitive type/void/[])的有关信息。

  • Class本身也是一个类
  • Class对象只能由系统建立对象
  • 一个加载的类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个Class 实例所生成
  • 通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

Class类的常用方法

方法名功能说明
static ClassforName(String name)返回指定类名name的Class对象
Object newlnstance()调用缺省构造函数,返回Class对象的一个实例
getName()返回此Class对象所表示的实体(类,接口,数组类或void)的名称
Class getSuperClass()返回当前Class对象的父类的Class对象
Class[] getinterfaces()获取当前Class对象的接口
ClassLoader getClassLoader()返回该类的类加载器
Constructor[]getConstructors()返回一个包含某些Constructor对象的数组
Method getMothed(String name,Class.. T)返回一个Method对象,此对象的形参类型为paramType
Field[] getDeclaredFields()返回Field对象的一个数组
| 方法名                                   | 功能说明                                                  |
| --------------------------------------- | --------------------------------------------------------- |
| static ClassforName(String name)        | 返回指定类名name的Class对象                               |
| Object newlnstance()                    | 调用缺省构造函数,返回Class对象的一个实例                 |
| getName()                               | 返回此Class对象所表示的实体(类,接口,数组类或void)的名称 |
| Class getSuperClass()                   | 返回当前Class对象的父类的Class对象                        |
| Class[] getinterfaces()                 | 获取当前Class对象的接口                                   |
| ClassLoader getClassLoader()            | 返回该类的类加载器                                        |
| Constructor[]getConstructors()          | 返回一个包含某些Constructor对象的数组                     |
| Method getMothed(String name,Class.. T) | 返回一个Method对象,此对象的形参类型为paramType           |
| Field[] getDeclaredFields()             | 返回Field对象的一个数组                                   |

获取Class类的实例

// 1. 若已知具体的类,通过类的class属性获取。该方法最为安全可靠,程序性能最高。
Class clazz= Person.class;

// 2. 已知某个类的实例,调用该实例的getClass()方法获取Class对象
Class clazz= person.getClass();

// 3. 已知一个类的全类名,且该类在类路径下。可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
Class clazz= Class.forName("demo01.Student");

// 4. 内置基本数据类型 (不是每个类都有) 可以直接用类名.Type

// 5. 还可以利用ClassLoader我们之后讲解

举例

// 测试class类的创建方式有哪些
public class Test03 {
	public static void main(String[] args) throws classNotFoundException {
		Person person = new Student();
		system.out.println("这个人是: "+person.name) ;
		
        // 方式一∶通过对象获得
		Class c1 = person.getclass( );
		system.out.println(c1.hashcode());

        // 方式二: forName获得
		Class c2 = Class.forName("com. kuang.reflection.student");
        system.out.println(c2.hashcode());
        
        // 方式三: 通过类名.class获得
        Class c3 = Student.class;
        system.out.println(c3.hashcode());
        
        // 方式四: 基本内置类型的包装类都有一个Type属性
        Class c4 = Integer.TYPE;
        system.out.println(c4); // int
        
        // 获得父类类型
        Class c5 = c1.getSuperclass();
         system.out.println(c5); // class com.kuang.reflection.Person
	}
}

// 类
class Person{...}

class student extends Person{
    public student(){
    	this.name ="学生";
    }
}

class Teacher extends Person{
	public Teacher(){
		this.name ="老师";
	}
}

反射

参考:《Java 核心技术》

API: java.lang.Class 1.0 API: java.lang.reflect.Constructor 1.1

基本介绍

  • 反射库
    • 反射库(reflection library)提供了一个非常丰富且精心设计的工具集
    • 应用
      • 这项功能被大量地应用于JavaBeans中
      • 使用反射,Java可以支持Visual Basic用户习惯使用的工具
      • 特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力
      • 视觉上加速程序加载速度 在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类,以此类推。 对于一个大型的应用程序来说,这将会消耗很多时间,用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。 不过,要确保包含main方法的类没有显式地引用其他的类。 首先,显示一个启动画面;然后,通过调用Class.forName手工地加载其他的类
  • 反射机制
    • 能够分析类能力的程序称为反射(reflective)
    • (没懂)使用它的主要人员是工具构造者,而不是应用程序员。如果仅对设计应用程序感兴趣,而对构造工具不感兴趣,可以跳过

与C++不同

反射机制

  • Java和C++一些相似的方法
    • java的 newInstance方法 对应C++中 虚拟构造器 的习惯用法。但有所不同
      • C++中的虚拟构造器不是一种语言特性,需要由专门的库支持
    • java的 Class类 与C++中的 type_info类 相似,java的 getClass方法 与C++中的 typeid 运算符等价
      • C++的type_info没Java的Class强大。只能以字符串的形式显示一个类型的名字,而不能创建那个类型的对象
  • C++本身没有自带的反射机制。至于虚拟构造器和type_info我也没用过、不懂

Class类

在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。

然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,这个名字很容易让人混淆。

本质是泛型类

补充:如同Enum类一样,Class类实际上是一个泛型类。

例如,Employee.class的类型是 Class<Employee>

没有说明这个问题的原因是:它将已经抽象的概念更加复杂化了。在大多数实际问题中,可以忽略类型参数,而使用原始的Class类。

获取Class类

getClass()

  • 根据实例 Object类中的 getClass() 方法将会返回一个Class类型的实例

    Empolyee e;
    ...
    Class cl = e.getClass();
    
    // 类似于Python中的 e.__class__
    

forName() (无论何时使用这个方法,都应该提供一个异常处理器)

  • 根据包名字符串 可以调用静态方法forName获得类名对应的Class对象。如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法

    String className = "java.util.Random";
    Class cl = Class.forName(className);
    
    // 报错则抛出一个checked exception(已检查异常)
    // 无论何时使用这个方法,都应该提供一个异常处理器(exception handler)
    

T.class

  • 根据类型 如果T是任意的Java类型(或void关键字),T.class将代表匹配的类对象

    Class cl1 = Random.class;
    Class cl2 = int.class;
    Class cl3 = Double[].class;
    

Class的基本方法

getName; 返回类名

最常用的Class方法是getName。这个方法将返回类的名字

System.out.println(e.getClass().getName);  // 如果类在一个包里,包的名字也作为类名的一部分

// 类似于Python中的 e.__class__.__name__

注意:不同于getName()方法???

  • getName;:返回类的名字
  • getName():返回项目的名称

newInstance() 创建实例

可以用来动态地创建一个类的实例

e.getClass().newInstance(); // 创建了一个与e具有相同类类型的实例
							// 如果这个类没有默认的构造器,就会抛出一个异常

// 将forName与newInstance配合起来使用
String s = "java.util.Random";
Object m = Class.forName(s).newInstance();
// 但如果需要以这种方式向希望按名称创建的类的构造器提供参数,就不要使用上面那条语句,而必须使用Constructor类中的newInstance方法

Class的比较

虚拟机为每个类型管理一个Class对象。因此,可以利用==运算符实现两个类对象比较的操作。例如

if (e.getClass() == Employee.class) {}

利用反射分析类

API:java.lang.Class 1.0 API:java.lang.reflect.Field 1.1 API:java.lang.reflect.Method 1.1 API:java.lang.reflect.Constructor 1.1 API:java.lang.reflect.Modifier 1.1

Field、Method、Constructor

反射机制最重要的内容——检查类的结构

java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器

Class类中的 getFields、getMethods、getConstructors 方法将分别返回类提供的public域、方法和构造器数组

他们的方法

getName()

这三个类都有一个叫做 getName 的方法,用来返回项目的名称

getType()

Field类有一个 getType 方法,用来返回描述域所属类型的Class对象

Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。

getModifiers()

这三个类还有一个叫做 getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况

还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值:如下

其他:getDeclaredFiled() / getDeclatedMethods()

可以获得一个类的某个字段或方法

一个demo示例

功能:提醒用户输入类名,然后输出类中所有的方法和构造器的签名

程序 reflection/ReflectionTest.java

其实这个程序应该用模板字符串来写的,字符串分散来写太不直观了

package reflection;

import java.util.*;
import java.lang.reflect.*;

/**
 * 这个程序使用反射去打印一个类的所有特征
 * @version 1.1 2004-02-21
 * @author Cay Horstmann
 */
public class ReflectionTest
{
    public static void main(String[] args)
    {
        // 从用户的终端输入中读取类名
        String name;
        if (argslength>0) name = args[0];
        else
        {
            Scanner in = new Scanner(System.in);
            Syste,.out.println("Enter class name (e.g. java.util.Date):");
            name = int.next();
        }
        try
        {
            // 获取类信息(Class, 父类的Class, 修饰符)
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            
            // 打印类名
            if(modifiers.length()>0) System.out.print(modifiers+"");
            System.out.print("class "+name);
            
            // 打印父类名称(如果不为Object类的话)
            if(supercl != null && supercl != Object.class) System.out.print(" extends "
                                                                           + supercl.getName());
            
            System.out.print("\n{n\n");
            printConstructors(cl);
            System.out.println();
            PrintMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        }
        // 找不到类的异常
        catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }
        System.exit(0);
    }
    
    /**
     * 打印类的所有结构
     * @param cl a class
     */
    public static void printConstructors(Class cl)
    {
        Constructor[] constructors = cl.getDeclaredConstructors();
        for (Constructor c: constructors)
        {
            String na,e = c.getName();
            System.out.print("    ");
            String modifiers = Modifier.toString(c.getModifiers());
			if(modifiers.length()>0) System.out.print(modifiers + " ");
            Syste,.out.print(name + "(");
            
            // 打印参数类型
            Class[] paramTypes = c.getParameterTypes();
            for (int j=0; j<paramTypes.length; j++){
                if(j>0) System.out.print(", ");
                System.out.print(para,Types[j].getName());
            }
            System.out.print(");");
        }
    }
    
    /**
     * 打印类的所有的方法
     */
    public static void printMethods(Class cl)
    {
        Method[] methods = cl.getDeclatedMethods();
        
        for (Method m : methods)
        {
            Class retType = m.getReturnType();
            String name = m.getName();
            
            System.out.print("    ");
            // 打印修改器,返回类型和方法名
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) System.out.print(modifiers + " ");
            System.out.print(retType.getName()+" "+name+"(");
            // 打印参数类型
            Class[] paramTypes = m.getParameterTypes();
            for(int j=0; jMparamTypes.length;j++)
            {
                if(j>0) System.out.print(", ");
                System.out.println(");");
            }
            System.out.println(");");
        }
    }
    
    /**
     * 打印一个类中所有的字段
     * @param cl a class
     */
    public static void printFields(Class cl)
    {
        Field[] fields = cl.getDeclaredFields();

        for (Field f:fields)
        {
            Class type = f.getType();
            String name = f.getName();
            System.out.print("    ");
            String modifiers = Modifier.toString(f.getModifiers());
            System.out.println(type.getName() + " " + name + ";");
        }
    }
}

getDeclatedMethods

输入输出

// 输入:
java.lang.Double
    
// 输出:
public class java.lang.Double extends java.lang.Number
{
	public java.lang.Double(java.lang.String);
	public java.lang.Double(double);
    
    public int hashCode();
    public int compareTo(java.lang.Object);
    ...... // 其他方法
        
    public static final double POSITIVE_INFINITY;
    public static final double NEGATIVE_INFINITY;
    ...... // 其他字段
}    

利用反射分析对象(运行时)

API:java.lang.reflect.AccessibleObject 1.2 API:java.lang.Class 1.1 API:java.lang.reflect.Field 1.1

作用:

在编写程序时,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事情。 而利用反射机制可以查看在编译时还不清楚的对象域。

方法

get()

查看对象域的关键方法是Field类中的get方法

f.get(obj) 将返回一个对象,其值为obj域的当前值

Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
	// 获取Class类:Employee_Class
Field f = cl.getDeclaredField("name");
	// 获取Employee类的Name字段
Object v = f.get(harry);
	// harry对象的name字段的值

setAccessible()

AccessibleObject类中的一个方法

/* 【访问权限问题】
 * 实际上,上面这段代码存在一个问题。由于name是一个私有域,所以get方法将会抛出一个IllegalAccessException
 * 除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。
 * 反射机制的默认行为受限于Java的访问控制
 */

/* 【解决方案】
 * 然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。
 * 为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。
 * setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。
 * 例如:
 */

f.setAccessible(true);
	// 设置为可访问权限,现在可以去调用 f.get(harry)了

getDouble()

get方法还有一个需要解决的问题。name域是一个String,因此把它作为Object返回没有什么问题。但是,假定我们想要查看salary域。它属于double类型,而Java中数值类型不是对象。要想解决这个问题,可以使用Field类中的getDouble方法,也可以调用get方法,此时,反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成Double。

set()

当然,可以获得就可以设置。调用f.set(obj,value)可以将obj对象的f域设置成新值。

一个demo示例

使用toString方法可以查看任意对象的内部信息,有点类似于Python的__str__魔术方法

ArrayLIst<Integer> squares = new ArrayList<>>();
for (int i=1; i<=5; i++) squares.add(i*i);
System.out.println(new ObjectAnalyzer().toString(squares));

/* print:
java.util.ArrayList[
	elementData=class java.lang.Object[]{
        java.lang.Interger[value=1][][],
        java.lang.Inter[value=4][][],
        java.lang.Interger[value=9][][],
        java.lang.Interger[value=16][][],
        java.lang.Interger[value=35][][],
        null,
        null,
        null,
        null
    },size=5
][modCount=5][][]
*/

使用通用的toString方法实现自己类中的toString方法

public String toString()
{
    return new ObjectAnalyzer().toString(this);
}

// 这是一种公认的提供toString方法的手段

编写一个可供任意类使用的通用toString方法

@略,详见书

使用反射编写泛型数组代码

java.lang.reflect包中的Array类允许动态地创建数组。

例如,将这个特性应用到Array类中的copyOf方法实现中

Employee[] a = new Employee[100];
...
// array is full
a = Arrays.copyOf(a, 2*a.length);

如何编写这样一个通用的方法呢?

@略

调用任意方法

API:java.lang.reflect.Method 1.1

与C++不同

  • C和C++
    • 可以从函数指针执行任意函数。
  • Java
    • 从表面上看,Java没有提供方法指针,即将一个方法的存储地址传给另外一个方法,以便第二个方法能够随后调用它。
    • 代替方案
      • 事实上,Java的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为Java提供的**接口(interface)**是一种更好的解决方案。
      • 然而,反射机制允许你调用任意方法。
  • J++/C#
    • 微软公司为自己的非标准Java语言J++(以及后来的C#)增加了另一种被称为**委托(delegate)**的方法指针类型, 它与Java中的Method类不同。Java中内部类比委托更加有用。

getMethod()

Object invoke(Object obj, Object... args);

double s = (Double) m2.invoke(harry);

Method getMethod(String name, Class... parameterTypes);


Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

注意:

可以使用method对象实现C(或C#中的委派)语言中函数指针的所有操作。同C一样,这种程序设计风格并不太简便,出错的可能性也比较大。如果在调用方法的时候提供了一个错误的参数,那么invoke方法将会抛出一个异常。

@略,详见书