注解和反射机制使用特别简单,但是它们在框架中被大量的使用,而如何灵活运用,想要深入理解框架,牢牢的掌握注解和反射机制的知识就显得极其的重要了。

注解

注解不同于注释,注释仅只用于写在源代码中,来使自己或者别人更容易的翻阅源代码。

注解是那些插入到源代码中使用其他工具可以对其进行处理的标签。这些工具可以在源代码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。

注解不会改变程序的编译方式。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)将使用这个新的默认值,甚至在那些在默认值修改之前就已经编译过的类文件中也是如此。

注解格式简化

有两个特殊的快捷方式可以用来简化注解。

  1. 如果没有指定元素,要么是因为注解中没有任何元素,要么是因为所有元素都使用默认值,那么你就不需要使用圆括号了。例如@Test和这个注解是一样的@Test(id=0,name="小王",age=21),这样的注解又称为标记注解。
  2. 另外一种快捷方式是单值注解。如果一个元素具有特殊的名字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(ref=@TestOneValue("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类的对象可以用以下方法获取:

  1. 运用getClass()。
  2. 运用.class语法。
  3. 运用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对象”常用方法的应用,一般有如下几种常见操作。

  1. 动态加载类、动态获取类的信息(属性、方法、构造器)
  2. 动态构造对象
  3. 动态调用类和对象的任意方法
  4. 动态调用和处理属性
  5. 获取泛型信息
  6. 处理注解

其中几种操作中常用的类如下表所示:

类名 类的作用
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的内容基本可以说得上大概了解和掌握了,最后祝各位大佬早日年薪百万。