21天学会Java之(Java SE第十四篇):注解、反射机制
注解和反射机制使用特别简单,但是它们在框架中被大量的使用,而如何灵活运用,想要深入理解框架,牢牢的掌握注解和反射机制的知识就显得极其的重要了。
注解
注解不同于注释,注释仅只用于写在源代码中,来使自己或者别人更容易的翻阅源代码。
注解是那些插入到源代码中使用其他工具可以对其进行处理的标签。这些工具可以在源代码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。
注解不会改变程序的编译方式。Java编译器对于包含注解和不包含注解的代码会生成相同的虚拟机指令。
注解主要用途有以下两点:
- 附属文件的自动生成,例如部署描述符或者bean信息类。
- 测试、日志、事务语义等代码的自动生成。
使用注解的前提下,首先我们应该知道注解本身不会做任何事情,它们只是存在于源文件中。编译器将它们置于类文件中,并且虚拟机会将它们载入。
注解的使用
注解接口
注解是由注解接口来定义的,语法格式如下:
修饰符 @interface 注解名{
元素类型声明1
元素类型声明2
...
}
每个元素类型声明语法格式如下(元素类型见下文):
元素类型 对象名(); //不带默认值就是类型的默认值
或者
元素类型 对象名() default value; //带默认值
举个栗子,下面的注解具有三个元素,id、name、age:
public @interface Test{
int id();
String name() default "小王";
int age() default 21;
}
所有的注解接口都隐性地扩展自java.lang.annotation.Annotation接口。这个接口是一个常规接口,不是一个注解接口。下表是这个接口的一些常用方法:
方法 | 用途 |
---|---|
Class<? extends Annotation> annotationType() | 返回Class对象,它用于描述该注解对象的注解接口。注意:调用注解对象上的getClass方法可以返回真正的类,而不是接口 |
boolean equals(Object other) | 如果other是一个实现了与该注解对象相同的注解接口的对象,并且如果该对象和other的所有元素彼此相等。那么返回True |
int hashCode() | 返回一个与equals方法兼容、由注解接口名以及元素值衍生而来的散列码 |
String toString() | 返回一个包含注解接口名以及元素值的字符串表示,例如,@Test(id=0,name=“小王”,age=21) |
所有的注解接口都直接扩展自java.lang.annotation.Annotation,我们不需要为注解接口提供实现类。
注解元素的类型为下列之一:
- 基础数据类型(byte、short、int、long、char、double、float或boolean)
- String
- Class(具有一个可选的类型参数,例如Class<? extends MyClass> )
- enum类型
- 注解类型
- 有前面所述类型组成的数组(由数组组成的多维数组不是合法的元素类型)
注解
注解的格式
每个注解都具有这种格式@注解名(元素名1=值1,元素名2=值2,....)
例如上文的注解他应该这种格式@Test(id=0,name="小王",age=21)
而元素的顺序无关紧要,@Test(name="小王",age=21,id=0)
这个注解和前面那个注解一样。
如果某个元素的值并未指定,那么就使用声明的默认值,或者元素类型的默认值。例如@Test(id=0)
,那么元素name的值就是字符串小王,元素age的值就是21。
注意事项:默认值并不是和注解存储在一起的;它们是动态计算而来的。例如,如果你将元素age的默认值改为21,然后重新编译Test接口,那么注释@Test(id=0)将使用这个新的默认值,甚至在那些在默认值修改之前就已经编译过的类文件中也是如此。
注解格式简化
有两个特殊的快捷方式可以用来简化注解。
- 如果没有指定元素,要么是因为注解中没有任何元素,要么是因为所有元素都使用默认值,那么你就不需要使用圆括号了。例如
@Test
和这个注解是一样的@Test(id=0,name="小王",age=21)
,这样的注解又称为标记注解。 - 另外一种快捷方式是
单值注解
。如果一个元素具有特殊的名字value,并且没有指定其他元素,那么你就可以忽略掉这个元素名以及等号。例如:
定义一个注解接口如下形式:
public @interface TestOneValue(){
String value();
}
那么,我们可以将这个注解书写成如下形式:
@TestOneValue("test")
而不是
@TestOneValue(value="test")
注解的使用
一个项可以有多个注解,例如:
@Test(id=0,name="小王",age=21)
@TestOneValue("test")
public void test01(){}
如果注解声明为可重复的,那么我们就可以重复使用同一个注解:
@Test(id=0,name="小王",age=21)
@Test(id=1,name="小二",age=2)
public void test02(){}
注意事项:一个注解元素永远不能设置为null,并且不允许其默认值为null。这样在实际应用中会相当不方便。你必须使用其他的默认值,泥例如“”或者Void.class。
如果元素值是一个数组,那么要将它的值用括号括起来,例如:@Test(...,score={100,101,102})
。
如果该元素只有一个值,那么可以忽略这些括号,例如:@Test(...,score=100)
,这个就和@Test(...,score={100})
一样。
既然个注解可以是另一个注解,那么就可以创建出任意复杂的注解,但是一般我们不这么用理解即可,例如:@Test([email protected]("test")
。
注意事项:在注解中引入循环依赖是一种错误。例如,因为Test具有一个注解类型为Reference的元素,所以Reference就不能再拥有一个类型为Test的元素。
注解各类声明
注解可以出现在许多地方,这些地方可以分为两类:声明和类型用法声明注解可以出现在下列声明处:
- 包
- 类(包括enum)
- 接口(包括注解接口)
- 方法
- 构造器
- 实例域(包含enum常量)
- 局部变量
- 参数变量
- 类型参数
对于类和接口,需要将注解放置在class和interface关键词的前面:
@Test
public class Student {...}
对于变量,需要将它们放置在类型的前面:
@SuppressWarnings("unchecked")
int age;
泛型或者方法中的类型参数可以想下面这样被注解:
public class Cache<@Immutable V>{...}
包是在文件package-info.java中注解的,该文件只包含以注解先导的包语句:
/**
Package-level Javadoc
*/
@GPL(version="3")
package cn.ac.whz.annotation;
import org.gnu.GPL;
注意事项:对局部变量的注解只能在源码级别上进行处理。类文件并不描述局部变量。因此,所有的局部变量注解在编译完一个类的时候就会被遗弃掉。同样的,对包的注解不能在源码级别之外存在。
注解类型用法
声明注解提供了正在被声明的项的相关信息。例如下面的声明中:
public User getUser(@NonNull String userId)
就断言userId参数不为空。
@NonNull注解是Checker Framework的一部分。通过使用这个框架,可以在程序中包含断言,例如某个参数不为空,或者某个String包含一个正则表达式。然后,静态分析工具将检查在给定的源代码段中这些断言是否有效。
现在,假设我们有一个类型为List<String>
的参数,并且想要表示其中所有的字符串都不为null。这就是类型用法注解大显身手之处,可以将该注解放置到类型参数之前:List<@NonNull String>
。
类型用法注解可以出现在下面的位置:
- 和泛型参数一起使用:List<@NonNull String>,Comparator.<@NonNull String> reverseOrder()
- 数组中的任何位置:@NonNull String[] [] words(word[i] [j]不为null),String @NonNull [] [] words(words不为null),String[] @NonNull [] words(word[i]不为null)
- 与超类和实现接口一起使用:class Warning extends @Localized Message
- 与构造器调用一起使用:new @Localized String(…)。
- 与强制类型和instanceof检查一起使用:(@Localized String) text,if (text instanceof @Localized String)。(这些注解只提供外部工具使用,它们对强制转型和instanceof检查不会产生任何影响)。
- 与异常规约一起使用:public String read() throws @Localized IOException
- 与通配符和类型边界一起使用:List<@Localized ? extends Message>,List<? extends @Localized Message>
- 与方法和构造器引用一起使用:@Localized Message::getText
以下的类型位置是不能被注解的:
@NonNull String.class
import java.lang.@NonNull String;
注意事项:注解的作者需要指定特定的注解可以出现在哪里。如果一个注解可以同时应用于变量和类型用法,并且它确实被应用到了某个变量声明上,那么该变量和类型用法就都被注解了。例如,public User getUser(@NonNull String userId)
,如果@NonNull可以同时应用于参数和类型用法,那么uesrId参数就被注解了,而其参数类型是@NonNull String。
标准注解
Java SE在java.lang、java.lang.annotation和javax.annotation包中定义了大量的注解接口。其中四个是元注解,用于描述注解接口的行为属性,其他的三个是规则接口,可以用它们来注解你的源代码中的项。下表中列出了这些注解,后文中将会将这些内容进行详细的介绍。
注解接口 | 应用场合 | 目的 |
---|---|---|
Deprecated | 全部 | 将项标记为过时的 |
SuppressWarnnings | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
SafeVarargs | 方法和构造器 | 断言varargs参数可安全使用 |
Override | 方法 | 检查该方法是否重写了某一个超类方法 |
FunctionalInterface | 接口 | 将接口标记为只有一个抽象方法的函数式接口 |
PostConstruct PreDestroy | 方法 | 被标记的方法应该在构造之后或移除之前立即被调用 |
Resource | 类、接口、方法、域 | 在类或接口上:标记为其他地方要用到的资源。在方法或域上:为“注入”而标记 |
Resources | 类、接口 | 一个资源数组 |
Generated | 全部 | |
Target | 注解 | 指明可以应用这个注解的那些项 |
Retention | 注解 | 指明这个注解可以保留多久 |
Documented | 注解 | 指明这个注解应该包含在注解项的文档中 |
Inherited | 注解 | 指明当这个注解应用于一个类的时候,能过自动被它的子类继承 |
Repeatable | 注解 | 指明这个注解可以在同一个项上应用多次 |
用于编译的注解
-
@Deprecated注解可以被添加到任何不再鼓励使用的项上。所以,当你使用一个已过时的项时,编译器将会发出警告。这个注解与Javadoc标签@deprecated具有同等功效。但是,该注解会一直持久化到运行时。
-
@SuppressWarnnings注解会告知编译器阻止特定类型的警告信息,例如:
@SuppressWarnnings("unchecked")
。 -
@Override这种注解只能用于方法上。编译器会检查具有这种注解的方法是否真正重写一个来自于父类/超类的方法。例如:
public class Test{ @Override public int toString(int a){...}; ... }
这样编译器就会报告一个错误。因为这个toString方法没有重写父类Object类的toString方法。
-
@Generated注解的目的是供代码生成工具来使用。任何生成的源代码都可以被注解,从而与程序员提供的代码区分开。例如,代码编辑器可以隐藏生成的代码,或者代码生成器可以移除生成代码的旧版本。每个注解都必须包含一个表示代码生成器的唯一标识符,而日期字符串和注释字符串是可选的。
用于管理资源的注解
-
@PostConstruct和@PreDestroy注解用于控制对象生命周期的环境中,例如Web容器和应用服务器。标记了这些注解的方法应该在对象被构建之后,或者在对象被移除之前,紧接着调用。
-
@Resource注解用于资源注入。例如,访问数据库的Web应用。数据库访问信息不应该被硬编码到Web应用中。而是应该让Web容器提供某种用户接口,以便设置连接参数和数据库资源的JNDI名字。在这个Web应用中,可以像下面这样应用数据源:
@Resource(name:"jdbc/mydb") private DataSource source;
当包含这个域的对象被构造时,容器会“注入”一个对该数据源的引用。
元注解(自定义注解时需要使用的)
- @Target元注解可以应用于一个注解,以限制该注解可以应用到哪些项上。例如:
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Test
下表是@Target注解所有可能的取值情况,它们属于ElementType枚举类。可以指定任意数量的元素类型,用括号括起来。
元素类型 | 注解适用场合 |
---|---|
ANNOTATION_TYPE | 注解类型声明 |
PACKAGE | 包 |
TYPE | 类(包括enum)及接口(包含注解类型) |
METHOD | 方法 |
CONSTRUCTOR | 构造器 |
FIELD | 成员域(包含enum常量) |
PARAMETER | 方法或构造器参数 |
LOCAL_VARIABLE | 局部变量 |
TYPE_PARAMETER | 类型参数 |
TYPE_USE | 类型用法 |
一条没有@Target限制的注解可以应用于任何项上。编译器将检查你是否将一条注解只应用到了某个允许的项上。例如,如果将@Test应用于一个成员域上,则会导致一个编译器错误。
- @Retention元注解用于指定一条注解应该保留多长时间。只能将其指定为下表中的任意值,其默认值是RetentionPolicy.CLASS。
保留规则 | 描述 |
---|---|
RetentionPolicy.SOURCE | 不包括在类文件的注解 |
RetentionPolicy.CLASS | 包括在类文件的注解,但是虚拟机不需要将它们载入 |
RetentionPolicy.RUNTIME | 包括在类文件的注解,并由虚拟机载入。通过反射API可获得它们 |
- Documented元注解为像Javadoc这样的归档工具提供了一些提示。应该像处理其他修饰符一样来处理归档注解,以实现其归档目的。其他注解的使用并不会纳入归档的范畴。
总结
以上就是常用的注解类的使用,需要了解别的类的使用可以翻阅API文档或者相关书籍。
反射
Java反射机制是Java语言一个很重要的特性,是Java"动态性"的重要体现。反射机制可以让程序在运行时加载编译期完全未知的类,使设计的程序更加灵活、开放。但是,反射机制的不足之处会大大降低程序运行的效率。
在实际开发中,直接使用反射机制的情况并不多,但是很多框架底层都会用到。为此,理解反射机制会与更加深入的学习非常重要。
动态语言
动态语言是指在程序运行时,可以改变程序结构或变量的类型。典型的动态语言有Python、Ruby、JavaScript等。动态语言可以使得在执行的时候就完全改变了源码的结构。这种动态性,可以让程序更加灵活,更加具有开放性。
Java语言虽然具有动态性,但并不是动态语言。我们可以利用反射机制或字节码操作获得类似动态语言的特性。
反射机制的本质和Class类
学习反射机制基本就等同于学习Class类的用法。理解了Class类也就理解了反射机制。
Java反射机制让我们在程序运行状态中,对于任意一个类,都能知道该类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。这种动态获取以及动态调用对象方法的功能就是“Java的反射机制”。
反射机制的本质
反射机制是Java动态性的重要表现,但是反射机制也有缺点,那就是效率问题。反射机制会大大降低程序的执行效率。由于反射机制绕过了源代码,也会给代码维护增加困难。
Java在加载任何一个类时都会在方法区中建立“这个类对应的Class对象”,由于“Class对象”包含了这个类的整个结构信息,所以可以通过这个”Class对象“来操作这个类。
在使用一个类之前先要加载它,在加载完类之后,会在堆内存中产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息,可以通过这个对象知道类的结构。这个对象就像一面镜子,透过它可以看到类的结构,因此被形象称之为反射。“Class对象”是反射机制的核心。
例如Class c = Class.forName(“cn.ac.whz.test.Student”);
,Class.forName()可以让程序员决定在程序运行时加载什么样的类,字符串传入什么类,程序就加载什么类,完全和源代码无关,这就是“动态性”。反射机制的应用实现了“运行时加载,探知与使用编译期间完全未知的类”的可能。
反射机制的核心是“Class对象”。获得了Class对象,就相当于获得了类结构。通过“Class对象”可以调用该类的所有属性、方法和构造器,这样就可以动态加载与运行相关的类。
java.lang.Class类
java.lang.Class类是实现反射的根源。针对任何想动态加载、运行的类,唯有先获得相应的Class对象。java.lang.Class类十分特殊,它用于表示Java中的类型(class,interface,enum,annotation,primitive type,void)本身。
Class类的对象可以用以下方法获取:
- 运用getClass()。
- 运用.class语法。
- 运用Class.forName(),这是最常用的方法。
以下是这三种方式的代码示例:
package cn.whz.reflection;
/**
* 测试各种类型对应的java.lang.Class对象的获取方式
* (class.interface,enum,annotation,primitive,type,void)
* @author eddie
*
*/
public class Demo01 {
public static void main(String[] args) {
String path="cn.whz.bean.User";
try {
Class clz1=Class.forName(path);
//对象是表示或封装一些数据。一个类被加载后,类的整个结构信息会放到对应的Class对象中
//这个Class对象就像一面镜子一样,通过这面镜子可以看到对应类的全部信息
System.out.println(clz1.hashCode());
Class clz2=Class.forName(path); //一个类只对应一个Class对象
System.out.println(clz2.hashCode());
Class strClz1=String.class;
Class strClz2=path.getClass();
System.out.println(strClz1==strClz2);
Class intClz=int.class;
int[] arr1=new int[10];
int[][] arr2=new int[10][3];
int[] arr3=new int[30];
double[] arr4=new double[10];
//由于系统针对每个类只会创建一个Class对象,因此arr1和arr3指向的就是同一个对象
System.out.println(arr1.getClass().hashCode());
System.out.println(arr2.getClass().hashCode());
System.out.println(arr3.getClass().hashCode());
System.out.println(arr4.getClass().hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
反射机制的常见操作
反射机制的常见操作,实际上就是“Class对象”常用方法的应用,一般有如下几种常见操作。
- 动态加载类、动态获取类的信息(属性、方法、构造器)
- 动态构造对象
- 动态调用类和对象的任意方法
- 动态调用和处理属性
- 获取泛型信息
- 处理注解
其中几种操作中常用的类如下表所示:
类名 | 类的作用 |
---|---|
Class类 | 代表类的构造信息 |
Method类 | 代表方法的结构信息 |
Field类 | 代表属性的结构信息 |
Constructor类 | 代表构造器的结构信息 |
Annotation类 | 代表注解的结构信息 |
为了方便测试,首先我们先定义一个简单的User类。
package cn.whz.bean;
public class User {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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 User(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
//javabean必须要有无参的构造方法
public User() {
}
}
利用反射的API获取类的信息(类的名字、属性、方法、构造器)
package cn.whz.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Demo02 {
public static void main(String[] args) {
String path="cn.whz.bean.User";
try {
Class clz=Class.forName(path);
//获取类的名字
System.out.println(clz.getName()); //获得包名+类名:cn.whz.bean.User
System.out.println(clz.getSimpleName()); //获得类名:User
//获得属性信息
// Field[] fields=clz.getFields(); 只能获取public的Field
Field[] fields=clz.getDeclaredFields(); //获得所有的Field
Field f=clz.getDeclaredField("id");
for (Field field : fields) {
System.out.println("属性:"+field);
}
System.out.println(f);
//获得方法信息
// Method[] methods=clz.getMethods(); 只能获取public的Method
Method[] methods=clz.getDeclaredMethods();
Method m1=clz.getDeclaredMethod("getId", null);
//如果方法有参数,则必须传递参数类型对应的Class对象
Method m2=clz.getDeclaredMethod("setId", int.class);
for (Method method : methods) {
System.out.println("方法:"+method);
}
//获得构造器信息
// Constructor[] constructors=clz.getConstructors(); 只能获取public的构造器
Constructor[] constructors=clz.getDeclaredConstructors();
Constructor c1=clz.getConstructor(null);
Constructor c2=clz.getConstructor(int.class,String.class,int.class);
System.out.println("c1:"+c1+"\tc2:"+c2);
for (Constructor constructor : constructors) {
System.out.println("构造器:"+constructor);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过反射API动态的操作:构造器、方法、属性
package cn.whz.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import cn.whz.bean.User;
public class Demo03 {
public static void main(String[] args) {
String path="cn.whz.bean.User";
try {
Class clz=Class.forName(path);
//通过反射API调用构造方法,构造对象
User u1=(User) clz.newInstance();
System.out.println(u1);
Constructor<User> c=clz.getDeclaredConstructor(int.class,String.class,int.class);
User u2=c.newInstance(1001,"小王",21);
System.out.println(u2.getName());
//通过反射API调用普通方法
User u3=(User) clz.newInstance();
// u3.setAge(21);
Method m=clz.getDeclaredMethod("setName", String.class);
m.invoke(u3, "大王"); //u3.setName("大王");
System.out.println(u3.getName());
//通过反射API操作属性
User u4=(User) clz.newInstance();
Field f=clz.getDeclaredField("name");
f.setAccessible(true); //这个属性不需要做安全检查了,可以直接访问
f.set(u4, "王一"); //通过反射直接写属性的值
System.out.println(f.get(u4)); //通过反射直接读属性的值
System.out.println(u4.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过反射获取泛型信息
package cn.whz.reflection;
import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;
import cn.whz.bean.User;
public class Demo04 {
public void test01(Map<String, User> map,List<User> list) {
System.out.println("Demo04.test01()");
}
public Map<Integer, User> test02() {
System.out.println("Demo04.test02()");
return null;
}
public static void main(String[] args) {
try {
//获取指定方法参数的泛型信息
Method m1=Demo04.class.getMethod("test01", Map.class,List.class);
Type[] t=m1.getGenericParameterTypes();
for (Type paramType : t) {
System.out.println("#"+paramType);
if (paramType instanceof ParameterizedType) {
Type[] gengricTypes=((ParameterizedType) paramType).getActualTypeArguments();
for (Type gengricType : gengricTypes) {
System.out.println("泛型类型:"+gengricType);
}
}
}
//获得指定方法返回值的泛型信息
Method m2=Demo04.class.getMethod("test02", null);
Type returnType=m2.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
Type[] gengricTypes=((ParameterizedType) returnType).getActualTypeArguments();
for (Type gengricType : gengricTypes) {
System.out.println("返回值的泛型信息:"+gengricType);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
反射机制的效率问题
反射机制的缺点是会大大降低程序的执行效率,采用反射机制的Java程序要经过字节码解析过程,将内存中的对象进行解析,包括类一些动态类型,而JVM无法对这些代码进行优化,因此,反射操作的效率要比那些非反射操作低得多。
结语
在写完这篇后,由于接下来准备开始着手毕设以及准备秋招,对于Java SE的内容就暂告一段落了,整系列的内容,完全足够小白学习,并且足够应对日常的需求。本系列内容缺少的大概大概就图形界面、Swing这些无关紧要的内容,在秋招之后,如果有时间我会将JUC、断言、日志、脚本引擎、XML以及JDBC的内容补上,并且会再独立的写GOF23种设计模式和项目实战的相关系列博文。
自此这个系列宣告结束,如果把这十几篇博文内容全部看完,对于Java SE的内容基本可以说得上大概了解和掌握了,最后祝各位大佬早日年薪百万。
上一篇: 鱼干怎么样做好吃,有关于鱼干的做法大全!