- Java的反射、依赖注入和注解
反射(Reflection)是程序的自我分析能力,通过反射可以确定类有哪些方法、有哪些构造方法以及有哪些成员变量。Java语言提供了反射机制,通过反射机制能够动态读取一个类的信息;能够在运行时动态加载类,而不是在编译期。反射可以应用于框架开发,它能够从配置文件中读取配置信息动态加载类、创建对象,以及调用方法和成员变量。
提示 Java反射机制在一般的Java应用开发中很少使用,即便是Java EE阶段也很少使用。除非你为了开发一个框架或出于兴趣对反射机制感兴趣,否则可以跳过本章内容。
2.1 Java反射机制API
Java反射机制API主要是 java.lang.Class类和java.lang.reflect包。
2.1.1 java.lang.Class类
java.lang.Class类是实现反射的关键所在,Class类的一个实例表示Java的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和void,void是“无类型”,主要用于方法返回值类型声明,表示不需要返回值。Class没有公有的构造方法,Class实例是由JVM在类加载时自动创建的。
在程序代码中获得Class实例可以通过如下代码实现;
//1.通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
//2.通过对象的getClass()方法
Class clz2 = str.getClass();
每一种类型包括类和接口等,都有一个class静态变量可以获得Class实例。另外,每一个对象都有getClass()方法可以获得Class实例,该方法是由Object类提供的实例方法。
Class类提供了很多方法可以获得运行时对象的相关信息,下面的程序代码展示了其中一些方法。
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
// 获得Class实例
// 1.通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
// 2.通过对象的getClass()方法
Class clz2 = str.getClass();
//获得int类型Class实例
Class clz3 = int.class; ①
//获得Integer类型Class实例
Class clz4 = Integer.class; ②
System.out.println("clz2类名称:" + clz2.getName());
System.out.println("clz2是否为接口:" + clz2.isInterface());
System.out.println("clz2是否为数组对象:" + clz2.isArray());
System.out.println("clz2父类名称:" + clz2.getSuperclass().getName());
System.out.println("clz2是否为基本类型:" + clz2.isPrimitive());
System.out.println("clz3是否为基本类型:" + clz3.isPrimitive());
System.out.println("clz4是否为基本类型:" + clz4.isPrimitive());
}
}
运行结果如下:
clz2类名称:java.lang.String
clz2是否为接口:false
clz2是否为数组对象:false
clz2父类名称:java.lang.Object
clz2是否为基本类型:false
clz3是否为基本类型:true
clz4是否为基本类型:false
注意上述代码第①行和第②行的区别,int和Integer的区别在于int是基本数据类型,所以输出结果为true,Integer是类,是引用类型。可见Class可以描述int等基本数据类型运行时实例。
2.1.2 java.lang.reflect包
java.lang.reflect包提供了反射中用到类,主要的类说明如下:
Constructor类:提供类的构造方法信息。
Field类:提供类或接口中成员变量信息。
Method类:提供类或接口成员方法信息。
Array类:提供了动态创建和访问Java数组的方法。
Modifier类:提供类和成员访问修饰符信息。
示例代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class HelloWorld {
public static void main(String[] args) {
try {
// 动态加载xx类的运行时对象
Class c = Class.forName("java.lang.String"); ①
// 获取成员方法集合
Method[] methods = c.getDeclaredMethods(); ②
// 遍历成员方法集合
for (Method method : methods) { ③
// 打印权限修饰符,如public、protected、private
System.out.print(Modifier.toString(method.getModifiers())); ④
// 打印返回值类型名称
System.out.print(" " + method.getReturnType().getName() + " ")⑤
// 打印方法名称
System.out.println(method.getName() + "();"); ⑥
}
} catch (ClassNotFoundException e) { ⑦
System.out.println("找不到指定类");
}
}
}
上述代码第①行是通过Class的静态方法forName(String)创建某个类的运行时对象,其中的参数是类全名字符串,如果在类路径中找不到这个类则抛出ClassNotFoundException异常,见代码第⑦行。
代码第②行是通过Class的实例方法getDeclaredMethods()返回某个类的成员方法对象数组。代码第③行是遍历成员方法集合,其中的元素是Method类型。
代码第④行的method.getModifiers()方法返回访问权限修饰符常量代码,是int类型,例如1代表public,这些数字代表的含义可以通过Modifier.toString(int)方法转换为字符串。代码第⑤行通过Method的getReturnType()方法获得方法返回值类型,然后再调用getName()方法返回该类型的名称。代码第⑥行method.getName()返回方法名称。
2.2 创建对象
Java反射机制提供了另外一种创建对象方法,Class类提供了一个实例方法newInstance(),通过该方法可以创建对象,使用起来比较简单,下面两条语句实现了创建字符串String对象。
Class clz = Class.forName("java.lang.String");
String str = (String) clz.newInstance();
这两条语句相当于String str = new String()语句。另外,需要注意newInstance()方法有可以会抛出InstantiationException和IllegalAccessException异常,InstantiationException不能实例化异常,IllegalAccessException是不能访问构造方法异常。
2.2.1 调用构造方法
调用方法newInstance()创建对象,这个过程中需要调用构造方法,上面的代码只是调用了String的默认构造方法。如果想要调用非默认构造方法,需要使用Constructor对象,它对应着一个构造方法,获得Constructor对象需要使用Class类的如下方法:
- Constructor[] getConstructors():返回所有公有构造方法Constructor对象数组。
- Constructor[] getDeclaredConstructors():返回所有构造方法Constructor对象数组。
- Constructor getConstructor(Class... parameterTypes):根据参数列表返回一个共有Constructor对象。参数parameterTypes是Class数组,指定构造方法的参数列表。
- Constructor getDeclaredConstructor(Class... parameterTypes):根据参数列表返回一个Constructor对象。参数parameterTypes同上。
示例代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.lang.reflect.Constructor;
public class HelloWorld {
public static void main(String[] args) {
try {
Class clz = Class.forName("java.lang.String");
// 调用默认构造方法
String str1 = (String) clz.newInstance(); ①
// 设置构造方法参数类型
Class[] params = new Class[1]; ②
// 第一个参数是String
params[0] = String.class; ③
// 获取与参数对应的构造方法
Constructor constructor = clz.getConstructor(params); ④
// 为构造方法传递参数
Object[] argObjs = new Object[1]; ⑤
// 第一个参数传递"Hello"
argObjs[0] = "Hello"; ⑥
// 调用非默认构造方法,构造方法第一个参数是String类型
String str2 = (String) constructor.newInstance(argObjs); ⑦
System.out.println(str2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述代码第①行是通过调用Class的newInstance()方法创建String对象,这个过程中使用String的默认构造方法public String()。
代码第②行~第⑦行通过反射机制调用String的public String(String original)构造方法。代码第②行和第③行是设置构造方法参数类型,参数有可能有多个需要Class数组类型。代码第④行是构造方法Constructor对象。代码第⑤行和第⑥行是为构造方法准备参数值,参数值放到Object数组中,与第②行的参数类型是一一对应的。
代码第⑦行是通过调用Constructor对象的newInstance(Object... initargs)方法创建String对象。
2.2.2 案例:依赖注入实现
Java反射机制能够在运行时动态加载类,而不是在编译期。在一些框架开发中经常将要实例化的类名保存到配置文件中,在运行时从配置文件中读取类名字符串,然后动态创建对象,建立依赖关系1。采用new创建对象依赖关系是在编译期建立的,反射机制能够将依赖关系推迟到运行时建立,这种依赖关系动态注入进来称为依赖注入。
依赖关系是一种非常普遍的关系,如果在A中使用了B,B变化会引起A的变化,A依赖于B。
例如:如图4-1所示有三个类,Student和Worker继承自Person,在HelloWorld类的main()方法中会创建Person子类实例,至于依赖哪一个类,Student还是Worker,可以在运行时从配置文件中读取,然后创建对象。
图2-1 Person继承类图
下面介绍一下这个案例实现过程,在Eclipse项目的根目录下创建一个文本文件 Configuration.ini,文件内容如下:
com.a51work6.Student
文件中只有一行字符串,前后没有空格,它是要配置的类全名,根据自己情况修改配置信息。
HelloWorld类代码如下:
//HelloWorld.java文件
package com.a51work6;
… …
public class HelloWorld {
public static void main(String[] args) {
try {
String className = readClassName(); ①
Class clz = Class.forName(className); ②
// 指定参数类型
Class[] params = new Class[3];
// 第一个参数是String
params[0] = String.class;
// 第二个参数是int
params[1] = int.class;
// 第三个参数是String
params[2] = String.class;
// 获取对应参数的构造方法
Constructor constructor = clz.getConstructor(params);
// 设置传递参数
Object[] argObjs = new Object[3];
// 第一个参数传递"Tony"
argObjs[0] = "Tony";
// 第二个参数传递18
argObjs[1] = 18;
// 第三个参数传递"清华大学"
argObjs[2] = "清华大学";
// 调用非默认构造方法
Object p = constructor.newInstance(argObjs); ③
System.out.println(p);
} catch (Exception e) {
e.printStackTrace();
}
}
// 从Configuration.ini文件中读取类名
public static String readClassName() {
FileInputStream readfile = null;
InputStreamReader ir = null;
BufferedReader in = null;
try {
readfile = new FileInputStream("Configuration.ini");
ir = new InputStreamReader(readfile);
in = new BufferedReader(ir);
// 读取文件中的一行数据
String str = in.readLine();
return str;
} catch (FileNotFoundException e) {
System.out.println("处理FileNotFoundException...");
e.printStackTrace();
} catch (IOException e) {
System.out.println("处理IOException...");
e.printStackTrace();
}
return null;
}
}
上述代码第①行通过调用readClassName()方法从Configuration.ini文件中读取类名,读取Configuration.ini文件内容,采用Java I/O技术,关于I/O流这里不再赘述。
代码第②行通过从配置文件Configuration.ini中读取的字符串创建Class对象。代码第③行是调用三个参数构造方法创建对象,这个对象是哪个类的实例,与你的Configuration.ini文件中配置字符串有关。
2.3 调用方法
通过反射机制还可以调用方法,这与调用构造方法类似。调用方法需要使用Method对象,它对应着一个方法,获得Method对象需要使用Class类的如下方法:
- Method[] getMethods():返回所有公有方法Method对象数组。
- Method[] getDeclaredMethods():返回所有方法Method对象数组。
- Method getMethod(String name, Class... parameterTypes):通过方法名和参数类型返回公有方法Method对象。参数parameterTypes是Class数组,指定方法的参数列表。
- Method getDeclaredMethod(String name, Class... parameterTypes):通过方法名和参数类型返回方法Method对象。参数parameterTypes同上。
现有一个Person类,它的代码如下:
//Person.java文件
package com.a51work6;
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setNameAndAge(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
现在编写一个程序通过反射机制调用Person类setNameAndAge和setName方法,具体代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.lang.reflect.Method;
public class HelloWorld {
public static void main(String[] args) {
try {
Class clz = Class.forName("com.a51work6.Person");
// 调用默认构造方法
Person person = (Person) clz.newInstance();
System.out.println(person);
// 指定参数类型
Class[] params = new Class[2];
// 第一个参数是String
params[0] = String.class;
// 第二个参数是int
params[1] = int.class;
// 获取setNameAndAge方法对象
Method method = clz.getMethod("setNameAndAge", params); ①
// 设置传递参数
Object[] argObjs = new Object[2];
// 第一个参数传递"Tony"
argObjs[0] = "Tony";
// 第二个参数传递18
argObjs[1] = 18;
//调用setNameAndAge方法
method.invoke(person, argObjs); ②
System.out.println(person);
// 获取getName方法对象
method = clz.getMethod("getName"); ③
// 调用getName方法
Object result = method.invoke(person); ④
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果如下:
Person [name=null, age=0]
Person [name=Tony, age=18]
Tony
上述代码实现了调用Person类setNameAndAge和setName方法。代码第①行是获取setNameAndAge方法对象,params参数是指定参数类型,这个过程与构造方法类似。代码第②行method.invoke(person, argObjs)语句是调用setNameAndAge方法,person是要调用的对象,argObjs是设置要传递的参数值。
代码第③行是获取getName方法对象,该方法没有参数。代码第④行method.invoke(person)语句是调用person的getName方法,invoke方法会返回一个Object对象,它是调用目标方法的返回数据,本例中相当于调用getName方法返回的String类型数据。
2.4 调用成员变量
通过反射机制还可以调用成员变量,调用方法需要使用Field对象,它对应着一个方法,获得Field对象需要使用Class类的如下方法:
- Field[] getFields():返回所有公有成员变量Field对象数组。
- Field[] getDeclaredFields():返回所有成员变量Field对象数组。
- Field getField(String name):通过指定公共成员变量名返回Field对象。
- Field getDeclaredField(String name):通过指定成员变量名返回Field对象。
现有一个Person类,它的代码如下:
//Person.java文件
package com.a51work6;
public class Person {
private String name = "";
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
Person类有两个私有的成员变量name和age。
提示 Java的反射机制非常强大,可在类外部调用类的私有成员变量和成员方法。这种功能看似强大,事实上却破坏了面向对象封装性。
现在编写一个程序通过反射机制调用Person类的私有成员变量name和age,具体代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.lang.reflect.Field;
public class HelloWorld {
public static void main(String[] args) {
try {
Class clz = Class.forName("com.a51work6.Person");
// 调用默认构造方法
Person person = (Person) clz.newInstance();
// 返回成员变量名为name的Field对象
Field name = clz.getDeclaredField("name"); ①
//设置成员变量accessible标志为true
name.setAccessible(true); ②
//为成员变量name赋值
name.set(person, "Tony"); ③
// 返回成员变量名为age的Field对象
Field age = clz.getDeclaredField("age"); ④
//设置成员变量accessible标志为true
age.setAccessible(true); ⑤
//为成员变量age赋值
age.set(person, 18); ⑥
// 获取成员变量保存的数据
System.out.printf("[name:%s, age:%d]",
name.get(person), age.get(person)); ⑦
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下:
Person [name=, age=0]
Person [name=Tony, age=18]
上述代码第①行和第④行是通过名字返回成员变量Field对象,注意这里是有的getDeclaredField方法,而不是getField方法,因为这两个成员变量都是私有的。
代码第②行和第⑤行是设置成员变量accessible标志为true,accessible是可访问性标志,值为 true 则指示反射的对象在使用时应该取消Java语言访问检查。值为false则指示反射的对象应该实施Java语言访问检查。不仅是成员变量,方法和构造方法也可以通过setAccessible(true)设置,实现对私有方法和构造方法的访问。
代码第③行和第⑥行是调用Field的void set(Object obj, Object value)方法为成员变量赋值,其中obj要访问的目标对象,value是要赋给成员变量的数据。
代码第⑦行通过调用Field的Object get(Object obj)方法获取成员变量保存的数据,其中obj要访问的目标对象,方法返回值是成员变量的保存的数据。
2.5 注解(Annotation)
Java 5之后可以在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),例如在方法覆盖中使用过的@Override注解,注解都是@符号开头的。
提示 Annotation可以翻译为“注解”或“注释”,笔者推荐翻译为“注解”,因为“注释”一词已经用于说明//、/**...*/和/*...*/等符号,这里的“注释”是英文Comment翻译。
注解并不能改变程序运行的结果,不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有的注解可以在运行时读写字节码文件信息。
提示 使用注解对于代码实现功能没有任何的影响。程序员即便是不知道注解,也完全可以编写Java程序代码。
2.5.1 基本注解
无论是哪一种注解,本质上都一种数据类型,是一种接口类型。到Java 8为止Java SE提供11种内置注解。其中有5是基本注解,它们来自于java.lang包。有6个是元注解1(Meta?Annotation),它们来自于java.lang.annotation包,自定义注解会用到元注解,元注解就是负责注解其他的注解。
基本注解包括:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs和@FunctionalInterface。下面逐一介绍一下。
@Override
@Override只能用于方法,子类覆盖父类方法(或者实现接口的方法)时可以@Override注解。编译器会检查被@Override注解的方法,确保该方法父类中存在的方法,否则会有编译错误。
使用@Override注解示例代码如下:
//Person.java文件
package com.a51work6;
public class Person {
private String name = "";
private int age;
... ...
@Override
public String t0String() { //toString() ①
return "Person [name=" + name
+ ", age=" + age + "]";
}
}
在代码第①行的方法toString()是覆盖Object类的方法,该方法使用@Override注解。如果toString()被错误成了t0String(),那么程序会发生编译错误。会有如下的代码提示:
类型为 Person 的方法t0String()必须覆盖或实现超类型方法
注意 当然如果该方法前面不加@Override注解,即便是方法写错误了,也不会有编译错误,但是Object父类的toString()方法并没有被覆盖。这会引起程序出现Bug(缺陷)。
@Deprecated
@Deprecated用来指示API已经过时了,@Deprecated可以用来注解类、接口、成员方法和成员变量。
使用@Deprecated注解示例代码如下:
//Person.java文件
package com.a51work6;
@Deprecated
public class Person{ ①
@Deprecated
protected String name; ②
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Deprecated
public void setNameAndAge(String name, int age) { ③
this.name= name;
this.age= age;
}
@Override
public String toString() {
return "Person [name=" + name+ ", age=" + age + "]";
}
}
上述代码第①行类Person、第②行的成员变量name和第③行的setNameAndAge方法都被@Deprecated注解。在Eclipse中这些被注解的API都画上删除线。调用这些API代码也会有删除线,示例代码如下。
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
Person p = new ();Person
p.("Tony", 20);setNameAndAge
p.= "Tom";name
}
}
不仅代码中有删除线,而且还有编译警告。
@SuppressWarnings
@SuppressWarnings注解用来抑制编译器警告,如果你确认程序中的警告没有问题,可以不用理会。但是就是不想看到这些警告,可以使用@SuppressWarnings注解消除这些警告。
使用@SuppressWarnings注解示例代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.util.ArrayList;
import java.util.List;
public class HelloWorld {
@SuppressWarnings({ "deprecation" }) ①
public static void main(String[] args) {
Person p = new (); ②Person
p.("Tony", 20); ③setNameAndAge
p.= "Tom"; ④name
}
}
上述代码第①行使用@SuppressWarnings({ "deprecation" })注解了main方法,这是由于代码第②行~第④行是编译警告,因为这些API已经过时,见上一节Person代码。@SuppressWarnings注解中的deprecation表示要抑制API已经过时。使用了@SuppressWarnings注解后会发现程序代码的警告没有了。
@SafeVarargs
在介绍@SafeVarargs注解用法之前,先来看看如下代码:
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
// 传递可变参数,参数是泛型集合
display(10, 20, 30); ①
// 传递可变参数,参数是非泛型集合
display("10", 20, 30);// 会有编译警告 ②
}
public static <T> void display(T... array) { ③
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}
}
代码第③行声明了一种可变参数方法display,display方法参数个数可以变化,它可以接受不确定数量的相同类型的参数。可以通过在参数类型名后面加入...的方式来表示这是可变参数。可变参数方法中的参数类型相同,为此声明参数是需要指定泛型。
但是调用可变参数方法时,应该提供相同类型的参数,代码第①行调用时没有警告,而代码第②行调用时则会发生警告,这个警告是unchecked“未检查不安全代码”,就是由于将非泛型变量赋值给泛型变量所发生的。
那么如何抑制编译器警告可用使用@SafeVarargs注解,修改代码如下:
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
// 传递可变参数,参数是泛型集合
display(10, 20, 30);
// 传递可变参数,参数是非泛型集合
display("10", 20, 30);// 没有@SafeVarargs会有编译警告
}
@SafeVarargs
public static <T> void display(T... array) {
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}
}
在可变参数display前添加@SafeVarargs注解。当然可用也可以使用@SuppressWarnings("unchecked")注解,但@SafeVarargs注解更适合。
@FunctionalInterface
@FunctionalInterface注解是Java 8增加的,用于接口的注解,声明接口是函数式接口,在前面讲解Lambda表达式时介绍过函数式接口,有关@FunctionalInterface注解不再赘述。
5.2.2 元注解
元注解包括:@Documented、@Target、@Retention、@Inherited、@Repeatable和@Native。元注解是为其他注解进行说明的注解,当自定义一个新的注解类型时,其中可以使用元注解。这一节先介绍一下这几种元注解含义,下一节在自定义注解中详细介绍它们的使用的。
@Documented
如果在一个自定义注解中引用@Documented注解,那么该注解可以修饰代码元素(类、接口、成员变量和成员方法等),javadoc等工具可以提取这些注解信息。
@Target
@Target注解用来指定一个新注解的适用目标。@Target注解有一个成员(value)用来设置适用目标,value是java.lang.annotation.ElementType枚举类型的数组,ElementType描述Java程序元素类型,它有10个枚举常量,如表4-1所示。
表 2-1 ElementType枚举类型中的枚举常量
@Retention
@Retention注解用来指定一个新注解的有效范围,@Retention注解有一个成员(value)用来设置保留策略,value是java.lang.annotation.RetentionPolicy枚举类型,RetentionPolicy描述注解保留策略,它有3个枚举常量,如表4-2所示。
表 2-2 RetentionPolicy枚举类型中的枚举常量
@Inherited
@Inherited注解用来指定一个新注解可以被继承。假定一个类A被该新注解修饰,那么这个A类的子类会继承该新注解。
@Repeatable
@Repeatable注解是Java 8新增加的,它允许在相同的程序元素中重复注释,可重复的注释必须使用@Repeatable进行注释。
@Native
@Native注解一个成员变量,指示这个变量可以被本地代码引用。常常被代码生成工具使用。
5.2.3 自定义注解
如果前面的Java SE提供的11内置注解不能满足你的需求,可以自定义注解,注解本质是一种接口,它是java.lang.annotation.Annotation接口的子接口,是引用数据类型。
5.2.3.1 声明注解
声明自定义注解可以使用@interface关键字实现,最简单形式的注解示例代码如下:
// Marker.java文件
package com.a51work6;
public @interface Marker{
}
上述代码声明一个Marker注解,@interface声明一个注解类型,它前面的访问限定修饰符与类一样有两种:公有访问权限和默认访问权限。
注意 关于注解源程序文件与类一样,一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的,源程序文件命名与公有访问权限的注解名一致。
Marker注解中不包含任何的成员,这种注解称为标记注解(Marked Annotation),基本注解中的@Override就属于标记注解。根据需要注解中可以包含一些成员,示例代码如下:
//Marker.java文件
package com.a51work6;
//单值注解
@interface MyAnnotation {
String value();
}
代码中声明MyAnnotation 注解,它有一个成员value,注意value后面是有一对小括号,value前面的是数据类型。成员也可以有访问权限修饰符,但是只能是公有权限和默认权限。
注解中的成员也可以有默认值,示例代码如下:
//Marker.java文件
package com.a51work6;
//带有默认值注解
@interface MyAnnotation1 {
String value() default "注解信息";
int count() default 0;
}
通过关键字default指定默认值。使用这些注解示例代码如下:
//HelloWorld.java文件
package com.a51work6;
@Marker ①
public class HelloWorld {
@MyAnnotation(value = "Annotation") ②
private String info = "";
@MyAnnotation1(count = 10) ③
public static void main(String[] args) {
}
}
默认情况下注解可以修饰任意的程序元素(类、接口、成员变量、成员方法和数据类型等)。代码第①行使用@Marker注解修饰类。代码第②行是@MyAnnotation(value = "Annotation")注解修饰成员变量,其中value = "Annotation"是为value成员提供数值。代码第③行是@MyAnnotation1(count = 10) 注解修饰成员方法,@MyAnnotation1有两个成员,但是只为count成员赋值,另外一个成员value使用默认值。
5.2.3.2 案例:使用元注解
上一节声明注解只是最基本形式的注解,对于复杂的注解可以在声明注解时使用元注解。下面通过一个案例介绍一下在自定义注解中使用元注解,在本案例中定义了两个注解。
首先看看第一个注解MyAnnotation,它用来修饰类或接口,MyAnnotation代码如下:
//MyAnnotation.java文件
package com.a51work6;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;
@Documented ①
@Target({ ElementType.TYPE }) ②
@Retention(RetentionPolicy.RUNTIME) ③
public @interface MyAnnotation { ④
String description(); ⑤
}
上述代码第⑤行是声明注解类型MyAnnotation,其中使用了三个元注解修饰MyAnnotation注解,代码第①行使用@Documented指定MyAnnotation注解信息可以被javadoc工具读取。代码第②行使用@Target({ ElementType.TYPE })指定MyAnnotation注解用于修饰类和接口等类型。代码第③行@Retention(RetentionPolicy.RUNTIME)指定MyAnnotation注解信息可以在运行时被读取。代码第⑤行的description是MyAnnotation注解的成员。
提示 Eclipse工具不仅可以创建类和接口,还可以创建注解,在Eclipse中选择菜单“文件”→“新建”→“注释”,打开如图4-1所示对话框,可以根据自己的需要添加@Retention、@Target或@Documented注解。注意Eclipse工具汉化包将Annotation翻译为“注释”。
图2-2 Eclipse工具创建注解对话框
第二个注解MemberAnnotation,它用来类中成员变量和成员方法,MemberAnnotation代码如下:
//MemberAnnotation.java文件
package com.a51work6;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME) ①
@Target({ ElementType.FIELD, ElementType.METHOD }) ②
public @interface MemberAnnotation { ③
Class<?> type() default void.class; ④
String description(); ⑤
}
上述代码第③行是声明注解类型MemberAnnotation,其中也使用了三个元注解修饰MemberAnnotation注解,代码第①行的@Retention(RetentionPolicy.RUNTIME)指定MemberAnnotation注解信息可以在运行时被读取。代码第②行@Target({ ElementType.FIELD, ElementType.METHOD })指定MemberAnnotation注解用于修饰类中成员。
代码第④行和第⑤行是声明两个成员,type类型是Class<?>,默认值是void.class,void.class是void类型表示方式。description类型是String,没有设置默认值。
提示 代码第④行中Class<?>类型,表示Class的泛型,?是泛型通配符,可以是任何类型。泛型多数情况下尖括号中指定的都某个具体类型,泛型也是为此而设计的。但是有时确实不需要知道具体类型,或者说什么类型都可以,此时可以使用?作为占位符。
使用了MyAnnotation和MemberAnnotation注解是Person类,Person类代码如下:
//Person.java文件
package com.a51work6;
@MyAnnotation(description = "这是一个测试类") ①
public class Person {
@MemberAnnotation(type = String.class, description = "名字") ②
private String name;
@MemberAnnotation(type = int.class, description = "年龄") ③
private int age;
@MemberAnnotation(type = String.class, description = "获得名字") ④
public String getName() {
return name;
}
@MemberAnnotation(type = int.class, description = "获得年龄") ⑤
public int getAge() {
return age;
}
@MemberAnnotation(description = "设置姓名和年龄") ⑥
public void setNameAndAge(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
使用注解时如果当前类与注解不在同一个包中,则需要将注解引入。代码第①行@MyAnnotation注解只能Person之前,修饰Person类型。代码第②行和第③行是使用@MemberAnnotation注解修饰成员变量。代码第④行、第⑤行和第⑥行是使用@MemberAnnotation注解修饰成员方法。
5.2.3.3 案例:读取运行时注解信息
注解是为工具读取信息而准备的。有些工具可以读取源代码文件中的注解信息;有的可以读取字节码文件中的注解信息;有的可以在运行时读取注解信息。但是读取这些注解信息的代码都是一样的,区别只在于自定义注解中@Retention的保留策略不同。
读取注解信息需要反射相关API,Class类如下方法:
- <A extends Annotation> A getAnnotation(Class<A> annotationClass):如果此元素存在annotationClass类型的注解,则返回注解,否则返回null。
- Annotation[] getAnnotations():返回此元素上存在的所有注解。
- Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与getAnnotations()区别在于该方法将不返回继承的注解。
- boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):如果此元素上存在annotationClass类型的注解,则返回true,否则返回false。
- boolean isAnnotation():如果此Class对象表示一个注解类型则返回true。
读者运行时Person类中注解信息代码如下:
//HelloWorld.java文件
package com.a51work6;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class HelloWorld {
public static void main(String[] args) {
try {
Class<?> clz = Class.forName("com.a51work6.Person"); ①
// 读取类注解
if (clz.isAnnotationPresent(MyAnnotation.class)) { ②
MyAnnotation ann = (MyAnnotation) clz.getAnnotation(MyAnnotation.class); ③
System.out.printf("类%s,读取注解描述: %s \n",
clz.getName(), ann.description()); ④
}
// 读取成员方法的注解信息
Method[] methods = clz.getDeclaredMethods(); ⑤
for (Method method : methods) {
if (method.isAnnotationPresent(MemberAnnotation.class)) { ⑥
MemberAnnotation ann = method.getAnnotation(MemberAnnotation.class); ⑦
System.out.printf("方法%s,读取注解描述: %s \n",
method.getName(), ann.description()); ⑧
}
}
// 读取成员变量的注解信息
Field[] fields = clz.getDeclaredFields(); ⑨
for (Field field : fields) {
if (field.isAnnotationPresent(MemberAnnotation.class)) { ⑩
MemberAnnotation ann = field.getAnnotation(MemberAnnotation.class);
System.out.printf("成员变量%s,读取注解描述: %s \n",
field.getName(), ann.description());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下:
类com.a51work6.Person,读取注解描述: 这是一个测试类
方法getName,读取注解描述: 获得名字
方法getAge,读取注解描述: 获得年龄
方法setNameAndAge,读取注解描述: 设置姓名和年龄
成员变量name,读取注解描述: 名字
成员变量age,读取注解描述: 年龄
上述代码第①行是创建Person类对应的Class对象,代码第②行是判断Person类是否存在MyAnnotation注解,如果存在则通过代码第③行的getAnnotation方法将MyAnnotation注解实例返回。代码第④行中ann.description()表达式读取MyAnnotation注解中description成员内容。
代码第⑤行是获得所有成员方法对象数组,通过遍历方法对象数组,在代码第⑥行判断方法中是否存在MemberAnnotation注解,如果存在则通过代码第⑦行的getAnnotation方法将MemberAnnotation注解实例返回。代码第⑧行中ann.description()表达式读取MemberAnnotation注解中description成员内容。
代码第⑨行是获得所有成员变量对象数组,代码第⑩行是判断成员变量中是否存在MemberAnnotation注解。其他的处理与成员方法类似,这里不再赘述。
0 条 查看最新 评论
没有评论