注解(java.Annotation)
1.什么是注解
注解的作用:
- 不是程序本身,可以对程序作出解释(这一点和注释(comment),没什么区别)
- 可以被其他程序(如编译器等)读取
- 生成帮助文档。这是最常见的,也是 Java 最早提供的注解。常用的有 @see、@param 和 @return 等;
- 跟踪代码依赖性,实现替代配置文件功能。比较常见的是 Spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;
- 在编译时进行格式检查。如把 @Override 注解放在方法前,如果这个方法并不是重写了父类方法,则编译时就能检查出。
注解的格式:
- 以"@注释名"在代码中存在,还可以添加一些参数,如@SuppressWarnings(value=“unchecked”)
注解在哪里使用:
- 可以附加在package,class,method,filed等上面,相当于添加了额外的辅助信息,可通过反射机制编程实现对这些元数据的访问
2.内置注解
内置注解 | 作用 |
---|---|
@Override | 用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。 |
@Deprecated | 可以用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时或不安全。当其他程序使用这些元素时,编译器将会给出警告。 |
@SuppressWarnings | 抑制编译时的警告信息,且会一直作用于该程序元素的所有子元素。需要添加关键字才能使用。关键字 |
@FunctionalIterface(Java 8新增) | 修饰函数式接口。Java8规定:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口 |
@SafeVarargs | DK 7 专门为抑制“堆污染”警告提供的 |
3.元注解
元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。
元注解(meta-annotation)作用:
- @Documented 用该注解修饰的注解类会被 JavaDoc 工具提取成文档(默认情况下,JavaDoc 是不包括注解的)
- @Target 指定一个注解的用于什么地方。有一个成员变量(value)用来设置适用目标,value 是 java.lang.annotation.ElementType 枚举类型的数组。格式为@Target({ ElementType.METHOD })
- CONSTRUCT 用于构造方法
- FIELD 用于成员变量(包括枚举常量)
- LOCAL_VARIABLE 用于局部变量
- METHOD 用于方法
- PACKAGE 用于包
- PARAMETER 用于类型参数(JDK 1.8新增)
- TYPE 用于类、接口(包括注解类型)或 enum 声明
- @Retention 用于描述该注解被保留的时间长短(也可理解为该注解在哪一级别可用)。成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型。如@Retention(RetentionPolicy.SOURCE )
- SOURCE 在源文件中有效(即源文件保留),所以该注解会被编译器抛弃
- CLASS 在 class 文件中有效(即 class 保留),所以该注解会被VM抛弃
- RUNTIME 在运行时有效(即运行时保留),所以可以通过反射机制读取注解信息
- 生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用
- @Inherited 指定该注解可以被继承
- @Repeatable 允许在相同的程序元素中重复注解(Java 8新增)。在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解
- @Native 修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
没有元素的注解称为标记注解
3.自定义注解
声明自定义注解使用 @interface 关键字,此时自动继承了java.lang.Annoation接口。注解也会被编译为class文件
自定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id(); //此处没有给定默认值,所以在使用该注解时必须提供id值
public String description() default "no description";
//无论是定义注解还是使用注解,元素值都不能为null,可以用-1、空字符串""代替
}
在方法上使用刚定义的注解
public class PasswordUtils {
@UseCase(id = 47, description = "Password must contain at least one numeric")
public boolean validatePassword(String password){
return password.matches("\\w*\\d\\w*");
}
@UseCase(id = 48)
public String encryptPassword(String password){
return new StringBuilder(password).reverse().toString();
}
@UseCase(id = 49, description = "New password can't equal previously used ones")
public boolean checkForNewPassword(
List<String> prevPassword, String newPassword){
return !prevPassword.contains(newPassword);
}
}
利用反射获取方法中的注解信息
public class UseCaseTracker {
public static void trackUseCases(List<Integer> useCases, Class<?> cl){
for(Method m : cl.getDeclaredMethods()){ //这里cl指PasswordUtils类,通过反射api获得类中的方法
UseCase uc = m.getAnnotation(UseCase.class); //通过反射中的getAnnotation方法得到方法m的注解信息
if(uc != null){
System.out.println("Found UseCase" + uc.id() + " " + uc.description());
useCases.remove(new Integer(uc.id()));
}
}
for (Integer id : useCases) {
System.out.println("Warning: Missing UseCase:" + id);
}
}
public static void main(String[] args) {
List<Integer> useCases = new ArrayList<Integer>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
}
/
/*output:
Found UseCase48 no description
Found UseCase47 Password must contain at least one numeric
Found UseCase49 New password can't equal previously used ones
Warning: Missing UseCase:50
*/
反射(java.Reflection)
反射机制是Java语言一个重要的特性,在学习之前先了解一下编译期和运行期。
- 编译期:在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
- 运行期:是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来。
在java中,所有的类型转换都是在运行时进行正确性检查的,这也是**RTTI(Run Time Type Identification)**的含义:即在运行时,识别一个对象的类型。
那么一个对象的类型信息是如何在运行时表示的呢?
在Java中,执行RTTI这项工作是借助Class对象的特殊对象来完成的,它包含了与类有关的所有信息,也可以理解为Class对象是用来创建类的所有”常规“对象的。
每当编写并编译了一个新类,就会产生一个Class对象(或者说是被保存到同名的.class文件中),而产生这个对象借助的便是运行此程序的JVM中的类加载器子系统。有关JVM以及类加载机制会在以后学习JVM时总结。下面先简单理解一下:
首先,Java程序在它开始运行之前并非完全被加载,其
各个部分是在必须时才加载的,称为动态加载
。所有的类都是在对其第一次使用时,动态加载到JVM中的
。当程序创建第一个对类的静态成员的引用
时(比如类中有使用static关键字,或new一个对象,因为构造方法也是静态方法),就会加载这个类。
如何得到一个Class对象的引用:
1.Class.forName(“类的全限定名”):如果类没被加载JVM便会加载它;如果没找到这个类,便会抛出ClassNotFoundException。
此方法的好处是:不需要获得某个类的对象就可以获得该类的Class引用
try{
Class<? extends ...> c = Class.forName("...")
}catch(ClassNotFoundException){
...
}
Class<? extends ...>
可以将Class引用限定为某种类型,为了提供编译期类型检查。
2.Object类中的getClass()方法:如果已经拥有了某个类的对象,可以直接调用Class<? extends ...>c = 对象.getClass();
来获得Class引用
3.类字面常量 Class<? extends ...> c = 类名.class;
,这样做更加简单且安全,因为此语句在编译时就会收到检查 (不需要t放入ry语句) 。与Class.forName不同的是,使用.class来创建Class对象的引用不会自动初始化改Class对象
并且此方法还可以用于接口、数组以及基本数据类型(如int.class)。对于基本数据类型及其包装器类,int.class
等价于Integer.TYPE
,TYPE字段是一个引用,指向基本数据类型的Class对象。
需要注意:只要元素类型与维度一样,就是同一个Class引用
得到Class引用后可以做什么?
1.通过引用可以获得类的成员信息
java.lang.reflect包:
- Constructor 类:提供类的构造方法信息。
- Field 类:提供类或接口中成员变量信息。
- Method 类:提供类或接口成员方法信息。
- Array 类:提供了动态创建和访问 Java 数组的方法。
- Modifier 类:提供类和成员访问修饰符信息。
类里面具体的方法
方法 | 作用 |
---|---|
Package对象 getPackage() | 获取该类的存放路径 |
String getName() | 获取该类的名称 |
Class getSuperclass() | 获取该类的父类 |
Class[] getlnterfaces() | 获取该类实现的所有接口 |
ClassLoader getClassLoader() | 返回类的类加载器 |
Constructor[] getConstructors() | 获取所有权限为public的构造方法 |
Constructor<?>[] getDeclaredContruectors() | 获取当前对象的所有构造方法 |
Method getMethod(String name,类<?>… parameterTypes) | 返回一个指定方法对象,name指所需方法的简单名称,此对象的形参为parameterTypes |
Method[] getMethods() | 获取所有权限为 public 的方法,包括继承的方法 |
Method[] getDeclaredMethods() | 获取当前对象的所有方法,不包括继承的方法 |
Filed getField(String name) | 返回一个指定的成员变量 |
Field[] getFields() | 获取所有权限为 public 的成员变量,包括继承的成员变量 |
Field[] getDeclareFileds() | 获取当前对象的所有成员变量,不包括继承的成员变量 |
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
Class c1 = Class.forName("com.kk.reflection.User");
//获得类的信息的基本操作
//获得指定属性的值
Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
System.out.println(c1.getDeclaredField("name")); //private java.lang.String com.kk.reflection.User.name
System.out.println(c1.getDeclaredMethod("getAge", null)); //public int com.kk.reflection.User.getAge()
System.out.println(c1.getDeclaredMethod("setName", String.class)); //public void com.kk.reflection.User.setName(java.lang.String)
//其余方法的调用都类似。
}
2.newInstance()
使用newInstance()可以创建一个确切类型的对象。
Class< A> aClass = A.class;
A a = aClass.newInstance();
3.instanceof、isInstance() & ==、equals() 这位两组方法在比较Class对象的等价形式有一定的区别。
class Base{}
class Derived extends Base{}
Base base = new Base();
Derived derived = new Derived();
base instanceof Base; // true 等价于Base.class.isInstance(base)
base instanceof Derived; // false
derived instanceof Base; //true
base.getClass() == Base.class; //true 等价于 base.getClass().equals(Base.class)
derived.getClass() == Base.class; //false,与上面的derived instanceof Base结果不同
可以看出instanceof、isInstance() 是等效的,它会判断对象是否为该类或是该类派生类的对象;而==、equals()不会考虑继承关系
反射的优点:
- 能够运行时动态获取或创建类的实例,大大提高系统的灵活性和扩展性。
- 与 Java 动态编译相结合,可以实现无比强大的功能。
- 对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
反射的缺点:
- 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射
- 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题