注解和反射
反射 Reflection
概念
静态语言和动态语言
动态语言是一类在运行时可以改变其结构的语言。例如新的函数、对象甚至代码都可以在运行时引进,已有的函数可以被删除,或者结构上做一些其他的变化。主要的动态语言有:C#、JavaScript、PHP、Python等。
静态语言是运行时结构不可变的语言。例如Java、C、C++。Java虽然不是动态语言,但可以称之为准动态语言。即Java有一定的动态性,可以利用反射机制获得类似动态语言的特性。
Class类
JRE为每个类都保留了一个不变的Class类型的对象。
- Class本身也是一个类
- Class对象只能由系统建立对象
- 一个加载的类在JVM中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的一个class文件
- 通过Class可以完整地得到一个类中的所有被加载的结构
- Class类是Reflection的根源,针对任何想动态加载、运行的类,只有先获得相应的Class对象
常用方法
方法名 | 功能说明 |
---|---|
static ClassforName(String name) |
返回指定类名name的Class对象 |
Object newInstance() |
调用默认构造函数,返回Class对象的一个实例 |
getName() |
返回此Class对象所表示的实体(类、接口、数组类或void)的名称 |
Class getSuperClass() |
返回当前Class对象的父类的Class对象 |
Class[] getInterfaces() |
获取当前Class对象的接口 |
ClassLoader getClassLoader() |
返回该类的类加载器 |
Constructor[] getConstructors() |
返回一个包含某些Constructor对象的数组 |
Method getMethod(String name, Class.. T) |
返回一个Method对象,此对象的参数类型是paramType |
Field[] getDeclaredFields() |
返回Field对象的一个数组 |
类的加载与ClassLoader的理解
加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
- 准备:正式为类变量分配内存并设置变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的**符号引用(常量名)替换为直接引用(地址)**的过程。
初始化
- 执行类构造器
<clinit>()
方法的过程。该方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的(静态代码合并,按先后顺序执行)。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先出发其父类的初始化。
- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确加锁和同步。
以下情况不会发生类的初始化:
- 访问一个静态域时,只有真正声明这个域的类才会被初始化。例如通过子类引用父类的静态变量,不会导致子类初始化。
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会出发此类的初始化(在链接阶段就存入调用类的常量池中了)
- 执行类构造器
类加载器的作用
类加载器将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时结构数据,然后在堆中生成一个代表这个类的java.lang.Class
对象,作为方法区中类数据的访问入口。
标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
引导类加载器 Bootstrap Classloader
C++编写,JVM自带的类加载器,负责Java平台核心库,用来装载核心类库,无法直接获取。
扩展类加载器 Extension Classloader
系统类加载器 System Classloader / Application Classloader
是最常用的类加载器
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。程序中一般的对象的类型都是在编译期就确定下来,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息,可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以形象地称之为反射。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先知道运行对象是谁。
Java 反射主要提供以下功能:
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
- 在运行时调用任意一个对象的方法
- 在运行时处理注解
- 生成动态代理
使用场景
很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。当我们在使用 IDE(如 Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。
反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的,为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
对于框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。
反射的使用
反射相关的类一般都在java.lang.relfect
包里。
获得Class对象
可以获得Class对象的类型:
- 各种Class(外部类、成员内部类、静态内部类、局部内部类、匿名内部类)
- interface
- 数组
- enum
- 注解
- 基本数据类型
- void
有4种方法可以获得Class对象:
- 若已知具体的类,通过类的class属性获取,该方法最安全可靠,程序性能最高。
1 | Class c1 = Person.class; |
已知某个类的实例对象,调用对象的
getClass()
方法获取Class对象1
Class c1 = person.getClass();
已知一个类的全类名,且在该类的类路径下,可使用Class类的静态方法
forName()
获取,可能抛出ClassNotFoundException
1 | Class c1 = Class.forName("com.hunter.reflection.User"); |
内置基本数据类型可以直接使用类名.Type
1
Class c3 = Integer.TYPE;
判断是否为某个类的实例
一般地,我们用 instanceof
关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance()
方法来判断是否为某个类的实例,它是一个 native 方法:
1 | public native boolean isInstance(Object obj); |
native方法:具体实现由非java语言实现的方法
创建实例
通过反射来生成对象主要有两种方式。
- 使用Class对象的
newInstance()
方法来创建Class对象对应类的实例
类必须要有一个无参构造器
1 | Class<?> c = String.class; // 当赋值的类型不确定的时候,泛型用通配符?代替 |
- 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。
这种方法可以用指定的构造器构造类的实例。
1 | Class<?> c = String.class; |
获取方法
获取某个Class对象的方法集合,主要有以下几个方法:
getDeclaredMethods
返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法
1
public Method[] getDeclaredMethods() throws SecurityException
getMethods
返回某个类的所有公共方法,包括继承的公用方法
1
public Method[] getMethods() throws SecurityException
getMethod
返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
1
public Method getMethod(String name, Class<?>... parameterTypes)
举例如下:
1 | public class test1 { |
获取构造器信息
获取类构造器的用法与上述获取方法的用法类似。
getDeclaredConstructors()
:所有public的构造方法getConstructors()
:全部构造方法getDeclaredConstructor
getConstructor
主要通过Class类的getConstructor
方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
1 | public T newInstance(Object... initargs) |
获取类的成员变量信息
getDeclaredFields
:所有已声明的成员变量,但不能得到父类的成员变量getFields
:访问公有的成员变量getField
:获取指定的公有成员变量getDeclaredField
:获取指定的已声明成员变量
反射调用方法
当从类中获取了一个方法后,就可以用invoke()
方法来调用这个方法。
若原方法声明为private,则需要在调用此invoke()
方法前,显式调用方法对象的setAccessible(true)
方法。
Method、Field和Constructor对象都有setAccessible()
方法,作用是启动和禁用访问安全检查的开关。
- 参数为true则指示反射的对象在使用时应该取消Java语言的访问检查。
- 提高了反射效率
- 可以访问私有成员
1 | public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException |
下面是一个实例:
1 | public class test1 { |
反射操作注解
1 | Annotation[] annotations = c1.getAnnotations(); // 获取注解 |
setAccessible
设置可访问方法、属性、构造器对象能否访问的方法。
setAccessible(true)
指示反射的对象在使用时取消Java语言的访问检查,有两点作用:
- 提高反射效率
- 可以访问原本无法访问的私有对象
反射的优点
灵活性:可以动态创建对象和编译。
可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
- 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
注解 Annotation
Annotation是JDK 1.5引入的技术,是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。它以**@注释名
的形式在代码中存在,还可以添加一些参数值**,例如@SuppressWarnings(value="unchecked")
。
Annotation可以附加在package、class、method、filed等上面,通过反射机制编程实现对这些元数据的访问。
内置注解
@Override
:定义在java.lang.Override
中,此注释只适用于修辞方法,表示一个方法声明打算重写超类中的另一个方法声明。@Deprecated
:定义在java.lang.Deprecated
中,此注释可以用于修辞方法、属性、类,表示不鼓励程序员使用这样的元素,通常是因为它很危险或者存在更好的选择。@SuppressWarnings
:定义在java.lang.Suppresswarnings
中,用来抑制编译时的警告信息。与前两个注释有所不同,需要添加一个参数才能正确使用。这些参数都是已经定义好了的,选择性使用即可:
@SuppressWarnings("all")
:镇压全部警告@SuppressWarnings("unchecked")
:镇压未检查的警告@SuppressWarnings(value = {"unchecked", "deprecation"})
元注解
元注解的作用就是负责注解其他注解,Java定义了4个标准的meta-annotation类型,他们被用来提供对其他 annotation类型作说明。这些类型和它们所支持的类在java.lang.annotation
包中可以找到:
@Target
:用于描述注解的使用范围@Retention
:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(SOURCE < CLASS < RUNTIME),(自定义的注解一般都写在RUNTIME)
@Document
:说明该注解将被包含在javadoc中@Inherited
:说明子类可以继承父类中的该注解
自定义一个注解:
1 | // value 是参数名 |
自定义注解
使用@interface
自定义注解时,自动继承了java.lang.annotation.Annotation
接口。
分析:
@interface
用来声明一个注解,格式:public @Interface 注解名{定义内容}
- 其中的每一个方法实际上是声明了一个配置参数
- 方法的名称就是参数的名称
- 返回值类型就是参数的类型(只能是基本类型、Class、String、enum)
- 可以通过default来声明参数的默认值
- 如果只有一个参数成员,一般参数名为value
- 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。
1 | public class Test { |