1. 简介
最近在学习框架技术时频繁地遇到注解与反射相关的原理,之前学习的javaSE中虽然学过,但是记忆模糊,因此打算重新复习一下这方面的知识。
2. 注解
2.1 概念
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。
注解与注释有相似的,他们都是对其标注的代码的一种说明。但是,注解更加复杂:
- 有明确的编写语法:注解的声明需要符合java的命名规范,且自定义注解需要满足Java的语法;
- 有明确的使用位置:注解的使用位置是需要指明的;
- 有明确的有效期:一个注解的生命周期包括:源代码,class文件,和运行时。
- 具有强制性:注解所规定的内容用户在使用时必须遵守,否则编译无法通过。
java中是使用反射的机制来读取注解的。
2.2 核心机制
1. 元注解 meta-annotation
java中提供了四个元注解,即注解的注解,我们在编写自己的注解时,需要关注这四个注解,并按照需求对这四个注解进行赋值。
- @Document:说明该注解将写入javadoc中;
- @Retention:说明该注解可以被保存到什么时候(SOURCE<CLASS<RUNTIME);
- @Target: 描述该注解的使用范围(METHOD,CLASS,FIELD);
- @Inherited:说明子类可以继承父类中的该注解。
我们来看一下Target注解的内容:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
ElementType是一个枚举类型,其可枚举的值包括:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE,
/**
* Module declaration.
*
* @since 9
*/
MODULE
}
2. 自定义注解
java中使用@interface来创建自己的注解,此时会自动帮我们继承
java.lang.annotation.Annotation
接口。格式为:
@元注解
@interface MyAnnotation {
}
我们可以在注解中定义参数,其格式为
参数类型 参数名();
且只能定义String、基本数据类型,数组和enum。注意:注解中参数的赋值是没有顺序的。如果该注解只有一个值,那么说明时可以使用
value=xxx
的方式赋值。
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
// 注解的参数,如果使用了注解并且没有为参数设置初始值,那么就会报错
String name();
// 如果该参数的值默认是空,那么不需要初始化
String addr() default "";
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
}
使用value作为注解的参数名时,当只有一个参数,在赋值时可忽略字符串。
@MyAnnotation3
public class AnnotationTest {
// 在使用时可以忽略
@MyAnnotation2("zhangsan")
@MyAnnotation3
public static void main(String[] args) {
}
}
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
// 注解的参数,如果使用了注解并且没有为参数设置初始值,那么就会报错
String name();
// 如果该参数的值默认是空,那么不需要初始化
String addr() default "";
int id() default 0; // 0表示默认为0
int sal() default -1; // -1表示报不存在
String[] client() default {"zhangsan", "lisi"};
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
// 使用value作为参数名
String value();
}
//Target注解的value是一个数组,所以可以指定多个作用域
@Target(value={ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation3 {
}
3. 反射
3.1 概念
反射机制使得java具备了动态性,即在java程序运行的时候,程序可以根据运行时的代码,改变自身的一些结构。
反射机制允许java程序在运行时,借助一些API来获取一个类的全部信息,并直接对该类中的属性、方法进行操作。
3.2 核心机制
我们知道,java的代码在编写完成,通过编译器编译之后会生成
.class
文件,这称为字节码文件。一个字节码文件会通过类加载器加载到Java虚拟机中,在被调用时进行实例化。
**反射机制想创建对象也必须获取到该对象的字节码文件。**在java中,class文件也是一个类。通常通过
Class c = Class.forName("java.lang.String");
,或者
Object.getClass()
获得。
类加载机制
首先我们先了解一下class文件。
先通过反射的方式获取到class文件:如下的三个HashCode值是相同的,说明class文件在JVM运行时只会保存一份。一个类在被加载到内存中,其所有的内容都会被封装到Class对象。
Class person1 = Class.forName("com.yindarui.annotation.Person");
Person person = new Person();
Class person2 = person.getClass();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
//结果是一样的
1967205423
1967205423
那么到底什么是Class类呢?所有的其他类都由Class类创建,但是Class类本身也是一个类。其封装了一个对象(编写的代码)里面的全部内容。JVM可以通过Class对象创建别的对象。同样的,对于任何一个对象,JVM都可以通过对象的全限定名找到其Class文件,并获取该Class文件的全部内容。
3.3 使用Class类
Class类用方法:
类型 | 方法 | 功能 |
---|---|---|
static | forName(String name) | 返回指定类名的对象 |
public | Object newInstance() | 通过class对象调用默认的构造函数返回一个对象 |
public | String getName() | 返回该class对象的全限定名 |
public | Class getSuperClass() | 返回该class对象的父类的class对象 |
public | Class[] getInterfaces() | 返回该class对象实现的全部接口 |
public | ClassLoader getClassLoader() | 返回该class对象的类加载器 |
public | Constructor[] getConstructors() | 返回该class对象实现的全部接口 |
public | Method getMethod(String name, Class<?>… parameterTypes)) | 返回该class对象指定的方法对象 |
public | Method[] getMethods()) | 返回该class对象的全部方法对象 |
public | Field getDeclaredFields() | 返回Field的一个数组 |
如果你熟悉java类包含的内容,那么你一定会很好理解Class类中提供的方法。这几乎包括了一个类中所有的信息。(还可以获取Annotation,这就是框架中大量使用的)
1. 获取Class文件
ArrayList<Class> classes = new ArrayList<>();
classes.add(Object.class); //根类
classes.add(Iterable.class); // 接口
classes.add(Override.class); // 注解
classes.add(ElementType.class); // 枚举类
classes.add(String[].class); // 数组
classes.add(String[][].class); // 数组
classes.add(int.class); // 基本数据类型
classes.add(void.class); // 空类型
classes.add(Class.class); // Class本身也是一个
for (int i = 0; i < 9; i++) {
System.out.println(classes.get(i).hashCode());
}
// 结果:对于数组而言,一维数组和二维数组即使类型相同,其对应的Class文件也不同。
1426407511
589431969
1967205423
42121758
20671747
257895351
874088044
783286238
705927765
2. java 的内存模型
类的加载过程是:
- load:将class文件读入内存,并创建一个Java.lang.Class对象。class文件是静态的代码,需要将这些代码转化成运行时的一些数据结构,比如为一些值设计数据结构。
-
link:将类的二进制数据处理并放入JRE;
1. 验证:检查代码,确保没有错误;
2. 准备:为类变量分配内存空间并设置默认的初始值;
3. 解析:虚拟机中的常量池的符号引用替换直接引用。
- initialize:JVM对类进行初始化。首先会执行
方法,即类构造器,完成对类的初始化。初始化类的时候,必须先初始化其父类。<clint>()
我们来看一下这个过程:静态代码块会在类被加载时就执行,我们在获取一个类的Class对象时就会把这个Class文件加载进来。
要明确:加载只是把文件读入,并进行分配一定的内存空间,而初始化一个类则会消耗大量的资源,使用反射的方式加载类属于被动加载,很多时候不会触发多余的类的初始化:图片来自b站狂神视频
3. 类加载器
在JVM的内存模型中,类加载器就是负责找到这个类的class文件并加载到JVM中。存储class文件的地方叫方法区,这个区中保存这加载过的class文件,并提供一定的缓存机制,以减少频繁的从磁盘中加载类文件。
java提供了几种类加载器:图片来自b站狂神视频
所谓的不同的类加载器,其实就是负责加载不同地方的class文件。
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // AppClassLoader
ClassLoader parent1 = parent.getParent(); // PlatformClassLoader
System.out.println(parent1); // null
System.out.println(Object.class.getClassLoader()); // null
双亲委派机制
JVM在加载类的时候是从启动加载器向下找的,即如果我们自定义一个java.lang.String类型,是无法被加载进JVM的。因为JVM通过boostrap ClassLoader 获取时就发现了这个类,加载后就停止寻找了。
这样做保证了Java代码的执行的正确性与安全性。
4. 通过反射创建对象
使用newInstance方法创建对象需要该类有无参构造器。
此外,我们还可以用别的方法创建:
- Class类的getDeclaredConstructor() 获取构造器;
- 向构造器中传递参数的class文件,包括该构造器的所有参数;
- 通过Constructor进行实例化。
package com.yindarui.annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
*
*/
public class Constructor {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class c1 = Class.forName("com.yindarui.annotation.Student");
java.lang.reflect.Constructor stuCon = c1.getDeclaredConstructor(int.class, String.class);
Object yindarui = stuCon.newInstance(1, "yindarui");
System.out.println(yindarui.toString());
Method setId = c1.getDeclaredMethod("setId", int.class);
System.out.println("通过invoke来修改id参数");
setId.invoke(yindarui,20);
System.out.println(yindarui.toString());
System.out.println("通过invoke来修改name参数, 此时setName方法是私有的");
Method setName = c1.getDeclaredMethod("setName", String.class);
setName.setAccessible(true);
setName.invoke(yindarui,"helloworld");
System.out.println(yindarui.toString());
}
}
class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public void setId(int id) {
this.id = id;
}
private void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
结果:
总之,通过反射来操作属性和方法时,一定要传递你要操作的对象。注意:如果你访问的属性、方法或者构造器是私有的,那么在调用之前一定要使用setAccessible来获取权限。
5. 反射获取注解
注解开发的核心就是利用反射机制获取一个class文件中的注解内容。通常有几种方式:
- 通过class获取全部注解;
- 通过指定某个注解名称获取注解;
- 通过某个属性或者方法获取标记在其上的注解;
package com.yindarui.annotation;
import java.lang.annotation.*;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
@MyAnnotation5("hello")
public class ReflectionForAnnotation {
@MyAnnotation4("id = 10")
public int id;
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class aClass = Class.forName("com.yindarui.annotation.ReflectionForAnnotation");
Field id = aClass.getDeclaredField("id");
// 获取全部的注解
Annotation[] annotation2 = aClass.getAnnotations();
// 获取指定的注解
MyAnnotation5 annotation1 = (MyAnnotation5) aClass.getAnnotation(MyAnnotation5.class);
System.out.println(annotation1);
System.out.println(annotation1.value());
// 通过属性名获取标记在属性上的注解,方法也一样
MyAnnotation4 annotation = id.getAnnotation(com.yindarui.annotation.MyAnnotation4.class);
// 获取在id属性上的全部注解
Annotation[] annotations = id.getAnnotations();
System.out.println(annotation.value());
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MyAnnotation4{
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation5{
String value();
}
结果: