JAVA注解与反射学习笔记
一、注解
1、注解的定义
在百度百科中,注解的定义是这样的:从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
从这段话不难看出,注解就是java中对代码的一种标记,利用这些标记我们可以对原有的代码进行一些补充。
在java中,注解其实无处不在,相信大家在很多地方都看到过,例如,我们在某个类中重写toString方法时,会看到这个注解:
public class testAnnotation {
private int age;
private String name;
@Override //@Override注解,标注这是一个重写的方法。
public String toString() {
return "testAnnotation{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
2、java的几个内置注解
在java中有几个内置注解(基本注解):
注解名 | 作用 |
@Deprecated | 用于标记过时的类、方法、成员变量等 |
@Override | 覆盖了父类方法 |
@SuppressWarning | 忽视警告 |
@FunctionaInterface | 指定接口为函数式接口 |
@SafeVarargs | 在声明含泛型可变参数的构造函数或方法时,忽略编译器unchecked警告。 |
3、元注解
元注解就是用在注解上的注解,java中定义了4个元注解,负责对其他注解进行说明
(1)@Target
@Tatget元注解用来标记某个注解可以使用的位置,一共有10个属性值
属性值 | 作用范围 |
TYPE | 标记该注解可用于类或者接口上 |
FIELD | 标记该注解可用于用于域上 |
METHOD | 标记该注解可用于方法上 |
PARAMETER | 标记该注解可用于参数上 |
CONSTRUCTOR | 标记该注解可用于构造方法上 |
LOCAL_VARIABLE | 标记该注解可用于局部变量上 |
ANNOTATION_TYPE | 标记该注解可用于注解类型上(被@interface修饰的类型) |
PACKAGE | 标记该注解用于记录java文件的package信息 |
TYPE_PARAMETER(JDK1.8新增) | 标记该注解可用于类型参数声明 |
TYPE_USE(JDK1.8新增) | 标记该注解可标注任何类型名称 |
关于TYPE_PARAMETER和TYPE_USE的使用,
@Target(ElementType.TYPE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeParameterAnnotation {
}
// 如下是该注解的使用例子
public class TypeParameterClass<@TypeParameterAnnotation T> {
public <@TypeParameterAnnotation U> T foo(T t) {
return null;
}
}
public class TestTypeUse {
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeUseAnnotation {
}
public static @TypeUseAnnotation class TypeUseClass<@TypeUseAnnotation T> extends @TypeUseAnnotation Object {
public void foo(@TypeUseAnnotation T t) throws @TypeUseAnnotation Exception {
}
}
// 如下注解的使用都是合法的
@SuppressWarnings({ "rawtypes", "unused", "resource" })
public static void main(String[] args) throws Exception {
TypeUseClass<@TypeUseAnnotation String> typeUseClass = new @TypeUseAnnotation TypeUseClass<>();
typeUseClass.foo("");
List<@TypeUseAnnotation Comparable> list1 = new ArrayList<>();
List<? extends Comparable> list2 = new ArrayList<@TypeUseAnnotation Comparable>();
@TypeUseAnnotation String text = (@TypeUseAnnotation String)new Object();
java.util. @TypeUseAnnotation Scanner console = new java.util.@TypeUseAnnotation Scanner(System.in);
}
}
(2)@Retention
@Retention元注解用来标注一个注解在什么级别仍然被保留,用来描述该注解的生命周期
同样的,该注解有三个可选值
属性值 | 保留范围 |
SOURCE | 源代码 |
CLASS | 编译后的class文件 |
RUNTIME | 运行时 |
其中:SOURCE<CLASS<RUNTIME
一般来说,我们自定义注解时@Retention的取值一般选择RUNTIME。
(3)@Document
@Document元注解表示被标记的注解将被含于javadoc中,这个没什么好解释的。
(4)@Inherited
@Inherited元注解说明子类可以继承父类的注解。
4、自定义注解
了解了注解的定义和元注解后,接下来我们就可以自定义我们的注解了。
自定义注解非常简单,代码如下:
@Target(ElementType.TYPE) //标记该注解可以用在类或接口上
@Retention(RetentionPolicy.RUNTIME) //标记该注解的生命周期为运行时仍然有效
public @interface testAnnotation {
}
这样,我们就自定义了一个testAnnotation注解,该注解可以用在类或者接口上,且运行时仍有效。
我们平时使用的注解有些是带有属性值的,那么如何在注解中定义属性值呢?在注解中定义属性其实也非常简单,如下:
@Target(ElementType.TYPE) //标记该注解可以用在类或接口上
@Retention(RetentionPolicy.RUNTIME) //标记该注解的生命周期为运行时仍然有效
public @interface testAnnotation {
int age(); //定义属性值,只需要类型+属性名+小括号即可
String name();
}
这样,我们在使用该注解时,就需要传入属性值。如果我们想让属性拥有默认值,只需要加上default+默认值
@Target(ElementType.TYPE) //标记该注解可以用在类或接口上
@Retention(RetentionPolicy.RUNTIME) //标记该注解的生命周期为运行时仍然有效
public @interface testAnnotation {
int age () default 1;
String name() default "ForYou丶";
}
使用该注解:
//注解的属性有默认值,所以不传值不会报错,如果没有默认值,就会报错。
@testAnnotation()
public class test {
}
//也可以传入我们自己的值
@testAnnotation(age = 18,name = "Me")
public class test {
}
//属性值的填写不分先后,上面的写法和下面的一样
@testAnnotation(name = "Me",age = 18)
public class test {
}
如果只有一个属性值,注解可以这么定义:
@Target(ElementType.TYPE) //标记该注解可以用在类或接口上
@Retention(RetentionPolicy.RUNTIME) //标记该注解的生命周期为运行时仍然有效
public @interface testAnnotation {
int value(); //只有一个属性值时,可以写成类型+value(必须是value)+小括号
}
这样,传值时就不需要写 属性 = 值,可以直接写值
@testAnnotation(25)
public class test {
}
二、反射
讲完了注解我们来讲讲反射,在java中,反射的其中一个用途是用来处理注解的。在前面我们注解的定义中讲到,注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。这里的进行相应的处理,就是利用反射完成的。那什么是反射呢?
1、反射的定义
java反射的定义如下:Java反射指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。
java的反射机制赋予了java准动态语言的能力。我们都知道,java是一门静态语言,相对于动态语言,如python,c#等,静态语言在运行时结构是不可变的。但是在jdk1.8之后,java新增了反射机制,可以在运行时构造类的实例对象,操作类的实例对象的属性和方法等,具有了一定的动态性,使得java成了一门准动态语言。
2、反射机制的使用
想要通过反射机制对类进行分析操作并不难,只需要获取相应类的class对象,调用方法即可。
举个例子,先来一个user类:
public class user {
private int age;
private String name;
public user() {
}
public user(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "user{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
获取user类的class对象有三种方式
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//通过user类的实例对象调用getClass方法
user user = new user();
Class c2 = user.getClass();
//直接对象.class拿到user类的class对象
Class c3 = user.class;
}
}
我们来调用一下这三个class对象的hashCode方法并打印,看看结果是什么
竟然是一致的。我们都知道hashcode一致代表着这是同一个对象,也就是说,这三种方法得到的class对象是同一个,在Java中,class对象是唯一的。
拓展:如何保证class类是唯一的?
在java虚拟机中,内存分为堆,栈和方法区。堆用来存放new的对象和数组,栈用来存放一些变量,方法区存放类的所有静态变量和class对象。(在jdk1.8后,class对象存放的位置变成了堆。)
java加载类到内存的过程是:加载->链接->初始化
加载是指jvm虚拟机将类的.class字节码文件装载进内存,并生成运行时的数据结构和类的class对象的一个过程
链接是将类的二进制代码合并到jvm中运行的过程,包含三个阶段
- 验证:确保加载的类符合jvm规范
- 准备:为static变量分配内存和设置初始值
- 解析:虚拟机常量池内的符号应用替换为直接引用。
初始化是执行类构造器初始化方法的过程。执行所有的static方法和为static变量赋值。
从这个过程我们可以看出,要保证class的唯一性,最重要的就是加载正确.class文件且只加载一次,而加载.class文件是由类加载器完成的。
在java中一共有三种类加载器
类加载器 | 加载的类 |
bootstrap classloader | 加载Java的核心类 |
extensions classloader | 加载JRE的扩展目录 |
system classloader | 加载用户自定义的类 |
那么问题来了,jvm虚拟机是怎么知道一个类是java的核心类还是用户自定义的类呢?你可以说classloader通过扫描路径来区分和加载类。那要是假如我也创建了java.lang包,并写了一个String类,我们知道class类对象是唯一的,那我写的这个类是不是就会覆盖jdk中的String类呢?
很明显这是不可能的,这就要提到一个机制叫双亲委派机制。
什么是双亲委派机制?
双亲委派机制是指在一个类提交到classloader里加载时,该classloader会先检查是否加载过该类,如果有,则不再加载,如果没有,就将类委托给他的父classloader进行加载而不是自己加载,父classloader进行同样的流程,如果父classloader无法加载该类,那就返回给原来的classloader进行加载。很明显,有了这个机制,就可以保证类只被加载一次(class对象唯一),而且保证了核心类的安全性。
类加载的源码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
3、使用class对象
反射机制十分强大,我们可以通过操作class对象来获取类的结构。
获取类的名称
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//获取类的全限定名称
c1.getName();
//获取类的简单名称
c1.getSimpleName();
}
}
打印输出:
com.example.demo.user
user
获取类的属性
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//获取类的所有public属性
Field[] fields = c1.getFields();
//获取类的所有属性
Field[] declaredFields = c1.getDeclaredFields();
//打印输出一下
for (Field field : fields) {
System.out.println("公开:"+field);
}
for (Field declaredField : declaredFields) {
System.out.println("所有:"+declaredField);
}
//获取类的一个public属性,user类中我们没有写public属性,这里就不测试了
//Field age = c1.getField("xxxx");
//获取类的一个属性(任意)
System.out.println("特定:"+c1.getDeclaredField("name"));
}
}
打印输出:
所有:private int com.example.demo.user.age
所有:private java.lang.String com.example.demo.user.name
特定:private java.lang.String com.example.demo.user.name
获取类的方法并调用
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//获取所有public方法
Method[] methods = c1.getMethods();
//获取所有方法
Method[] declaredMethods = c1.getDeclaredMethods();
//获取特定方法,参数为方法名和方法的参数类型
Method setName = c1.getDeclaredMethod("setName", String.class);
//打印输出获取到的方法
for (Method method : methods) {
System.out.println("公开"+method);
}
for (Method declaredMethod : declaredMethods) {
System.out.println("所有"+declaredMethod);
}
//通过反射创建对象,这里的newInstance方法本质上使用了类的无参构造器
user u = (user)c1.newInstance();
//通过反射调用方法,需要两个参数,一个是调用对象,一个是方法所需的参数
setName.invoke(u, "ForYou丶");
//测试一下是否调用成功
System.out.println(u.getName());
}
}
打印输出:
公开public java.lang.String com.example.demo.user.toString()
公开public java.lang.String com.example.demo.user.getName()
公开public void com.example.demo.user.setName(java.lang.String)
公开public int com.example.demo.user.getAge()
公开public void com.example.demo.user.setAge(int)
公开public final void java.lang.Object.wait() throws java.lang.InterruptedException
公开public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
公开public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
公开public boolean java.lang.Object.equals(java.lang.Object)
公开public native int java.lang.Object.hashCode()
公开public final native java.lang.Class java.lang.Object.getClass()
公开public final native void java.lang.Object.notify()
公开public final native void java.lang.Object.notifyAll()
所有public java.lang.String com.example.demo.user.toString()
所有public java.lang.String com.example.demo.user.getName()
所有public void com.example.demo.user.setName(java.lang.String)
所有public int com.example.demo.user.getAge()
所有public void com.example.demo.user.setAge(int)
ForYou丶
还有其他的包括获取类的构造器方法什么的我们就不再测试了,这里写一下如何通过反射获取注解信息
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//获取类的所有注解
Annotation[] annotations = c1.getAnnotations();
//获取特定一个注解
Annotation annotation1 = c1.getAnnotation(testAnnotation.class);
//打印输出
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
System.out.println(annotation1);
}
}
打印输出:
@com.example.demo.testAnnotation(value=20)
@com.example.demo.testAnnotation(value=20)
我们前面写注解的时候提过通过反射可以对注解标记的内容进行处理,假设我们这个testAnnotation是想要为user对象的age属性赋一个值,我们可以这么做
首先是user类标注testAnnotation注解
@testAnnotation(22)
public class user {
private int age;
private String name;
public user() {
}
public user(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "com.example.demo.user{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
通过反射获取注解的值并设置
public class testReflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
//通过Class类的静态方法forName,参数为类的全限定名称
Class c1 = Class.forName("com.example.demo.user");
//通过反射创建对象,这里的newInstance方法本质上使用了类的无参构造器
user u = (user)c1.newInstance();
//获取注解上的值
testAnnotation annotation = (testAnnotation) c1.getAnnotation(testAnnotation.class);
int value = annotation.value();
//设置值
u.setAge(value);
System.out.println(u.getAge());
}
}
打印输出:
22
这个例子写的比较简单,但是大体上使用反射对注解标记的类加载,运行时被读取,并执行相应的处理是这个思路。
写在最后
这篇博客本质上是对自己学习的一个总结和分享,通过写这篇博客我也体会到了从对内吸收到对外输出的转变的乐趣。学会一样东西,不仅仅是会用,而是要有把它对外输出的能力。
如果有错误请各位大佬指正。不胜感谢。